{
  "markdown": "<!-- mcp-name: io.github.shigechika/mcp-stdio -->\n\n# mcp-stdio\n\nEnglish | [日本語](README.ja.md)\n\nA stdio↔HTTP MCP gateway that works in **both directions** — the same\nbinary is both an MCP-over-HTTP **client** gateway and, via `serve` mode, a\nfull MCP-over-HTTP **server** (with an optional embedded OAuth 2.1\nAuthorization Server), so it can sit on either end of an MCP connection.\n\n📖 **New here? Start with the [user guide](https://shigechika.github.io/mcp-stdio/)** — task-oriented docs for connecting a client or publishing a server. This README is the full reference.\n\n## Overview\n\n**Client gateway (default mode) — stdio → HTTP.** [MCP](https://modelcontextprotocol.io/) clients like Claude Desktop and Claude Code see mcp-stdio as a locally running self-hosted MCP server, while it relays all requests to a remote MCP server with support for various authentication methods:\n\n```mermaid\nflowchart BT\n    A[Claude<br>Desktop/Code] <-- stdio --> B(mcp-stdio)\n    B <== \"<b>HTTPS</b><br>Streamable HTTP / SSE<br>Bearer Token<br>Header<br>OAuth\" ==> C[Remote<br>MCP Server]\n    B -. \"OAuth 2.1<br>(PKCE)\" .-> D[Authorization<br>Server]\n    D -. callback .-> B\n    style B fill:#4a5,stroke:#333,color:#fff\n```\n\nBearer tokens, custom headers, and OAuth 2.1 credentials are forwarded to the remote server.\n\n**Reverse gateway (`mcp-stdio serve`) — HTTP → stdio.** The mirror image: takes a\nlocal stdio MCP server (any language, any framework) and exposes it as a\nStreamable HTTP endpoint, with optional bearer-token or embedded-OAuth-2.1\nauthentication, per-user backend isolation when OAuth is enabled, and a\nrestart-durable token store —\nsee [Reverse gateway: `serve` mode](#reverse-gateway-serve-mode) below. This\nis how mcp-stdio can stand in for a framework's own HTTP/OAuth hosting layer\n(e.g. instead of depending on a Python web framework's built-in server for\njust that part) when the tool definitions themselves already run fine over\nstdio.\n\n## Features\n\n- **Both MCP transports supported** — Streamable HTTP (current spec, default) and SSE (MCP 2024-11-05 legacy), selectable with `--transport`. SSE parser follows the [WHATWG Server-Sent Events spec](https://html.spec.whatwg.org/multipage/server-sent-events.html).\n- **OAuth 2.1 client** — built-in authorization code flow with PKCE, dynamic client registration, token refresh, and secure token persistence. Implements the full MCP authorization spec at the section level:\n  - [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) Protected Resource Metadata\n    - §3 discovery of authorization servers via `/.well-known/oauth-protected-resource`\n    - §3.1 path-aware well-known URL construction for path-based reverse-proxy deployments, with host-root fallback; preserves the resource URL's query component on the constructed metadata URL\n    - §3.3 `resource` field validation — warn on mismatch, continue\n    - §5.1 `WWW-Authenticate: Bearer resource_metadata=` hint — probes the server before discovery so servers that publish PRM at a non-standard URL are found without well-known path guessing\n  - [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) Authorization Server Metadata\n    - §3.1 well-known URL construction, including path insertion for issuers with path components\n    - §3.3 `issuer` validation — reject a cross-origin issuer (AS mix-up guard), warn on a same-origin mismatch (trailing slash / path / case) and continue\n    - §3 OpenID Connect Discovery 1.0 fallback — when the OAuth well-known 404s, probe `/.well-known/openid-configuration` (path-append and path-insertion) for ASes that expose only the OIDC form (Auth0, Okta, Azure AD, Google)\n  - [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707) Resource Indicators\n    - §2 `resource` parameter in authorization, token exchange, **and refresh** requests\n  - [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636) PKCE\n    - §4.1–4.2 S256 `code_challenge_method` with an 86-char `code_verifier`\n  - [RFC 8628](https://www.rfc-editor.org/rfc/rfc8628) Device Authorization Grant\n    - §3.1 device authorization request with `resource` indicator (RFC 8707)\n    - §3.4–3.5 token polling with `authorization_pending` / `slow_down` (interval +=5 s) / `expired_token` / `access_denied` handling\n    - DCR registers `urn:ietf:params:oauth:grant-type:device_code` in `grant_types` (RFC 7591 §2)\n  - [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591) Dynamic Client Registration\n    - §3 client registration request; `token_endpoint_auth_method` chosen from `token_endpoint_auth_methods_supported` in AS metadata (prefers `none` → `client_secret_post` → `client_secret_basic`)\n    - §3.2.1 `client_secret_expires_at` handling — auto re-register on expiry\n    - `application_type: \"native\"` in DCR ([RFC 8252](https://www.rfc-editor.org/rfc/rfc8252) §8.4 / MCP SEP-837): the loopback auth-code and headless device flows are native clients, so the loopback redirect is not rejected as the RFC 7591 default `\"web\"`\n  - [Client ID Metadata Documents](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents) (MCP 2025-11-25 / [draft-ietf-oauth-client-id-metadata-document-00](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00))\n    - `--client-metadata-url` presents an operator-hosted HTTPS document URL as `client_id`, skipping Dynamic Client Registration; honoured when set even if the AS metadata does not (yet) advertise `client_id_metadata_document_supported` (warns instead of silently falling back), and outranked by a pre-registered `client_id` (`--client-id` or `MCP_OAUTH_CLIENT_ID`) (#60)\n    - the hosted document's `redirect_uris` must include mcp-stdio's loopback callback **without a port** (`http://127.0.0.1/callback`) — the actual callback binds a fresh ephemeral port every run, and the AS must accept any port for a loopback redirect URI ([RFC 8252](https://www.rfc-editor.org/rfc/rfc8252) §7.3 / §8.4)\n  - [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749) OAuth 2.0\n    - §2.3.1 `client_secret_basic`: `Authorization: Basic` header with percent-encoded credentials (applied to code exchange, token refresh, and Device Authorization Grant polling)\n  - [RFC 6750](https://www.rfc-editor.org/rfc/rfc6750) Bearer Token usage\n    - §2.1 `Authorization: Bearer <token>` request header\n- **Works with MCP 2026-07-28 servers** — add `--protocol-era auto` and mcp-stdio asks the server which protocol it speaks and adapts; your MCP client needs no changes. Without the flag nothing changes, so upgrading is safe. `mcp-stdio serve` answers both old and new clients on the same address automatically — or only the newer ones with `--modern-only`, and `--modern-idle-ttl` reclaims idle backends. Newer clients can also hold one connection open and hear when your server's tool, prompt or resource lists change, or when a specific resource they named is updated (mcp-stdio subscribes on their behalf when your server declares `resources.subscribe`). Verified end to end against python-sdk v2.0.0 in both directions. → [Working with MCP 2026-07-28 servers](https://shigechika.github.io/mcp-stdio/modes/#protocol-eras)\n- **Retry with backoff** — retries up to 3 times on connection errors\n- **HTTP 429 / 503 handling** — honours `Retry-After` (delta-seconds or HTTP-date) up to a 60-second cap on both 429 (Too Many Requests) and 503 (Service Unavailable) — the two spec-sanctioned Retry-After carriers (RFC 9110 §10.2.3) — then surfaces the status so the client can decide (cf. modelcontextprotocol/typescript-sdk#1892)\n- **Auto-pagination** (Streamable HTTP transport) — transparently follows `nextCursor` for `tools/list` / `resources/list` / `resources/templates/list` / `prompts/list` and merges the pages into one response, so clients that drop pages beyond the first still see the full list (cf. anthropics/claude-code#39586)\n- **Streaming resilience** — streams SSE responses in real time; auto-reconnects on mid-stream disconnect\n- **Line-separator safety** — escapes raw `U+2028` / `U+2029` (legal in JSON, but JavaScript line terminators) in upstream responses so clients that treat them as line breaks cannot mis-frame the output; lossless (cf. modelcontextprotocol/typescript-sdk#2155)\n- **Argument normalization** — rewrites a `tools/call` request whose `arguments` is `null` to `{}` so strict servers that reject the null form accept the call; on by default, opt out with `--no-normalize-arguments` (cf. modelcontextprotocol/typescript-sdk#2012)\n- **Cancellation-aware filtering** — tracks request ids cancelled via `notifications/cancelled` on stdin and drops any late upstream response carrying one of those ids before it reaches the client, per the MCP cancellation spec; on by default (60 s TTL), opt out with `--no-cancel-filter` (cf. anthropics/claude-code#51073)\n- **SSE in-flight error synthesis** — on the legacy SSE transport, replies arrive only on the long-lived GET stream; when that stream drops, requests already POSTed would otherwise hang forever. mcp-stdio tracks the ids in flight on the current stream and synthesizes a JSON-RPC `-32000` error for each on a drop — so the client can retry instead of hanging — while auto-reconnecting; cancelled ids are skipped (cf. anthropics/claude-code#60061)\n- **Session recovery** — resets MCP session ID on 404 and retries\n- **Protocol version header** — captures the negotiated `protocolVersion` from the `initialize` response and injects `MCP-Protocol-Version` on every subsequent Streamable HTTP request (MCP spec rev 2025-06-18); servers that enforce the header would otherwise reject post-initialize requests with `400 Bad Request`\n- **Token refresh on 401** — automatically refreshes expired OAuth tokens mid-session (OAuth mode only)\n- **Proactive token refresh** — a background timer refreshes the OAuth token shortly before it expires (lead time: `--oauth-refresh-leeway`), so a long-lived session survives gateways that signal token expiry as an HTTP 200 tool-error instead of a transport 401 (e.g. Atlassian's MCP gateway); on by default in OAuth mode, opt out with `--no-proactive-refresh` (#242)\n- **Step-up authorization on 403** — on a `Bearer error=\"insufficient_scope\"` challenge, re-authorizes for the union of the granted and required scopes ([RFC 9470](https://www.rfc-editor.org/rfc/rfc9470) / MCP step-up; cf. anthropics/claude-code#44652)\n- **Cold-start (`--oauth-eager`)** — answers `initialize` locally and runs the interactive OAuth flow on a background thread, so a 30–180 s browser/SSO/MFA login does not exceed the client's ~60 s initialize timeout. Gated methods return `-32002` until login completes, then `notifications/*/list_changed` tells the client to fetch the now-available lists. Streamable HTTP only; a warm (valid/refreshable) cache is unaffected (#296)\n- **Bearer token auth** — via `--bearer-token` flag or `MCP_BEARER_TOKEN` env var\n- **Custom headers** — pass any header with `-H` / `--header`\n- **Graceful shutdown** — handles SIGTERM/SIGINT\n- **Proxy support** — respects `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` env vars via [httpx](https://www.python-httpx.org/)\n- **Minimal dependencies** — only [httpx](https://www.python-httpx.org/); OAuth uses stdlib only\n\n## Install\n\n```bash\npip install mcp-stdio\n```\n\nOr with [uv](https://docs.astral.sh/uv/):\n\n```bash\nuv tool install mcp-stdio\n```\n\nOr run directly without installing:\n\n```bash\nuvx mcp-stdio https://your-server.example.com:8080/mcp\n```\n\nOr with [Homebrew](https://brew.sh/):\n\n```bash\nbrew install shigechika/tap/mcp-stdio\n```\n\n## Quick Start\n\n```bash\nmcp-stdio https://your-server.example.com:8080/mcp\n```\n\nWith Bearer token authentication:\n\n```bash\n# Recommended: use env var (token is hidden from `ps`)\nMCP_BEARER_TOKEN=YOUR_TOKEN mcp-stdio https://your-server.example.com:8080/mcp\n\n# Or pass directly (token is visible in `ps` output)\nmcp-stdio https://your-server.example.com:8080/mcp --bearer-token YOUR_TOKEN\n```\n\nWith custom headers:\n\n```bash\nmcp-stdio https://your-server.example.com:8080/mcp --header \"X-API-Key: YOUR_KEY\"\n```\n\nWith OAuth 2.1 authentication (for servers that require it):\n\n```bash\nmcp-stdio --oauth https://your-server.example.com:8080/mcp\n\n# With a pre-registered client ID (skips dynamic registration)\nmcp-stdio --oauth --client-id YOUR_CLIENT_ID https://your-server.example.com:8080/mcp\n```\n\nWith OAuth 2.1 Device Authorization Grant (RFC 8628, for headless/SSH environments):\n\n```bash\nmcp-stdio --oauth-device https://your-server.example.com:8080/mcp\n```\n\nFor legacy MCP servers using the 2024-11-05 SSE transport:\n\n```bash\nmcp-stdio --transport sse https://your-server.example.com:8080/sse\n```\n\nCheck connectivity before use:\n\n```bash\nmcp-stdio --check https://your-server.example.com:8080/mcp\n\n# For an SSE server, pass --transport sse so --check runs the legacy\n# GET/endpoint/POST handshake instead of a Streamable HTTP probe:\nmcp-stdio --check --transport sse https://your-server.example.com:8080/sse\n```\n\n## Claude Desktop Configuration\n\nAdd to `claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"my-remote-server\": {\n      \"command\": \"mcp-stdio\",\n      \"args\": [\"https://your-server.example.com:8080/mcp\"],\n      \"env\": {\n        \"MCP_BEARER_TOKEN\": \"YOUR_TOKEN\"\n      }\n    }\n  }\n}\n```\n\nConfig file locations:\n- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`\n- Windows: `%APPDATA%\\Claude\\claude_desktop_config.json`\n- Linux: `~/.config/Claude/claude_desktop_config.json`\n\n## Claude Code Configuration\n\n```bash\nclaude mcp add my-remote-server \\\n  -e MCP_BEARER_TOKEN=YOUR_TOKEN \\\n  -- mcp-stdio https://your-server.example.com:8080/mcp\n```\n\n## Usage\n\n```\nmcp-stdio [OPTIONS] URL\n\nArguments:\n  URL                    Remote MCP server URL\n\nOptions:\n  --bearer-token TOKEN   Bearer token (or set MCP_BEARER_TOKEN env var)\n  --oauth                Enable OAuth 2.1 authentication (browser flow)\n  --oauth-device         Enable OAuth 2.1 Device Authorization Grant (RFC 8628, headless)\n  --client-id ID         Pre-registered OAuth client ID (or set MCP_OAUTH_CLIENT_ID)\n  --client-metadata-url URL\n                         HTTPS URL of a Client ID Metadata Document you host\n                         (draft-ietf-oauth-client-id-metadata-document-00), used\n                         as client_id instead of Dynamic Client Registration.\n                         Ignored if --client-id is also given (#60)\n  --oauth-scope SCOPE    OAuth scope to request\n  --oauth-use-id-token   Present the OIDC id_token as the Bearer credential\n                         instead of the access_token (AWS Bedrock AgentCore /\n                         Cognito); falls back to access_token if none is returned (#59)\n  --oauth-eager          Cold-start: answer initialize locally and run the\n                         interactive OAuth flow in the background, so a long\n                         browser/SSO/MFA login does not blow the client's ~60 s\n                         initialize timeout. Streamable HTTP only; ignored on\n                         --transport sse. Warm cache unaffected (#296)\n  --oauth-refresh-leeway SECONDS\n                         Proactively refresh tokens this many seconds before\n                         expiry (default: 60, or MCP_OAUTH_REFRESH_LEEWAY)\n  --no-proactive-refresh\n                         Disable the background timer that refreshes the OAuth\n                         token before it expires. On by default in OAuth mode;\n                         keeps long sessions alive against gateways that signal\n                         expiry as an HTTP 200 tool-error rather than a 401 (#242)\n  --oauth-timeout SECONDS\n                         Seconds to wait for the interactive OAuth flow (browser\n                         callback / device-code confirmation) before giving up\n                         (default: 120; OAuth only)\n  --no-resource-indicator\n                         Omit the RFC 8707 resource parameter from all OAuth\n                         requests. Required for AS that reject it, such as\n                         Microsoft Entra ID v2 with api:// scopes (AADSTS9010010).\n                         Persisted in the token store so proactive refreshes\n                         and step-up flows stay consistent\n  --oauth-resource URI   Send this exact RFC 8707 resource value on every OAuth\n                         request instead of the server-URL-derived one. Required\n                         for AS that demand a specific resource identifier, e.g.\n                         Microsoft Entra ID's App ID URI api://<app-id>. Persisted\n                         in the token store. Mutually exclusive with\n                         --no-resource-indicator\n  -H, --header 'Key: Value'  Custom header (can be repeated)\n  --transport {streamable-http,sse}\n                         Transport type (default: streamable-http)\n  --timeout-connect SEC  Connection timeout (default: 10)\n  --timeout-read SEC     Read timeout (default: 120)\n  --sse-read-timeout SEC Idle read timeout on the SSE GET stream\n                         (default: 300; 0 disables; SSE transport only)\n  --no-tcp-keepalive     Disable TCP keepalive on the HTTP socket\n  --max-message-size BYTES\n                         Cap on a single upstream response body (JSON or\n                         cumulative SSE stream) buffered before parsing\n                         (default: 10 MiB; 0 disables the cap; #416).\n                         Sends Accept-Encoding: identity by default (#417);\n                         -H 'Accept-Encoding: gzip'/deflate opts back in\n                         with a genuinely size-bounded decoder (#418) —\n                         anything else needs --max-message-size 0.\n                         Also covers this relay's own OAuth traffic (#419)\n  --no-cancel-filter     Disable the cancel-aware response filter (drops late\n                         responses for ids cancelled via notifications/cancelled)\n  --no-normalize-arguments\n                         Disable rewriting a tools/call request's\n                         arguments:null to {} before forwarding\n  --check                Check connection and exit\n  -V, --version          Show version\n  -h, --help             Show help\n```\n\nRun `mcp-stdio --help` for the full per-flag detail (platform notes and issue references are more verbose than this table).\n\n## Reverse gateway: `serve` mode\n\nThe default mode bridges **stdio → HTTP** (client side). The `serve` subcommand\nis the mirror image — **HTTP → stdio** — exposing a local stdio MCP server as a\nStreamable HTTP MCP endpoint so clients that cannot spawn it locally can reach\nit over the network:\n\n```mermaid\nflowchart BT\n    A[\"MCP client<br>Claude Code / Desktop<br>(or mcp-stdio --oauth)\"]\n    B(\"mcp-stdio serve<br><b>HTTP → stdio</b> gateway<br>auth: none / static token /<br>embedded OAuth 2.1 AS\")\n    C[\"local stdio<br>MCP server\"]\n    A <== \"Streamable HTTP<br>Bearer / OAuth 2.1 (PKCE)\" ==> B\n    B <-- \"stdio (spawned child)\" --> C\n```\n\nThis is the mirror of the client-side diagram at the top: there mcp-stdio is\n**stdio → HTTP**; here it is **HTTP → stdio**.\n\n```bash\nmcp-stdio serve --port 8080 -- python -m my_mcp_server\n```\n\nThen point any MCP client (including mcp-stdio itself) at it:\n\n```bash\nmcp-stdio --check http://127.0.0.1:8080/mcp\n```\n\n- Stdlib only (`http.server`) — adds no runtime dependency.\n- Implements the Streamable HTTP request/response and notification semantics,\n  session management, plus a GET SSE channel for server-initiated messages.\n- **Authentication is optional and layered:**\n  - *No token* — the endpoint is open (run it behind a TLS-terminating proxy).\n  - *Static token* (`--auth-token` / `MCP_STDIO_SERVE_TOKEN`) — acts as an OAuth\n    Resource Server: MCP requests require `Authorization: Bearer <token>`, and a\n    401 advertises [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) Protected\n    Resource Metadata at `/.well-known/oauth-protected-resource`.\n  - *Embedded OAuth AS* (`--enable-oauth`) — a minimal OAuth 2.1 Authorization\n    Server (PKCE auth-code, [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)\n    dynamic client registration with the `invalid_redirect_uri` error per §3.2.2,\n    refresh, opaque in-memory tokens, stdlib only). An https issuer echoes the\n    [RFC 9207](https://www.rfc-editor.org/rfc/rfc9207) `iss` parameter on the\n    authorization response (mix-up defence) and advertises it in metadata.\n    The mcp-stdio client's `--oauth` flow then works against the gateway.\n- Embedded-AS token security: the Resource Server validates token audience\n  ([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707) / MCP — a token issued for\n  another resource is rejected); a presented-but-invalid token gets\n  `error=\"invalid_token\"` ([RFC 6750](https://www.rfc-editor.org/rfc/rfc6750)\n  §3.1); and replaying an authorization code or a rotated refresh token revokes\n  the whole grant family ([RFC 6749](https://www.rfc-editor.org/rfc/rfc6749)\n  §4.1.2 / [RFC 9700](https://www.rfc-editor.org/rfc/rfc9700) §4.14.2), with a\n  brief grace window so a benign client retry is not punished.\n- **Multi-client isolation by session** — each MCP session gets its own spawned\n  backend child, so concurrent clients are isolated by process boundary (a\n  JSON-RPC id collision across clients can never cross responses). Per the MCP\n  Streamable HTTP spec, `initialize` mints an `Mcp-Session-Id`, every later\n  request carries it, an unknown/terminated id gets `404` (the client then\n  re-initializes), and a `DELETE` tears that session's child down. A\n  concurrent-session cap guards an open gateway against unbounded child spawns.\n  When OAuth is enabled each session is **bound to the authenticated user** — a\n  session id presented with a different user's token is rejected (`404`), so a\n  leaked id cannot cross tenants.\n\nStatic-token example (token via env so it is not visible in `ps`):\n\n```bash\nMCP_STDIO_SERVE_TOKEN=your-secret mcp-stdio serve --port 8080 -- python -m my_mcp_server\nmcp-stdio --bearer-token your-secret --check http://127.0.0.1:8080/mcp\n```\n\nEmbedded-OAuth example. User authentication is **delegated to a fronting\nreverse proxy** that asserts the logged-in user via a header\n(`--trusted-user-header`, only trusted behind a proxy that strips client copies).\n`--dev-user` is an **insecure** loopback-only shortcut for local testing:\n\n```bash\nmcp-stdio serve --enable-oauth --public-url http://127.0.0.1:8080 \\\n  --dev-user alice --port 8080 -- python -m my_mcp_server\nmcp-stdio --oauth http://127.0.0.1:8080/mcp\n```\n\nOptions: `--host` (default `127.0.0.1`), `--port` (default `8080`), `--path`\n(default `/mcp`), `--auth-token TOKEN` (or `MCP_STDIO_SERVE_TOKEN`, preferred);\nsession limits `--max-sessions N` (default `100`; an `initialize` past the cap\ngets `503`), `--session-idle-ttl SECONDS` (evict a session and its child\nafter this much inactivity so a client that disconnects without `DELETE` does\nnot pin a slot; `0` = disabled, the default), and `--max-sessions-per-owner N`\n(on a new `initialize`, LRU-evict that OAuth user's older sessions down to `N`,\nreclaiming ghosts left by a client that reconnects without `DELETE`; `0` =\ndisabled, the default; static-token and open-gateway sessions are exempt);\n`--max-message-size BYTES` (reject a request whose declared `Content-Length`\nexceeds this with `413`, before reading any of the body; default 10 MiB,\n`0` disables the cap; #416); `--user-env VAR` (inject the authenticated principal into each spawned\nchild's environment under this name, so a multi-user-aware backend can read\nits caller's identity without its own OAuth stack — requires\n`--enable-oauth`; the open-gateway and shared static-token principals are\nexempt, same as `--max-sessions-per-owner`; refuses a search-path /\ndynamic-linker variable name such as `PATH`, `LD_PRELOAD`, or `PYTHONPATH`\nthat the child's own runtime needs to start); and for the embedded AS:\n`--enable-oauth`, `--public-url URL` (pins the issuer; recommended behind a\nproxy), `--trusted-user-header HEADER`, `--dev-user USER` (insecure, testing\nonly), `--access-token-ttl SECONDS`, `--allow-redirect-uri URL` (repeatable;\nsee below), `--token-store PATH` or `--token-store-firestore COLLECTION/DOCUMENT`\n(mutually exclusive, see below). Without either, tokens are in-memory only\nand a restart invalidates them (the client re-runs `--oauth`). The backend\ncommand follows the options (an optional `--` separator is supported).\n\n  - *Non-loopback remote clients* — DCR only accepts an RFC 8252 loopback\n    `http://` `redirect_uri` by default, which a browser-based remote MCP\n    client (a web app with a fixed HTTPS OAuth callback, not a locally-run\n    CLI/native app) cannot satisfy. `--allow-redirect-uri URL` (repeatable)\n    trusts one additional `redirect_uri` **byte-for-byte** — no host, prefix,\n    or port matching — so add only a URL you have verified belongs to a\n    client you actually trust; each entry is exactly as trusted as a\n    hardcoded redirect target. It is independent of the loopback path (adding\n    one never widens the other) and requires `--enable-oauth`.\n  - *Restart-durable tokens* — `--token-store PATH` persists the issued\n    tokens, rotation tombstones, and client registrations to a JSON file\n    (created `0600`, written atomically on every state change), so a client\n    that held a valid token before a restart keeps calling tools without a\n    new interactive authorization, and a refresh presented after the restart\n    is honored. This keeps deploys transparent for remote clients that do not\n    re-authorize on `401`/`invalid_grant` (they would otherwise replay the\n    dead token indefinitely and appear connected while their tools silently\n    vanish). Refresh-token reuse detection and grant-family revocation\n    survive the restart too — the consumption ledger is part of the persisted\n    state. The file is credential material: guard it like a private key, and\n    give each `serve` process its own path — a sidecar `.lock` file refuses a\n    second process at startup (sharing one store would silently clobber\n    issued tokens), and the path is probe-written at launch so a\n    misconfigured target fails the start instead of silently disabling\n    persistence. Requires `--enable-oauth` (#277).\n  - *Restart-durable tokens, no local disk* — `--token-store-firestore\n    COLLECTION/DOCUMENT` persists the same state as `--token-store` (same\n    JSON-shaped snapshot, same probe-write-at-launch fail-fast behavior) to\n    one [Firestore](https://cloud.google.com/firestore) document instead of\n    a local file, for a deployment with no durable local disk (Cloud Run and\n    similar). The GCP project is resolved the standard google-cloud way\n    (`GOOGLE_CLOUD_PROJECT` env var, or ADC on Cloud Run) — there is no\n    separate `--project` flag. Requires the `google-cloud-firestore` package\n    (`pip install mcp-stdio[firestore]`, an optional extra — plain\n    `pip install mcp-stdio` never needs it) and `--enable-oauth`. **Unlike\n    `--token-store` there is no lock against two processes sharing one\n    document**, but each write goes through a read-merge-write transaction\n    rather than a blind overwrite (#406), so a brief overlap between two\n    writers (e.g. a Cloud Run revision cutover) does not silently discard\n    tokens either side issued or rotated during the overlap. One residual\n    gap remains: a token revoked (replay detected) or evicted for capacity\n    during that same overlap window can still be resurrected by a\n    concurrently-stale writer, since those removals carry no tombstone\n    (#428). Mutually exclusive with `--token-store`.\n  - *Path-scoped issuer* — `--public-url` retains a path, so several\n    `--enable-oauth` backends can share one host behind a reverse proxy, each\n    under its own prefix (e.g. `--public-url https://gw.example.org/team-a`\n    serving `https://gw.example.org/team-a/mcp`). The issuer becomes\n    `https://gw.example.org/team-a`, its AS endpoints live under the prefix\n    (`/team-a/authorize`, `/token`, `/register`), and the well-known documents\n    sit at the [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) §3.1 /\n    [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) §3.1 root-inserted\n    locations (`/.well-known/oauth-authorization-server/team-a`,\n    `/.well-known/oauth-protected-resource/team-a/mcp`) — byte-symmetric with\n    the client's path-aware discovery. A bare-origin `--public-url` behaves\n    exactly as before (#245).\n\n### Multi-user deployment\n\n`serve` is built for multiple concurrent users. Each MCP session gets its own\nspawned backend child and — with OAuth enabled — is bound to the authenticated\nuser, so users are isolated by **process boundary** and a leaked session id\ncannot cross tenants.\n\nEnd-user login is delegated to a fronting reverse proxy that performs the real\nSSO and asserts the user via `--trusted-user-header` (trusted ONLY because the\nproxy strips any client-supplied copy). The embedded AS then mints per-user\ntokens, and the gateway binds each session to that user.\n\n```mermaid\nflowchart TD\n    UA[\"User A<br>mcp-stdio --oauth\"]\n    UB[\"User B<br>mcp-stdio --oauth\"]\n    RP[\"Reverse proxy<br>SSO login, sets X-Forwarded-User<br>strips any client-supplied copy\"]\n    GW[\"mcp-stdio serve --enable-oauth<br>--trusted-user-header X-Forwarded-User\"]\n    CA[\"stdio child<br>session of A\"]\n    CB[\"stdio child<br>session of B\"]\n    UA == \"Streamable HTTP<br>OAuth 2.1 (PKCE)\" ==> RP\n    UB == \"Streamable HTTP<br>OAuth 2.1 (PKCE)\" ==> RP\n    RP ==> GW\n    GW -- \"spawn per session\" --> CA\n    GW -- \"spawn per session\" --> CB\n```\n\nGateway (bound to loopback, behind the proxy):\n\n```bash\nmcp-stdio serve --enable-oauth \\\n  --public-url https://mcp.example.org \\\n  --trusted-user-header X-Forwarded-User \\\n  --max-sessions 200 --session-idle-ttl 900 --max-sessions-per-owner 1 \\\n  --host 127.0.0.1 --port 8080 -- python -m my_mcp_server\n```\n\n- `--public-url` pins the issuer to the external HTTPS URL the proxy serves.\n- `--trusted-user-header` is the header the proxy sets after login; the gateway\n  trusts it only because the proxy strips any client-supplied copy.\n- `--max-sessions` caps concurrent per-user children; `--session-idle-ttl`\n  reclaims a child after a user disconnects without sending `DELETE`.\n- `--max-sessions-per-owner` reclaims a user's *previous* sessions the moment\n  they re-initialize, so a client that reconnects without `DELETE` (common for\n  hosted connectors) no longer leaves ghosts. Because ghosts are reclaimed at\n  reconnect rather than only by the idle reaper, a longer `--session-idle-ttl`\n  no longer risks ghosts filling `--max-sessions` — which lets an interactive\n  client with pauses avoid frequent re-initialization.\n\nEach user points their client at the gateway, runs the OAuth flow once, and is\nserved by a dedicated child:\n\n```bash\nmcp-stdio --oauth https://mcp.example.org/mcp\n```\n\nNotes:\n\n- **Isolation is by process boundary** — user A and user B never share a child,\n  so per-connection backend state (or a JSON-RPC id collision) cannot leak\n  across them.\n- The backend command is a **template**: every session spawns the same command\n  as a fresh child. Identity is enforced at the gateway (session→user binding);\n  it is not injected into the child today, so a backend that needs per-user\n  context should derive it from the request, or run one gateway per backend\n  configuration (optionally multiplexed by path — see *Path-scoped issuer*).\n\n## Workarounds\n\nSee [WORKAROUNDS.md](WORKAROUNDS.md) for known issues in Claude Code, Claude Desktop, mcp-remote, the MCP SDKs, and Windows that mcp-stdio addresses.\n\n## How It Works\n\n1. If `--oauth` (browser) or `--oauth-device` (headless, RFC 8628) is set, obtains an access token (cached → refresh → browser/device flow)\n2. Reads JSON-RPC messages from stdin (sent by Claude Desktop/Code)\n3. Relays them over HTTPS to the remote MCP server\n4. Parses responses and writes them to stdout\n5. On 401 (OAuth mode only), refreshes the access token and retries; with static `--bearer-token` / `-H` auth the 401 is surfaced to the client\n6. In OAuth mode a background timer also refreshes the token shortly before it expires (`--oauth-refresh-leeway`), independent of request flow — this keeps long sessions alive against gateways that report token expiry as an HTTP 200 tool-error rather than a 401 (opt out with `--no-proactive-refresh`)\n\nTransport details:\n\n- **Streamable HTTP** (default) — each stdin message is a single POST; session state is tracked via the `Mcp-Session-Id` header and re-initialized automatically on 404. The negotiated `MCP-Protocol-Version` header is sent on every post-initialize request (spec rev 2025-06-18).\n- **SSE** (MCP 2024-11-05 legacy) — a persistent `GET` stream delivers responses and the initial `endpoint` event containing the POST URL; the stream auto-reconnects on disconnect.\n\nOAuth tokens are stored in `~/.config/mcp-stdio/tokens.json` (permissions 0600).\n\n## License\n\nMIT\n",
  "bytes": 32943,
  "sha": "a8f337a8dd5453494345d5439d27c09259d1698d1c75b2f975a3c2c0e1620b3a",
  "repo_slug": "shigechika/mcp-stdio",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_shigechika_mcp_stdio_5bd82ab9/readme"
}