cd /news/agent-protocols/mcp-transports-in-2026-stdio-and-str… · home › topics › agent-protocols › article
[ARTICLE · art-148821] src=pournasserian.com ↗ pub= topic=agent-protocols verified=true sentiment=· neutral

MCP transports in 2026: stdio and Streamable HTTP

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.

by read8 min views1 publishedOct 9, 2026
MCP transports in 2026: stdio and Streamable HTTP
Image: source

What 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.

This 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. The facts are as I read them in October 2026.

In brief #

  1. 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.
  2. On stdio, stdout carries protocol messages and nothing else. Logs go to stderr, and credentials come from the environment.
  3. 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 .
  4. A broken response stream loses its request, so as I see it, proxy idle timeouts are the main operational risk.

Two transports #

Side by side:

stdio Streamable HTTP
Typical deployment A local process launched by the host A remote service, in the cloud or on premises
Connection One process, stdin and stdout pipes Independent HTTP POSTs
Authorization Credentials from the environment OAuth 2.1 bearer tokens on every request, where authorization is used
Cancellation notifications/cancelled with the request ID Close the response stream
Notification streams Multiplexed on the one channel, tagged with subscriptionId A subscriptions/listen POST with a long-lived response stream
Logging Write to stderr , neverstdout OpenTelemetry or your platform’s logging

A third, older transport, HTTP+SSE (HTTP with Server-Sent Events, or SSE), has been deprecated since 2025-03-26; part 2 has its removal timeline.

stdio: stdout belongs to the protocol #

The host launches your server as a child process and exchanges JSON-RPC messages over its stdin and stdout. The rules:

  • Only protocol messages go to stdout. A stray print statement corrupts the stream.
  • Credentials come from the environment. Implementations using stdio SHOULD NOT follow theauthorization specification .

In 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:

import logging

from mcp.server import MCPServer

mcp = MCPServer("docs")  # also sets up logging on stderr, at INFO
log = logging.getLogger("docs")

@mcp.tool()
def search_docs(query: str) -> str:
    """Search the documentation."""
    log.info("search_docs query=%r", query)  # stderr, never stdout
    return f"No results for {query!r}"

if __name__ == "__main__":
    mcp.run()  # stdio by default: stdout carries the protocol

Under the SDK’s client over stdio, the call returned and the log line went to stderr.

The 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.

Shipping a local server

A 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, for example, can sandbox stdio servers on macOS and Linux as of October 2026, with filesystem and network allowlists.

Streamable HTTP: one POST per message #

The 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:

The 2026-07-28 revision drops three things from Streamable HTTP:

  • Sessions: theMcp-Session-Id header is gone;part 5 shows where state goes instead.
  • The GET endpoint: notifications move tosubscriptions/listen .
  • Resumability: a broken response stream loses the request in flight, and the client MUST re-issue it with a new ID.

The request headers

Header Sent on What it carries
MCP-Protocol-Version Every request The protocol version, such as 2026-07-28
Mcp-Method Every request The JSON-RPC method, such as tools/call
Mcp-Name tools/call ,prompts/get andresources/read The tool or prompt name, or the resource’s Uniform Resource Identifier (URI)
Mcp-Param-{name} tools/call , for a parameter marked withx-mcp-header That argument’s value
Authorization Every request to a server that uses OAuth Bearer <token>

With these, a gateway can apply per-method and per-tool policy, or route on a value such as a region:

A 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.

Mirroring a parameter into a header

A 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:

  • Its value, the header name, is a valid HTTP header token, non-empty and unique within the schema, ignoring case.
  • It sits on a string, integer or boolean parameter, never number .
  • That parameter is statically reachable from the schema root.

A 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.

In Python, the annotation goes in Pydantic’s Field, and the SDK’s in-memory Client shows the schema a client receives:

import json
from typing import Annotated

import anyio
from mcp import Client
from mcp.server import MCPServer
from pydantic import Field

mcp = MCPServer("docs")

@mcp.tool()
def search_docs(
    query: str,
    region: Annotated[str, Field(json_schema_extra={"x-mcp-header": "Region"})],
) -> str:
    """Search the documentation served from one region."""
    return f"No results for {query!r} in {region}"

async def main():
    async with Client(mcp) as client:
        tools = await client.list_tools()
        print(json.dumps(tools.tools[0].input_schema, indent=2))

if __name__ == "__main__":
    anyio.run(main)

It printed this, with the annotation on region:

{
  "type": "object",
  "properties": {
    "query": {
      "title": "Query",
      "type": "string"
    },
    "region": {
      "title": "Region",
      "type": "string",
      "x-mcp-header": "Region"
    }
  },
  "required": [
    "query",
    "region"
  ],
  "title": "search_docsArguments"
}

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):

mcp-protocol-version: 2026-07-28
mcp-method: tools/call
mcp-name: search_docs
mcp-param-region: us-west1

The 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.

Proxies, long calls and local HTTP servers #

On 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:

  • Send progress notifications as the work runs, so the stream doesn’t go quiet. They don’t buy unlimited time: thecancellation page says a client MAY reset its timeout clock on progress but SHOULD still enforce a maximum.
  • Set intermediary idle timeouts above your longest expected call.
  • Move anything that runs longer than about a minute to Tasks, covered in a later part. That minute is my rule of thumb. TheTasks overview sets the bar lower: it says that many clients and transport intermediaries impose timeouts that make blocking impractical beyond a few seconds.

A local server on HTTP

Treat 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.

Method and caveats #

  • Built from my guide’s transports chapter and the local stdio variant of its reference architecture, written against the 2026-07-28 specification.
  • 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.
  • Where the Streamable HTTP page is more precise than my notes (which headers each request carries), this article and its sequence diagram follow it.
  • The one-minute line for Tasks is my rule of thumb; the Tasks overview and the cancellation page were checked on 8 October 2026.

Series What an MCP server actually does in 2026 Part 7 of 35 #

  1. What an MCP server does in 2026: much more than a list of tools
  2. MCP goes stateless: what changed in the 2026-07-28 specification
  3. server/discover: the one method every MCP server must implement
  4. Server instructions: the paragraph every model reads first
  5. Stateless MCP requests: _meta, resultType and explicit handles
  6. Caching hints and pagination in MCP
  7. MCP transports in 2026: stdio and Streamable HTTP
── more in #agent-protocols 4 stories · sorted by recency
github.com · · #agent-protocols
iMCP
── more on @model context protocol 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/mcp-transports-in-20…] indexed:0 read:8min 2026-10-09 · —