22
33These exercise the closed set of extension contribution kinds - tools,
44resources, request methods, and the single `tools/call` interceptor - through
5- the highest-level public surface (in-memory `Client`), plus the
6- `compose_tool_call_interceptor` helper directly.
5+ the highest-level public surface (in-memory `Client`).
76"""
87
9- from typing import Any , Literal , cast
8+ from importlib .metadata import version
9+ from typing import Any , Literal
1010
1111import mcp_types as types
1212import pytest
1313from inline_snapshot import snapshot
1414from mcp_types import (
1515 METHOD_NOT_FOUND ,
1616 MISSING_REQUIRED_CLIENT_CAPABILITY ,
17+ SERVER_INFO_META_KEY ,
1718 CallToolResult ,
1819 TextContent ,
1920)
2627 MethodBinding ,
2728 ResourceBinding ,
2829 ToolBinding ,
29- compose_tool_call_interceptor ,
3030)
3131from mcp .server .mcpserver import Context , MCPServer , require_client_extension
3232from mcp .server .mcpserver .resources import TextResource
@@ -222,23 +222,28 @@ async def test_short_circuiting_interceptor_replaces_tool_result() -> None:
222222 assert result == snapshot (CallToolResult (content = [TextContent (text = "intercepted" )]))
223223
224224
225- def test_plain_extension_installs_no_tool_call_interceptor () -> None :
226- """SDK-defined: an extension that does not override `intercept_tool_call` adds no
227- middleware - the composed interceptor exists only when at least one extension
228- overrides it."""
229- baseline = len (MCPServer ("test" )._lowlevel_server .middleware )
225+ def test_plain_extension_leaves_the_tool_call_handler_bare () -> None :
226+ """SDK-defined: an extension that does not override `intercept_tool_call` leaves
227+ `tools/call` registered as the server's own handler - the interceptor chain is
228+ composed only when at least one extension overrides it."""
230229 server = MCPServer ("test" , extensions = [_AdditiveExt ()])
231230
232- assert len (server ._lowlevel_server .middleware ) == baseline
231+ entry = server ._lowlevel_server .get_request_handler ("tools/call" )
232+ assert entry is not None
233+ assert entry .handler == server ._handle_call_tool
233234
234235
235- def test_overriding_extension_installs_one_tool_call_interceptor () -> None :
236- """SDK-defined: an extension that overrides `intercept_tool_call` composes exactly
237- one additional `tools/call` middleware."""
236+ def test_overriding_extension_wraps_the_tool_call_handler () -> None :
237+ """SDK-defined: an extension that overrides `intercept_tool_call` re-registers
238+ `tools/call` with the interceptor chain wrapped around the server's own handler,
239+ and installs no middleware."""
238240 baseline = len (MCPServer ("test" )._lowlevel_server .middleware )
239241 server = MCPServer ("test" , extensions = [_ReplacingExt ()])
240242
241- assert len (server ._lowlevel_server .middleware ) == baseline + 1
243+ entry = server ._lowlevel_server .get_request_handler ("tools/call" )
244+ assert entry is not None
245+ assert entry .handler != server ._handle_call_tool
246+ assert len (server ._lowlevel_server .middleware ) == baseline
242247
243248
244249async def test_default_interceptor_passes_through_alongside_an_overriding_one () -> None :
@@ -255,7 +260,7 @@ async def test_default_interceptor_passes_through_alongside_an_overriding_one()
255260
256261
257262async def test_interceptors_run_in_registration_order_with_threaded_params () -> None :
258- """SDK-defined: `compose_tool_call_interceptor ` nests extensions first-outermost, so
263+ """SDK-defined: `compose_tool_call_handler ` nests extensions first-outermost, so
259264 two passing-through interceptors record in registration order, each seeing the
260265 validated `tools/call` params (the real tool name)."""
261266 log : list [tuple [str , str ]] = []
@@ -271,26 +276,18 @@ async def test_interceptors_run_in_registration_order_with_threaded_params() ->
271276 assert log == [("com.example/first" , "echo" ), ("com.example/second" , "echo" )]
272277
273278
274- async def test_compose_tool_call_interceptor_passes_through_non_tools_call () -> None :
275- """SDK-defined: the composed middleware is a no-op for any method other than
276- `tools/call` - it forwards to `call_next` without touching the interceptors."""
277- sentinel = types .EmptyResult ()
278-
279- async def call_next (ctx : ServerRequestContext [Any , Any ]) -> HandlerResult :
280- return sentinel
281-
282- middleware = compose_tool_call_interceptor ([_ReplacingExt ()])
283- ctx = ServerRequestContext (
284- session = cast ("Any" , None ),
285- lifespan_context = {},
286- protocol_version = "2026-07-28" ,
287- method = "tasks/get" ,
288- params = {"taskId" : "t-1" },
289- )
279+ async def test_short_circuited_interceptor_result_carries_the_server_info_stamp () -> None :
280+ """Spec-mandated (2026-07-28, #3002): an interceptor that answers without running
281+ the tool still produces a stamped result - interception happens at the handler
282+ layer, below the runner's outbound envelope pass."""
283+ server = MCPServer ("test" , extensions = [_ReplacingExt ()])
284+ server .tool (name = "echo" , structured_output = False )(_echo )
290285
291- result = await middleware (ctx , call_next )
286+ async with Client (server ) as client :
287+ result = await client .call_tool ("echo" , {"value" : "hi" })
292288
293- assert result is sentinel
289+ assert result .content == [TextContent (text = "intercepted" )]
290+ assert result .meta == {SERVER_INFO_META_KEY : {"name" : "test" , "version" : version ("mcp" )}}
294291
295292
296293def test_extension_subclass_without_prefixed_identifier_is_rejected_at_definition () -> None :
0 commit comments