MCP stdio, and when you need the bridge.
Updated November 6, 2026
stdio is the local MCP transport. Your client starts the server as a
child process and talks to it over standard input and output. DialMCP
is a hosted server, so it doesn't run over stdio itself. It only
touches stdio through a small bridge, dialmcp-connector,
which turns the hosted endpoint into a local stdio server for clients
that can only launch commands. This page explains how stdio works,
shows it on the wire with a server you can run in a minute, and helps
you decide whether you need the bridge at all. The remote side
(Streamable HTTP, SSE and sessions) is covered in
MCP Streamable HTTP vs SSE,
and the install steps are in Install DialMCP.
What the stdio transport is
The current spec, revision 2026-07-28, defines two standard transports: stdio and Streamable HTTP. The stdio rules are short. We checked them against the spec on November 6, 2026:
- The client launches the server as a subprocess. That's why a stdio entry in a config file is a
commandplusargs, not a URL. - One message per line. The server reads JSON-RPC from
stdinand writes JSON-RPC tostdout. Messages are UTF-8, separated by newlines, and must not contain embedded newlines. - stdout is only for protocol messages. The server "MUST NOT write anything to its
stdoutthat is not a valid MCP message". The client has the same rule for the server'sstdin. - stderr is for logs. The server may write any logging there, and the client should not treat stderr output as an error. Many clients copy it into their MCP log, which is why that log is the first place to look when a server misbehaves.
- No headers, no URL, no OAuth. Version and capabilities ride inside each message. The authorization spec says stdio implementations "SHOULD NOT follow this specification, and instead retrieve credentials from the environment", which is why local servers take API keys through
env. - Shutdown is closing stdin. The client closes the server's input, waits, and then force-kills it (
SIGTERM, thenSIGKILLon macOS and Linux). Servers should exit as soon as stdin hits end-of-file. Cancelling a single request is anotifications/cancelledmessage, because there is no per-request stream to close.
stdio isn't deprecated. Its framing, one JSON-RPC message per line, is
the same as in 2024, while the remote transport has been replaced
twice. What rides on it did change in 2026-07-28: per-request metadata
replaces the initialize handshake, and the
server no longer sends requests of its own. The spec even tells custom
transports over sockets or TCP to reuse the same newline-delimited framing.
What stdio looks like on the wire
You can watch the whole thing with no client at all. Here is a complete stdio server using the official Python SDK (version 2.3.0 on PyPI when we tested):
from mcp.server import MCPServer
mcp = MCPServer("echo")
@mcp.tool()
def echo(text: str) -> str:
"""Echo text back."""
return text
if __name__ == "__main__":
mcp.run() # stdio is the default transport
Put four messages in a file, one per line: the
initialize request, the
notifications/initialized notification, a
tools/list and a tools/call.
Then pipe the file in, keeping stdin open for a few seconds. You are
doing exactly what a client does.
$ (cat msgs.jsonl; sleep 3) | uv run --with mcp python server.py
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{...},"protocolVersion":"2025-06-18","serverInfo":{"name":"echo",...}}}
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"echo","description":"Echo text back.",...}]}}
{"jsonrpc":"2.0","id":3,"result":{"content":[{"text":"hi","type":"text"}],"isError":false,...}}
Three requests in, three responses out, one line each, matched by
id. The notification gets no reply, and
nothing was written to stderr. Why the sleep?
With a plain < msgs.jsonl, stdin hits
end-of-file right after the last line. The server treats that as the
shutdown signal, exactly as the spec asks, and in our runs it exited
before answering the tool call, so only two lines came back. This run
used the 2025-era initialize handshake,
which many clients still send. A client that also speaks 2026-07-28 probes first
with server/discover and falls back to
initialize if the server doesn't recognise it. The JSON-RPC methods themselves are listed in
MCP server commands and tool calls.
The stdout rule, tested
Almost every "my stdio server connects and then nothing happens" report comes back to something other than JSON reaching stdout. We broke the server above on purpose in three ways and connected the official Python and TypeScript SDK clients (TypeScript SDK 1.32.2) to see what actually happens.
- A
print()inside a tool. Nothing broke. While the stdio transport is running, Python SDK 2.3.0 points the process's stdout at stderr and keeps a private handle for the protocol, so the stray line showed up in the client's log instead of on the wire. Don't rely on that: older Python SDKs and servers in other languages get no such protection, and even in 2.3.0 the diversion only starts once the transport is running. - A full line printed before
mcp.run(). That line reached the real stdout. Both clients reported a parse error (Python:Invalid JSON: expected value at line 1 column 1; TypeScript:Unexpected token 's', "server starting" is not valid JSON), skipped the line, and carried on. The tool call still worked. - Output with no newline before
mcp.run(). A prompt-styleready>was glued to the front of the initialize response. The client loggedUnexpected token 'r', "ready> {"j"... is not valid JSON, threw that whole line away, and then waited for an initialize reply that never came. The connection hung until we killed it after 20 seconds.
So the rule in practice: send every log line to stderr. In Python, use
logging (which writes to stderr by default)
or print(..., file=sys.stderr). In Node,
console.log writes to stdout, so use
console.error. Keep banners, progress bars
and package-manager notices out of stdout too. When a client's MCP log
shows "is not valid JSON", the text quoted in the error is the thing
writing to stdout.
stdio bridge or hosted URL: which to use for DialMCP
Use the URL whenever your client accepts one. Add
https://mcp.dialmcp.com/mcp as a remote,
custom or HTTP server. Claude Code, Claude custom connectors, ChatGPT
developer mode, Codex, Cursor, VS Code, Windsurf (now Devin Desktop)
and Gamut all do this. The client handles sign-in and token refresh,
there is no Node.js to install, and no tokens sit in files on your
disk. Setup for each one is in client setup.
Use the stdio bridge only when:
- The place you are adding servers only takes a command. The common case is Claude Desktop's
claude_desktop_config.json. That file only launches local stdio servers, and aurlentry there is not supported (users report it failing to load). Remote servers belong under Customize → Connectors instead, so the bridge is for people who want DialMCP in the file. The details are in claude_desktop_config.json explained. Some agent frameworks and older tools are command-only too. - You want to hide tools from one client. The bridge can filter tools locally (see below). A remote connection always shows the client every tool the server offers.
The bridge doesn't make DialMCP local, and it isn't more private. Every
call still goes through the hosted service, with the same disclosure,
calling-hours rules and opt-out list. What the bridge changes is where
your OAuth tokens live. With a URL, the client stores them. With the
bridge, they sit in plain files under ~/.mcp-auth/
on your machine (details).
It also adds a Node.js 20.18.1+ dependency.
What the bridge actually is
dialmcp-connector (open source, MIT) is one
short Node script. It starts mcp-remote
with https://mcp.dialmcp.com/mcp filled in,
passes any extra arguments through, and shares its own stdin, stdout and
stderr with it. The chain looks like this:
your MCP client
| stdio (stdin / stdout), no auth
mcp-remote, on your machine
| Streamable HTTP + OAuth
https://mcp.dialmcp.com/mcp
That explains how a stdio server can have a browser sign-in. The stdio
leg has no authentication, exactly as the spec intends. The OAuth flow
runs on the HTTP leg, between mcp-remote
and DialMCP. We ran npx -y dialmcp-connector
by hand on Node 22 on November 6, 2026 and captured the two streams
separately. Before sign-in, stdout carried 0 bytes.
Everything else went to stderr (abridged; each real line starts with the process ID):
Using automatically selected callback port: 36396
Discovering OAuth server configuration...
Discovered authorization server: https://mcp.dialmcp.com
Connecting to remote server: https://mcp.dialmcp.com/mcp
Using transport strategy: http-first
Please authorize this client by visiting:
https://mcp.dialmcp.com/authorize?response_type=code&client_id=...&code_challenge_method=S256&...
OAuth callback server running at http://127.0.0.1:36396
Authentication required. Waiting for authorization...
That is a well-behaved stdio server: protocol on stdout, diagnostics on stderr. It's also why these lines appear in your client's MCP log rather than breaking the connection. The full first-run walkthrough, including what to do when no browser opens, is in the install guide.
mcp-remote, for any remote server
mcp-remote is the general-purpose version of
the same idea. Its README describes it as a way to "connect an MCP
Client that only supports local (stdio) servers to a Remote MCP Server,
with auth support". You can point it at any remote server:
{
"mcpServers": {
"some-remote-server": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://example.com/mcp"]
}
}
}
For DialMCP, use dialmcp-connector instead,
so the URL is right and mcp-remote stays on a tested release line. It declares
mcp-remote ^0.1.38,
so npx installs the newest 0.1.x release (0.1.49 today), not the newer
0.2+ line. npm's latest mcp-remote is
0.16.1. Its README documents flags that 0.1.49 doesn't have, so check
the README that ships with the version you are actually running. These
flags exist in 0.1.49, and the bridge passes them straight through:
--debugwrites a timestamped log of discovery, sign-in and token refreshes under~/.mcp-auth/.--silentsuppresses the default stderr log.--transportpickshttp-first(the default),sse-first,http-onlyorsse-only. DialMCP is Streamable HTTP only, so leave the default or usehttp-only.--enable-proxyroutes traffic throughHTTPS_PROXY/HTTP_PROXY, and--auth-timeoutextends the 30-second wait for the OAuth callback.--headeradds a header to every request. DialMCP doesn't use API keys, so you don't need it here.
Hiding tools with --ignore-tool
--ignore-tool removes matching tools from
tools/list and blocks
tools/call requests for them. Patterns accept
*, and you can repeat the flag. That gives you a
read-only DialMCP setup: the client can review call history and
transcripts but cannot dial anyone.
{
"mcpServers": {
"dialmcp-readonly": {
"command": "npx",
"args": ["-y", "dialmcp-connector",
"--ignore-tool", "place_call",
"--ignore-tool", "end_call"]
}
}
}
The filter runs on your machine, in the bridge. It is a convenience, not an access control: the same sign-in used from a URL-based client still has every tool. What each tool does is on the tool reference.
stdio troubleshooting
- "is not valid JSON" or "Unexpected token" in the client log. Something wrote to stdout. The quoted text tells you what. Move it to stderr.
- The server starts but the client never lists tools. Look for output with no newline (a prompt, a progress bar) glued to the first response. Run the server by hand with a piped
initializeline, as shown above, and check that each response is exactly one line. - The server exits immediately. Read its stderr in the client's MCP log. With the DialMCP bridge,
ReferenceError: File is not definedmeans Node 18. You need 20.18.1 or newer (fix). spawn npx ENOENT. The client can't find the command on its own PATH, which is often shorter than your terminal's. Use the full path tonpx.- An environment variable is missing. Clients such as Claude Desktop pass stdio servers only part of your shell environment. Put what the server needs in the entry's
envblock. - The bridge keeps asking you to sign in. That's an OAuth problem on the HTTP leg, not stdio. See install problems and OAuth and phone verification.
FAQ
What is MCP stdio?
stdio is the local transport in the Model Context Protocol. The client launches the MCP server as a subprocess and exchanges newline-delimited JSON-RPC messages with it over standard input and output. The server's stdout carries only protocol messages, and logs go to stderr.
Is the MCP stdio transport deprecated?
No. The 2026-07-28 spec still defines two standard transports, stdio and Streamable HTTP. What was deprecated is the old HTTP+SSE remote transport. stdio's newline-delimited framing is unchanged since 2024, though 2026-07-28 replaced the initialize handshake with per-request metadata.
What is the difference between MCP stdio and Streamable HTTP?
stdio runs the server as a local child process with no network, URL or OAuth, and takes credentials from environment variables. Streamable HTTP reaches a server at a URL, usually remote, with each message sent as an HTTP POST and access usually protected by OAuth.
Why does my stdio MCP server say "is not valid JSON"?
Something other than a JSON-RPC message reached stdout, such as a print statement, a banner or a progress bar. The client quotes the offending text in its log. Send all logging to stderr. Output without a trailing newline is worse, because it merges with the next real message and the client can hang waiting for a reply.
Can I run DialMCP as a stdio server?
Only through the dialmcp-connector bridge, which runs mcp-remote locally and forwards to the hosted endpoint. Calls, guardrails and recordings all run on DialMCP's hosted service. If your client accepts a server URL, add https://mcp.dialmcp.com/mcp instead and skip the bridge.
What is mcp-remote?
mcp-remote is an open-source npm package that lets a stdio-only MCP client connect to a remote MCP server. It runs locally as a stdio server, handles the OAuth browser sign-in and token refresh, and forwards messages over HTTP. The DialMCP bridge runs mcp-remote 0.1.x with the DialMCP URL filled in.
Next: install DialMCP with the URL or the bridge, check which file each client reads, or read remote vs local MCP servers for the bigger picture. Back to the docs hub.