CM

chawengwit/mcp

API development
0 stars 0 forks Quality 69 Trend 4

Phases 1–9.5 shipped — stdio, HTTP, OAuth 2.0 Authorization Server (RFC 8414/7591/7636/9728), per-user multi-tenant sessions, CORS, STDIO keyring auth, and LLM-facing endpoint metadata.

Overview

Phases 1–9.5 shipped — stdio, HTTP, OAuth 2.0 Authorization Server (RFC 8414/7591/7636/9728), per-user multi-tenant sessions, CORS, STDIO keyring auth, and LLM-facing endpoint metadata. Verified end-to-end against (STDIO + natural language) and (HTTP + OAuth dance against a real Service API). . Public-deploy recipe (Phase 10) is the next milestone. See docs/testing-user-ux.md for the test playbook, the Development Roadmap, or docs/plan.md. A Python-based that acts as a unified data gateway, enabling Claude (and other MCP clients) to send and receive data across multiple external APIs through a single, secure interface. This MCP server provides: - for multiple data types - supporting any REST or GraphQL endpoint - via an operator-run scripts/oauth_login.py (PKCE flow, 127.0.0.1 callback) - using system keyring - evolution in the future Files marked exist today. Files marked are upcoming phases. Per-module responsibilities and detailed module-by-module breakdown: docs/plan.

README

MCP Data Gateway

