Client-side 2026-07-28 support: .discover()/.adopt() + Client(mode=); request-metadata green (#2950)

This commit is contained in:
Max 2026-06-25 16:09:23 +02:00 • committed by GitHub
commit f226d00d0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 2533 additions and 993 deletions

View file

@ -181,7 +181,7 @@ async def test_client_tolerates_405_on_get_and_delete() -> None:
):
transport = streamable_http_client(f"{BASE_URL}/mcp", http_client=http_client)
with anyio.fail_after(5): # pragma: no branch
async with Client(transport) as client: # pragma: no branch
async with Client(transport, mode="legacy") as client: # pragma: no branch
result = await client.list_tools()
assert [tool.name for tool in result.tools] == ["echo"]
@ -240,7 +240,7 @@ async def test_a_404_mid_session_surfaces_as_a_session_terminated_error() -> Non
):
transport = streamable_http_client(f"{BASE_URL}/mcp", http_client=http_client)
with anyio.fail_after(5): # pragma: no branch
async with Client(transport) as client: # pragma: no branch
async with Client(transport, mode="legacy") as client: # pragma: no branch
with pytest.raises(MCPError) as exc_info: # pragma: no branch
await client.list_tools()

View file

@ -20,6 +20,7 @@ from mcp import MCPError
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamable_http_client
from mcp.server import Server, ServerRequestContext
from mcp.shared.version import LATEST_MODERN_VERSION
from mcp.types import (
CLIENT_CAPABILITIES_META_KEY,
INTERNAL_ERROR,
@ -28,6 +29,7 @@ from mcp.types import (
MISSING_REQUIRED_CLIENT_CAPABILITY,
CallToolRequestParams,
CallToolResult,
DiscoverResult,
EmptyResult,
Implementation,
JSONRPCError,
@ -35,6 +37,7 @@ from mcp.types import (
ListToolsResult,
PaginatedRequestParams,
RequestParams,
ServerCapabilities,
TextContent,
Tool,
)
@ -43,8 +46,6 @@ from tests.interaction._requirements import requirement
pytestmark = pytest.mark.anyio
MODERN_VERSION = "2026-07-28"
def _modern_headers(*, method: str, name: str | None = None) -> dict[str, str]:
"""Request headers for a 2026-07-28 POST.
@ -52,7 +53,7 @@ def _modern_headers(*, method: str, name: str | None = None) -> dict[str, str]:
The Accept/Content-Type baseline plus the ``MCP-Protocol-Version`` routing header and the
``Mcp-Method`` / ``Mcp-Name`` advisory headers a 2026-era client always sends.
"""
headers = base_headers() | {"mcp-protocol-version": MODERN_VERSION, "mcp-method": method}
headers = base_headers() | {"mcp-protocol-version": LATEST_MODERN_VERSION, "mcp-method": method}
if name is not None:
headers["mcp-name"] = name
return headers
@ -65,7 +66,7 @@ def _meta_envelope() -> dict[str, object]:
capabilities travel on each request instead of once per session.
"""
return {
"io.modelcontextprotocol/protocolVersion": MODERN_VERSION,
"io.modelcontextprotocol/protocolVersion": LATEST_MODERN_VERSION,
"io.modelcontextprotocol/clientInfo": {"name": "raw", "version": "0.0.0"},
"io.modelcontextprotocol/clientCapabilities": {},
}
@ -328,12 +329,16 @@ async def test_pinned_client_stateless_tools_call_round_trips_against_the_modern
with anyio.fail_after(5):
async with (
mounted_app(server, on_request=on_request, on_response=on_response) as (http, _),
streamable_http_client(f"{BASE_URL}/mcp", http_client=http, protocol_version=MODERN_VERSION) as (
read,
write,
),
ClientSession(read, write, client_info=client_info, protocol_version=MODERN_VERSION) as session,
streamable_http_client(f"{BASE_URL}/mcp", http_client=http) as (read, write),
ClientSession(read, write, client_info=client_info) as session,
):
session.adopt(
DiscoverResult(
supported_versions=[LATEST_MODERN_VERSION],
capabilities=ServerCapabilities(),
server_info=Implementation(name="srv", version="0"),
)
)
result = await session.call_tool(
"add",
{"a": 2, "b": 3},

View file

@ -21,8 +21,8 @@ from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamable_http_client
from mcp.server.mcpserver import Context, MCPServer
from mcp.shared.message import ClientMessageMetadata
from mcp.shared.version import LATEST_HANDSHAKE_VERSION
from mcp.types import (
LATEST_PROTOCOL_VERSION,
CallToolRequest,
CallToolRequestParams,
CallToolResult,
@ -431,7 +431,7 @@ async def test_a_captured_resumption_token_replays_missed_messages_on_a_new_conn
# The session id is only observable via the manager (the client transport does not expose it).
(session_id,) = manager._server_instances
http.headers["mcp-session-id"] = session_id
http.headers["mcp-protocol-version"] = LATEST_PROTOCOL_VERSION
http.headers["mcp-protocol-version"] = LATEST_HANDSHAKE_VERSION
tg.cancel_scope.cancel()
with anyio.fail_after(5): # pragma: no branch

View file

@ -66,7 +66,7 @@ async def test_legacy_streamable_http_exchange_carries_no_modern_protocol_vocabu
async with mounted_app(_server(), on_request=on_request, on_response=on_response) as (http, _):
recording = RecordingTransport(streamable_http_client(f"{BASE_URL}/mcp", http_client=http))
async with Client(recording) as client:
async with Client(recording, mode="legacy") as client:
result = await client.call_tool("echo", {"text": "legacy"})
assert result == snapshot(CallToolResult(content=[TextContent(text="legacy")]))

View file

@ -51,10 +51,10 @@ async def test_endpoint_event_names_the_message_endpoint_with_a_fresh_session_id
f"{BASE_URL}/sse", httpx_client_factory=httpx_client_factory, on_session_created=captured_session_id.append
)
with anyio.fail_after(5):
async with Client(transport) as client:
async with Client(transport, mode="legacy") as client:
assert len(captured_session_id) == 1
assert UUID(hex=captured_session_id[0]) in sse._read_stream_writers
assert await client.send_ping() == snapshot(EmptyResult())
assert await client.send_ping() == snapshot(EmptyResult()) # pyright: ignore[reportDeprecated]
assert sse._read_stream_writers == {}

View file

@ -89,8 +89,8 @@ async def test_tool_call_and_notification_round_trip_over_a_stdio_subprocess(
# Must exceed session time plus the patched PROCESS_TERMINATION_TIMEOUT (20s).
with anyio.fail_after(30):
async with Client(transport, logging_callback=collect) as client:
assert client.initialize_result.server_info.name == "stdio-echo"
async with Client(transport, mode="legacy", logging_callback=collect) as client:
assert client.server_info.name == "stdio-echo"
result = await client.call_tool("echo", {"text": "across\nprocesses"})
errlog.seek(0)

View file

@ -69,7 +69,7 @@ def _smoke_server() -> MCPServer:
async def test_tool_call_over_streamable_http_with_json_responses() -> None:
"""The round trip works when the server answers with a single JSON body instead of an SSE stream."""
async with connect_over_streamable_http(_smoke_server(), json_response=True) as client:
assert client.initialize_result.server_info.name == "smoke"
assert client.server_info.name == "smoke"
result = await client.call_tool("echo", {"text": "as json"})
assert result == snapshot(