Skip to content

Commit 0c1c904

Browse files
committed
Drive the Mcp-Param-* tests through the high-level Client
The three x-mcp-header emission tests used a raw ClientSession with a manual adopt(); switch them to the high-level Client over the same mounted HTTP transport (mode=2026-07-28 + prior_discover reproduces the adopt). The cache eviction test moves from tests/client (where it reached into session._resolve_param_headers) to a wire-level assertion: list a tool valid, call it (Mcp-Param-Region present), re-list it invalid, call again (no header).
1 parent 33c0d76 commit 0c1c904

2 files changed

Lines changed: 77 additions & 53 deletions

File tree

‎tests/client/test_client.py‎

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -504,38 +504,6 @@ async def on_list_tools(
504504
assert [t.name for t in result.tools] == ["ok", "dropme"]
505505

506506

507-
@pytest.mark.anyio
508-
async def test_modern_list_tools_evicts_header_map_when_a_tool_turns_invalid() -> None:
509-
"""A re-list that drops a previously valid tool must also evict its cached `x-mcp-header` map,
510-
so a later `tools/call` can't mirror stale `Mcp-Param-*` headers for a now-dropped tool."""
511-
valid = types.Tool(
512-
name="run",
513-
input_schema={"type": "object", "properties": {"a": {"type": "string", "x-mcp-header": "Region"}}},
514-
)
515-
invalid = types.Tool(
516-
name="run",
517-
input_schema={"type": "object", "properties": {"a": {"type": "string", "x-mcp-header": "bad name"}}},
518-
)
519-
listings = iter([valid, invalid])
520-
521-
async def on_list_tools(
522-
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
523-
) -> types.ListToolsResult:
524-
return types.ListToolsResult(tools=[next(listings)])
525-
526-
server = Server("test", on_list_tools=on_list_tools)
527-
528-
with anyio.fail_after(5):
529-
async with Client(server) as client:
530-
session = client.session
531-
await client.list_tools()
532-
assert session._resolve_param_headers("run", {"a": "x"}) == {"Mcp-Param-Region": "x"}
533-
534-
result = await client.list_tools()
535-
assert [t.name for t in result.tools] == []
536-
assert session._resolve_param_headers("run", {"a": "x"}) == {}
537-
538-
539507
def test_client_rejects_handshake_era_mode_at_construction() -> None:
540508
"""A handshake-era protocol-version string passed as `mode=` is rejected by
541509
`__post_init__` with a hint to use `mode='legacy'` — the version-pin path is

‎tests/interaction/transports/test_hosting_http_modern.py‎

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from mcp_types.version import LATEST_MODERN_VERSION
3939

4040
from mcp import MCPError
41+
from mcp.client.client import Client
4142
from mcp.client.session import ClientSession
4243
from mcp.client.streamable_http import streamable_http_client
4344
from mcp.server import Server, ServerRequestContext
@@ -433,21 +434,22 @@ async def test_modern_client_mirrors_x_mcp_header_args_into_mcp_param_headers()
433434
async def on_request(request: httpx.Request) -> None:
434435
requests.append(request)
435436

437+
discover = DiscoverResult(
438+
supported_versions=[LATEST_MODERN_VERSION],
439+
capabilities=ServerCapabilities(),
440+
server_info=Implementation(name="srv", version="0"),
441+
)
436442
with anyio.fail_after(5):
437443
async with (
438444
mounted_app(_custom_header_server(), on_request=on_request) as (http, _),
439-
streamable_http_client(f"{BASE_URL}/mcp", http_client=http) as (read, write),
440-
ClientSession(read, write) as session,
445+
Client(
446+
streamable_http_client(f"{BASE_URL}/mcp", http_client=http),
447+
mode=LATEST_MODERN_VERSION,
448+
prior_discover=discover,
449+
) as client,
441450
):
442-
session.adopt(
443-
DiscoverResult(
444-
supported_versions=[LATEST_MODERN_VERSION],
445-
capabilities=ServerCapabilities(),
446-
server_info=Implementation(name="srv", version="0"),
447-
)
448-
)
449-
await session.list_tools()
450-
await session.call_tool("run", {"region": "us-west1", "priority": 42, "verbose": False, "note": "héllo"})
451+
await client.list_tools()
452+
await client.call_tool("run", {"region": "us-west1", "priority": 42, "verbose": False, "note": "héllo"})
451453

452454
call = next(r for r in requests if json.loads(r.content)["method"] == "tools/call")
453455
assert {k: v for k, v in call.headers.items() if k.startswith("mcp-param-")} == snapshot(
@@ -478,19 +480,73 @@ async def on_request(request: httpx.Request) -> None:
478480
if json.loads(request.content)["method"] == "tools/call":
479481
requests.append(request)
480482

483+
discover = DiscoverResult(
484+
supported_versions=[LATEST_MODERN_VERSION],
485+
capabilities=ServerCapabilities(),
486+
server_info=Implementation(name="srv", version="0"),
487+
)
481488
with anyio.fail_after(5):
482489
async with (
483490
mounted_app(_custom_header_server(), on_request=on_request) as (http, _),
484-
streamable_http_client(f"{BASE_URL}/mcp", http_client=http) as (read, write),
485-
ClientSession(read, write) as session,
491+
Client(
492+
streamable_http_client(f"{BASE_URL}/mcp", http_client=http),
493+
mode=LATEST_MODERN_VERSION,
494+
prior_discover=discover,
495+
) as client,
486496
):
487-
session.adopt(
488-
DiscoverResult(
489-
supported_versions=[LATEST_MODERN_VERSION],
490-
capabilities=ServerCapabilities(),
491-
server_info=Implementation(name="srv", version="0"),
492-
)
493-
)
494-
await session.call_tool("run", {"region": "us-west1"})
497+
await client.call_tool("run", {"region": "us-west1"})
495498

496499
assert not any(k.startswith("mcp-param-") for k in requests[0].headers)
500+
501+
502+
@requirement("client-transport:http:custom-param-headers")
503+
async def test_modern_client_stops_mirroring_after_a_re_list_drops_the_tool() -> None:
504+
"""A re-list that drops a previously valid tool stops mirroring its `x-mcp-header` args.
505+
506+
The tool is first listed with a valid annotation (so a call mirrors `Mcp-Param-Region`), then re-listed
507+
with an invalid annotation -- the modern client drops it and evicts the cached map, so a later `tools/call`
508+
by name carries no `Mcp-Param-*` header. Asserted at the wire, where the eviction is observable.
509+
"""
510+
schema = {"type": "object", "properties": {"a": {"type": "string", "x-mcp-header": "Region"}}}
511+
bad_schema = {"type": "object", "properties": {"a": {"type": "string", "x-mcp-header": "bad name"}}}
512+
valid = Tool(name="run", input_schema=schema)
513+
invalid = Tool(name="run", input_schema=bad_schema)
514+
listings = iter([valid, invalid])
515+
516+
async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
517+
return ListToolsResult(tools=[next(listings)], ttl_ms=0, cache_scope="public")
518+
519+
async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
520+
return CallToolResult(content=[TextContent(text="ok")])
521+
522+
server = Server("evict", on_list_tools=list_tools, on_call_tool=call_tool)
523+
524+
tool_calls: list[httpx.Request] = []
525+
526+
async def on_request(request: httpx.Request) -> None:
527+
if json.loads(request.content)["method"] == "tools/call":
528+
tool_calls.append(request)
529+
530+
discover = DiscoverResult(
531+
supported_versions=[LATEST_MODERN_VERSION],
532+
capabilities=ServerCapabilities(),
533+
server_info=Implementation(name="srv", version="0"),
534+
)
535+
with anyio.fail_after(5):
536+
async with (
537+
mounted_app(server, on_request=on_request) as (http, _),
538+
Client(
539+
streamable_http_client(f"{BASE_URL}/mcp", http_client=http),
540+
mode=LATEST_MODERN_VERSION,
541+
prior_discover=discover,
542+
) as client,
543+
):
544+
assert [t.name for t in (await client.list_tools()).tools] == ["run"]
545+
await client.call_tool("run", {"a": "x"})
546+
547+
assert [t.name for t in (await client.list_tools()).tools] == []
548+
await client.call_tool("run", {"a": "x"})
549+
550+
before, after = tool_calls
551+
assert before.headers.get("mcp-param-region") == "x"
552+
assert not any(k.startswith("mcp-param-") for k in after.headers)

0 commit comments

Comments
 (0)