built by the makers of Gamut
Transport

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:

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.

  1. 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.
  2. 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.
  3. Output with no newline before mcp.run(). A prompt-style ready> was glued to the front of the initialize response. The client logged Unexpected 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 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:

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

Same rules either way. Over the bridge or the URL, DialMCP places calls only from your own SMS-verified number, discloses that it is an AI, and enforces calling hours and the opt-out list on the server. See the safety page.

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.