Skip to content

Commit 8f1d353

Browse files
committed
Send SEP-2243 Mcp-Method/Mcp-Name headers from the StreamableHTTP client
Per SEP-2243, MCP clients should send routing headers on POST requests so spec-compliant servers and intermediaries can route without parsing the JSON-RPC body. The StreamableHTTP client did not send them. Add Mcp-Method (the JSON-RPC method, for requests and notifications) and Mcp-Name (params.name for tools and prompts, else params.uri for resources) to outgoing POST headers. Responses and errors, which have no method, send neither. Fixes #2715
1 parent 616476f commit 8f1d353

2 files changed

Lines changed: 76 additions & 1 deletion

File tree

src/mcp/client/streamable_http.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,37 @@
4343

4444
MCP_SESSION_ID = "mcp-session-id"
4545
MCP_PROTOCOL_VERSION = "mcp-protocol-version"
46+
MCP_METHOD = "mcp-method"
47+
MCP_NAME = "mcp-name"
4648
LAST_EVENT_ID = "last-event-id"
4749

4850
# Reconnection defaults
4951
DEFAULT_RECONNECTION_DELAY_MS = 1000 # 1 second fallback when server doesn't provide retry
5052
MAX_RECONNECTION_ATTEMPTS = 2 # Max retry attempts before giving up
5153

5254

55+
def _set_mcp_request_headers(headers: dict[str, str], message: JSONRPCMessage) -> None:
56+
"""Add SEP-2243 routing headers for an outgoing POST message.
57+
58+
``Mcp-Method`` carries the JSON-RPC method for requests and notifications.
59+
``Mcp-Name`` carries the target ``params.name`` (tools, prompts) or, when
60+
that is absent, ``params.uri`` (resources). JSON-RPC responses and errors,
61+
which have no method, receive neither header.
62+
63+
See https://modelcontextprotocol.io/specification (SEP-2243).
64+
"""
65+
if not isinstance(message, JSONRPCRequest | JSONRPCNotification):
66+
return
67+
headers[MCP_METHOD] = message.method
68+
params = message.params
69+
if params is None:
70+
return
71+
name = params.get("name")
72+
mcp_name = name if isinstance(name, str) else params.get("uri")
73+
if isinstance(mcp_name, str):
74+
headers[MCP_NAME] = mcp_name
75+
76+
5377
class StreamableHTTPError(Exception):
5478
"""Base exception for StreamableHTTP transport errors."""
5579

@@ -255,6 +279,7 @@ async def _handle_post_request(self, ctx: RequestContext) -> None:
255279
"""Handle a POST request with response processing."""
256280
headers = self._prepare_headers()
257281
message = ctx.session_message.message
282+
_set_mcp_request_headers(headers, message)
258283
is_initialization = self._is_initialization_request(message)
259284

260285
async with ctx.client.stream(

tests/shared/test_streamable_http.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929

3030
from mcp import MCPError, types
3131
from mcp.client.session import ClientSession
32-
from mcp.client.streamable_http import StreamableHTTPTransport, streamable_http_client
32+
from mcp.client.streamable_http import (
33+
StreamableHTTPTransport,
34+
_set_mcp_request_headers,
35+
streamable_http_client,
36+
)
3337
from mcp.server import Server, ServerRequestContext
3438
from mcp.server.streamable_http import (
3539
MCP_PROTOCOL_VERSION_HEADER,
@@ -2318,3 +2322,49 @@ async def test_streamable_http_client_preserves_custom_with_mcp_headers(
23182322

23192323
assert "content-type" in headers_data
23202324
assert headers_data["content-type"] == "application/json"
2325+
2326+
2327+
@pytest.mark.parametrize(
2328+
("message", "expected"),
2329+
[
2330+
# Request with params.name (tools/prompts) -> Mcp-Name is the name.
2331+
(
2332+
types.JSONRPCRequest(jsonrpc="2.0", id=1, method="tools/call", params={"name": "read_file"}),
2333+
{"mcp-method": "tools/call", "mcp-name": "read_file"},
2334+
),
2335+
# Request with params.uri but no name (resources) -> Mcp-Name is the uri.
2336+
(
2337+
types.JSONRPCRequest(jsonrpc="2.0", id=2, method="resources/read", params={"uri": "file:///README.md"}),
2338+
{"mcp-method": "resources/read", "mcp-name": "file:///README.md"},
2339+
),
2340+
# Request without params -> only Mcp-Method.
2341+
(
2342+
types.JSONRPCRequest(jsonrpc="2.0", id=3, method="initialize", params=None),
2343+
{"mcp-method": "initialize"},
2344+
),
2345+
# Request whose name is not a string and has no uri -> only Mcp-Method.
2346+
(
2347+
types.JSONRPCRequest(jsonrpc="2.0", id=4, method="tools/call", params={"name": 123}),
2348+
{"mcp-method": "tools/call"},
2349+
),
2350+
# Notification -> Mcp-Method, never Mcp-Name.
2351+
(
2352+
types.JSONRPCNotification(jsonrpc="2.0", method="notifications/initialized"),
2353+
{"mcp-method": "notifications/initialized"},
2354+
),
2355+
# Response and error have no method -> no MCP routing headers.
2356+
(
2357+
types.JSONRPCResponse(jsonrpc="2.0", id=5, result={}),
2358+
{},
2359+
),
2360+
(
2361+
types.JSONRPCError(jsonrpc="2.0", id=6, error=types.ErrorData(code=types.INTERNAL_ERROR, message="boom")),
2362+
{},
2363+
),
2364+
],
2365+
)
2366+
def test_set_mcp_request_headers(message: types.JSONRPCMessage, expected: dict[str, str]) -> None:
2367+
"""SEP-2243: POST messages carry Mcp-Method, and Mcp-Name when a target is present."""
2368+
headers: dict[str, str] = {}
2369+
_set_mcp_request_headers(headers, message)
2370+
assert headers == expected

0 commit comments

Comments
 (0)