Skip to content

Commit ca0ba11

Browse files
committed
test: declare capabilities the notification tests rely on
Nine notification tests sent capability-gated messages (log, resource_updated, roots/list_changed, *_list_changed) from a peer that had not declared the capability, which only works because the SDK does not yet enforce capability gating. Adding stub handlers / list_roots_callback so the capability is advertised makes the tests match their requirement preconditions and survive the gate fix unchanged. The three list_changed tests cannot set listChanged=True without threading NotificationOptions through the in-memory and HTTP-manager connection paths; the requirement behavior text now describes what the tests prove (send -> arrives) and the module docstring records the remaining coupling on lifecycle:capability:server-not-advertised.
1 parent 05a41e1 commit ca0ba11

9 files changed

Lines changed: 109 additions & 17 deletions

File tree

tests/interaction/_requirements.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -586,8 +586,8 @@ def __post_init__(self) -> None:
586586
"tools:list-changed": Requirement(
587587
source=f"{SPEC_BASE_URL}/server/tools#list-changed-notification",
588588
behavior=(
589-
"When the tool set changes, a server that declared the tools listChanged capability sends "
590-
"notifications/tools/list_changed and it reaches the client's handler."
589+
"When the tool set changes, the server sends notifications/tools/list_changed and it reaches "
590+
"the client's handler."
591591
),
592592
),
593593
"tools:list:basic": Requirement(
@@ -787,8 +787,8 @@ def __post_init__(self) -> None:
787787
"resources:list-changed": Requirement(
788788
source=f"{SPEC_BASE_URL}/server/resources#list-changed-notification",
789789
behavior=(
790-
"When the resource set changes, a server that declared the resources listChanged capability "
791-
"sends notifications/resources/list_changed and it reaches the client's handler."
790+
"When the resource set changes, the server sends notifications/resources/list_changed and it "
791+
"reaches the client's handler."
792792
),
793793
),
794794
"resources:list:basic": Requirement(
@@ -959,8 +959,8 @@ def __post_init__(self) -> None:
959959
"prompts:list-changed": Requirement(
960960
source=f"{SPEC_BASE_URL}/server/prompts#list-changed-notification",
961961
behavior=(
962-
"When the prompt set changes, a server that declared the prompts listChanged capability sends "
963-
"notifications/prompts/list_changed and it reaches the client's handler."
962+
"When the prompt set changes, the server sends notifications/prompts/list_changed and it "
963+
"reaches the client's handler."
964964
),
965965
),
966966
"prompts:list:basic": Requirement(

tests/interaction/lowlevel/test_flows.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ElicitRequestFormParams,
2424
ElicitRequestURLParams,
2525
ElicitResult,
26+
EmptyResult,
2627
ListToolsResult,
2728
ReadResourceResult,
2829
ResourceLink,
@@ -167,7 +168,16 @@ async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestPara
167168
)
168169
return CallToolResult(content=[TextContent(text="contents")])
169170

170-
server = Server("gatekeeper", on_list_tools=_list_tools("read_files"), on_call_tool=call_tool)
171+
async def set_logging_level(ctx: ServerRequestContext, params: types.SetLevelRequestParams) -> EmptyResult:
172+
"""Registered so the logging capability is advertised; the client never sets a level."""
173+
raise NotImplementedError
174+
175+
server = Server(
176+
"gatekeeper",
177+
on_list_tools=_list_tools("read_files"),
178+
on_call_tool=call_tool,
179+
on_set_logging_level=set_logging_level,
180+
)
171181

172182
async def collect(message: IncomingMessage) -> None:
173183
if isinstance(message, ElicitCompleteNotification):

tests/interaction/lowlevel/test_list_changed.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
as ``transports/test_streamable_http.py::test_unrelated_server_messages_arrive_on_the_standalone_stream``.
77
The collector still records every message it receives, so the snapshot also proves nothing else
88
was delivered.
9+
10+
The servers register the parent capability (resources/prompts) so that part of the spec's
11+
precondition holds, but the ``listChanged`` sub-capability stays ``False``: ``NotificationOptions``
12+
is not threaded through any of the suite's connection paths. The tests therefore rely on the
13+
recorded ``lifecycle:capability:server-not-advertised`` divergence and will need updating
14+
alongside the fix that introduces capability gating.
915
"""
1016

1117
import anyio
@@ -78,7 +84,13 @@ async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestPara
7884
await ctx.session.send_resource_list_changed()
7985
return CallToolResult(content=[TextContent(text="mounted")])
8086

81-
server = Server("registry", on_list_tools=list_tools, on_call_tool=call_tool)
87+
async def list_resources(
88+
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
89+
) -> types.ListResourcesResult:
90+
"""Registered so the resources capability is advertised; the client never lists resources."""
91+
raise NotImplementedError
92+
93+
server = Server("registry", on_list_tools=list_tools, on_call_tool=call_tool, on_list_resources=list_resources)
8294

8395
async with connect(server, message_handler=collect) as client:
8496
await client.call_tool("mount", {})
@@ -108,7 +120,13 @@ async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestPara
108120
await ctx.session.send_prompt_list_changed()
109121
return CallToolResult(content=[TextContent(text="learned")])
110122

111-
server = Server("registry", on_list_tools=list_tools, on_call_tool=call_tool)
123+
async def list_prompts(
124+
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
125+
) -> types.ListPromptsResult:
126+
"""Registered so the prompts capability is advertised; the client never lists prompts."""
127+
raise NotImplementedError
128+
129+
server = Server("registry", on_list_tools=list_tools, on_call_tool=call_tool, on_list_prompts=list_prompts)
112130

113131
async with connect(server, message_handler=collect) as client:
114132
await client.call_tool("learn", {})

tests/interaction/lowlevel/test_logging.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestPara
7676
)
7777
return CallToolResult(content=[TextContent(text="done")])
7878

79-
server = Server("logger", on_list_tools=list_tools, on_call_tool=call_tool)
79+
async def set_logging_level(ctx: ServerRequestContext, params: types.SetLevelRequestParams) -> EmptyResult:
80+
"""Registered so the logging capability is advertised; the client never sets a level."""
81+
raise NotImplementedError
82+
83+
server = Server("logger", on_list_tools=list_tools, on_call_tool=call_tool, on_set_logging_level=set_logging_level)
8084

8185
async with connect(server, logging_callback=collect) as client:
8286
result = await client.call_tool("chatty", {})
@@ -111,7 +115,11 @@ async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestPara
111115
)
112116
return CallToolResult(content=[TextContent(text="logged")])
113117

114-
server = Server("logger", on_list_tools=list_tools, on_call_tool=call_tool)
118+
async def set_logging_level(ctx: ServerRequestContext, params: types.SetLevelRequestParams) -> EmptyResult:
119+
"""Registered so the logging capability is advertised; the client never sets a level."""
120+
raise NotImplementedError
121+
122+
server = Server("logger", on_list_tools=list_tools, on_call_tool=call_tool, on_set_logging_level=set_logging_level)
115123

116124
async with connect(server, logging_callback=collect) as client:
117125
await client.call_tool("siren", {})

tests/interaction/lowlevel/test_resources.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,23 @@ async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestPara
281281
await ctx.session.send_resource_updated("file:///watched.txt")
282282
return CallToolResult(content=[TextContent(text="touched")])
283283

284-
server = Server("library", on_list_tools=list_tools, on_call_tool=call_tool)
284+
async def list_resources(
285+
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
286+
) -> ListResourcesResult:
287+
"""Registered so the resources capability is advertised; the client never lists resources."""
288+
raise NotImplementedError
289+
290+
async def subscribe_resource(ctx: ServerRequestContext, params: types.SubscribeRequestParams) -> EmptyResult:
291+
"""Registered so the resources subscribe sub-capability is advertised; the client never subscribes."""
292+
raise NotImplementedError
293+
294+
server = Server(
295+
"library",
296+
on_list_tools=list_tools,
297+
on_call_tool=call_tool,
298+
on_list_resources=list_resources,
299+
on_subscribe_resource=subscribe_resource,
300+
)
285301

286302
async with connect(server, message_handler=collect) as client:
287303
await client.call_tool("touch", {})

tests/interaction/lowlevel/test_roots.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,11 @@ async def roots_list_changed(ctx: ServerRequestContext, params: types.Notificati
154154

155155
server = Server("rooted", on_roots_list_changed=roots_list_changed)
156156

157-
async with connect(server) as client:
157+
async def list_roots(context: ClientRequestContext) -> ListRootsResult:
158+
"""Registered so the client declares the roots capability; the server never asks for roots."""
159+
raise NotImplementedError
160+
161+
async with connect(server, list_roots_callback=list_roots) as client:
158162
await client.send_roots_list_changed()
159163
with anyio.fail_after(5):
160164
await delivered.wait()

tests/interaction/lowlevel/test_wire.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from inline_snapshot import snapshot
1616

1717
from mcp import MCPError, types
18-
from mcp.client import ClientSession
18+
from mcp.client import ClientRequestContext, ClientSession
1919
from mcp.client._memory import InMemoryTransport
2020
from mcp.client.client import Client
2121
from mcp.server import Server, ServerRequestContext
@@ -33,6 +33,7 @@
3333
JSONRPCNotification,
3434
JSONRPCRequest,
3535
JSONRPCResponse,
36+
ListRootsResult,
3637
TextContent,
3738
)
3839
from tests.interaction._helpers import RecordingTransport, _RecordingReadStream
@@ -87,9 +88,14 @@ async def test_notifications_are_never_answered() -> None:
8788
the messages received from the server must be exactly one response per request, each carrying
8889
the id of the request it answers, and nothing else.
8990
"""
91+
92+
async def list_roots(context: ClientRequestContext) -> ListRootsResult:
93+
"""Registered so the client declares the roots capability; the server never asks for roots."""
94+
raise NotImplementedError
95+
9096
recording = RecordingTransport(InMemoryTransport(_echo_server()))
9197

92-
async with Client(recording) as client:
98+
async with Client(recording, list_roots_callback=list_roots) as client:
9399
await client.send_roots_list_changed()
94100
await client.send_ping()
95101

tests/interaction/transports/_stdio_server.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
from mcp.types import (
1616
CallToolRequestParams,
1717
CallToolResult,
18+
EmptyResult,
1819
ListToolsResult,
1920
PaginatedRequestParams,
21+
SetLevelRequestParams,
2022
TextContent,
2123
Tool,
2224
)
@@ -41,7 +43,12 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
4143
return CallToolResult(content=[TextContent(text=text)])
4244

4345

44-
server = Server("stdio-echo", on_list_tools=list_tools, on_call_tool=call_tool)
46+
async def set_logging_level(ctx: ServerRequestContext, params: SetLevelRequestParams) -> EmptyResult:
47+
"""Registered so the logging capability is advertised; the client never sets a level."""
48+
raise NotImplementedError
49+
50+
51+
server = Server("stdio-echo", on_list_tools=list_tools, on_call_tool=call_tool, on_set_logging_level=set_logging_level)
4552

4653

4754
async def main() -> None:

tests/interaction/transports/test_hosting_http.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@
1919
PARSE_ERROR,
2020
CallToolRequestParams,
2121
CallToolResult,
22+
EmptyResult,
2223
JSONRPCError,
2324
JSONRPCNotification,
2425
JSONRPCRequest,
2526
JSONRPCResponse,
27+
ListResourcesResult,
2628
ListToolsResult,
2729
PaginatedRequestParams,
30+
SetLevelRequestParams,
31+
SubscribeRequestParams,
2832
TextContent,
2933
)
3034
from tests.interaction._connect import (
@@ -52,7 +56,26 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
5256
await ctx.session.send_resource_updated("file:///watched.txt")
5357
return CallToolResult(content=[TextContent(text="done")])
5458

55-
return Server("hosted", on_list_tools=list_tools, on_call_tool=call_tool)
59+
async def set_logging_level(ctx: ServerRequestContext, params: SetLevelRequestParams) -> EmptyResult:
60+
"""Registered so the logging capability is advertised; the client never sets a level."""
61+
raise NotImplementedError
62+
63+
async def list_resources(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListResourcesResult:
64+
"""Registered so the resources capability is advertised; the client never lists resources."""
65+
raise NotImplementedError
66+
67+
async def subscribe_resource(ctx: ServerRequestContext, params: SubscribeRequestParams) -> EmptyResult:
68+
"""Registered so the resources subscribe sub-capability is advertised; the client never subscribes."""
69+
raise NotImplementedError
70+
71+
return Server(
72+
"hosted",
73+
on_list_tools=list_tools,
74+
on_call_tool=call_tool,
75+
on_set_logging_level=set_logging_level,
76+
on_list_resources=list_resources,
77+
on_subscribe_resource=subscribe_resource,
78+
)
5679

5780

5881
@requirement("hosting:http:method-405")

0 commit comments

Comments
 (0)