Status: OAuth Provider live. Phases 1–9.5 shipped — stdio, HTTP, OAuth 2.0 Authorization Server (RFC 8414/7591/7636/9728), per-user multi-tenant sessions, CORS, STDIO keyring auth, and LLM-facing endpoint metadata. Verified end-to-end against Claude Desktop (STDIO + natural language) and MCP Inspector (HTTP

A Python-based Model Context Protocol (MCP) server that acts as a unified data gateway, enabling Claude (and other MCP clients) to send and receive data across multiple external APIs through a single, secure interface.

Overview

This MCP server provides:

  • Generic data handling for multiple data types
  • Generic API gateway supporting any REST or GraphQL endpoint
  • OAuth 2.0 authentication via an operator-run scripts/oauth_login.py (PKCE flow, 127.0.0.1 callback)
  • Secure credential storage using system keyring
  • Foundation for MCP App evolution in the future

Features

Feature Description
Multi-API Support Connect to any number of external services through unified configuration
REST + GraphQL Native support for both REST and GraphQL APIs
OAuth 2.0 Full authorization code flow + PKCE, run once via scripts/oauth_login.py
Token Refresh Silent refresh inside Credentials.get() when ≤ 5 min from expiry
Secure Storage Credentials stored in OS keyring (Keychain / Credential Manager / Secret Service)
Generic Data Models Flexible schemas to handle any data shape
Predictable Auth Surface Tools return AUTH_REQUIRED when no token; operator runs scripts/oauth_login.py to obtain one

Architecture

Files marked (implemented ✓) exist today. Files marked (planned) are upcoming phases.

MCP/
├── src/
│   ├── server.py              # MCP server entry point (implemented ✓)
│   ├── auth/                  # OAuth 2.0 + keyring (implemented ✓)
│   │   ├── oauth.py           # PKCE auth-code flow, callback server bound to 127.0.0.1
│   │   └── credentials.py     # Keyring-backed store with peek/get/store/clear
│   ├── gateway/               # REST/GraphQL HTTP client (implemented ✓)
│   │   ├── api_client.py      # RestClient + GraphQLClient with retry, redacted logging
│   │   └── handlers.py        # Response normalization, size enforcement, error mapping
│   ├── models/                # Pydantic data models (planned)
│   │   └── data_models.py
│   ├── tools/                 # MCP tool definitions (implemented ✓)
│   │   ├── builtin.py         # list_apis tool
│   │   ├── registry.py        # ToolRegistry / ToolSpec
│   │   ├── context.py         # ToolContext dependency container
│   │   ├── auth_resolver.py   # auth.type branching (oauth2/bearer/api_key/None)
│   │   └── mcp_tools.py       # fetch_data/send_data/execute_graphql/get_status
│   ├── config.py              # API config loader with ${VAR} substitution (implemented ✓)
│   ├── events/                # Activity logging (implemented ✓)
│   │   ├── schemas.py         # Pydantic models (audit/debug/usage/insight)
│   │   ├── redaction.py       # Sensitive data redaction
│   │   ├── retention.py       # Per-month file rotation cleanup
│   │   ├── writers.py         # Async JSONL writer + queue
│   │   └── recorder.py        # Public Recorder API
│   └── transport/             # Transport layer (Phase 8 — implemented ✓)
│       ├── stdio.py           # stdio transport (default)
│       └── http.py            # Streamable HTTP + Bearer middleware + loopback guard
├── config/
│   ├── api_configs.json       # API service configurations (committed; uses ${VAR} placeholders)
│   └── api_configs.example.json  # Template with all four auth.type variants
├── docs/
│   └── plan.md                # Implementation plan / roadmap
├── scripts/                   # Operator CLI helpers (implemented ✓)
│   └── oauth_login.py         # Drive OAuth 2.0 + PKCE flow, persist token in keyring
├── tests/
│   ├── auth/                  # Unit tests for src/auth/ (49 cases — implemented ✓)
│   ├── events/                # Unit tests for src/events/ (27 cases, 51 collected w/ parametrize)
│   ├── gateway/               # Unit tests for src/gateway/ (61 cases — implemented ✓)
│   ├── tools/                 # Unit tests for src/tools/ (37 cases — implemented ✓)
│   ├── scripts/               # Unit tests for scripts/ (15 cases — implemented ✓)
│   ├── transport/             # Unit tests for src/transport/ (23 cases — implemented ✓)
│   ├── integration/           # Full-flow + subprocess smoke tests (5 cases — implemented ✓)
│   ├── test_config.py         # Config loader tests
│   ├── test_server.py         # Server bootstrap + _build_oauth_configs tests
│   └── test_example_config.py # Schema-drift guard for api_configs.example.json
├── .claude/commands/          # Slash commands for the dev workflow
│   ├── generate-prp.md        #   /generate-prp INITIAL.md  → PRPs/{feature}.md
│   └── execute-prp.md         #   /execute-prp PRPs/{...}   → implements + validates
├── PRPs/
│   ├── templates/prp_base.md  # Template each PRP fills in
│   └── {feature}.md           # Generated implementation blueprints
├── INITIAL.md                 # Per-feature scope delta (input to /generate-prp)
├── .env.example               # Environment variables template
├── .gitignore                 # Excludes secrets and build artifacts
├── pyproject.toml             # pytest + ruff + mypy configuration
├── requirements.txt           # Runtime dependencies
├── requirements-dev.txt       # Dev/test deps (pytest, ruff, mypy)
├── CLAUDE.md                  # Project rules + Context Engineering workflow
└── README.md                  # This file

MCP Tools

Tool Description
fetch_data GET data from a configured API (auto-OAuth if required)
send_data POST/PUT data to a configured API (auto-OAuth if required)
execute_graphql Run a GraphQL query or mutation (auto-OAuth if required)
list_apis List all configured API services
get_status Show authentication and connection status

Per-module responsibilities and detailed module-by-module breakdown: docs/plan.md § Architecture Overview. Activity logging contract (four categories, retention, redaction): CLAUDE.md § Activity Logging.

Authentication Flow

OAuth login is operator-initiated (one-time, per provider) and tools are read-only with respect to credential acquisition — they never auto-open the browser. This split keeps the request path predictable for clients (Claude Desktop, Codex CLI, etc.) and concentrates the browser flow in a single CLI surface that’s easy to script and test.

A) First-time login (operator runs once per provider)
─────────────────────────────────────────────────────
$ python -m scripts.oauth_login github
        ↓
1. Script builds authorize URL with PKCE
2. Browser opens at provider's authorize page
3. User clicks "Authorize"
4. Callback at http://127.0.0.1:8765/callback receives auth code
5. Script exchanges code for tokens
6. TokenInfo persisted in OS keyring (service=mcp-data-gateway, account=)

