Align with spec #3002: optional clientInfo, serverInfo in result _meta (#3143)

This commit is contained in:
Max 2026-07-23 12:00:36 +01:00 • committed by GitHub
commit 837ef904f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 1757 additions and 567 deletions

42
tests/_stamp.py Normal file
View file

@ -0,0 +1,42 @@
"""Shared helper: strip the 2026-era serverInfo `_meta` stamp from a result.
Servers stamp `io.modelcontextprotocol/serverInfo` into every 2026-era
result's `_meta` and never into handshake-era ones. Suites whose expected
payloads should stay identity-free strip the stamp before exact comparison -
and the strip is strict, so a modern result that lost its stamp fails the
test instead of passing silently.
The interaction matrix does not use this function directly: its `unstamped`
fixture (tests/interaction/conftest.py) resolves per cell to this strict
strip on modern cells and to a must-not-be-stamped assertion on
handshake-era cells, so one comparison line enforces both eras.
"""
from typing import Any, Protocol, TypeVar
from mcp_types import SERVER_INFO_META_KEY, Result
R = TypeVar("R", bound=Result)
class Unstamp(Protocol):
"""An era-appropriate stamp normalizer: strips or forbids the stamp."""
def __call__(self, result: R) -> R: ...
def unstamped(result: R) -> R:
"""Assert the result carries a well-formed serverInfo stamp, then remove it.
Returns the result for inline use in comparisons. Use only where a stamp
is required (a 2026-era result); the interaction matrix's `unstamped`
fixture handles the era split.
"""
meta = result.meta
assert meta is not None and SERVER_INFO_META_KEY in meta, "expected a serverInfo stamp on this result"
stamp: Any = meta.pop(SERVER_INFO_META_KEY)
assert isinstance(stamp, dict)
assert "name" in stamp and "version" in stamp
if not meta:
result.meta = None
return result

View file

