{"slug": "mcp-transports-in-2026-stdio-and-streamable-http", "title": "MCP transports in 2026: stdio and Streamable HTTP", "summary": "Under the 2026-07-28 Model Context Protocol specification, MCP servers use two standard transports for JSON-RPC 2.0 messages: stdio for local processes launched by the host, where stdout carries protocol messages only and logs go to stderr, and Streamable HTTP for remote services, where request headers identify the method and tool and a header that disagrees with the body returns error -32020. The author, writing in October 2026, identifies proxy idle timeouts as the main operational risk because a broken response stream loses its request, and notes the older HTTP+SSE transport has been deprecated since 2025-03-26. The official Python SDK, mcp 2.3.0, routes logging output to stderr once an MCPServer is created, though a test on Windows showed an unflushed print() stayed buffered until the server stopped and then reached the client as an unparseable line.", "body_md": "# MCP transports in 2026: stdio and Streamable HTTP\n\nWhat surprised me in the 2026-07-28 transport rules is how much is now mirrored into HTTP headers: a gateway in front of a Model Context Protocol (MCP) server can see which method and tool a request is for without reading its body. The failures to watch for are small: a stray line on stdout, or a proxy that cuts a quiet call.\n\nThis is part 7 of my series on what an MCP server does under the 2026-07-28 specification, after [caching hints and pagination in part 6](https://pournasserian.com/writing/mcp-2026-caching-and-pagination). The facts are as I read them in October 2026.\n\n## In brief\n\n1. **MCP has two standard transports for its JSON-RPC 2.0 messages:** stdio, typically for a local process the host launches, and Streamable HTTP, typically for a remote service.\n2. **On stdio, stdout carries protocol messages and nothing else.** Logs go to stderr, and credentials come from the environment.\n3. **On Streamable HTTP, headers say what each request is,** so a gateway can act on them, and a header that disagrees with the body gets`-32020` .\n4. **A broken response stream loses its request,** so as I see it, proxy idle timeouts are the main operational risk.\n\n## Two transports\n\nSide by side:\n\n|  | stdio | Streamable HTTP | \n|---|---|---|\n| Typical deployment | A local process launched by the host | A remote service, in the cloud or on premises | \n| Connection | One process, stdin and stdout pipes | Independent HTTP POSTs | \n| Authorization | Credentials from the environment | OAuth 2.1 bearer tokens on every request, where authorization is used | \n| Cancellation | `notifications/cancelled` with the request ID | Close the response stream | \n| Notification streams | Multiplexed on the one channel, tagged with `subscriptionId` | A `subscriptions/listen` POST with a long-lived response stream | \n| Logging | Write to `stderr` , never`stdout` | OpenTelemetry or your platform’s logging | \n\nA third, older transport, HTTP+SSE (HTTP with Server-Sent Events, or SSE), has been deprecated since 2025-03-26; [part 2](https://pournasserian.com/writing/mcp-2026-what-changed) has its removal timeline.\n\n## stdio: stdout belongs to the protocol\n\nThe host launches your server as a child process and exchanges JSON-RPC messages over its stdin and stdout. The rules:\n\n- **Only protocol messages go to stdout.** A stray print statement corrupts the stream.\n- **Credentials come from the environment.** Implementations using stdio SHOULD NOT follow the[authorization specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) .\n\nIn Python, log with the `logging` module: the official Python software development kit (SDK), `mcp` 2.3.0, routes its output to stderr once you create an `MCPServer`:\n\n``` python\nimport logging\n\nfrom mcp.server import MCPServer\n\nmcp = MCPServer(\"docs\")  # also sets up logging on stderr, at INFO\nlog = logging.getLogger(\"docs\")\n\n@mcp.tool()\ndef search_docs(query: str) -> str:\n    \"\"\"Search the documentation.\"\"\"\n    log.info(\"search_docs query=%r\", query)  # stderr, never stdout\n    return f\"No results for {query!r}\"\n\nif __name__ == \"__main__\":\n    mcp.run()  # stdio by default: stdout carries the protocol\n```\n\nUnder the SDK’s client over stdio, the call returned and the log line went to stderr.\n\nThe SDK points stdout at stderr while it serves stdio, a safety net I wouldn’t lean on: in my test on Windows, an unflushed `print()` sat in Python’s buffer until the server stopped, then reached the client as a line it couldn’t parse.\n\n### Shipping a local server\n\nA local server is simple to operate but runs arbitrary code on the user’s machine, so its risks are the supply chain and trust. My advice: publish signed, versioned packages, keep dependencies minimal, read secrets from the environment, and support the hosts’ sandboxing. [VS Code](https://code.visualstudio.com/docs/agent-customization/mcp-servers), for example, can sandbox stdio servers on macOS and Linux as of October 2026, with filesystem and network allowlists.\n\n## Streamable HTTP: one POST per message\n\nThe client sends each JSON-RPC message as an HTTP POST to the server’s single MCP endpoint. The server answers with one JSON response, or an SSE stream that carries request-scoped notifications, such as progress, then the final result. Here a gateway or Web Application Firewall (WAF) decides from the headers alone:\n\nThe 2026-07-28 revision drops three things from [Streamable HTTP](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http):\n\n- **Sessions:** the`Mcp-Session-Id` header is gone;[part 5](https://pournasserian.com/writing/mcp-2026-stateless-requests) shows where state goes instead.\n- **The GET endpoint:** notifications move to`subscriptions/listen` .\n- **Resumability:** a broken response stream loses the request in flight, and the client MUST re-issue it with a new ID.\n\n### The request headers\n\n| Header | Sent on | What it carries | \n|---|---|---|\n| `MCP-Protocol-Version` | Every request | The protocol version, such as `2026-07-28` | \n| `Mcp-Method` | Every request | The JSON-RPC method, such as `tools/call` | \n| `Mcp-Name` | `tools/call` ,`prompts/get` and`resources/read` | The tool or prompt name, or the resource’s Uniform Resource Identifier (URI) | \n| `Mcp-Param-{name}` | `tools/call` , for a parameter marked with`x-mcp-header` | That argument’s value | \n| `Authorization` | Every request to a server that uses OAuth | `Bearer <token>` | \n\nWith these, a gateway can apply per-method and per-tool policy, or route on a value such as a region:\n\nA header that disagrees with the body gets `HeaderMismatchError` (`-32020`). Without that check, an attacker could slip past header-based policy by naming one tool in the header and another in the body.\n\n### Mirroring a parameter into a header\n\nA tool marks a parameter by adding `\"x-mcp-header\": \"Region\"` to that property in its `inputSchema`, and the client then sends `Mcp-Param-Region: us-west1`. The rules for the annotation:\n\n- Its value, the header name, is a valid HTTP header token, non-empty and unique within the schema, ignoring case.\n- It sits on a string, integer or boolean parameter, never `number` .\n- That parameter is statically reachable from the schema root.\n\nA client MUST drop a tool that breaks these rules. Servers SHOULD NOT mark sensitive parameters, such as passwords, keys, tokens or personally identifiable information, because intermediaries can see header values.\n\nIn Python, the annotation goes in Pydantic’s `Field`, and the SDK’s in-memory `Client` shows the schema a client receives:\n\n``` python\nimport json\nfrom typing import Annotated\n\nimport anyio\nfrom mcp import Client\nfrom mcp.server import MCPServer\nfrom pydantic import Field\n\nmcp = MCPServer(\"docs\")\n\n@mcp.tool()\ndef search_docs(\n    query: str,\n    region: Annotated[str, Field(json_schema_extra={\"x-mcp-header\": \"Region\"})],\n) -> str:\n    \"\"\"Search the documentation served from one region.\"\"\"\n    return f\"No results for {query!r} in {region}\"\n\nasync def main():\n    async with Client(mcp) as client:\n        tools = await client.list_tools()\n        print(json.dumps(tools.tools[0].input_schema, indent=2))\n\nif __name__ == \"__main__\":\n    anyio.run(main)\n```\n\nIt printed this, with the annotation on `region`:\n\n```\n{\n  \"type\": \"object\",\n  \"properties\": {\n    \"query\": {\n      \"title\": \"Query\",\n      \"type\": \"string\"\n    },\n    \"region\": {\n      \"title\": \"Region\",\n      \"type\": \"string\",\n      \"x-mcp-header\": \"Region\"\n    }\n  },\n  \"required\": [\n    \"query\",\n    \"region\"\n  ],\n  \"title\": \"search_docsArguments\"\n}\n```\n\n`MCPServer` checks the rules at registration: a `float` parameter, or a header name with a space, raises `InvalidSignature`. Over Streamable HTTP, my call with `region` set to `us-west1` carried these MCP headers (lower-cased by the server):\n\n```\nmcp-protocol-version: 2026-07-28\nmcp-method: tools/call\nmcp-name: search_docs\nmcp-param-region: us-west1\n```\n\nThe client’s `server/discover` and `tools/list` POSTs carried only the first two. A header I sent that disagreed with the body got `-32020` and HTTP 400.\n\n## Proxies, long calls and local HTTP servers\n\nOn Streamable HTTP, the main operational risk I see is a proxy or load balancer with an idle timeout. A long tool call that sends nothing for 60 seconds can be cut by an intermediary, and since the stream can’t be resumed, the work is lost. My advice:\n\n- **Send progress notifications as the work runs,** so the stream doesn’t go quiet. They don’t buy unlimited time: the[cancellation page](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation) says a client MAY reset its timeout clock on progress but SHOULD still enforce a maximum.\n- **Set intermediary idle timeouts above your longest expected call.**\n- **Move anything that runs longer than about a minute to Tasks,** covered in a later part. That minute is my rule of thumb. The[Tasks overview](https://modelcontextprotocol.io/extensions/tasks/overview) sets the bar lower: it says that many clients and transport intermediaries impose timeouts that make blocking impractical beyond a few seconds.\n\n### A local server on HTTP\n\nTreat it as a remote server that happens to be close: bind it to `localhost` only, validate the `Origin` header so a malicious web page can’t reach it through Domain Name System (DNS) rebinding, and still require authentication for sensitive data. The Python SDK does the first two by default: `mcp.run(\"streamable-http\")` binds to 127.0.0.1 and checks `Host` and `Origin`, and my POST with `Origin: http://evil.example` got a 403.\n\n## Method and caveats\n\n- Built from my guide’s transports chapter and the local stdio variant of its reference architecture, written against the 2026-07-28 specification.\n- Both Python samples, and small variants of them, ran against `mcp` 2.3.0 on Windows on 8 October 2026; every output and result in the text comes from those runs.\n- Where the [Streamable HTTP page](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) is more precise than my notes (which headers each request carries), this article and its sequence diagram follow it.\n- The one-minute line for Tasks is my rule of thumb; the Tasks overview and the cancellation page were checked on 8 October 2026.\n\n## Series What an MCP server actually does in 2026 Part 7 of 35\n\n1. [What an MCP server does in 2026: much more than a list of tools](https://pournasserian.com/writing/mcp-2026-what-a-server-does)\n2. [MCP goes stateless: what changed in the 2026-07-28 specification](https://pournasserian.com/writing/mcp-2026-what-changed)\n3. [server/discover: the one method every MCP server must implement](https://pournasserian.com/writing/mcp-2026-server-discover)\n4. [Server instructions: the paragraph every model reads first](https://pournasserian.com/writing/mcp-2026-server-instructions)\n5. [Stateless MCP requests: _meta, resultType and explicit handles](https://pournasserian.com/writing/mcp-2026-stateless-requests)\n6. [Caching hints and pagination in MCP](https://pournasserian.com/writing/mcp-2026-caching-and-pagination)\n7. MCP transports in 2026: stdio and Streamable HTTP", "url": "https://wpnews.pro/news/mcp-transports-in-2026-stdio-and-streamable-http", "canonical_source": "https://pournasserian.com/writing/mcp-2026-transports", "published_at": "2026-10-09 00:00:00+00:00", "updated_at": "2026-10-10 16:48:37.366800+00:00", "lang": "en", "topics": ["agent-protocols", "ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Model Context Protocol", "Streamable HTTP", "stdio", "HTTP+SSE", "Python SDK", "mcp 2.3.0", "VS Code", "OAuth 2.1"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/mcp-transports-in-2026-stdio-and-streamable-http", "markdown": "https://wpnews.pro/news/mcp-transports-in-2026-stdio-and-streamable-http.md", "text": "https://wpnews.pro/news/mcp-transports-in-2026-stdio-and-streamable-http.txt", "jsonld": "https://wpnews.pro/news/mcp-transports-in-2026-stdio-and-streamable-http.jsonld"}}