B) Subsequent tool calls (auto, no UI)
──────────────────────────────────────
1. Claude invokes tool (e.g., fetch_data)
        ↓
2. MCP checks credentials in keyring
        ↓
3a. Valid token                                       →  proceed with API call
3b. Token < 5 min from expiry, refresh_token present  →  silent refresh, then proceed
3c. No token / expired with no refresh                →  return AUTH_REQUIRED *
        ↓
4. Tool returns response (data + metadata, or {error: AUTH_REQUIRED})

* On AUTH_REQUIRED, operator re-runs `python -m scripts.oauth_login `
  and then retries the original tool call.

Tech Stack

  • Python 3.10+
  • mcp — Model Context Protocol Python SDK
  • httpx — Async HTTP client (REST + GraphQL)
  • keyring — Cross-platform secure credential storage
  • pydantic — Data validation and modeling
  • python-dotenv — Environment variable management
  • uvicorn + starlette — ASGI server + framework for the HTTP transport (Phase 8)

Quickstart

Prerequisites: Python 3.10+ and pip (or uv).

# 1. Clone and enter the repo
git clone https://github.com/Chawengwit/MCP.git mcp-data-gateway
cd mcp-data-gateway

# 2. Create a virtual environment
python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate

# 3. Install runtime + dev dependencies
pip install -r requirements.txt
pip install -r requirements-dev.txt # only if you want to run pytest

# 4. Copy the env + config templates and edit them
cp .env.example .env                       # add provider client_id / secrets
cp config/api_configs.example.json config/api_configs.json

# 5. Run the server (talks MCP over stdio)
python -m src.server

The server boots, loads config/api_configs.json, starts the Recorder, builds the ToolContext, and registers all five tools (list_apis, fetch_data, send_data, execute_graphql, get_status). On SIGINT/SIGTERM the Recorder queue drains and the server exits cleanly.

First-run note. With the default example config the server will warn “Skipping OAuth config for example_rest_api: missing client_id, client_secret” until you populate EXAMPLE_REST_CLIENT_ID and EXAMPLE_REST_CLIENT_SECRET in .env. The other tools (list_apis, get_status, plus any bearer / api_key / no-auth APIs) work without OAuth setup.

Configuring an API

The shipped config/api_configs.example.json covers all four auth.type paths:

Example entry auth.type Use when
example_rest_api oauth2 Provider supports OAuth 2.0 auth-code flow (Google, GitHub, custom)
example_graphql_api bearer You already have a long-lived token in an env var
example_apikey_api api_key Provider uses a static key in a custom header
public_no_auth_api null Public endpoints with no auth

Never commit literal secrets — every credential field in the file uses a ${ENV_VAR} placeholder. The config loader substitutes from your .env (or the process environment) at startup.

Environment variables

See .env.example for the full annotated template. Most-used variables:

Var Default What it does
MCP_API_CONFIG_PATH config/api_configs.json Override the API config file path (useful for XDG_CONFIG_HOME, per-environment configs, or CI isolation)
OAUTH_CALLBACK_PORT 8765 Port the OAuth callback HTTP server binds to
MCP_LOG_LEVEL INFO DEBUG / INFO / WARN / ERROR
MCP_LOG_DIR ./logs Where activity-log JSONL files go
MCP_LOG_RETENTION_DAYS 365 Older monthly files are pruned at startup
MCP_LOG_DEBUG_ENABLED true Toggles per-request HTTP debug events
MCP_MAX_RESPONSE_BYTES 10485760 (10 MiB) Response-size cap before truncation / RESPONSE_TOO_LARGE
MCP_REQUEST_TIMEOUT_SEC 30 Per-request timeout for outbound HTTP

Usage

Running Tests

# After installing requirements-dev.txt:
pytest tests/

# Run a specific test file with verbose output
pytest tests/events/test_writers.py -v