@ -119,6 +119,7 @@ async def test_client_is_initialized(app: MCPServer):
tools=ToolsCapability(list_changed=False),
)
)
assert client.server_info is not None
assert client.server_info.name == "test"
@ -134,7 +135,8 @@ async def test_client_with_simple_server(simple_server: Server):
resources = await client.list_resources()
assert resources == snapshot(
ListResourcesResult(
resources=[Resource(name="Test Resource", uri="memory://test", description="A test resource")]
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test_server", "version": ""}},
resources=[Resource(name="Test Resource", uri="memory://test", description="A test resource")],
)
)
@ -150,6 +152,7 @@ async def test_client_list_tools(app: MCPServer):
result = await client.list_tools()
assert result == snapshot(
ListToolsResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test", "version": ""}},
tools=[
Tool(
name="greet",
@ -167,7 +170,7 @@ async def test_client_list_tools(app: MCPServer):
"type": "object",
},
)
]
],
)
)
@ -177,6 +180,7 @@ async def test_client_call_tool(app: MCPServer):
result = await client.call_tool("greet", {"name": "World"})
assert result == snapshot(
CallToolResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test", "version": ""}},
content=[TextContent(text="Hello, World!")],
structured_content={"result": "Hello, World!"},
)
@ -189,7 +193,8 @@ async def test_read_resource(app: MCPServer):
result = await client.read_resource("test://resource")
assert result == snapshot(
ReadResourceResult(
contents=[TextResourceContents(uri="test://resource", mime_type="text/plain", text="Test content")]
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test", "version": ""}},
contents=[TextResourceContents(uri="test://resource", mime_type="text/plain", text="Test content")],
)
)
@ -269,6 +274,7 @@ async def test_get_prompt(app: MCPServer):
result = await client.get_prompt("greeting_prompt", {"name": "Alice"})
assert result == snapshot(
GetPromptResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test", "version": ""}},
description="A greeting prompt.",
messages=[PromptMessage(role="user", content=TextContent(text="Please greet Alice warmly."))],
)
@ -335,6 +341,7 @@ async def test_client_list_resources_with_params(app: MCPServer):
result = await client.list_resources()
assert result == snapshot(
ListResourcesResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test", "version": ""}},
resources=[
Resource(
name="test_resource",
@ -342,7 +349,7 @@ async def test_client_list_resources_with_params(app: MCPServer):
description="A test resource.",
mime_type="text/plain",
)
]
],
)
)
@ -351,7 +358,11 @@ async def test_client_list_resource_templates(app: MCPServer):
"""Test listing resource templates with params parameter."""
async with Client(app) as client:
result = await client.list_resource_templates()
assert result == snapshot(ListResourceTemplatesResult(resource_templates=[]))
assert result == snapshot(
ListResourceTemplatesResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test", "version": ""}}, resource_templates=[]
)
)
async def test_list_prompts(app: MCPServer):
@ -360,13 +371,14 @@ async def test_list_prompts(app: MCPServer):
result = await client.list_prompts()
assert result == snapshot(
ListPromptsResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test", "version": ""}},
prompts=[
Prompt(
name="greeting_prompt",
description="A greeting prompt.",
arguments=[PromptArgument(name="name", required=True)],
)
]
],
)
)
@ -376,7 +388,12 @@ async def test_complete_with_prompt_reference(simple_server: Server):
async with Client(simple_server) as client:
ref = types.PromptReference(type="ref/prompt", name="test_prompt")
result = await client.complete(ref=ref, argument={"name": "arg", "value": "test"})
assert result == snapshot(types.CompleteResult(completion=types.Completion(values=[])))
assert result == snapshot(
types.CompleteResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test_server", "version": ""}},
completion=types.Completion(values=[]),
)
)
def test_client_with_url_initializes_streamable_http_transport():
@ -573,6 +590,7 @@ async def test_client_auto_mode_falls_back_to_initialize_on_legacy_signal(code:
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 is not None
assert client.server_info.name == "legacy-only"
assert methods_seen == ["server/discover", "initialize", "notifications/initialized"]
@ -754,7 +772,11 @@ async def test_call_tool_auto_loop_dispatches_elicitation_then_returns_final_res
result = await client.call_tool("greet")
assert result == snapshot(
CallToolResult(content=[TextContent(text="Hello, Ada!")], structured_content={"result": "Hello, Ada!"})
CallToolResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test", "version": ""}},
content=[TextContent(text="Hello, Ada!")],
structured_content={"result": "Hello, Ada!"},
)
)
assert len(callback_params) == 1
assert isinstance(callback_params[0], types.ElicitRequestFormParams)
@ -800,7 +822,9 @@ async def test_call_tool_auto_loop_dispatches_sampling_then_returns_final_result
assert result == snapshot(
CallToolResult(
content=[TextContent(text="Model said: Paris")], structured_content={"result": "Model said: Paris"}
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test", "version": ""}},
content=[TextContent(text="Model said: Paris")],
structured_content={"result": "Model said: Paris"},
)
)
assert len(callback_params) == 1
@ -833,6 +857,7 @@ async def test_call_tool_auto_loop_dispatches_list_roots_then_returns_final_resu
assert result == snapshot(
CallToolResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test", "version": ""}},
content=[TextContent(text="Client exposed 1 root(s).")],
structured_content={"result": "Client exposed 1 root(s)."},
)
@ -917,7 +942,12 @@ async def test_get_prompt_auto_loop_resolves_input_required_via_callbacks() -> N
with anyio.fail_after(5):
async with Client(server, mode="2026-07-28", elicitation_callback=elicitation_callback) as client:
result = await client.get_prompt("summary")
assert result == snapshot(GetPromptResult(messages=[PromptMessage(role="user", content=TextContent(text="ok"))]))
assert result == snapshot(
GetPromptResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test", "version": ""}},
messages=[PromptMessage(role="user", content=TextContent(text="ok"))],
)
)
async def test_read_resource_auto_loop_resolves_input_required_via_callbacks() -> None:
@ -944,5 +974,8 @@ async def test_read_resource_auto_loop_resolves_input_required_via_callbacks() -
async with Client(server, mode="2026-07-28", elicitation_callback=elicitation_callback) as client:
result = await client.read_resource("memory://gated")
assert result == snapshot(
ReadResourceResult(contents=[TextResourceContents(uri="memory://gated", text="unlocked")])
ReadResourceResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "test", "version": ""}},
contents=[TextResourceContents(uri="memory://gated", text="unlocked")],
)
)

