Skip to content

Publish your stdio server

Goal: a stdio MCP server on your host becomes an HTTPS Streamable HTTP endpoint that remote MCP clients — Claude Desktop, Claude Code, even Claude.ai custom connectors — can use, each user isolated in their own child process behind a real OAuth 2.1 login.

1. Smoke test on loopback

mcp-stdio serve --enable-oauth --public-url http://127.0.0.1:8080 \
  --dev-user alice --port 8080 -- python -m my_mcp_server

and from a second terminal, connect to it with mcp-stdio's own client mode:

mcp-stdio --check --oauth http://127.0.0.1:8080/mcp

--dev-user is an insecure stand-in identity for loopback testing only — it lets you exercise the whole OAuth flow without a real login. If --check reports a successful initialize, the gateway works; everything after this is deployment.

2. Deploy behind your reverse proxy

In production the identity comes from your SSO. Terminate TLS and authentication at a reverse proxy (nginx, Apache, …) and pass the logged-in user to mcp-stdio in a trusted header:

mcp-stdio serve --enable-oauth \
  --public-url https://mcp.example.com \
  --trusted-user-header X-Forwarded-User \
  --token-store /var/lib/mcp-stdio/state.json \
  --session-idle-ttl 180 \
  -- python -m my_mcp_server
  • --public-url pins the OAuth issuer to your public address (required behind a proxy).
  • --trusted-user-header names the header your proxy sets and strips from client requests — that stripping is what makes it trustworthy.
  • --token-store persists issued tokens, registrations, and replay tombstones (0600) so a restart or redeploy does not log everyone out. Without it, every deploy invalidates all tokens. One path per serve process — a sidecar .lock refuses accidental sharing.
  • --session-idle-ttl reaps children whose client vanished without a clean disconnect.

What you get per connecting user: their own child process (spawned at initialize, destroyed on disconnect or idle timeout), their session bound to their authenticated identity — another user's session id is a 404, not a crossover.

3. Optional add-ons

You need… Do this
Claude.ai custom connectors (browser-based clients) --allow-redirect-uri https://claude.ai/api/mcp/auth_callback
Several backends on one host run one serve process per backend with path-scoped issuers: --public-url https://mcp.example.com/team-a, …/team-b — AS endpoints and well-known documents nest under each prefix
Cap concurrent users --max-sessions N (default 100; excess initialize gets 503)
Static token instead of OAuth drop --enable-oauth, set MCP_STDIO_SERVE_TOKEN
Longer / shorter access tokens --access-token-ttl SECONDS (default 3600)

Notes for operators

  • The embedded authorization server implements Dynamic Client Registration, PKCE (S256 only), refresh rotation with replay detection, and RFC 8707 audience binding — MCP clients discover all of it via the standard well-known documents; there is nothing to configure client-side.
  • Treat --token-store's file like a private key. It is created 0600 in a 0700 directory.
  • Every request is logged to stderr with query strings redacted.

Real-world reference: this is the exact setup used to publish several stdio MCP servers under one host, surviving routine redeploys without users noticing. If your backend needs per-user credentials inside the child process, that is a design decision of your server — the gateway intentionally does not forward identity into the child.