Move messages: stdio and Streamable HTTP
See how messages travel to local and remote servers, and why HTTP headers must match the body.
- Describe the stdio transport’s framing and rules
- List the headers every Streamable HTTP request carries
- Explain why servers reject header/body mismatches
stdio: a local subprocess
With stdio, the client launches the server as a subprocess and they talk over its standard streams:
- the client writes requests to the server’s stdin, one JSON message per line,
- the server writes responses to stdout, one per line - messages must not contain embedded newlines,
- stdout is only for MCP messages; logs go to stderr,
- the client shuts the server down by closing its stdin.
Streamable HTTP: a remote server
A remote server exposes one MCP endpoint, like https://example.com/mcp. Every message is its own HTTP POST. The server answers with either plain JSON or a Server-Sent Events stream (for progress notifications before the final result).
Each POST also mirrors key body fields into headers, so load balancers, gateways and firewalls can route and rate-limit without parsing JSON:
| Header | Copied from |
|---|---|
MCP-Protocol-Version | _meta protocol version |
Mcp-Method | method |
Mcp-Name | params.name or params.uri (for tools/call, resources/read, prompts/get) |
1POST /mcp HTTP/1.1
2Content-Type: application/json
3Accept: application/json, text/event-stream
4MCP-Protocol-Version: 2026-07-28
5Mcp-Method: tools/call
6Mcp-Name: get_weather
7
8{
9 "jsonrpc": "2.0",
10 "id": 1,
11 "method": "tools/call",
12 "params": {
13 "name": "get_weather",
14 "arguments": {
15 "location": "Seattle, WA"
16 },
17 "_meta": {
18 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
19 "io.modelcontextprotocol/clientInfo": {
20 "name": "ExampleClient",
21 "version": "1.0.0"
22 },
23 "io.modelcontextprotocol/clientCapabilities": {}
24 }
25 }
26}If a header and the body disagree - the gateway routes on Mcp-Name: get_weather but the body calls delete_account - an attacker could slip past policies. So servers must reject mismatches with 400 Bad Request and error -32020 (HeaderMismatch). Values that aren’t plain ASCII are sent Base64-encoded as =?base64?…?=.
Try it
Accept or reject?
Each line summarizes an HTTP request to an MCP server. Should the server accept it, or reject it?
“Mcp-Method: tools/call, body method tools/call, Mcp-Name matches the tool name”
“Mcp-Name: get_weather, body calls tool delete_files”
“MCP-Protocol-Version: 2026-07-28, body _meta says 2025-11-25”
“tools/call with no Mcp-Name header”
“Origin: https://evil.example on a server only meant for a local app”
Key takeaways
stdio: newline-delimited JSON over stdin/stdout; stdout is for messages only, logs go to stderr.
Streamable HTTP: one POST endpoint; headers mirror version, method and name for routing.
Header/body mismatches are rejected (
-32020); local servers bind to localhost and checkOrigin.
Lesson quiz
6 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: write Python
Write Python in the editor and run it against sample inputs. Python runs locally in your browser using a WebAssembly runtime.
Validate request headers
The first line is a JSON object of HTTP headers (header names may use any letter case); the second line is the JSON-RPC body. Check, in this order:
MCP-Protocol-Versionequals the body’sparams._meta["io.modelcontextprotocol/protocolVersion"]Mcp-Methodequals the body’smethod- for
tools/callandprompts/get,Mcp-Nameequalsparams.name; forresources/read, it equalsparams.uri
Print ok, or -32020 HeaderMismatch: NAME for the first failing header (NAME as written above). A missing header counts as a mismatch.
- All match
- Smuggled tool
- Version mismatch
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Encode a header value
Read one value (the rest of the line, exactly as given - it may have leading or trailing spaces). If every character is printable ASCII (codes 32–126), there is no leading or trailing whitespace, and it doesn’t already look like =?base64?…?=, print it unchanged. Otherwise print =?base64? + the Base64 of its UTF-8 bytes + ?=.
- Plain ASCII
- Non-ASCII
- Padded
- Looks encoded
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…