Common source of bugsArchitecture & Components · 14%Track 1 · 5 / 9
stdio or HTTP: the pipes, and keeping them clean
Two transports. stdio is a subprocess talking newline-delimited JSON; Streamable HTTP is one POST endpoint. Each has a handful of rules that cause most real-world breakage.
A developer adds console.log("server starting") to their stdio MCP server to debug a problem. Everything breaks: the client tries to parse server starting as JSON-RPC and drops the connection.
stdout is the protocol. The fix is one character away: log to stderr, which the spec explicitly reserves for logging.
Same week, different team: a local HTTP MCP server bound to 0.0.0.0, with no Origin check. A malicious web page uses DNS rebinding to make the user's own browser call http://localhost:3000/mcp and run tools. Two rules stop it: validate Origin (answer an invalid one with 403) and bind to 127.0.0.1.
The client launches the server as a subprocess. Messages are newline-delimited and MUST NOT contain embedded newlines.
The server MUST NOT write anything to stdout that isn't a valid MCP message. It MAY log to stderr, and the client SHOULD NOT treat stderr output as an error.
Shutdown: the client closes stdin, waits, then terminates if needed (SIGTERM, then SIGKILL). Servers SHOULD exit promptly when stdin closes. If the server crashes, the client SHOULD restart it; in-flight requests are simply lost.
stdio servers don't use the OAuth flow: they SHOULD take credentials from the environment.
Streamable HTTP
One endpoint, POST only. Servers MUST validate Origin and answer an invalid one with 403. Local servers SHOULD bind to 127.0.0.1, not 0.0.0.0. All connections SHOULD be authenticated.
Progress: the client puts a progressToken (string or integer, unique among active requests) in _meta. progress MUST increase with each notification, even without a total, and notifications MUST stop after completion.
Cancellation: on HTTP, closing the response stream is the signal. On stdio, the client MUST send notifications/cancelled with the requestId. Either way the server stops and sends nothing more for that request. Both sides SHOULD enforce a maximum timeout.
Most "MCP is flaky" bug reports are transport hygiene: a stray print on stdout, a server that ignores a closed stdin, a local endpoint reachable from any website. These rules are short, and following them removes a whole class of outages and attacks.
First time here? Set up the test helper (once per terminal)
ShellSetup
# 1. In a SECOND terminal, start the reference server (Node 18+, no dependencies)curl -sO https://www.diegozuluaga.dev/mcpa/reference-server.mjsnode reference-server.mjs # http://localhost:3000/mcp, logs appear here# 2. In THIS terminal, define the helper every test uses (bash or zsh)export MCP=http://localhost:3000/mcpMETA='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}'mcp() { # usage: mcp <method> '<json body>' [extra curl args...] curl -sS -N "$MCP" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -H 'MCP-Protocol-Version: 2026-07-28' \ -H "Authorization: Bearer ${MCP_USER:-alice}" \ -H "Mcp-Method: $1" "${@:3}" -d "$2" \ -w '\nHTTP %{http_code}\n'}# Demo auth: the reference server treats the bearer token as the user's name.# Prefix a command with MCP_USER=bob to act as someone else.
ShellTests
# 1. stdio: stdout carries only JSON (logs go to stderr, hidden here with 2>/dev/null){ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'; sleep 1; } | node reference-server.mjs --stdio 2>/dev/null | head -c 120; echo# 2. stdio: start a slow call with a progress token, cancel it after 2.5 s{ printf '%s\n' '{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"build_report","arguments":{"month":"2026-09"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"progressToken":"p7"}}}'; sleep 2.5; printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":7,"reason":"User requested cancellation"}}'; sleep 2; } | node reference-server.mjs --stdio# expect: two progress notifications, the stderr line "build_report cancelled", and NO result for id 7# 3. HTTP: a request from a foreign Origin is refusedcurl -sS -o /dev/null -w 'evil origin -> %{http_code}\n' "$MCP" -H 'Origin: https://evil.example' -H 'Content-Type: application/json' -d '{}'# expect: 403