View file

@ -24,7 +24,6 @@ from mcp_types import (
ElicitRequest,
ElicitRequestFormParams,
ElicitResult,
Implementation,
InputRequiredResult,
ListPromptsResult,
ListResourcesResult,
@ -845,7 +844,6 @@ async def test_a_cache_hit_listing_still_mirrors_x_mcp_headers_on_tools_call() -
discover = DiscoverResult(
supported_versions=[LATEST_MODERN_VERSION],
capabilities=ServerCapabilities(),
server_info=Implementation(name="srv", version="0"),
)
with anyio.fail_after(5):

View file

@ -25,8 +25,8 @@ from mcp_types import (
METHOD_NOT_FOUND,
PARSE_ERROR,
REQUEST_TIMEOUT,
SERVER_INFO_META_KEY,
UNSUPPORTED_PROTOCOL_VERSION,
Implementation,
ServerCapabilities,
)
from mcp_types.version import (
@ -85,7 +85,7 @@ def _discover_dict(versions: list[str] | None = None) -> dict[str, Any]:
return types.DiscoverResult(
supported_versions=versions or list(MODERN_PROTOCOL_VERSIONS),
capabilities=ServerCapabilities(),
server_info=Implementation(name="stub", version="0"),
_meta={SERVER_INFO_META_KEY: {"name": "stub", "version": "0"}},
).model_dump(by_alias=True, mode="json", exclude_none=True)
@ -101,11 +101,13 @@ def _err_32022(supported: Any) -> MCPError:
async def test_a_valid_discover_result_is_adopted_without_initializing() -> None:
"""A parseable `DiscoverResult` from the probe is adopted; `initialize()` is never called."""
"""A parseable `DiscoverResult` from the probe is adopted intact — including the
`_meta` serverInfo stamp — and `initialize()` is never called."""
session = _StubSession(_discover_dict())
await _negotiate(session)
assert session.adopted is not None
assert session.adopted.server_info.name == "stub"
assert session.adopted.meta is not None
assert session.adopted.meta[SERVER_INFO_META_KEY] == {"name": "stub", "version": "0"}
assert not session.initialized
assert session.probed_at == [LATEST_MODERN_VERSION]
@ -119,6 +121,16 @@ async def test_an_unparseable_discover_result_falls_back_to_initialize() -> None
assert session.adopted is None
async def test_a_discover_result_advertising_only_legacy_versions_falls_back_to_initialize() -> None:
"""A server that answers the probe but lists no modern version has made an
explicit legacy advertisement (go-sdk's default stateful streamable server
does this), so the policy runs the handshake instead of raising on adopt."""
session = _StubSession(_discover_dict(list(HANDSHAKE_PROTOCOL_VERSIONS)))
await _negotiate(session)
assert session.initialized
assert session.adopted is None
# --- the denylist: every JSON-RPC error code falls back ---

View file

@ -101,7 +101,6 @@ def _adopt_modern(session: ClientSession) -> None:
types.DiscoverResult(
supported_versions=[LATEST_MODERN_VERSION],
capabilities=ServerCapabilities(),
server_info=Implementation(name="stub", version="0"),
)
)

View file

@ -16,6 +16,7 @@ from mcp_types import (
METHOD_NOT_FOUND,
PROTOCOL_VERSION_META_KEY,
REQUEST_TIMEOUT,
SERVER_INFO_META_KEY,
UNSUPPORTED_PROTOCOL_VERSION,
CallToolResult,
Implementation,
@ -1323,7 +1324,6 @@ def test_adopt_raises_when_no_mutual_modern_version_is_supported() -> None:
types.DiscoverResult(
supported_versions=["1999-01-01"],
capabilities=types.ServerCapabilities(),
server_info=types.Implementation(name="s", version="0"),
result_type="complete",
ttl_ms=0,
cache_scope="public",
@ -1514,7 +1514,6 @@ def _discover_result_dict() -> dict[str, Any]:
return types.DiscoverResult(
supported_versions=["2026-07-28"],
capabilities=ServerCapabilities(),
server_info=Implementation(name="stub", version="0"),
).model_dump(by_alias=True, mode="json", exclude_none=True)
@ -1654,12 +1653,13 @@ def test_era_neutral_properties_are_none_before_any_handshake() -> None:
@pytest.mark.anyio
async def test_era_neutral_properties_after_discover() -> None:
"""SDK-defined: after `discover()` the era-neutral accessors read from the
DiscoverResult; `initialize_result` stays None."""
DiscoverResult; `server_info` comes from the `_meta` serverInfo stamp and
`initialize_result` stays None."""
raw = types.DiscoverResult(
supported_versions=["2026-07-28"],
capabilities=ServerCapabilities(tools=types.ToolsCapability(list_changed=True)),
server_info=Implementation(name="discovered", version="2.0"),
instructions="hello",
_meta={SERVER_INFO_META_KEY: {"name": "discovered", "version": "2.0"}},
).model_dump(by_alias=True, mode="json", exclude_none=True)
dispatcher = _ScriptedDispatcher(raw)
with anyio.fail_after(5):
@ -1673,6 +1673,40 @@ async def test_era_neutral_properties_after_discover() -> None:
assert isinstance(session.discover_result, types.DiscoverResult)
@pytest.mark.anyio
async def test_server_info_is_none_when_the_discover_result_carries_no_stamp() -> None:
"""Spec-mandated (2026-07-28, #3002): the serverInfo result-`_meta` stamp is
optional, so a server that does not identify itself reads as `None` rather
than failing the connection."""
raw = types.DiscoverResult(
supported_versions=["2026-07-28"],
capabilities=ServerCapabilities(),
).model_dump(by_alias=True, mode="json", exclude_none=True)
dispatcher = _ScriptedDispatcher(raw)
with anyio.fail_after(5):
async with ClientSession(dispatcher=dispatcher) as session:
await session.discover()
assert session.protocol_version == "2026-07-28"
assert session.server_info is None
@pytest.mark.anyio
async def test_a_malformed_server_info_stamp_reads_as_absent() -> None:
"""Spec-mandated (2026-07-28, #3002): the stamp is self-reported and
display-only, so a value that is not an `Implementation` must not fail the
call; it reads as if the server sent none."""
raw = types.DiscoverResult(
supported_versions=["2026-07-28"],
capabilities=ServerCapabilities(),
_meta={SERVER_INFO_META_KEY: {"version": "no name makes this invalid"}},
).model_dump(by_alias=True, mode="json", exclude_none=True)
dispatcher = _ScriptedDispatcher(raw)
with anyio.fail_after(5):
async with ClientSession(dispatcher=dispatcher) as session:
await session.discover()
assert session.server_info is None
@pytest.mark.anyio
async def test_discover_reraises_unsupported_version_with_malformed_error_data() -> None:
"""SDK-defined: a -32022 reply whose `data` is not a valid

View file

@ -107,7 +107,6 @@ def _adopt_modern(session: ClientSession) -> None:
types.DiscoverResult(
supported_versions=[LATEST_MODERN_VERSION],
capabilities=ServerCapabilities(),
server_info=Implementation(name="stub", version="0"),
)
)

View file

@ -68,9 +68,21 @@ async def test_concurrent_tool_calls_resolve_out_of_order_to_their_own_callers()
assert completion_order == ["c", "b", "a"]
assert results == snapshot(
{
"c": CallToolResult(content=[TextContent(text="result:c")], structured_content={"result": "result:c"}),
"b": CallToolResult(content=[TextContent(text="result:b")], structured_content={"result": "result:b"}),
"a": CallToolResult(content=[TextContent(text="result:a")], structured_content={"result": "result:a"}),
"c": CallToolResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "parking", "version": ""}},
content=[TextContent(text="result:c")],
structured_content={"result": "result:c"},
),
"b": CallToolResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "parking", "version": ""}},
content=[TextContent(text="result:b")],
structured_content={"result": "result:b"},
),
"a": CallToolResult(
_meta={"io.modelcontextprotocol/serverInfo": {"name": "parking", "version": ""}},
content=[TextContent(text="result:a")],
structured_content={"result": "result:a"},
),
}
)

View file

@ -7,7 +7,7 @@ import logging
import anyio
import mcp_types as types
import pytest
from mcp_types import EmptyResult, Implementation, ServerCapabilities
from mcp_types import EmptyResult, ServerCapabilities
from mcp_types.version import LATEST_MODERN_VERSION
from pydantic import BaseModel
@ -42,7 +42,6 @@ def _adopt_modern(session: ClientSession) -> None:
types.DiscoverResult(
supported_versions=[LATEST_MODERN_VERSION],
capabilities=ServerCapabilities(),
server_info=Implementation(name="stub", version="0"),
)
)

View file

@ -408,7 +408,6 @@ async def test_listen_on_a_never_entered_session_raises_runtime_error():
types.DiscoverResult(
supported_versions=["2026-07-28"],
capabilities=types.ServerCapabilities(),
server_info=types.Implementation(name="stub", version="0"),
)
)
with pytest.raises(RuntimeError, match="entered session"):

View file

@ -0,0 +1,23 @@
"""Shared helpers for the docs_src tests."""
from typing import TypeVar
from mcp_types import SERVER_INFO_META_KEY, Result
from mcp.server import Server
from mcp.server.mcpserver import MCPServer
R = TypeVar("R", bound=Result)
def strip_server_info(result: R, server: Server | MCPServer) -> R:
"""Assert the 2026-era serverInfo stamp, then drop it so snapshots stay focused.
The doc snippets set no explicit version, so the stamp's version is empty;
the fenced outputs in the docs pages leave the stamp out, and the tests
mirror the fences.
"""
assert result.meta is not None
assert result.meta[SERVER_INFO_META_KEY] == {"name": server.name, "version": ""}
result.meta = None
return result

View file

@ -27,6 +27,7 @@ async def test_every_client_program_on_the_page_runs(capsys: pytest.CaptureFixtu
async def test_connected_properties_are_populated_inside_the_block() -> None:
"""tutorial001: server_info, server_capabilities, protocol_version and instructions are just there."""
async with Client(tutorial001.mcp) as client:
assert client.server_info is not None
assert client.server_info.name == "Bookshop"
assert client.protocol_version == "2026-07-28"
assert client.instructions == "Search the catalog before recommending a book."
@ -38,6 +39,7 @@ async def test_a_client_is_not_reusable_after_the_block_ends() -> None:
"""tutorial001: `async with` is the whole lifecycle. Construct a new Client per connection."""
client = Client(tutorial001.mcp)
async with client:
assert client.server_info is not None
assert client.server_info.name == "Bookshop"
with pytest.raises(RuntimeError, match="cannot reenter"):
await client.__aenter__()

View file

@ -22,6 +22,7 @@ async def test_the_in_memory_program_on_the_page_runs(capsys: pytest.CaptureFixt
async def test_in_memory_client_talks_to_the_server_object() -> None:
"""tutorial001: passing the server object connects in-process. No subprocess, no port."""
async with Client(tutorial001.mcp) as client:
assert client.server_info is not None
assert client.server_info.name == "Bookshop"
assert client.protocol_version == "2026-07-28"
result = await client.call_tool("search_books", {"query": "dune"})

View file

@ -6,6 +6,7 @@ from mcp_types import CallToolResult, TextContent, TextResourceContents
from docs_src.index.tutorial001 import mcp
from mcp import Client
from tests.docs_src._helpers import strip_server_info
# `pyproject.toml` globally downgrades `mcp.MCPDeprecationWarning` to *ignore* because the
# SDK still calls those methods internally. A documentation example must never lean on
@ -18,6 +19,7 @@ pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDepre
async def test_add_tool() -> None:
async with Client(mcp) as client:
result = await client.call_tool("add", {"a": 1, "b": 2})
result = strip_server_info(result, mcp)
assert result == snapshot(
CallToolResult(content=[TextContent(type="text", text="3")], structured_content={"result": 3})
)

View file

@ -9,6 +9,7 @@ from mcp_types import CallToolResult, TextContent
from docs_src.logging import tutorial001
from mcp import Client
from mcp.server import MCPServer
from tests.docs_src._helpers import strip_server_info
# See test_index.py for why this is a per-module mark and not a conftest hook.
pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")]
@ -28,6 +29,7 @@ async def test_the_log_line_never_reaches_the_client() -> None:
"""tutorial001: the result is only the return value. Log output is invisible to the model."""
async with Client(tutorial001.mcp) as client:
result = await client.call_tool("search_books", {"query": "dune"})
result = strip_server_info(result, tutorial001.mcp)
assert result == snapshot(
CallToolResult(
content=[TextContent(type="text", text="Found 3 books matching 'dune'.")],

View file

@ -2,7 +2,15 @@
import pytest
from inline_snapshot import snapshot
from mcp_types import INTERNAL_ERROR, CallToolRequestParams, CallToolResult, ErrorData, RequestParams, TextContent
from mcp_types import (
INTERNAL_ERROR,
SERVER_INFO_META_KEY,
CallToolRequestParams,
CallToolResult,
ErrorData,
RequestParams,
TextContent,
)
from docs_src.lowlevel import tutorial001, tutorial002, tutorial003, tutorial004, tutorial005, tutorial006
from mcp import Client, MCPError
@ -79,8 +87,17 @@ async def test_output_schema_and_structured_content_are_both_yours_to_build() ->
}
)
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
assert result.content == [TextContent(type="text", text="Found 3 books matching 'dune'.")]
assert result.structured_content == {"matches": 3, "query": "dune"}
# The page shows this exact payload; tutorial003 pins `version="2.0.0"` so
# the identity stamp is deterministic and the fence is proved verbatim.
assert result.model_dump(by_alias=True, exclude_none=True) == snapshot(
{
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}},
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
"structuredContent": {"matches": 3, "query": "dune"},
"isError": False,
"resultType": "complete",
}
)
async def test_the_client_checks_the_schema_you_promised() -> None:
@ -99,6 +116,10 @@ async def test_meta_reaches_the_client_application() -> None:
"""tutorial004: `_meta=` on the result comes back as `result.meta` and serialises under `_meta`."""
async with Client(tutorial004.server) as client:
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
assert result.meta is not None
# The server identity stamp shares `_meta` with the handler's keys without clobbering
# them. Remove it before the exact compares: the page's fence leaves the stamp out.
del result.meta[SERVER_INFO_META_KEY]
assert result.meta == {"bookshop/record_ids": ["bk_17", "bk_42", "bk_99"]}
assert result.model_dump(by_alias=True, exclude_none=True) == snapshot(
{

View file

@ -93,6 +93,7 @@ def test_raw_data_without_a_format_falls_back_to_a_default_mime_type() -> None:
async def test_icons_are_visible_where_they_were_declared() -> None:
"""tutorial004: server icons land on `server_info`, tool icons on the `Tool`, resource icons on the `Resource`."""
async with Client(tutorial004.mcp) as client:
assert client.server_info is not None
assert client.server_info.icons == [
Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])
]

View file

@ -22,6 +22,7 @@ from docs_src.mrtr import tutorial001, tutorial002, tutorial003, tutorial004, tu
from mcp import Client, MCPError
from mcp.client import ClientRequestContext
from mcp.server.mcpserver import InvalidRequestState
from tests.docs_src._helpers import strip_server_info
# See test_index.py for why this is a per-module mark and not a conftest hook.
pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")]
@ -31,6 +32,7 @@ async def test_first_call_returns_an_input_required_result() -> None:
"""tutorial001: a tool that is missing input returns `InputRequiredResult` instead of calling back."""
async with Client(tutorial001.server) as client:
result = await client.session.call_tool("provision", {"name": "orders"}, allow_input_required=True)
result = strip_server_info(result, tutorial001.server)
assert result == snapshot(
InputRequiredResult(
result_type="input_required",
@ -57,6 +59,7 @@ async def test_the_auto_loop_drives_the_call_to_completion() -> None:
"""tutorial003: register `elicitation_callback`, call the tool, get a plain `CallToolResult` back."""
async with Client(tutorial001.server, elicitation_callback=tutorial003.handle_elicitation) as client:
result = await client.call_tool("provision", {"name": "orders"})
result = strip_server_info(result, tutorial001.server)
assert result == snapshot(
CallToolResult(content=[TextContent(type="text", text="Provisioned 'orders' in eu-west-1.")])
)
@ -80,6 +83,7 @@ async def test_retry_with_input_responses_and_request_state_completes_the_call()
input_responses={"region": ElicitResult(action="accept", content={"region": "eu-west-1"})},
request_state="provision-v1",
)
result = strip_server_info(result, tutorial001.server)
assert result == snapshot(
CallToolResult(content=[TextContent(type="text", text="Provisioned 'orders' in eu-west-1.")])
)
@ -89,6 +93,7 @@ async def test_the_manual_loop_drives_the_call_to_completion() -> None:
"""tutorial002: `client.session.call_tool(..., allow_input_required=True)` for callers who own the loop."""
async with Client(tutorial001.server) as client:
result = await tutorial002.provision(client, "billing")
result = strip_server_info(result, tutorial001.server)
assert result == snapshot(
CallToolResult(content=[TextContent(type="text", text="Provisioned 'billing' in eu-west-1.")])
)
@ -121,6 +126,7 @@ async def test_a_prompt_returns_an_input_required_result_on_the_first_round() ->
returns the `InputRequiredResult` itself."""
async with Client(tutorial004.mcp) as client:
result = await client.session.get_prompt("briefing", allow_input_required=True)
result = strip_server_info(result, tutorial004.mcp)
assert result == snapshot(
InputRequiredResult(
result_type="input_required",
@ -151,6 +157,7 @@ async def test_the_prompt_auto_loop_returns_the_final_messages() -> None:
caller sees only the complete `GetPromptResult`."""
async with Client(tutorial004.mcp, elicitation_callback=_answer_audience) as client:
result = await client.get_prompt("briefing")
result = strip_server_info(result, tutorial004.mcp)
assert result == snapshot(
GetPromptResult(
description="Draft a briefing tuned to its audience.",

View file

@ -8,6 +8,7 @@ from mcp_types import PromptArgument, PromptMessage, TextContent
from docs_src.prompts import tutorial001, tutorial002, tutorial003
from mcp import Client, MCPError
from tests.docs_src._helpers import strip_server_info
# See test_index.py for why this is a per-module mark and not a conftest hook.
pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")]
@ -30,6 +31,7 @@ async def test_returned_string_becomes_one_user_message() -> None:
"""tutorial001: a `str` return value is rendered as a single `user` message."""
async with Client(tutorial001.mcp) as client:
result = await client.get_prompt("review_code", {"code": "def add(a, b): return a + b"})
result = strip_server_info(result, tutorial001.mcp)
assert result.model_dump(mode="json", by_alias=True, exclude_none=True) == snapshot(
{
"description": "Review a piece of code.",

View file

@ -3,7 +3,7 @@
import re
import pytest
from mcp_types import DiscoverResult, Implementation, ServerCapabilities
from mcp_types import SERVER_INFO_META_KEY, DiscoverResult, Implementation, ServerCapabilities
from docs_src.protocol_versions import tutorial001, tutorial002, tutorial003, tutorial004
from mcp import Client
@ -16,6 +16,7 @@ async def test_auto_lands_on_the_modern_version() -> None:
"""tutorial001: the default `mode="auto"` probes `server/discover` and adopts the result."""
async with Client(tutorial001.mcp) as client:
assert client.protocol_version == "2026-07-28"
assert client.server_info is not None
assert client.server_info.name == "Bookshop"
assert client.session.discover_result is not None
assert client.session.initialize_result is None
@ -25,18 +26,18 @@ async def test_legacy_forces_the_initialize_handshake() -> None:
"""tutorial002: `mode="legacy"` runs `initialize` against the very same server."""
async with Client(tutorial002.mcp, mode="legacy") as client:
assert client.protocol_version == "2025-11-25"
assert client.server_info is not None
assert client.server_info.name == "Bookshop"
assert client.session.initialize_result is not None
assert client.session.discover_result is None
async def test_version_pin_sends_nothing_and_knows_nothing() -> None:
"""tutorial003: a pin adopts the version locally; `server_info` and capabilities are blank."""
"""tutorial003: a pin adopts the version locally; `server_info` is None and capabilities are blank."""
async with Client(tutorial003.mcp, mode="2026-07-28") as client:
assert client.protocol_version == "2026-07-28"
assert client.server_info == Implementation(name="", version="")
# The `!!! check` fence is the literal `print(client.server_info)` output.
assert str(client.server_info) == "name='' title=None version='' description=None website_url=None icons=None"
# The `!!! check` fence is the literal `print(client.server_info)` output: None.
assert client.server_info is None
assert client.server_capabilities == ServerCapabilities()
result = await client.call_tool("search_books", {"query": "dune"})
assert result.structured_content == {"result": "Found 3 books matching 'dune'."}
@ -63,6 +64,7 @@ async def test_prior_discover_round_trips() -> None:
async with Client(tutorial004.mcp, mode="2026-07-28", prior_discover=saved) as client:
assert client.protocol_version == "2026-07-28"
assert client.server_info is not None
assert client.server_info.name == "Bookshop"
assert client.server_capabilities.tools is not None
@ -77,6 +79,7 @@ async def test_discover_result_survives_json() -> None:
assert restored == saved
async with Client(tutorial004.mcp, mode="2026-07-28", prior_discover=restored) as client:
assert client.server_info is not None
assert client.server_info.name == "Bookshop"
@ -85,9 +88,14 @@ async def test_prior_discover_is_ignored_unless_mode_is_a_pin() -> None:
stale = DiscoverResult(
supported_versions=["2026-07-28"],
capabilities=ServerCapabilities(),
server_info=Implementation(name="Stale", version="0.0.0"),
_meta={
SERVER_INFO_META_KEY: Implementation(name="Stale", version="0.0.0").model_dump(
by_alias=True, mode="json", exclude_none=True
)
},
)
async with Client(tutorial004.mcp, prior_discover=stale) as client:
assert client.server_info is not None
assert client.server_info.name == "Bookshop"
async with Client(tutorial004.mcp, mode="legacy", prior_discover=stale) as client:
assert client.session.discover_result is None

Some files were not shown because too many files have changed in this diff Show more