The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the MCP Stdio listing page.
English | 日本語
A stdio↔HTTP MCP gateway that works in both directions — the same
binary is both an MCP-over-HTTP client gateway and, via serve mode, a
full MCP-over-HTTP server (with an optional embedded OAuth 2.1
Authorization Server), so it can sit on either end of an MCP connection.
📖 New here? Start with the user guide — task-oriented docs for connecting a client or publishing a server. This README is the full reference.
Client gateway (default mode) — stdio → HTTP. MCP 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:
Bearer tokens, custom headers, and OAuth 2.1 credentials are forwarded to the remote server.
Reverse gateway (mcp-stdio serve) — HTTP → stdio. The mirror image: takes a
local stdio MCP server (any language, any framework) and exposes it as a
Streamable HTTP endpoint, with optional bearer-token or embedded-OAuth-2.1
authentication, per-user backend isolation when OAuth is enabled, and a
restart-durable token store —
see Reverse gateway: serve mode below. This
is how mcp-stdio can stand in for a framework's own HTTP/OAuth hosting layer
(e.g. instead of depending on a Python web framework's built-in server for
just that part) when the tool definitions themselves already run fine over
stdio.
--transport. SSE parser follows the WHATWG Server-Sent Events spec./.well-known/oauth-protected-resourceresource field validation — warn on mismatch, continueWWW-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 guessingissuer validation — reject a cross-origin issuer (AS mix-up guard), warn on a same-origin mismatch (trailing slash / path / case) and continue/.well-known/openid-configuration (path-append and path-insertion) for ASes that expose only the OIDC form (Auth0, Okta, Azure AD, Google)resource parameter in authorization, token exchange, and refresh requestscode_challenge_method with an 86-char code_verifierresource indicator (RFC 8707)authorization_pending / slow_down (interval +=5 s) / expired_token / access_denied handlingurn:ietf:params:oauth:grant-type:device_code in grant_types (RFC 7591 §2)token_endpoint_auth_method chosen from token_endpoint_auth_methods_supported in AS metadata (prefers none → client_secret_post → client_secret_basic)client_secret_expires_at handling — auto re-register on expiryapplication_type: "native" in DCR (RFC 8252 §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"--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)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 §7.3 / §8.4)client_secret_basic: Authorization: Basic header with percent-encoded credentials (applied to code exchange, token refresh, and Device Authorization Grant polling)Authorization: Bearer <token> request header--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 serversRetry-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)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)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)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)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)-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)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--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)Bearer error="insufficient_scope" challenge, re-authorizes for the union of the granted and required scopes (RFC 9470 / MCP step-up; cf. anthropics/claude-code#44652)--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)--bearer-token flag or MCP_BEARER_TOKEN env var-H / --headerHTTP_PROXY, HTTPS_PROXY, NO_PROXY env vars via httpxOr with uv:
Or run directly without installing:
Or with Homebrew:
With Bearer token authentication:
With custom headers:
With OAuth 2.1 authentication (for servers that require it):
With OAuth 2.1 Device Authorization Grant (RFC 8628, for headless/SSH environments):
For legacy MCP servers using the 2024-11-05 SSE transport:
Check connectivity before use:
Add to claude_desktop_config.json:
Config file locations:
~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.json~/.config/Claude/claude_desktop_config.jsonRun mcp-stdio --help for the full per-flag detail (platform notes and issue references are more verbose than this table).
serve modeThe default mode bridges stdio → HTTP (client side). The serve subcommand
is the mirror image — HTTP → stdio — exposing a local stdio MCP server as a
Streamable HTTP MCP endpoint so clients that cannot spawn it locally can reach
it over the network:
This is the mirror of the client-side diagram at the top: there mcp-stdio is stdio → HTTP; here it is HTTP → stdio.
Then point any MCP client (including mcp-stdio itself) at it:
http.server) — adds no runtime dependency.--auth-token / MCP_STDIO_SERVE_TOKEN) — acts as an OAuth
Resource Server: MCP requests require Authorization: Bearer <token>, and a
401 advertises RFC 9728 Protected
Resource Metadata at /.well-known/oauth-protected-resource.--enable-oauth) — a minimal OAuth 2.1 Authorization
Server (PKCE auth-code, RFC 7591
dynamic client registration with the invalid_redirect_uri error per §3.2.2,
refresh, opaque in-memory tokens, stdlib only). An https issuer echoes the
RFC 9207 iss parameter on the
authorization response (mix-up defence) and advertises it in metadata.
The mcp-stdio client's --oauth flow then works against the gateway.error="invalid_token" (RFC 6750
§3.1); and replaying an authorization code or a rotated refresh token revokes
the whole grant family (RFC 6749
§4.1.2 / RFC 9700 §4.14.2), with a
brief grace window so a benign client retry is not punished.initialize mints an Mcp-Session-Id, every later
request carries it, an unknown/terminated id gets 404 (the client then
re-initializes), and a DELETE tears that session's child down. A
concurrent-session cap guards an open gateway against unbounded child spawns.
When OAuth is enabled each session is bound to the authenticated user — a
session id presented with a different user's token is rejected (404), so a
leaked id cannot cross tenants.Static-token example (token via env so it is not visible in ps):
Embedded-OAuth example. User authentication is delegated to a fronting
reverse proxy that asserts the logged-in user via a header
(--trusted-user-header, only trusted behind a proxy that strips client copies).
--dev-user is an insecure loopback-only shortcut for local testing:
Options: --host (default 127.0.0.1), --port (default 8080), --path
(default /mcp), --auth-token TOKEN (or MCP_STDIO_SERVE_TOKEN, preferred);
session limits --max-sessions N (default 100; an initialize past the cap
gets 503), --session-idle-ttl SECONDS (evict a session and its child
after this much inactivity so a client that disconnects without DELETE does
not pin a slot; 0 = disabled, the default), and --max-sessions-per-owner N
(on a new initialize, LRU-evict that OAuth user's older sessions down to N,
reclaiming ghosts left by a client that reconnects without DELETE; 0 =
disabled, the default; static-token and open-gateway sessions are exempt);
--max-message-size BYTES (reject a request whose declared Content-Length
exceeds this with 413, before reading any of the body; default 10 MiB,
0 disables the cap; #416); --user-env VAR (inject the authenticated principal into each spawned
child's environment under this name, so a multi-user-aware backend can read
its caller's identity without its own OAuth stack — requires
--enable-oauth; the open-gateway and shared static-token principals are
exempt, same as --max-sessions-per-owner; refuses a search-path /
dynamic-linker variable name such as PATH, LD_PRELOAD, or PYTHONPATH
that the child's own runtime needs to start); and for the embedded AS:
--enable-oauth, --public-url URL (pins the issuer; recommended behind a
proxy), --trusted-user-header HEADER, --dev-user USER (insecure, testing
only), --access-token-ttl SECONDS, --allow-redirect-uri URL (repeatable;
see below), --token-store PATH or --token-store-firestore COLLECTION/DOCUMENT
(mutually exclusive, see below). Without either, tokens are in-memory only
and a restart invalidates them (the client re-runs --oauth). The backend
command follows the options (an optional -- separator is supported).
http:// redirect_uri by default, which a browser-based remote MCP
client (a web app with a fixed HTTPS OAuth callback, not a locally-run
CLI/native app) cannot satisfy. --allow-redirect-uri URL (repeatable)
trusts one additional redirect_uri byte-for-byte — no host, prefix,
or port matching — so add only a URL you have verified belongs to a
client you actually trust; each entry is exactly as trusted as a
hardcoded redirect target. It is independent of the loopback path (adding
one never widens the other) and requires --enable-oauth.--token-store PATH persists the issued
tokens, rotation tombstones, and client registrations to a JSON file
(created 0600, written atomically on every state change), so a client
that held a valid token before a restart keeps calling tools without a
new interactive authorization, and a refresh presented after the restart
is honored. This keeps deploys transparent for remote clients that do not
re-authorize on 401/invalid_grant (they would otherwise replay the
dead token indefinitely and appear connected while their tools silently
vanish). Refresh-token reuse detection and grant-family revocation
survive the restart too — the consumption ledger is part of the persisted
state. The file is credential material: guard it like a private key, and
give each serve process its own path — a sidecar .lock file refuses a
second process at startup (sharing one store would silently clobber
issued tokens), and the path is probe-written at launch so a
misconfigured target fails the start instead of silently disabling
persistence. Requires --enable-oauth (#277).--token-store-firestore COLLECTION/DOCUMENT persists the same state as --token-store (same
JSON-shaped snapshot, same probe-write-at-launch fail-fast behavior) to
one Firestore document instead of
a local file, for a deployment with no durable local disk (Cloud Run and
similar). The GCP project is resolved the standard google-cloud way
(GOOGLE_CLOUD_PROJECT env var, or ADC on Cloud Run) — there is no
separate --project flag. Requires the google-cloud-firestore package
(pip install mcp-stdio[firestore], an optional extra — plain
pip install mcp-stdio never needs it) and --enable-oauth. Unlike
--token-store there is no lock against two processes sharing one
document, but each write goes through a read-merge-write transaction
rather than a blind overwrite (#406), so a brief overlap between two
writers (e.g. a Cloud Run revision cutover) does not silently discard
tokens either side issued or rotated during the overlap. One residual
gap remains: a token revoked (replay detected) or evicted for capacity
during that same overlap window can still be resurrected by a
concurrently-stale writer, since those removals carry no tombstone
(#428). Mutually exclusive with --token-store.--public-url retains a path, so several
--enable-oauth backends can share one host behind a reverse proxy, each
under its own prefix (e.g. --public-url https://gw.example.org/team-a
serving https://gw.example.org/team-a/mcp). The issuer becomes
https://gw.example.org/team-a, its AS endpoints live under the prefix
(/team-a/authorize, /token, /register), and the well-known documents
sit at the RFC 8414 §3.1 /
RFC 9728 §3.1 root-inserted
locations (/.well-known/oauth-authorization-server/team-a,
/.well-known/oauth-protected-resource/team-a/mcp) — byte-symmetric with
the client's path-aware discovery. A bare-origin --public-url behaves
exactly as before (#245).serve is built for multiple concurrent users. Each MCP session gets its own
spawned backend child and — with OAuth enabled — is bound to the authenticated
user, so users are isolated by process boundary and a leaked session id
cannot cross tenants.
End-user login is delegated to a fronting reverse proxy that performs the real
SSO and asserts the user via --trusted-user-header (trusted ONLY because the
proxy strips any client-supplied copy). The embedded AS then mints per-user
tokens, and the gateway binds each session to that user.
Gateway (bound to loopback, behind the proxy):
--public-url pins the issuer to the external HTTPS URL the proxy serves.--trusted-user-header is the header the proxy sets after login; the gateway
trusts it only because the proxy strips any client-supplied copy.--max-sessions caps concurrent per-user children; --session-idle-ttl
reclaims a child after a user disconnects without sending DELETE.--max-sessions-per-owner reclaims a user's previous sessions the moment
they re-initialize, so a client that reconnects without DELETE (common for
hosted connectors) no longer leaves ghosts. Because ghosts are reclaimed at
reconnect rather than only by the idle reaper, a longer --session-idle-ttl
no longer risks ghosts filling --max-sessions — which lets an interactive
client with pauses avoid frequent re-initialization.Each user points their client at the gateway, runs the OAuth flow once, and is served by a dedicated child:
Notes:
See WORKAROUNDS.md for known issues in Claude Code, Claude Desktop, mcp-remote, the MCP SDKs, and Windows that mcp-stdio addresses.
--oauth (browser) or --oauth-device (headless, RFC 8628) is set, obtains an access token (cached → refresh → browser/device flow)--bearer-token / -H auth the 401 is surfaced to the client--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)Transport details:
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).GET stream delivers responses and the initial endpoint event containing the POST URL; the stream auto-reconnects on disconnect.OAuth tokens are stored in ~/.config/mcp-stdio/tokens.json (permissions 0600).
MIT