# Currently 288 tests passing across src/events/, src/auth/, src/gateway/, src/tools/, src/transport/, scripts/, plus integration + example-config drift tests.

Running the MCP Server

python -m src.server

The server boots, loads config/api_configs.json, starts the Recorder, builds the ToolContext, and registers all five tools (list_apis, fetch_data, send_data, execute_graphql, get_status). On SIGINT/SIGTERM the Recorder queue drains and the server exits cleanly.

Connecting to Claude Desktop

server.py performs its own bootstrap (sys.path + os.chdir + load_dotenv), so a cwd field in the client config is not required — point args at the script directly:

{
  "mcpServers": {
    "data-gateway": {
      "command": "/path/to/mcp-data-gateway/.venv/bin/python",
      "args": ["/path/to/mcp-data-gateway/src/server.py"]
    }
  }
}

On macOS, this lives at ~/Library/Application Support/Claude/claude_desktop_config.json. After editing, fully quit and relaunch Claude Desktop; the 🔌 menu should list data-gateway with all five tools.

Connecting to OpenAI Codex CLI

Codex CLI uses TOML at ~/.codex/config.toml:

[mcp_servers.data-gateway]
command = "/path/to/mcp-data-gateway/.venv/bin/python"
args = ["/path/to/mcp-data-gateway/src/server.py"]

Verified against codex CLI — list_apis, get_status, and fetch_data round-trip identically to Claude Desktop.

Other MCP clients (Cursor, Cline, etc.)

Any client that supports stdio MCP servers will work with the same shape: a command and an args list.

HTTP transport (ChatGPT Connectors, MCP Inspector, custom HTTP clients)

Set MCP_TRANSPORT=http to expose the server as a Streamable HTTP endpoint instead of stdio.

# Generate a strong bearer token
export MCP_HTTP_BEARER_TOKEN="$(python -c 'import secrets; print(secrets.token_hex(32))')"

# Run on loopback (default host/port)
MCP_TRANSPORT=http \
MCP_HTTP_HOST=127.0.0.1 MCP_HTTP_PORT=8080 \
python -m src.server

The server prints its bind address to stderr and serves the MCP protocol at POST /mcp, GET /mcp, and DELETE /mcp.

Test from curl:

# Without the token → 401
curl -i -X POST http://127.0.0.1:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'

# With the token → initializes a session
curl -i -X POST http://127.0.0.1:8080/mcp \
  -H "Authorization: Bearer $MCP_HTTP_BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-11-25","capabilities":{},
                 "clientInfo":{"name":"curl","version":"1.0"}}}'

The response includes a Mcp-Session-Id header — echo it on every subsequent request via -H "mcp-session-id: ".

Connect from MCP Inspector (great for interactive debugging):

npx @modelcontextprotocol/inspector
# UI opens at http://localhost:6274
# Transport: Streamable HTTP
# URL:       http://127.0.0.1:8080/mcp
# Auth:      Bearer 

Connect from ChatGPT Custom Connectors: paste the public URL of the server (via reverse proxy / Cloudflare Tunnel / ngrok) and the bearer token. ChatGPT talks server-to-server, so no CORS configuration is needed.

Loopback safety guard. The server refuses to start if MCP_HTTP_HOST is non-loopback (0.0.0.0, public IP, etc.) and MCP_HTTP_BEARER_TOKEN is unset or empty — preventing accidental unauthenticated public binds. Set the token, bind to 127.0.0.1, or enable the OAuth Provider (see below).

OAuth Provider — Claude.ai “Add custom connector” (Phase 9)

When MCP_OAUTH_ENCRYPTION_KEY is set, the HTTP transport additionally exposes an OAuth 2.0 Authorization Server so Claude.ai (and any MCP client that speaks OAuth 2.1 + RFC 9728) can connect.

# 1. Generate a Fernet key once and add it to .env. Rotating it later invalidates
#    every stored Service API session — keep it stable.
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

# 2. Configure the Service API entry in config/api_configs.json with
#    auth.type=session_login. See config/api_configs.example.json for the shape.

