chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 12:10:27 +08:00
commit 49b9bb6724
992 changed files with 161690 additions and 0 deletions
@@ -0,0 +1,370 @@
"""Client connect-time negotiation: mode selection, server/discover, and the per-request envelope.
These tests pin what `Client(..., mode=...)` puts on the wire BEFORE the caller's first call --
the legacy initialize handshake, the modern `server/discover` probe, or nothing at all -- and
that a modern-negotiated session stamps the three-key `io.modelcontextprotocol/*` `_meta`
envelope on every subsequent request. Each test drives the highest public surface (`Client`)
and observes traffic at a recording seam: `RecordingTransport` for the legacy stream pair, and
`mounted_app`'s httpx event hook for the in-process streamable-HTTP transport.
The fallback test alone hand-plays the server's side of the wire, because no real `Server`
answers `server/discover` with -32601.
"""
import json
from collections.abc import AsyncIterator, Awaitable, Callable
from contextlib import asynccontextmanager
import anyio
import httpx
import mcp_types as types
import pytest
from mcp_types import (
CLIENT_CAPABILITIES_META_KEY,
CLIENT_INFO_META_KEY,
INVALID_REQUEST,
METHOD_NOT_FOUND,
PROTOCOL_VERSION_META_KEY,
UNSUPPORTED_PROTOCOL_VERSION,
DiscoverResult,
Implementation,
InitializeResult,
JSONRPCError,
JSONRPCNotification,
JSONRPCRequest,
JSONRPCResponse,
ServerCapabilities,
ToolsCapability,
)
from mcp_types.version import LATEST_HANDSHAKE_VERSION, LATEST_MODERN_VERSION, MODERN_PROTOCOL_VERSIONS
from mcp import MCPError
from mcp.client._memory import InMemoryTransport
from mcp.client._transport import TransportStreams
from mcp.client.client import Client
from mcp.client.streamable_http import streamable_http_client
from mcp.server import Server, ServerRequestContext
from mcp.shared.memory import MessageStream, create_client_server_memory_streams
from mcp.shared.message import SessionMessage
from tests.interaction._connect import BASE_URL, Connect, mounted_app
from tests.interaction._helpers import RecordingTransport
from tests.interaction._requirements import requirement
pytestmark = pytest.mark.anyio
def _tools_server(name: str = "negotiator") -> Server:
"""A low-level server with one list-tools handler, so a feature request has something to reach."""
async def list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
return types.ListToolsResult(tools=[types.Tool(name="noop", input_schema={"type": "object"})])
return Server(name, on_list_tools=list_tools)
def _request_recorder() -> tuple[list[httpx.Request], Callable[[httpx.Request], Awaitable[None]]]:
"""Return a list and an `on_request` hook that appends each outgoing httpx request to it."""
captured: list[httpx.Request] = []
async def on_request(request: httpx.Request) -> None:
captured.append(request)
return captured, on_request
@requirement("lifecycle:mode:legacy-never-probes")
async def test_legacy_mode_sends_initialize_and_never_probes_discover() -> None:
"""`Client(server, mode='legacy')` opens with `initialize` and never sends `server/discover`.
Requirement `lifecycle:mode:legacy-never-probes` (sdk-defined): ``mode='legacy'`` must remain
byte-identical to the pre-2026 client so a 2025-era server never observes modern vocabulary.
"""
recording = RecordingTransport(InMemoryTransport(_tools_server()))
with anyio.fail_after(5):
async with Client(recording, mode="legacy") as client:
await client.list_tools()
sent = [m.message for m in recording.sent]
methods = [m.method for m in sent if isinstance(m, JSONRPCRequest | JSONRPCNotification)]
assert methods[0] == "initialize"
assert "server/discover" not in methods
assert "notifications/initialized" in methods
@requirement("lifecycle:mode:pin-never-handshakes")
async def test_pinned_mode_sends_no_connect_time_traffic() -> None:
"""`Client(..., mode='2026-07-28')` sends nothing on entry; the caller's first call is the first wire request.
Requirement `lifecycle:mode:pin-never-handshakes` (sdk-defined): a version pin adopts a
synthesized DiscoverResult locally, so no `initialize` and no `server/discover` ever cross
the wire. Asserted at the in-process streamable-HTTP seam via the httpx event hook.
"""
requests, on_request = _request_recorder()
with anyio.fail_after(5):
async with (
mounted_app(_tools_server(), on_request=on_request) as (http, _),
Client(streamable_http_client(f"{BASE_URL}/mcp", http_client=http), mode=LATEST_MODERN_VERSION) as client,
):
assert requests == [] # entering the Client produced zero HTTP traffic
result = await client.list_tools()
bodies = [json.loads(r.content) for r in requests]
assert [b["method"] for b in bodies] == ["tools/list"]
assert PROTOCOL_VERSION_META_KEY in bodies[0]["params"]["_meta"]
assert [t.name for t in result.tools] == ["noop"]
@requirement("lifecycle:mode:prior-discover-zero-rtt")
async def test_prior_discover_populates_state_with_zero_connect_time_traffic() -> None:
"""`Client(..., mode=<pin>, prior_discover=...)` sends nothing on entry and exposes the prior server_info.
Requirement `lifecycle:mode:prior-discover-zero-rtt` (sdk-defined): a previously-obtained
DiscoverResult is installed via `adopt()` so server_info and capabilities are available
immediately with zero round trips.
"""
prior = DiscoverResult(
supported_versions=[LATEST_MODERN_VERSION],
capabilities=ServerCapabilities(tools=ToolsCapability(list_changed=False)),
server_info=Implementation(name="cached-server", version="9.9.9"),
)
requests, on_request = _request_recorder()
with anyio.fail_after(5):
async with (
mounted_app(_tools_server(), on_request=on_request) as (http, _),
Client(
streamable_http_client(f"{BASE_URL}/mcp", http_client=http),
mode=LATEST_MODERN_VERSION,
prior_discover=prior,
) as client,
):
assert requests == []
assert client.server_info == Implementation(name="cached-server", version="9.9.9")
assert client.server_capabilities.tools == ToolsCapability(list_changed=False)
await client.list_tools()
assert [json.loads(r.content)["method"] for r in requests] == ["tools/list"]
@requirement("lifecycle:discover:basic")
async def test_auto_mode_probes_server_discover_and_adopts_the_result() -> None:
"""`Client(..., mode='auto')` sends `server/discover` first and adopts the returned version and server_info.
Requirement `lifecycle:discover:basic` (spec basic/lifecycle#discover): the probe is a
single `server/discover` request whose result carries supported versions, capabilities,
server_info and the cache-hint fields, after which the session is modern-negotiated.
"""
requests, on_request = _request_recorder()
server = _tools_server("discoverable")
with anyio.fail_after(5):
async with (
mounted_app(server, on_request=on_request) as (http, _),
Client(streamable_http_client(f"{BASE_URL}/mcp", http_client=http), mode="auto") as client,
):
assert client.protocol_version == LATEST_MODERN_VERSION
assert client.server_info.name == "discoverable"
await client.list_tools()
bodies = [json.loads(r.content) for r in requests]
assert bodies[0]["method"] == "server/discover"
assert "initialize" not in [b["method"] for b in bodies]
@requirement("lifecycle:discover:retry-on-32022")
async def test_auto_mode_retries_discover_once_on_unsupported_protocol_version() -> None:
"""A -32022 from `server/discover` triggers exactly one retry at the highest mutual modern version.
Requirement `lifecycle:discover:retry-on-32022` (spec basic/lifecycle#version-errors): the
client intersects `error.data.supported` with its own modern versions and re-probes once;
the second success is adopted. The server's `server/discover` handler is overridden to fail
the first call and succeed on the second.
"""
calls: list[str | None] = []
async def discover(ctx: ServerRequestContext, params: types.RequestParams | None) -> DiscoverResult:
proposed = ctx.meta.get(PROTOCOL_VERSION_META_KEY) if ctx.meta else None
calls.append(proposed)
if len(calls) == 1:
raise MCPError(
code=UNSUPPORTED_PROTOCOL_VERSION,
message="unsupported protocol version",
data={"supported": list(MODERN_PROTOCOL_VERSIONS), "requested": proposed},
)
return DiscoverResult(
supported_versions=list(MODERN_PROTOCOL_VERSIONS),
capabilities=ServerCapabilities(),
server_info=Implementation(name="picky", version="1.0.0"),
)
server = _tools_server("picky")
server.add_request_handler("server/discover", types.RequestParams, discover)
requests, on_request = _request_recorder()
with anyio.fail_after(5):
async with (
mounted_app(server, on_request=on_request) as (http, _),
Client(streamable_http_client(f"{BASE_URL}/mcp", http_client=http), mode="auto") as client,
):
assert client.protocol_version == LATEST_MODERN_VERSION
assert calls == [LATEST_MODERN_VERSION, LATEST_MODERN_VERSION]
assert [json.loads(r.content)["method"] for r in requests][:2] == ["server/discover", "server/discover"]
@requirement("lifecycle:discover:network-error-raises")
async def test_auto_mode_propagates_a_network_error_from_discover_without_initializing() -> None:
"""A network/connection error during `server/discover` propagates to the caller without falling back.
Requirement `lifecycle:discover:network-error-raises` (sdk-defined): under the denylist policy
every server-sent rpc-error and every transport-layer 4xx falls back to `initialize()`; the
only probe failures that reach the caller are real outages — network errors, anyio resource
errors, and the disjoint-modern -32022 case. Exercised here as an `httpx.ConnectError` from
the underlying transport, which the policy must not classify as an era verdict. The error
reaches the test wrapped in the streamable-http transport's task-group teardown, so
`pytest.RaisesGroup` flattens before matching. The probe POST is recorded before the
transport raises, so the `initialize` fallback observably did not happen.
"""
requests: list[httpx.Request] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
raise httpx.ConnectError("connection refused")
with anyio.fail_after(5):
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http:
with pytest.RaisesGroup(httpx.ConnectError, flatten_subgroups=True): # pragma: no branch
async with Client(streamable_http_client(f"{BASE_URL}/mcp", http_client=http), mode="auto"):
raise NotImplementedError("entering the Client should have raised") # pragma: no cover
assert [json.loads(r.content)["method"] for r in requests] == ["server/discover"]
@requirement("lifecycle:discover:fallback-method-not-found")
@pytest.mark.parametrize(
("probe_code", "probe_message"),
[
(METHOD_NOT_FOUND, "Method not found"),
(INVALID_REQUEST, "Bad Request: Missing session ID"),
],
ids=["method-not-found", "invalid-request"],
)
async def test_auto_mode_falls_back_to_initialize_on_a_legacy_probe_rejection(
probe_code: int, probe_message: str
) -> None:
"""A legacy server's rejection of `server/discover` makes an auto-negotiating client fall back to `initialize`.
Requirement `lifecycle:discover:fallback-method-not-found` (spec stdio#backward-compatibility):
a legacy-era server that does not implement `server/discover` is connected to via the
handshake, and the session lands at a handshake-era protocol version. The probe rejection
arrives as METHOD_NOT_FOUND from a server that routes the unknown method, or as
INVALID_REQUEST from a deployed v1.x stateful streamable-HTTP server that rejects the
session-id-less probe before dispatch. A real `Server` always implements `server/discover`,
so this test plays the server's side of the wire by hand. Reserve this pattern for behaviour
no real server can be made to produce.
"""
methods_seen: list[str] = []
async def scripted_server(streams: MessageStream) -> None:
server_read, server_write = streams
async for message in server_read:
assert isinstance(message, SessionMessage)
frame = message.message
assert isinstance(frame, JSONRPCRequest | JSONRPCNotification)
methods_seen.append(frame.method)
if isinstance(frame, JSONRPCRequest) and frame.method == "server/discover":
error = types.ErrorData(code=probe_code, message=probe_message)
await server_write.send(SessionMessage(JSONRPCError(jsonrpc="2.0", id=frame.id, error=error)))
elif isinstance(frame, JSONRPCRequest) and frame.method == "initialize":
result = InitializeResult(
protocol_version=LATEST_HANDSHAKE_VERSION,
capabilities=ServerCapabilities(),
server_info=Implementation(name="legacy-only", version="0.0.1"),
)
await server_write.send(
SessionMessage(
JSONRPCResponse(
jsonrpc="2.0",
id=frame.id,
result=result.model_dump(by_alias=True, mode="json", exclude_none=True),
)
)
)
# notifications/initialized (and anything else) is observed and ignored.
@asynccontextmanager
async def scripted_transport() -> AsyncIterator[TransportStreams]:
async with (
create_client_server_memory_streams() as ((client_read, client_write), server_streams),
anyio.create_task_group() as tg,
):
tg.start_soon(scripted_server, server_streams)
yield client_read, client_write
tg.cancel_scope.cancel()
with anyio.fail_after(5):
async with Client(scripted_transport(), mode="auto") as client:
assert client.protocol_version == LATEST_HANDSHAKE_VERSION
assert client.server_info.name == "legacy-only"
assert methods_seen == ["server/discover", "initialize", "notifications/initialized"]
@requirement("lifecycle:envelope:stamped-on-every-request")
async def test_every_request_on_a_modern_session_carries_the_three_key_meta_envelope(connect: Connect) -> None:
"""Each modern-session request's `params._meta` carries protocolVersion, clientInfo and clientCapabilities.
Requirement `lifecycle:envelope:stamped-on-every-request` (spec basic#_meta): the per-request
envelope replaces the initialize handshake's once-per-session exchange. Asserted server-side
by capturing `ctx.meta` inside the handler.
"""
observed: list[dict[str, object]] = []
async def list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
assert ctx.meta is not None
observed.append(dict(ctx.meta))
return types.ListToolsResult(tools=[])
server = Server("stamped", on_list_tools=list_tools)
with anyio.fail_after(5):
async with connect(server, client_info=Implementation(name="enveloper", version="1.2.3")) as client:
await client.list_tools()
await client.list_tools()
assert len(observed) == 2
for meta in observed:
assert meta[PROTOCOL_VERSION_META_KEY] == LATEST_MODERN_VERSION
assert meta[CLIENT_INFO_META_KEY] == {"name": "enveloper", "version": "1.2.3"}
assert CLIENT_CAPABILITIES_META_KEY in meta
@requirement("lifecycle:envelope:header-matches-meta")
async def test_http_protocol_version_header_matches_meta_protocol_version_on_every_post() -> None:
"""On streamable-HTTP, the `MCP-Protocol-Version` header on each POST equals `_meta.protocolVersion` in its body.
Requirement `lifecycle:envelope:header-matches-meta` (spec streamable-http#headers): the
body-derived header and the envelope's protocol version are kept in lockstep so the server's
header-based routing and body-based validation never disagree.
"""
requests, on_request = _request_recorder()
with anyio.fail_after(5):
async with (
mounted_app(_tools_server(), on_request=on_request) as (http, _),
Client(streamable_http_client(f"{BASE_URL}/mcp", http_client=http), mode=LATEST_MODERN_VERSION) as client,
):
await client.list_tools()
await client.list_tools()
assert requests, "no HTTP traffic recorded"
for request in requests:
body = json.loads(request.content)
assert request.headers["mcp-protocol-version"] == body["params"]["_meta"][PROTOCOL_VERSION_META_KEY]
assert request.headers["mcp-protocol-version"] == LATEST_MODERN_VERSION