Skip to content

Commit cce06b2

Browse files
committed
test: correct spec anchors and record further divergences in the requirements manifest
Fixes the spec deep links that pointed at non-existent anchors, records the divergences for the client's default not-supported answers (the spec names -32601 for roots and -32602 for an undeclared elicitation mode; the default callbacks answer -32600), and adds a logging:capability requirement noting that MCPServer emits log message notifications without declaring the logging capability. Also tightens behaviour sentences and docstrings to match what the tests assert, and adds a test pinning that Context.report_progress is a silent no-op when the caller supplied no progress token, removing the pragma on that path.
1 parent 2f0da6e commit cce06b2

6 files changed

Lines changed: 87 additions & 20 deletions

File tree

src/mcp/server/mcpserver/context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ async def report_progress(self, progress: float, total: float | None = None, mes
9494
"""
9595
progress_token = self.request_context.meta.get("progress_token") if self.request_context.meta else None
9696

97-
if progress_token is None: # pragma: no cover
97+
if progress_token is None:
9898
return
9999

100100
await self.request_context.session.send_progress_notification(

tests/interaction/_requirements.py

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -240,11 +240,11 @@ class Requirement:
240240
# Request metadata
241241
# ═══════════════════════════════════════════════════════════════════════════
242242
"meta:request-to-handler": Requirement(
243-
source=f"{SPEC_BASE_URL}/basic#meta",
243+
source=f"{SPEC_BASE_URL}/basic#_meta",
244244
behavior="The _meta object the client attaches to a request is visible to the server handler.",
245245
),
246246
"meta:result-to-client": Requirement(
247-
source=f"{SPEC_BASE_URL}/basic#meta",
247+
source=f"{SPEC_BASE_URL}/basic#_meta",
248248
behavior="The _meta object a handler attaches to its result is delivered to the client.",
249249
),
250250
# ═══════════════════════════════════════════════════════════════════════════
@@ -337,7 +337,7 @@ class Requirement:
337337
behavior="completion/complete with a ref/resource returns suggested values for a URI template variable.",
338338
),
339339
"completion:complete:context": Requirement(
340-
source=f"{SPEC_BASE_URL}/server/utilities/completion#context",
340+
source=f"{SPEC_BASE_URL}/server/utilities/completion#requesting-completions",
341341
behavior="Previously-resolved argument values supplied in context.arguments reach the completion handler.",
342342
),
343343
"completion:complete:not-supported": Requirement(
@@ -351,7 +351,7 @@ class Requirement:
351351
# Logging
352352
# ═══════════════════════════════════════════════════════════════════════════
353353
"logging:set-level": Requirement(
354-
source=f"{SPEC_BASE_URL}/server/utilities/logging#log-levels",
354+
source=f"{SPEC_BASE_URL}/server/utilities/logging#setting-log-level",
355355
behavior="logging/setLevel delivers the requested level to the server's handler and returns an empty result.",
356356
),
357357
"logging:message:notification": Requirement(
@@ -365,8 +365,22 @@ class Requirement:
365365
source=f"{SPEC_BASE_URL}/server/utilities/logging#log-levels",
366366
behavior="All eight RFC 5424 severity levels are deliverable as log message notifications.",
367367
),
368+
"logging:capability": Requirement(
369+
source=f"{SPEC_BASE_URL}/server/utilities/logging#capabilities",
370+
behavior=(
371+
"MCPServer tools emit log message notifications through the Context helpers while the server's "
372+
"advertised capabilities omit logging."
373+
),
374+
divergence=Divergence(
375+
note=(
376+
"The spec says servers that emit log message notifications MUST declare the logging "
377+
"capability; MCPServer registers no setLevel handler, so capability derivation leaves "
378+
"logging unset even though the Context helpers send the notifications."
379+
),
380+
),
381+
),
368382
"logging:set-level:filtering": Requirement(
369-
source=f"{SPEC_BASE_URL}/server/utilities/logging#log-levels",
383+
source=f"{SPEC_BASE_URL}/server/utilities/logging#setting-log-level",
370384
behavior=(
371385
"MCPServer registers no logging/setLevel handler (the request is rejected with method-not-found) "
372386
"and log messages are delivered at every severity regardless of any requested level."
@@ -477,7 +491,7 @@ class Requirement:
477491
),
478492
),
479493
"sampling:create-message:image-content": Requirement(
480-
source=f"{SPEC_BASE_URL}/client/sampling#message-content",
494+
source=f"{SPEC_BASE_URL}/client/sampling#image-content",
481495
behavior="Sampling messages can carry image content: base64 data with a mimeType.",
482496
),
483497
"sampling:create-message:tools:not-supported": Requirement(
@@ -506,18 +520,19 @@ class Requirement:
506520
"sampling:create-message:not-supported": Requirement(
507521
source=f"{SPEC_BASE_URL}/client/sampling#capabilities",
508522
behavior=(
509-
"A sampling request to a client that did not declare the sampling capability fails with an "
510-
"error rather than hanging or being silently dropped."
523+
"A sampling request to a client that did not declare the sampling capability fails with the "
524+
"client's default-callback error (-32600 Invalid request) rather than hanging or being "
525+
"silently dropped; the spec names no error code for this case."
511526
),
512527
),
513528
# ═══════════════════════════════════════════════════════════════════════════
514529
# Elicitation (server → client)
515530
# ═══════════════════════════════════════════════════════════════════════════
516531
"elicitation:form:accept": Requirement(
517-
source=f"{SPEC_BASE_URL}/client/elicitation#form-mode-elicitation",
532+
source=f"{SPEC_BASE_URL}/client/elicitation#form-mode-elicitation-requests",
518533
behavior=(
519534
"A form-mode elicitation answered with action 'accept' returns the user's content to the "
520-
"requesting handler, validated against the requested schema."
535+
"requesting handler."
521536
),
522537
),
523538
"elicitation:form:decline": Requirement(
@@ -529,7 +544,7 @@ class Requirement:
529544
behavior="A form-mode elicitation answered with action 'cancel' returns no content to the handler.",
530545
),
531546
"elicitation:url:accept": Requirement(
532-
source=f"{SPEC_BASE_URL}/client/elicitation#url-mode-elicitation",
547+
source=f"{SPEC_BASE_URL}/client/elicitation#url-mode-elicitation-requests",
533548
behavior=(
534549
"A URL-mode elicitation delivers the message, URL, and elicitationId to the client; an accept "
535550
"response carries no content (accept means the user agreed to visit the URL, not that the "
@@ -545,7 +560,7 @@ class Requirement:
545560
behavior="A URL-mode elicitation answered with cancel returns the action with no content.",
546561
),
547562
"elicitation:complete-notification": Requirement(
548-
source=f"{SPEC_BASE_URL}/client/elicitation#completion-notification",
563+
source=f"{SPEC_BASE_URL}/client/elicitation#completion-notifications-for-url-mode-elicitation",
549564
behavior=(
550565
"An elicitation/complete notification sent by the server after an out-of-band elicitation "
551566
"finishes reaches the client carrying the elicitationId."
@@ -559,11 +574,18 @@ class Requirement:
559574
),
560575
),
561576
"elicitation:form:not-supported": Requirement(
562-
source=f"{SPEC_BASE_URL}/client/elicitation#capabilities",
577+
source=f"{SPEC_BASE_URL}/client/elicitation#error-handling",
563578
behavior=(
564579
"An elicitation request to a client that did not declare the elicitation capability fails with "
565580
"an error rather than hanging or being silently dropped."
566581
),
582+
divergence=Divergence(
583+
note=(
584+
"The spec says a request for an elicitation mode the client has not declared MUST be "
585+
"answered with -32602 Invalid params; the client's default callback answers with -32600 "
586+
"Invalid request."
587+
),
588+
),
567589
),
568590
# ═══════════════════════════════════════════════════════════════════════════
569591
# Roots (server → client)
@@ -580,11 +602,17 @@ class Requirement:
580602
behavior="An empty roots list is a valid response and reaches the handler as such.",
581603
),
582604
"roots:list:not-supported": Requirement(
583-
source=f"{SPEC_BASE_URL}/client/roots#capabilities",
605+
source=f"{SPEC_BASE_URL}/client/roots#error-handling",
584606
behavior=(
585607
"A roots/list request to a client that did not declare the roots capability fails with an "
586608
"error rather than hanging or being silently dropped."
587609
),
610+
divergence=Divergence(
611+
note=(
612+
"The spec says a client that does not support roots SHOULD answer with -32601 Method not "
613+
"found; the client's default callback answers with -32600 Invalid request."
614+
),
615+
),
588616
),
589617
"roots:list-changed": Requirement(
590618
source=f"{SPEC_BASE_URL}/client/roots#root-list-changes",

tests/interaction/lowlevel/test_elicitation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ async def test_elicit_form_without_callback_is_error() -> None:
145145
"""Eliciting from a client that configured no elicitation callback fails with an error.
146146
147147
The client's default callback answers with an Invalid request error, which the server-side
148-
elicit call raises as an MCPError; the tool reports the code and message it caught.
148+
elicit call raises as an MCPError; the tool reports the code and message it caught. The spec
149+
requires -32602 for an undeclared mode (see the divergence note on the requirement).
149150
"""
150151

151152
async def list_tools(

tests/interaction/lowlevel/test_roots.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ async def list_roots(context: ClientRequestContext) -> ListRootsResult:
8282
async def test_list_roots_without_callback_is_error() -> None:
8383
"""A roots/list request to a client with no roots callback fails with an error the handler can observe.
8484
85-
The client's default callback answers with INVALID_REQUEST rather than leaving the server hanging.
85+
The client's default callback answers with INVALID_REQUEST rather than leaving the server
86+
hanging; the spec names -32601 for this case (see the divergence note on the requirement).
8687
"""
8788

8889
async def list_tools(

tests/interaction/lowlevel/test_wire.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestPara
4141
async def test_request_ids_are_unique_and_never_null() -> None:
4242
"""Every request the client sends carries a distinct, non-null id.
4343
44-
The id sequence is pinned: sequential integers from zero, in send order, including the
45-
schema-cache refresh the client performs after the first successful tool call.
44+
The id sequence is pinned: sequential integers from zero, in send order.
4645
"""
4746
recording = RecordingTransport(InMemoryTransport(_echo_server()))
4847

tests/interaction/mcpserver/test_context.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,24 @@
1616
ElicitRequestParams,
1717
ElicitResult,
1818
ErrorData,
19+
LoggingMessageNotification,
1920
LoggingMessageNotificationParams,
2021
TextContent,
2122
)
23+
from tests.interaction._helpers import IncomingMessage
2224
from tests.interaction._requirements import requirement
2325

2426
pytestmark = pytest.mark.anyio
2527

2628

2729
@requirement("mcpserver:context:logging")
30+
@requirement("logging:capability")
2831
async def test_context_logging_helpers_send_log_notifications() -> None:
2932
"""Each Context logging helper sends a log message notification at the matching severity.
3033
3134
All four notifications reach the client's logging callback before the tool call returns; none
32-
of them carry a logger name unless one is passed explicitly.
35+
of them carry a logger name unless one is passed explicitly. The server emits these without
36+
advertising the logging capability (see the divergence note on logging:capability).
3337
"""
3438
received: list[LoggingMessageNotificationParams] = []
3539
mcp = MCPServer("chatty")
@@ -47,6 +51,7 @@ async def collect(params: LoggingMessageNotificationParams) -> None:
4751

4852
async with Client(mcp, logging_callback=collect) as client:
4953
result = await client.call_tool("narrate", {})
54+
advertised_logging = client.initialize_result.capabilities.logging
5055

5156
assert result == snapshot(CallToolResult(content=[TextContent(text="done")], structured_content={"result": "done"}))
5257
assert received == snapshot(
@@ -57,6 +62,8 @@ async def collect(params: LoggingMessageNotificationParams) -> None:
5762
LoggingMessageNotificationParams(level="error", data="e"),
5863
]
5964
)
65+
# The spec requires servers that emit log notifications to declare the logging capability.
66+
assert advertised_logging is None
6067

6168

6269
@requirement("mcpserver:context:progress")
@@ -86,6 +93,37 @@ async def on_progress(progress: float, total: float | None, message: str | None)
8693
assert received == snapshot([(1.0, 3.0, None), (2.0, 3.0, "halfway there")])
8794

8895

96+
@requirement("progress:no-token")
97+
async def test_report_progress_without_a_progress_token_sends_nothing() -> None:
98+
"""When the caller supplied no progress callback, Context.report_progress is a silent no-op.
99+
100+
The tool also emits one log message as a sentinel: the message handler receives only that,
101+
proving the notification pipeline works and no progress notification was sent for the
102+
token-less request.
103+
"""
104+
received: list[IncomingMessage] = []
105+
mcp = MCPServer("quiet")
106+
107+
@mcp.tool()
108+
async def mill(ctx: Context) -> str:
109+
await ctx.report_progress(1, 3)
110+
await ctx.info("milling done")
111+
return "milled"
112+
113+
async def collect(message: IncomingMessage) -> None:
114+
received.append(message)
115+
116+
async with Client(mcp, message_handler=collect) as client:
117+
result = await client.call_tool("mill", {})
118+
119+
assert result == snapshot(
120+
CallToolResult(content=[TextContent(text="milled")], structured_content={"result": "milled"})
121+
)
122+
assert received == snapshot(
123+
[LoggingMessageNotification(params=LoggingMessageNotificationParams(level="info", data="milling done"))]
124+
)
125+
126+
89127
@requirement("mcpserver:context:elicit")
90128
async def test_context_elicit_returns_typed_result() -> None:
91129
"""Context.elicit sends a form elicitation built from a pydantic schema and returns a typed result.

0 commit comments

Comments
 (0)