# 3. Start the server with OAuth Provider on (loopback or ngrok-tunnelled HTTPS).
MCP_TRANSPORT=http \
MCP_OAUTH_ENCRYPTION_KEY= \
MCP_OAUTH_ISSUER=https://.ngrok-free.app \
python -m src.server

# 4. In Claude.ai, "Add custom connector" → paste the issuer URL.
#    Claude.ai will discover /.well-known/oauth-authorization-server,
#    POST /register, then redirect users to /authorize. Each user pastes
#    their Service API api_key + secret_key into the consent form;
#    the encrypted session is stored under their Service API user_id.

Operator inspection (no plaintext output):

python -m scripts.oauth_admin list-clients
python -m scripts.oauth_admin list-tokens
python -m scripts.oauth_admin list-sessions
python -m scripts.oauth_admin revoke-token --token 

The Phase 8 static-bearer path is unchanged when the OAuth Provider is on; both auth methods are accepted at /mcp. OAuth tokens carry a user_id into audit/usage/insight logs, static-bearer tokens carry user_id=null.

Example Interactions

Once connected, Claude can:

  • List configured APIs: “Show me the available API services”
  • Fetch data: “Get the user list from example_api”
  • Send data: “Create a new record in example_api with this data…”
  • Execute GraphQL: “Run this GraphQL query against my API…”

The first time Claude uses a tool requiring authentication, your browser will open automatically for OAuth login.

Response Format

All MCP tools return structured JSON: {data, metadata} on success, {error} on failure. Large responses truncate (success + cursor) where safe; binary/streaming emit RESPONSE_TOO_LARGE. GraphQL surfaces partial-success (data + errors).

For the full spec — exact field shapes, the error-code table, the truncation rule, and the GraphQL handling — see CLAUDE.md § Response Format Conventions.

OAuth Setup

For each oauth2 API in api_configs.json:

  1. Register an OAuth application with the provider (GitHub OAuth Apps, Google Cloud OAuth client, etc.) and set the redirect URI to http://127.0.0.1:8765/callback exactly. Use 127.0.0.1, not localhost — browsers may treat them as different origins for OAuth state tracking.
  2. Copy the resulting client ID and client secret into .env under names matching your api_configs.json placeholders. By convention: ${API_ID_UPPER}_CLIENT_ID and ${API_ID_UPPER}_CLIENT_SECRET — e.g. GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET for api_id="github".
  3. Run the operator login helper once per provider:
    python -m scripts.oauth_login github            # initial login
    python -m scripts.oauth_login github --clear    # delete + re-auth
    
    The script opens your browser at the provider’s authorize URL, captures the auth code via the one-shot 127.0.0.1 callback server, exchanges it for tokens, and stores them in your OS keyring. Subsequent runs reuse the stored token; the gateway auto-refreshes when ≤ 5 minutes remain.

Why a separate script? MCP tools (fetch_data, send_data, execute_graphql) intentionally do not auto-trigger the browser flow — they return AUTH_REQUIRED so the client (Claude / Codex / etc.) can surface it cleanly. The login script is the operator-side counterpart.

If the default port 8765 is in use on your machine, set OAUTH_CALLBACK_PORT= in .env and update the redirect URI you registered with the provider.

Keyring Setup (per OS)

Tokens are stored in the OS-native secure keyring. No additional setup is needed on most desktops:

OS Backend Action required
macOS Keychain None — works out of the box
Windows Credential Manager None — works out of the box
Linux (desktop) Secret Service (gnome-keyring / KWallet) Ensure your session has one running (most distros do)
Linux (headless / CI / Docker) None by default pip install keyrings.alt for a file backend, OR provide tokens via bearer auth (token_env) and skip OAuth

If keyring is unavailable, Credentials raises CredentialStorageError("No keyring backend available. Install 'keyrings.alt' …") on the first OAuth API call. The error is fail-loud by design — the gateway will not silently fall back to a less secure store.

Troubleshooting

Symptom Likely cause Fix
Server logs “Skipping OAuth config for X: missing client_id, client_secret” .env placeholders not set Populate the matching ${VAR} in .env, restart
OAuth tool hangs after the browser popup OAUTH_CALLBACK_PORT blocked or in use Free the port or set a different one in .env; update the provider’s redirect URI to match
OAuth callback returns 400 with “State parameter mismatch” Browser opened a stale tab from a previous flow Close all browser tabs pointing at 127.0.0.1:8765, retry
CredentialStorageError: No keyring backend available Headless Linux without DBus / Secret Service pip install keyrings.alt, OR use bearer-token auth
Tool returns RESPONSE_TOO_LARGE on a binary download Hit MCP_MAX_RESPONSE_BYTES cap (binary cannot be safely truncated) Raise the cap, or use the API’s pagination, or stream out-of-band
GraphQL response shows both data and errors Intentional — GraphQL allows partial success; both are surfaced to Claude Not a bug. See CLAUDE.md § GraphQL Specifics
Server smoke test fails with “Server exited before ready marker” A required env var (MCP_LOG_DIR etc.) is unset or points to an unwritable path Check stderr for the actual exception; fix permissions or the var
401 returned after the tool call worked previously Refresh token expired or revoked at the provider Delete the keyring entry (or call Credentials.clear(api_id)) and re-auth
Module import error: ModuleNotFoundError: keyrings.alt (Linux only) Tried to use the keyrings.alt fallback but didn’t install it pip install keyrings.alt

For the full failure-modes table that this section is derived from, see CLAUDE.md § Common Failure Modes.

Logging (Operator-Only)

The Recorder writes one append-only JSONL file per (category, month) under $MCP_LOG_DIR (default ./logs/):

logs/
├── audit/   YYYY-MM.jsonl   who/when/what — security & compliance
├── debug/   YYYY-MM.jsonl   full HTTP exchange (redacted) — troubleshooting
├── usage/   YYYY-MM.jsonl   per-call latency / sizes — analytics
└── insight/ YYYY-MM.jsonl   tool args + response summaries — Claude's request patterns
  • Rotation is automatic per month; the current month is never deleted.
  • Old files are pruned on server startup according to MCP_LOG_RETENTION_DAYS (default 365).
  • All four streams pass through the central redaction helpers (src/events/redaction.py); Authorization headers, access_token, refresh_token, client_secret, password, api_key, secret, and any per-API redact_fields are replaced with ``.

These logs are operator-only. No MCP tool exposes them to Claude. Treat the directory like any production audit log: review before sharing, retain per your org’s policy, and back up if you need historical analytics.

To enable verbose request tracing temporarily:

MCP_LOG_DEBUG_ENABLED=true MCP_LOG_LEVEL=DEBUG python -m src.server

All logs go to stderr (stdout is reserved for the MCP JSON-RPC protocol).

For the full debug/logging strategy and env-var reference, see CLAUDE.md § Debug & Logging Strategy.

Development Workflow

This project uses a Context Engineering workflow for non-trivial features. The full description lives in CLAUDE.md § Context Engineering Workflow. Quick summary:

1. Edit INITIAL.md          ← describe ONE feature (delta vs docs/plan.md)
2. /generate-prp INITIAL.md ← AI researches and writes PRPs/{feature}.md
3. /execute-prp PRPs/{...}  ← AI implements + runs ruff/mypy/pytest until green

src/events/ is the project’s reference implementation — new code mirrors its patterns. See CLAUDE.md § Reference Implementation.

For small fixes (single-line changes, doc edits, etc.) skip the workflow and edit directly.

Development Roadmap

Phase What Status
1 Project Setup ✅ done
2 Core MCP Server ✅ done — list_apis, registry, config loader, graceful shutdown
3 Authentication (OAuth + keyring) ✅ done — PKCE flow, callback on 127.0.0.1, Credentials with concurrent-refresh lock, 49 tests
4 API Gateway (REST + GraphQL) ✅ done — RestClient + GraphQLClient, retry on 429/5xx + transport errors, redacted logging, response normalization, GraphQL partial-success preserved, 61 tests
5 Tools & Integration ✅ done — fetch_data/send_data/execute_graphql/get_status, auth.type branching (oauth2/bearer/api_key/null), Recorder triple per call, secret redaction in insight events, 37 tests
6 Testing & Polish ✅ done — subprocess smoke test, example-config schema-drift guard, README expanded with Quickstart / OAuth / Keyring per OS / Troubleshooting / Logging
7 Activity Logging (src/events/) ✅ done — 27 test cases (51 collected with parametrization)
Post-v0 GitHub OAuth integration ✅ done — scripts/oauth_login.py operator CLI, real GitHub OAuth flow, verified on Claude Desktop and Codex CLI, 15 new tests
8 HTTP Transport ✅ done — MCP_TRANSPORT={stdio,http} switch, Streamable HTTP via uvicorn + Starlette, Bearer-token middleware, loopback-bind safety guard, per-arg env fallback for run_http settings, single-tenant; 41 new tests (38 transport unit + 3 HTTP subprocess smoke) — 288 total
9 OAuth Provider (RFC 8414/7591/7636/9728) ✅ done — src/oauth_provider/, encrypted SQLite store, consent HTML form, opaque tokens, per-user service_session via contextvar, 93 new tests
9.1 Form-encoded login + api_key fingerprint user_id ✅ done — Taximail-style Service APIs (POST form login, no stable per-user id in response)
9.2 Contextvar bridge for per-request session ✅ done — OAuth middleware publishes user_id; tool handlers fetch the matching Service-API session at call time
9.3 CORS middleware + RFC 9728 strict URL ✅ done — src/transport/cors.py, slash-boundary path match, strict-variant protected-resource discovery
9.4 Keyring-backed session_login for STDIO ✅ done — scripts/session_login.py operator CLI; Claude Desktop natural language reaches Service APIs
9.5 LLM-facing endpoint metadata ✅ done — description / required_params / param_hints surfaced through list_apis; LLM picks correct filters on the first try; 432 total tests
10 Production Deploy Recipe 🔵 next — Dockerfile + Caddy (Let’s Encrypt) + VPS guide; unlocks Claude Desktop’s “Add custom connector” and Claude.ai web

Per-phase deliverables and verification plan: docs/plan.md. Operator playbook for testing both transports end-to-end: docs/testing-user-ux.md. Future scalability ideas (multi-tenant, caching, additional transports) live in docs/plan.md § Future Scalability.

Security

  • All credentials stored in OS-level secure keyring (Keychain on macOS, Credential Manager on Windows, Secret Service on Linux)
  • .env file excluded from version control via .gitignore
  • OAuth uses standard authorization code flow with PKCE (no implicit grant)
  • Tokens never logged or exposed in error messages — Pydantic Field(repr=False) keeps secret fields out of repr() and f-string output
  • OAuthConfig rejects non-HTTPS authorize_url / token_url at validation time
  • Local callback server binds to 127.0.0.1 (not localhost) and only during the OAuth flow; closes immediately after the auth code is received

License

TBD

Contributing

Phases 1–9.5 are shipped — STDIO + HTTP transports, GitHub OAuth, the full OAuth Provider (RFC 8414/7591/7636/9728), keyring-backed STDIO session_login, CORS, and LLM-facing endpoint metadata are all in main with 432 passing tests. The active feature delta is Phase 10 — Production Deploy Recipe (Dockerfile + Caddy + VPS guide) in INITIAL.md; implementation goes through /generate-prp INITIAL.md → /execute-prp PRPs/{...}.md (see the Development Workflow section). For bug fixes and small edits, open a PR directly. Contribution guidelines (style, commit format, review process) will be formalized as the project gains contributors.

View the full README on GitHub

Install

npx @modelcontextprotocol/inspector
New to MCP? Start with the mcpskill.org guide →

Configuration

{ "mcpServers": { "data-gateway": { "command": "/path/to/mcp-data-gateway/.venv/bin/python", "args": ["/path/to/mcp-data-gateway/src/server.py"] } } }