|
38 | 38 | from mcp_types.version import LATEST_MODERN_VERSION |
39 | 39 |
|
40 | 40 | from mcp import MCPError |
| 41 | +from mcp.client.client import Client |
41 | 42 | from mcp.client.session import ClientSession |
42 | 43 | from mcp.client.streamable_http import streamable_http_client |
43 | 44 | from mcp.server import Server, ServerRequestContext |
@@ -433,21 +434,22 @@ async def test_modern_client_mirrors_x_mcp_header_args_into_mcp_param_headers() |
433 | 434 | async def on_request(request: httpx.Request) -> None: |
434 | 435 | requests.append(request) |
435 | 436 |
|
| 437 | + discover = DiscoverResult( |
| 438 | + supported_versions=[LATEST_MODERN_VERSION], |
| 439 | + capabilities=ServerCapabilities(), |
| 440 | + server_info=Implementation(name="srv", version="0"), |
| 441 | + ) |
436 | 442 | with anyio.fail_after(5): |
437 | 443 | async with ( |
438 | 444 | 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, |
441 | 450 | ): |
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"}) |
451 | 453 |
|
452 | 454 | call = next(r for r in requests if json.loads(r.content)["method"] == "tools/call") |
453 | 455 | 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: |
478 | 480 | if json.loads(request.content)["method"] == "tools/call": |
479 | 481 | requests.append(request) |
480 | 482 |
|
| 483 | + discover = DiscoverResult( |
| 484 | + supported_versions=[LATEST_MODERN_VERSION], |
| 485 | + capabilities=ServerCapabilities(), |
| 486 | + server_info=Implementation(name="srv", version="0"), |
| 487 | + ) |
481 | 488 | with anyio.fail_after(5): |
482 | 489 | async with ( |
483 | 490 | 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, |
486 | 496 | ): |
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"}) |
495 | 498 |
|
496 | 499 | 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