Client-side 2026-07-28 support: .discover()/.adopt() + Client(mode=); request-metadata green (#2950)
This commit is contained in:
parent
ae13ede143
commit
f226d00d0a
7 changed files with 2533 additions and 993 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")]))
|
||||
|
|
|
|||
|
|
@ -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 == {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue