Skip to content

Commit 5710662

Browse files
committed
test: add lifecycle, completion, logging, and MCPServer feature interaction tests
Extends the interaction suite with the initialize handshake (server identity, instructions, capability derivation, client identity and capabilities as seen by the server), completion round trips, logging notifications, and the MCPServer resource/prompt/structured-output behaviours. Records two more divergences on the requirements manifest: MCPServer reports unknown resources and prompts with error code 0 rather than the codes the spec documents. Removes the 'pragma: no cover' from the method-not-found fallback now that it is covered.
1 parent b04d7e0 commit 5710662

8 files changed

Lines changed: 877 additions & 1 deletion

File tree

src/mcp/server/lowlevel/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ async def _handle_request(
513513
if raise_exceptions: # pragma: no cover
514514
raise err
515515
response = types.ErrorData(code=0, message=str(err))
516-
else: # pragma: no cover
516+
else:
517517
response = types.ErrorData(code=types.METHOD_NOT_FOUND, message="Method not found")
518518

519519
if isinstance(response, types.ErrorData) and span is not None:

tests/interaction/_requirements.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,42 @@ class Requirement:
6060
),
6161
),
6262
# ═══════════════════════════════════════════════════════════════════════════
63+
# Lifecycle
64+
# ═══════════════════════════════════════════════════════════════════════════
65+
"lifecycle:initialize:server-info": Requirement(
66+
source=f"{SPEC_BASE_URL}/basic/lifecycle#initialization",
67+
behavior="The initialize result identifies the server: name and version, plus title when declared.",
68+
),
69+
"lifecycle:initialize:instructions": Requirement(
70+
source=f"{SPEC_BASE_URL}/basic/lifecycle#initialization",
71+
behavior=(
72+
"Server-declared instructions are returned in the initialize result, and omitted when the "
73+
"server declares none."
74+
),
75+
),
76+
"lifecycle:initialize:capabilities:from-handlers": Requirement(
77+
source=f"{SPEC_BASE_URL}/basic/lifecycle#capability-negotiation",
78+
behavior=(
79+
"The server advertises a capability for each feature area it has a registered handler for, "
80+
"and omits the capability for areas it does not."
81+
),
82+
),
83+
"lifecycle:initialize:capabilities:minimal": Requirement(
84+
source=f"{SPEC_BASE_URL}/basic/lifecycle#capability-negotiation",
85+
behavior="A server with no feature handlers advertises no feature capabilities.",
86+
),
87+
"lifecycle:initialize:client-info": Requirement(
88+
source=f"{SPEC_BASE_URL}/basic/lifecycle#initialization",
89+
behavior="The client's name, version, and title are visible to server handlers after initialization.",
90+
),
91+
"lifecycle:initialize:client-capabilities": Requirement(
92+
source=f"{SPEC_BASE_URL}/basic/lifecycle#capability-negotiation",
93+
behavior=(
94+
"The client capabilities visible to the server reflect which client callbacks are configured "
95+
"(sampling, elicitation, roots)."
96+
),
97+
),
98+
# ═══════════════════════════════════════════════════════════════════════════
6399
# Ping
64100
# ═══════════════════════════════════════════════════════════════════════════
65101
"ping:client-to-server": Requirement(
@@ -123,6 +159,53 @@ class Requirement:
123159
source=f"{SPEC_BASE_URL}/server/tools#error-handling",
124160
behavior="tools/call for a name the server does not recognise returns a JSON-RPC error.",
125161
),
162+
"tools:call:invalid-arguments": Requirement(
163+
source=f"{SPEC_BASE_URL}/server/tools#error-handling",
164+
behavior=(
165+
"Arguments that fail the tool's input validation produce a tool execution error (isError true "
166+
"with the validation failure described in content), not a protocol error."
167+
),
168+
),
169+
# ═══════════════════════════════════════════════════════════════════════════
170+
# Completion
171+
# ═══════════════════════════════════════════════════════════════════════════
172+
"completion:complete:prompt-ref": Requirement(
173+
source=f"{SPEC_BASE_URL}/server/utilities/completion#requesting-completions",
174+
behavior="completion/complete with a ref/prompt returns suggested values for the named prompt argument.",
175+
),
176+
"completion:complete:resource-ref": Requirement(
177+
source=f"{SPEC_BASE_URL}/server/utilities/completion#requesting-completions",
178+
behavior="completion/complete with a ref/resource returns suggested values for a URI template variable.",
179+
),
180+
"completion:complete:context": Requirement(
181+
source=f"{SPEC_BASE_URL}/server/utilities/completion#context",
182+
behavior="Previously-resolved argument values supplied in context.arguments reach the completion handler.",
183+
),
184+
"completion:complete:not-supported": Requirement(
185+
source=f"{SPEC_BASE_URL}/server/utilities/completion#capabilities",
186+
behavior=(
187+
"A server with no completion handler does not advertise the completions capability and rejects "
188+
"completion/complete with METHOD_NOT_FOUND."
189+
),
190+
),
191+
# ═══════════════════════════════════════════════════════════════════════════
192+
# Logging
193+
# ═══════════════════════════════════════════════════════════════════════════
194+
"logging:set-level": Requirement(
195+
source=f"{SPEC_BASE_URL}/server/utilities/logging#log-levels",
196+
behavior="logging/setLevel delivers the requested level to the server's handler and returns an empty result.",
197+
),
198+
"logging:message:notification": Requirement(
199+
source=f"{SPEC_BASE_URL}/server/utilities/logging#log-message-notifications",
200+
behavior=(
201+
"A log message sent by a server handler is delivered to the client's logging callback with its "
202+
"severity level, logger name, and data, in the order the server sent them."
203+
),
204+
),
205+
"logging:message:all-levels": Requirement(
206+
source=f"{SPEC_BASE_URL}/server/utilities/logging#log-levels",
207+
behavior="All eight RFC 5424 severity levels are deliverable as log message notifications.",
208+
),
126209
# ═══════════════════════════════════════════════════════════════════════════
127210
# Resources
128211
# ═══════════════════════════════════════════════════════════════════════════
@@ -167,6 +250,61 @@ class Requirement:
167250
# ═══════════════════════════════════════════════════════════════════════════
168251
# MCPServer behavioural guarantees (not spec-mandated)
169252
# ═══════════════════════════════════════════════════════════════════════════
253+
"mcpserver:tools:output-schema:model": Requirement(
254+
source="sdk",
255+
behavior=(
256+
"A tool returning a typed model advertises a matching generated outputSchema and returns the "
257+
"model's fields as structuredContent alongside a serialised text block."
258+
),
259+
),
260+
"mcpserver:tools:output-schema:wrapped": Requirement(
261+
source="sdk",
262+
behavior=(
263+
"A tool returning a non-object type (primitive or list) wraps the value as {'result': ...} in "
264+
"structuredContent, with a matching generated outputSchema."
265+
),
266+
),
267+
"mcpserver:resources:static": Requirement(
268+
source="sdk",
269+
behavior=(
270+
"A function registered with @mcp.resource() for a fixed URI is listed by resources/list and "
271+
"served by resources/read at that URI."
272+
),
273+
),
274+
"mcpserver:resources:template": Requirement(
275+
source="sdk",
276+
behavior=(
277+
"A function registered with a URI template is listed by resources/templates/list and matched "
278+
"by resources/read, receiving the parameters extracted from the requested URI."
279+
),
280+
),
281+
"mcpserver:resources:unknown-uri": Requirement(
282+
source="sdk",
283+
behavior="resources/read for a URI matching no registered resource returns a JSON-RPC error.",
284+
divergence=Divergence(
285+
note=(
286+
"The spec reserves -32002 for resource-not-found; MCPServer raises ResourceError, which "
287+
"the low-level server converts to error code 0."
288+
),
289+
),
290+
),
291+
"mcpserver:prompts:decorated": Requirement(
292+
source="sdk",
293+
behavior=(
294+
"A function registered with @mcp.prompt() is listed with arguments derived from its signature "
295+
"and rendered into prompt messages by prompts/get."
296+
),
297+
),
298+
"mcpserver:prompts:unknown-name": Requirement(
299+
source="sdk",
300+
behavior="prompts/get for a name that was never registered returns a JSON-RPC error.",
301+
divergence=Divergence(
302+
note=(
303+
"The spec's example uses -32602 Invalid params for unknown prompts; MCPServer raises "
304+
"ValueError, which the low-level server converts to error code 0."
305+
),
306+
),
307+
),
170308
"mcpserver:tools:handler-exception": Requirement(
171309
source="sdk",
172310
behavior=(
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Completion interactions against the low-level Server, driven through the public Client API."""
2+
3+
import pytest
4+
from inline_snapshot import snapshot
5+
6+
from mcp import MCPError, types
7+
from mcp.client.client import Client
8+
from mcp.server import Server, ServerRequestContext
9+
from mcp.types import (
10+
METHOD_NOT_FOUND,
11+
CompleteResult,
12+
Completion,
13+
ErrorData,
14+
PromptReference,
15+
ResourceTemplateReference,
16+
)
17+
from tests.interaction._requirements import requirement
18+
19+
pytestmark = pytest.mark.anyio
20+
21+
22+
@requirement("completion:complete:prompt-ref")
23+
async def test_complete_prompt_argument() -> None:
24+
"""Completing a prompt argument delivers the ref, argument name, and current value to the handler.
25+
26+
The returned values are filtered by the argument's value, proving the value reached the handler.
27+
"""
28+
29+
async def completion(ctx: ServerRequestContext, params: types.CompleteRequestParams) -> CompleteResult:
30+
assert isinstance(params.ref, PromptReference)
31+
assert params.ref.name == "code_review"
32+
assert params.argument.name == "language"
33+
candidates = ["python", "pytorch", "ruby"]
34+
matches = [candidate for candidate in candidates if candidate.startswith(params.argument.value)]
35+
return CompleteResult(completion=Completion(values=matches, total=len(matches), has_more=False))
36+
37+
server = Server("completer", on_completion=completion)
38+
39+
async with Client(server) as client:
40+
result = await client.complete(
41+
PromptReference(name="code_review"), argument={"name": "language", "value": "py"}
42+
)
43+
44+
assert result == snapshot(
45+
CompleteResult(completion=Completion(values=["python", "pytorch"], total=2, has_more=False))
46+
)
47+
48+
49+
@requirement("completion:complete:resource-ref")
50+
async def test_complete_resource_template_variable() -> None:
51+
"""Completing a URI template variable delivers the template URI and variable name to the handler."""
52+
53+
async def completion(ctx: ServerRequestContext, params: types.CompleteRequestParams) -> CompleteResult:
54+
assert isinstance(params.ref, ResourceTemplateReference)
55+
assert params.ref.uri == "github://repos/{owner}/{repo}"
56+
assert params.argument.name == "owner"
57+
return CompleteResult(completion=Completion(values=[f"{params.argument.value}contextprotocol"]))
58+
59+
server = Server("completer", on_completion=completion)
60+
61+
async with Client(server) as client:
62+
result = await client.complete(
63+
ResourceTemplateReference(uri="github://repos/{owner}/{repo}"),
64+
argument={"name": "owner", "value": "model"},
65+
)
66+
67+
assert result == snapshot(CompleteResult(completion=Completion(values=["modelcontextprotocol"])))
68+
69+
70+
@requirement("completion:complete:context")
71+
async def test_complete_receives_context_arguments() -> None:
72+
"""Previously-resolved arguments passed as completion context reach the handler.
73+
74+
The returned value is derived from the context, proving it arrived.
75+
"""
76+
77+
async def completion(ctx: ServerRequestContext, params: types.CompleteRequestParams) -> CompleteResult:
78+
assert params.argument.name == "repo"
79+
assert params.context is not None
80+
assert params.context.arguments is not None
81+
return CompleteResult(completion=Completion(values=[f"{params.context.arguments['owner']}/python-sdk"]))
82+
83+
server = Server("completer", on_completion=completion)
84+
85+
async with Client(server) as client:
86+
result = await client.complete(
87+
ResourceTemplateReference(uri="github://repos/{owner}/{repo}"),
88+
argument={"name": "repo", "value": ""},
89+
context_arguments={"owner": "modelcontextprotocol"},
90+
)
91+
92+
assert result == snapshot(CompleteResult(completion=Completion(values=["modelcontextprotocol/python-sdk"])))
93+
94+
95+
@requirement("completion:complete:not-supported")
96+
async def test_complete_without_handler_is_method_not_found() -> None:
97+
"""A server with no completion handler advertises no completions capability and rejects the request."""
98+
server = Server("incomplete")
99+
100+
async with Client(server) as client:
101+
assert client.initialize_result.capabilities.completions is None
102+
103+
with pytest.raises(MCPError) as exc_info:
104+
await client.complete(PromptReference(name="anything"), argument={"name": "topic", "value": ""})
105+
106+
assert exc_info.value.error == snapshot(ErrorData(code=METHOD_NOT_FOUND, message="Method not found"))

0 commit comments

Comments
 (0)