MCP Data Gateway
by Chawengwit
README.md
# 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
> + OAuth dance against a real Service API). **432 passing tests**. Public-deploy
> recipe (Phase 10) is the next milestone. See [`docs/testing-user-ux.md`](docs/testing-user-ux.md)
> for the test playbook, the [Development Roadmap](#development-roadmap), or
> [`docs/plan.md`](docs/plan.md).
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](docs/plan.md). Activity logging contract
(four categories, retention, redaction): [`CLAUDE.md` § Activity Logging](CLAUDE.md).
## 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=<api_id>)
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 <api_id>`
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`).
```bash
# 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`](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`](.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
```bash
# 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
```bash
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:
```json
{
"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`:
```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.
```bash
# 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:**
```bash
# 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: <value>"`.
**Connect from MCP Inspector** (great for interactive debugging):
```bash
npx @modelcontextprotocol/inspector
# UI opens at http://localhost:6274
# Transport: Streamable HTTP
# URL: http://127.0.0.1:8080/mcp
# Auth: Bearer <your token>
```
**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.
```bash
# 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=<paste fernet key> \
MCP_OAUTH_ISSUER=https://<your-ngrok-host>.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):
```bash
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 <full opaque 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](CLAUDE.md).
## 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:**
```bash
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=<free-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](CLAUDE.md) |
| 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](CLAUDE.md).
## 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 `<redacted>`.
> **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:
```bash
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](CLAUDE.md).
## Development Workflow
This project uses a **Context Engineering** workflow for non-trivial features.
The full description lives in
[`CLAUDE.md` § Context Engineering Workflow](CLAUDE.md). 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](CLAUDE.md).
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`](docs/plan.md).
Operator playbook for testing both transports end-to-end:
[`docs/testing-user-ux.md`](docs/testing-user-ux.md). Future scalability
ideas (multi-tenant, caching, additional transports) live in
[`docs/plan.md` § Future Scalability](docs/plan.md).
## 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`](INITIAL.md); implementation
goes through `/generate-prp INITIAL.md` → `/execute-prp PRPs/{...}.md` (see
the [Development Workflow](#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.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues