This commit is contained in:
parent
3a6f2996cd
commit
837ef904f8
9 changed files with 1757 additions and 567 deletions
|
|
@ -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")],
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
Loading…
Reference in a new issue