Skip to content

Two ways to use it

mcp-stdio is one command with two distinct roles. Everything else in these docs hangs off this choice, so take a moment here.

As a client-side gateway (default) As a server gateway (serve)
You are… using someone's remote MCP server publishing your own MCP server
Your MCP server runs… somewhere else, behind HTTPS on your machine, speaking stdio
mcp-stdio runs… next to your MCP client (laptop) next to your MCP server (host)
It translates… stdio → Streamable HTTP / SSE Streamable HTTP → stdio
OAuth role client: logs in, stores and refreshes your tokens authorization server: registers clients, issues and validates tokens
Typical user anyone using Claude Desktop / Claude Code with a remote server the operator of a stdio MCP server that remote users should reach
Start here Connect to a remote MCP server Publish your stdio server

As a client-side gateway (default mode)

Your MCP client (Claude Desktop, Claude Code, …) only launches local stdio processes, but the server you want lives on the network. mcp-stdio is that local process: your client talks stdio to it, and it relays every message to the remote server over HTTPS — handling the OAuth login, token cache, and refresh so the connection survives longer than an access token does.

graph TD
    A["MCP client<br/>(Claude Desktop / Claude Code)"]
    B["mcp-stdio<br/>OAuth client · token cache · transport translation"]
    C["Remote MCP server<br/>(Streamable HTTP or legacy SSE)"]
    A -- "stdio (JSON-RPC lines)" --> B
    B -- "HTTPS (+ Bearer token)" --> C
    C --> B
    B --> A
mcp-stdio --oauth https://mcp.example.com/mcp

You want this mode when:

  • a vendor / your team hosts an MCP server and you want it in Claude Desktop or Claude Code;
  • the server needs an OAuth login your client cannot complete on its own;
  • the server still speaks the legacy SSE transport your client dropped.

→ Continue with Connect to a remote MCP server.

As a server gateway (serve mode)

You wrote (or run) an MCP server that speaks stdio on your machine, and remote users should reach it from their MCP clients. mcp-stdio serve turns it into a proper Streamable HTTP endpoint: it accepts HTTPS on one side, spawns an isolated stdio child process per user session on the other, and — with --enable-oauth — acts as the OAuth 2.1 authorization server that registers clients and issues the tokens guarding it all.

graph TD
    A1["Remote user A<br/>(Claude Desktop)"]
    A2["Remote user B<br/>(Claude.ai connector)"]
    B["mcp-stdio serve<br/>OAuth authorization server · sessions"]
    C1["stdio child for A<br/>(your MCP server)"]
    C2["stdio child for B<br/>(your MCP server)"]
    A1 -- "HTTPS + OAuth" --> B
    A2 -- "HTTPS + OAuth" --> B
    B -- "stdio" --> C1
    B -- "stdio" --> C2
mcp-stdio serve --enable-oauth \
  --public-url https://mcp.example.com \
  --token-store /var/lib/mcp-stdio/state.json \
  -- python -m my_mcp_server

You want this mode when:

  • your MCP server is stdio-only and remote clients need to reach it;
  • several users must share one deployment without sharing a process — each session gets its own child, bound to its authenticated user;
  • you need real OAuth in front of it but do not want to run Keycloak for a single endpoint.

→ Continue with Publish your stdio server.

Working with MCP 2026-07-28 servers

Usually you do not have to do anything. mcp-stdio keeps talking to servers the way it always has, so upgrading changes nothing.

If you connect to a server built for the newer MCP spec (2026-07-28), add one flag and mcp-stdio figures the rest out:

mcp-stdio --protocol-era auto https://mcp.example.com/mcp

Your MCP client — Claude Desktop, Claude Code, anything else — needs no changes at all. It keeps speaking the dialect it already knows, and mcp-stdio translates.

Which one am I getting?

mcp-stdio prints the answer to stderr as it starts:

[mcp-stdio] protocol era: modern (auto-detected)
What you see What it means
protocol era: modern (auto-detected) the server is a new one, and mcp-stdio is using the new protocol
protocol era: legacy (auto-detected) the server is an older one — nothing changes
nothing printed you did not pass --protocol-era, so the old protocol is in use

The flag

Value When to use it
legacy (default) you don't think about it; behaves exactly as before
auto you don't know what the server is — mcp-stdio asks it once at startup and picks
modern you know the server is a new one and want to skip the extra question

auto costs one extra request when mcp-stdio starts. That is the only reason it is not the default.

Not for the old SSE transport

--protocol-era only applies to the default transport. With --transport sse it is ignored, and mcp-stdio tells you so:

warning: --protocol-era auto is ignored on --transport sse
(always the pre-Streamable-HTTP legacy transport)

What mcp-stdio does for you against a new server

You should not notice any of this — that is the point — but if you are wondering what changed under the hood:

  • Tools, resources and prompts work as usual. The newer spec reorganised how a client and server introduce themselves, and mcp-stdio handles both sides of that.
  • Notifications still arrive. Newer servers deliver them over a long-lived connection that mcp-stdio holds open for you, including updates for resources you subscribed to.
  • Prompts back to you still work. When a server needs to ask you something mid-call — a confirmation, a piece of text, permission to sample — mcp-stdio turns that into the ordinary request your client already knows how to show you, then continues the original call with your answer.
  • Cancelling actually stops the work. Pressing escape now aborts the request on the server instead of just hiding the reply. A few cases still cannot be cut short mid-flight — a cancel that arrives in the same instant as the request, a server that does all its work before it starts replying, and long paginated lists — and in those the reply is still discarded, so you never see a result you cancelled.

