Skip to content

Commit 9c80936

Browse files
committed
Testing-standards fixes: hollow-proof, provenance docstrings, constants, assertion patterns
Fixes from the testing-standards review of the new/modified test files. Hollow-proof (the two SERIOUS findings): - test_standalone_outbound_defaults_to_request_outbound: drop outbound= from from_envelope so conn.outbound is the no-channel sentinel; traffic on the request outbound now proves the default - test_server_info_version_falls_back_to_package: assert == importlib.metadata.version('mcp') Assertion patterns: - test_stateless_mode.py: 8x match= -> exc.value.method (structured attribute, not message text) - test_streamable_http_modern.py: literal 404/400 status asserts (impl reads same table, so table[X]==table[X] proved nothing); restore full-object json equality with id:None pin - test_inbound.py: assert isinstance(route, InboundModernRoute) before frozen check Constants: replace version/header/meta-key string literals with named imports across test_session.py / test_streamable_http_modern.py. Hygiene: drop from __future__ import annotations from tests/shared/test_inbound.py and src/mcp/shared/inbound.py (no forward refs); duplicate StubOutbound locally in test_stateless_mode.py (no cross-file test imports); _StubDispatchContext methods raise NotImplementedError; private _request_handlers -> public get_request_handler. Docstrings: add Spec-mandated:/SDK-defined: provenance tags to ~50 tests across 7 files. Naming: test_cacheable_defaults -> test_discover_result_defaults_to_immediately_stale_private_cache; test_header_rung_skipped_when_headers_none -> test_header_rung_does_not_reject_when_headers_arg_is_none; drop 'handler' from test_serve_one_maps_error_... (it's kernel METHOD_NOT_FOUND).
1 parent bb9b134 commit 9c80936

8 files changed

Lines changed: 189 additions & 78 deletions

File tree

src/mcp/shared/inbound.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
status.
99
"""
1010

11-
from __future__ import annotations
12-
1311
from collections.abc import Mapping, Sequence
1412
from dataclasses import dataclass
1513
from types import MappingProxyType

tests/server/lowlevel/test_server_discover.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Direct-handler tests for the auto-derived `server/discover` handler.
22
3-
These call the registered handler straight from `Server._request_handlers`
4-
without spinning up a `ServerRunner` or any transport, so they verify the
5-
handler's contract in isolation from the dispatch pipeline.
3+
These call the registered handler via the public `Server.get_request_handler`
4+
accessor without spinning up a `ServerRunner` or any transport, so they verify
5+
the handler's contract in isolation from the dispatch pipeline.
66
"""
77

8+
import importlib.metadata
89
from typing import Any, cast
910

1011
import pytest
@@ -13,23 +14,26 @@
1314
from mcp.server import Server, ServerRequestContext
1415
from mcp.shared.version import MODERN_PROTOCOL_VERSIONS
1516

16-
# The default handler ignores its `ctx` argument entirely (it derives the
17+
# `Server._handle_discover` ignores its `ctx` argument entirely (it derives the
1718
# result from server state), so a sentinel keeps the call site type-correct
1819
# without dragging session machinery into a unit test.
1920
_UNUSED_CTX = cast("ServerRequestContext[Any]", None)
2021

2122

2223
async def _discover(server: Server[Any]) -> types.DiscoverResult:
23-
entry = server._request_handlers["server/discover"]
24+
entry = server.get_request_handler("server/discover")
25+
assert entry is not None
2426
result = await entry.handler(_UNUSED_CTX, types.RequestParams())
2527
assert isinstance(result, types.DiscoverResult)
2628
return result
2729

2830

2931
def test_registered_by_default() -> None:
32+
"""SDK-defined: a bare `Server` registers a `server/discover` handler out of
33+
the box, typed for the base `RequestParams`."""
3034
server = Server("test-server")
31-
assert "server/discover" in server._request_handlers
32-
entry = server._request_handlers["server/discover"]
35+
entry = server.get_request_handler("server/discover")
36+
assert entry is not None
3337
assert entry.params_type is types.RequestParams
3438

3539

@@ -43,6 +47,8 @@ async def test_supported_versions_is_modern_set() -> None:
4347

4448
@pytest.mark.anyio
4549
async def test_server_info_reflects_constructor_fields() -> None:
50+
"""SDK-defined: `serverInfo` is built field-for-field from the `Server`
51+
constructor arguments."""
4652
icons = [types.Icon(src="https://example.test/icon.png")]
4753
server = Server(
4854
"info-server",
@@ -65,15 +71,16 @@ async def test_server_info_reflects_constructor_fields() -> None:
6571

6672
@pytest.mark.anyio
6773
async def test_server_info_version_falls_back_to_package() -> None:
68-
"""When no explicit version is supplied, `serverInfo.version` is still a
69-
non-empty string (the installed `mcp` package version)."""
74+
"""SDK-defined: when no explicit version is supplied, `serverInfo.version`
75+
falls back to the installed `mcp` package version."""
7076
result = await _discover(Server("unversioned"))
71-
assert isinstance(result.server_info.version, str)
72-
assert result.server_info.version
77+
assert result.server_info.version == importlib.metadata.version("mcp")
7378

7479

7580
@pytest.mark.anyio
7681
async def test_instructions_threaded_through() -> None:
82+
"""SDK-defined: the `instructions` constructor argument is passed through
83+
verbatim, defaulting to `None` when omitted."""
7784
server = Server("inst-server", instructions="Read the docs first.")
7885
result = await _discover(server)
7986
assert result.instructions == "Read the docs first."
@@ -84,8 +91,9 @@ async def test_instructions_threaded_through() -> None:
8491

8592
@pytest.mark.anyio
8693
async def test_capabilities_derived_from_registered_handlers() -> None:
87-
"""Capabilities are computed at handler call time from the live registry,
88-
so post-construction `add_request_handler` calls are reflected."""
94+
"""SDK-defined: capabilities are computed at handler call time from the
95+
live registry, so post-construction `add_request_handler` calls are
96+
reflected."""
8997

9098
async def list_tools(
9199
ctx: ServerRequestContext[Any], params: types.PaginatedRequestParams | None
@@ -111,16 +119,18 @@ async def list_prompts(
111119

112120

113121
@pytest.mark.anyio
114-
async def test_cacheable_defaults() -> None:
115-
"""`DiscoverResult` is cacheable; the auto-derived handler relies on the
116-
model defaults (immediately-stale, private)."""
122+
async def test_discover_result_defaults_to_immediately_stale_private_cache() -> None:
123+
"""SDK-defined: `DiscoverResult` is cacheable; the auto-derived handler
124+
relies on the model defaults (immediately-stale, private)."""
117125
result = await _discover(Server("cache-server"))
118126
assert result.ttl_ms == 0
119127
assert result.cache_scope == "private"
120128

121129

122130
@pytest.mark.anyio
123131
async def test_overridable_via_add_request_handler() -> None:
132+
"""SDK-defined: a custom `server/discover` handler registered via
133+
`add_request_handler` replaces the auto-derived default wholesale."""
124134
server = Server("custom-server", version="1.0.0")
125135
custom = types.DiscoverResult(
126136
supported_versions=list(MODERN_PROTOCOL_VERSIONS),

tests/server/test_connection.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ async def notify(self, method: str, params: Mapping[str, Any] | None) -> None:
6868

6969

7070
def test_from_envelope_is_born_ready_with_no_back_channel():
71-
"""`from_envelope` populates `protocol_version`, sets `initialized`, and
72-
holds the no-channel sentinel so `has_standalone_channel` derives False."""
71+
"""SDK-defined: `from_envelope` populates `protocol_version`, sets `initialized`,
72+
and holds the no-channel sentinel so `has_standalone_channel` derives False."""
7373
conn = Connection.from_envelope(_MODERN, None, None)
7474
assert conn.protocol_version == _MODERN
7575
assert conn.initialized.is_set()
@@ -80,6 +80,8 @@ def test_from_envelope_is_born_ready_with_no_back_channel():
8080

8181

8282
def test_from_envelope_records_client_params_when_both_info_and_caps_supplied():
83+
"""SDK-defined: when both client info and capabilities are supplied,
84+
`from_envelope` synthesizes `client_params` so capability checks can run."""
8385
caps = ClientCapabilities(sampling=SamplingCapability())
8486
conn = Connection.from_envelope(_MODERN, _CLIENT_INFO, caps)
8587
assert conn.client_params is not None
@@ -95,12 +97,14 @@ def test_from_envelope_records_client_params_when_both_info_and_caps_supplied():
9597
def test_from_envelope_leaves_client_params_none_when_either_is_missing(
9698
info: Implementation | None, caps: ClientCapabilities | None
9799
):
100+
"""SDK-defined: `client_params` is only synthesized when both info and
101+
caps are present; either missing leaves it `None`."""
98102
conn = Connection.from_envelope(_MODERN, info, caps)
99103
assert conn.client_params is None
100104

101105

102106
def test_from_envelope_with_explicit_outbound_has_standalone_channel():
103-
"""Duplex modern transports pass an outbound; `has_standalone_channel`
107+
"""SDK-defined: duplex modern transports pass an outbound; `has_standalone_channel`
104108
derives True since the held outbound is not the no-channel sentinel."""
105109
out = StubOutbound()
106110
conn = Connection.from_envelope(_MODERN, None, None, outbound=out)
@@ -110,6 +114,8 @@ def test_from_envelope_with_explicit_outbound_has_standalone_channel():
110114

111115

112116
def test_for_loop_seeds_version_from_hint_or_latest_and_is_not_born_ready():
117+
"""SDK-defined: `for_loop` seeds `protocol_version` from the hint when given,
118+
else `LATEST_PROTOCOL_VERSION`; the connection awaits the initialize handshake."""
113119
out = StubOutbound()
114120
conn = Connection.for_loop(out)
115121
assert conn.protocol_version == LATEST_PROTOCOL_VERSION
@@ -118,9 +124,14 @@ def test_for_loop_seeds_version_from_hint_or_latest_and_is_not_born_ready():
118124
assert conn.initialize_accepted is False
119125
assert conn.client_params is None
120126

121-
hinted = Connection.for_loop(out, session_id="sess-1", protocol_version_hint=_MODERN)
127+
hinted = Connection.for_loop(out, protocol_version_hint=_MODERN)
122128
assert hinted.protocol_version == _MODERN
123-
assert hinted.session_id == "sess-1"
129+
130+
131+
def test_for_loop_records_session_id_when_supplied():
132+
"""SDK-defined: `for_loop` stores the `session_id` kwarg verbatim."""
133+
conn = Connection.for_loop(StubOutbound(), session_id="sess-1")
134+
assert conn.session_id == "sess-1"
124135

125136

126137
# --- outbound channel ----------------------------------------------------------
@@ -145,7 +156,7 @@ async def test_connection_notify_swallows_broken_stream_and_debug_logs(caplog: p
145156

146157
@pytest.mark.anyio
147158
async def test_connection_notify_drops_when_no_standalone_channel(caplog: pytest.LogCaptureFixture):
148-
"""The no-channel sentinel debug-logs and drops; `notify` never raises."""
159+
"""SDK-defined: the no-channel sentinel debug-logs and drops; `notify` never raises."""
149160
caplog.set_level(logging.DEBUG, logger="mcp.server.connection")
150161
conn = Connection.from_envelope(LATEST_PROTOCOL_VERSION, None, None)
151162
await conn.notify("notifications/message", {"data": "x"}) # must not raise
@@ -154,7 +165,7 @@ async def test_connection_notify_drops_when_no_standalone_channel(caplog: pytest
154165

155166
@pytest.mark.anyio
156167
async def test_connection_send_raw_request_raises_nobackchannel_when_no_standalone_channel():
157-
"""The no-channel sentinel raises structurally; `Connection` does no pre-check."""
168+
"""SDK-defined: the no-channel sentinel raises structurally; `Connection` does no pre-check."""
158169
conn = Connection.from_envelope(LATEST_PROTOCOL_VERSION, None, None)
159170
with pytest.raises(NoBackChannelError):
160171
await conn.send_raw_request("ping", None)
@@ -312,6 +323,8 @@ async def test_connection_send_tool_list_changed_with_meta_includes_meta_only_pa
312323

313324

314325
def test_connection_check_capability_false_when_no_client_params_recorded():
326+
"""SDK-defined: `check_capability` returns False when no `client_params`
327+
were recorded, regardless of which factory built the connection."""
315328
conn = Connection.for_loop(StubOutbound())
316329
assert conn.check_capability(ClientCapabilities(sampling=SamplingCapability())) is False
317330
# Same for a born-ready connection that supplied neither info nor caps.

tests/server/test_runner.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,9 @@ async def _append(i: int) -> None:
11811181

11821182
@pytest.mark.anyio
11831183
async def test_to_jsonrpc_response_wraps_success_as_jsonrpc_response():
1184+
"""SDK-defined: a handler coroutine resolving to a result dict is wrapped as a
1185+
`JSONRPCResponse` carrying the supplied id and the dict verbatim as `result`."""
1186+
11841187
async def ok() -> dict[str, Any]:
11851188
return {"k": "v"}
11861189

@@ -1192,6 +1195,9 @@ async def ok() -> dict[str, Any]:
11921195

11931196
@pytest.mark.anyio
11941197
async def test_to_jsonrpc_response_maps_mcp_error_to_jsonrpc_error():
1198+
"""SDK-defined: an `MCPError` raised by the handler coroutine is wrapped as a
1199+
`JSONRPCError` whose `error` carries the same code, message, and data."""
1200+
11951201
async def fail() -> dict[str, Any]:
11961202
raise MCPError(code=METHOD_NOT_FOUND, message="nope", data="x")
11971203

@@ -1203,6 +1209,10 @@ async def fail() -> dict[str, Any]:
12031209

12041210
@pytest.mark.anyio
12051211
async def test_to_jsonrpc_response_maps_validation_error_to_invalid_params():
1212+
"""SDK-defined: a pydantic `ValidationError` escaping the handler coroutine is
1213+
mapped to `INVALID_PARAMS` with a generic message (validator detail does not
1214+
reach the wire)."""
1215+
12061216
async def fail() -> dict[str, Any]:
12071217
Tool.model_validate({"name": 123}) # raises ValidationError
12081218
raise NotImplementedError
@@ -1216,6 +1226,10 @@ async def fail() -> dict[str, Any]:
12161226
async def test_to_jsonrpc_response_maps_unmapped_exception_to_internal_error_and_logs(
12171227
caplog: pytest.LogCaptureFixture,
12181228
):
1229+
"""SDK-defined: an unmapped exception is logged server-side and surfaced as
1230+
`INTERNAL_ERROR` with a generic message; the exception text never reaches the
1231+
wire."""
1232+
12191233
async def fail() -> dict[str, Any]:
12201234
raise RuntimeError("boom")
12211235

@@ -1271,13 +1285,13 @@ def can_send_request(self) -> bool:
12711285
async def send_raw_request(
12721286
self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None
12731287
) -> dict[str, Any]:
1274-
raise AssertionError("serve_one tests do not send requests on the back-channel")
1288+
raise NotImplementedError
12751289

12761290
async def notify(self, method: str, params: Mapping[str, Any] | None) -> None:
1277-
pass
1291+
raise NotImplementedError
12781292

12791293
async def progress(self, progress: float, total: float | None = None, message: str | None = None) -> None:
1280-
pass
1294+
raise NotImplementedError
12811295

12821296

12831297
async def _append_async(dst: list[int], v: int) -> None:
@@ -1305,7 +1319,10 @@ async def test_serve_one_runs_handler_and_returns_jsonrpc_response(server: SrvT)
13051319

13061320

13071321
@pytest.mark.anyio
1308-
async def test_serve_one_maps_handler_error_to_jsonrpc_error_and_still_closes_exit_stack(server: SrvT):
1322+
async def test_serve_one_maps_error_to_jsonrpc_error_and_still_closes_exit_stack(server: SrvT):
1323+
"""SDK-defined: a kernel-produced error (here `METHOD_NOT_FOUND` for an
1324+
unregistered method) is wrapped as a `JSONRPCError`, and the per-request
1325+
exit stack is closed on the error path too."""
13091326
conn = Connection.from_envelope(LATEST_PROTOCOL_VERSION, None, None)
13101327
cleaned: list[int] = []
13111328
conn.exit_stack.push_async_callback(_append_async, cleaned, 1)

tests/server/test_session.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from mcp.server.session import ServerSession
1818
from mcp.shared.dispatcher import CallOptions, Outbound
1919
from mcp.shared.message import ServerMessageMetadata
20+
from mcp.shared.version import MODERN_PROTOCOL_VERSIONS
2021
from mcp.types import (
2122
LATEST_PROTOCOL_VERSION,
2223
ClientCapabilities,
@@ -60,6 +61,7 @@ def _make_session(
6061

6162

6263
def _two_channel_session(request_ch: Outbound, standalone_ch: Outbound) -> ServerSession:
64+
"""Distinct request/standalone outbounds so routing assertions can tell the channels apart."""
6365
conn = Connection.from_envelope(LATEST_PROTOCOL_VERSION, None, None, outbound=standalone_ch)
6466
return ServerSession(request_ch, conn, standalone_outbound=standalone_ch)
6567

@@ -105,7 +107,7 @@ async def test_send_request_timeout_zero_is_forwarded():
105107

106108
@pytest.mark.anyio
107109
async def test_send_request_without_related_id_routes_to_standalone_channel():
108-
"""No `related_request_id`: the request rides the connection's standalone channel."""
110+
"""SDK-defined: no `related_request_id` routes the request onto the connection's standalone channel."""
109111
request_ch = StubOutbound()
110112
standalone_ch = StubOutbound(result={"roots": []})
111113
session = _two_channel_session(request_ch, standalone_ch)
@@ -116,7 +118,7 @@ async def test_send_request_without_related_id_routes_to_standalone_channel():
116118

117119
@pytest.mark.anyio
118120
async def test_send_request_with_related_id_routes_to_request_channel():
119-
"""With `related_request_id`: the request rides the per-request channel
121+
"""SDK-defined: with `related_request_id` the request rides the per-request channel
120122
(the originating POST's response stream over streamable HTTP)."""
121123
request_ch = StubOutbound(result={"action": "cancel"})
122124
standalone_ch = StubOutbound()
@@ -133,7 +135,7 @@ async def test_send_request_with_related_id_routes_to_request_channel():
133135

134136
@pytest.mark.anyio
135137
async def test_send_notification_routes_by_related_request_id():
136-
"""Notifications select channel by `related_request_id` exactly like requests."""
138+
"""SDK-defined: notifications select channel by `related_request_id` exactly like requests."""
137139
request_ch = StubOutbound()
138140
standalone_ch = StubOutbound()
139141
session = _two_channel_session(request_ch, standalone_ch)
@@ -145,10 +147,14 @@ async def test_send_notification_routes_by_related_request_id():
145147

146148
@pytest.mark.anyio
147149
async def test_standalone_outbound_defaults_to_request_outbound():
148-
"""Omitting `standalone_outbound` collapses both channels onto the request
149-
one — the duplex case (stdio) where a single stream serves both."""
150+
"""SDK-defined: omitting `standalone_outbound` collapses both channels onto the
151+
request one — the duplex case (stdio) where a single stream serves both.
152+
153+
`conn.outbound` is the no-channel sentinel here, so traffic landing on `only`
154+
proves the default is the request outbound (the alternate impl — defaulting to
155+
`conn.outbound` — would raise `NoBackChannelError`)."""
150156
only = StubOutbound(result={"roots": []})
151-
conn = Connection.from_envelope(LATEST_PROTOCOL_VERSION, None, None, outbound=only)
157+
conn = Connection.from_envelope(LATEST_PROTOCOL_VERSION, None, None)
152158
session = ServerSession(only, conn)
153159
await session.send_request(types.ListRootsRequest(), types.ListRootsResult)
154160
await session.send_notification(types.ToolListChangedNotification())
@@ -178,7 +184,7 @@ async def test_send_request_passes_a_spec_valid_client_result():
178184
async def test_send_request_skips_the_surface_gate_when_method_absent_at_version():
179185
"""Surface row absent for the connection's version: gate is bypassed and only
180186
`result_type` validates."""
181-
session = _make_session(StubOutbound(result={}), protocol_version="2026-07-28")
187+
session = _make_session(StubOutbound(result={}), protocol_version=MODERN_PROTOCOL_VERSIONS[0])
182188
result = await session.send_request(types.PingRequest(), types.EmptyResult)
183189
assert isinstance(result, types.EmptyResult)
184190

@@ -219,8 +225,9 @@ def test_check_client_capability_delegates_to_connection():
219225

220226

221227
def test_protocol_version_proxies_connection():
222-
"""`session.protocol_version` reads through to the held `Connection`."""
223-
conn = Connection.from_envelope("2025-03-26", None, None)
228+
"""SDK-defined: `session.protocol_version` reads through to the held `Connection`."""
229+
_ARBITRARY_VERSION = "sentinel-version" # identity-only: any string the connection holds
230+
conn = Connection.from_envelope(_ARBITRARY_VERSION, None, None)
224231
session = ServerSession(StubOutbound(), conn, standalone_outbound=conn.outbound)
225-
assert session.protocol_version == "2025-03-26"
232+
assert session.protocol_version == _ARBITRARY_VERSION
226233
assert session.client_params is None

0 commit comments

Comments
 (0)