Publishing your own server with serve

mcp-stdio serve handles both kinds of client on the same address, and your stdio server does not need to know which is which:

mcp-stdio serve -- python -m my_mcp_server
  • A newer client connects without a handshake and without a session, and mcp-stdio answers on your server's behalf — including how long results may be cached (tune with --cache-ttl-ms).
  • An older client works exactly as it always has, with its own isolated child process per session — unless you pass --modern-only, which turns older clients away instead.

For newer clients, mcp-stdio starts one copy of your server per authenticated user (or a single shared one if you run without authentication), because those clients have no session to tie a process to.

Telling clients your lists changed

If your server announces that its tools, prompts or resources changed, newer clients now hear about it. They open one long-lived connection and mcp-stdio pushes your server's announcements down it as they happen — your stdio server keeps sending the same notifications it always did, and nothing about it changes.

Four announcements travel this way:

What your server sends What the client does
notifications/tools/list_changed re-fetches your tool list
notifications/prompts/list_changed re-fetches your prompt list
notifications/resources/list_changed re-fetches your resource list
notifications/resources/updated re-reads that one resource

A client asks for the ones it cares about and gets only those. It may keep up to four such connections open at once; a fifth is refused rather than queued.

If mcp-stdio is shut down, the client is told the stream is over and does not try to reconnect. If your server process dies instead, the stream just drops — which tells the client to reconnect and re-fetch, because mcp-stdio itself is still there.

Watching individual resources

The last row works a little differently, because the client names which resources it wants to watch rather than just switching a notification on.

Your server needs to declare resources.subscribe. If it does, mcp-stdio subscribes on the client's behalf — it sends your server the ordinary resources/subscribe it already understands, and forwards the resulting notifications/resources/updated to whichever clients asked for that URI. If your server does not declare it, mcp-stdio says so in its reply and never sends a subscription your server cannot serve.

Worth knowing:

  • Your server is told once per resource, however many clients are watching it. mcp-stdio counts the watchers and unsubscribes only when the last one goes away.
  • URIs are matched exactly. file:///a and file:///a/ are two different subscriptions, because there is no way to ask your server which spelling it means.
  • Up to 256 resources per connection. Ask for more and mcp-stdio keeps the first 256 and tells the client exactly which ones it got.
  • The reply does not wait for your server. mcp-stdio answers the client immediately and subscribes in the background, so a slow server cannot stall the connection. The trade-off is that a subscription your server rejects is logged rather than reported back — the client is told it is watching, and simply never sees an update.

Not supported: log messages

mcp-stdio never sends notifications/message on these connections. The spec forbids it there, and the logging feature is deprecated as of MCP 2026-07-28, so this is a permanent decision rather than a gap.

Answering your server's mid-call questions

If your server is still on an older protocol version but wants to ask something during a call it is handling — elicit input, request a sampling completion, or list the client's roots — a newer client can answer it, over MRTR — the reverse direction of the same multi round-trip pattern that MCP_STDIO_MRTR_STRIP lets you escape on the client side.

Off by default, behind MCP_STDIO_MRTR_REVERSE_ENABLE (reference). With it unset, mcp-stdio tells your server up front that it cannot ask, and a well-behaved server never does. Turning it on is a real, observable handshake change on every child process mcp-stdio spawns. The flag is read fresh on every request — nothing caches it — but it is an ordinary environment variable, so a change only reaches an already-running gateway through a restart. Unsetting it and restarting is how an operator withdraws it.

OAuth-authenticated callers only. A caller with no session — no auth, or a shared static token — cannot be told apart from any other caller, and answering the wrong one's prompt would be worse than refusing. Those callers keep today's behavior even with the flag on: your server's question gets -32601, exactly as if it had never asked.

What the client sees: the call it made comes back as an input_required result instead of the answer it expected, carrying your server's question and an opaque requestState. It answers by retrying the same request with inputResponses and that requestState attached. mcp-stdio delivers the answer to your server and, once your server finishes handling it, returns the original result on that retry.

Worth knowing:

  • One eligible call per child at a time. A second tools/call, resources/read, or prompts/get on the same child while one is still in progress — in flight, or already parked waiting for its answer — gets a 503. The caller retries once the first one resolves.
  • The client is not required to come back. If it never retries, mcp-stdio eventually gives up and answers your server's question with an error rather than leaving it blocked forever.
  • Only three requests bridge: elicitation/create, sampling/createMessage, roots/list. Anything else your server raises mid-call still gets the -32601 it always would.

Both at once

The two roles compose. A common pattern: an operator publishes an internal server with serve on one host, and every team member connects to it with plain client-mode mcp-stdio from their laptop — the same package on both ends, each side doing its half of the OAuth dance.

graph TD
    A["Claude Desktop"] -- "stdio" --> B["mcp-stdio<br/>(client mode)"]
    B -- "HTTPS + OAuth" --> C["mcp-stdio serve<br/>(server mode)"]
    C -- "stdio" --> D["your MCP server"]