diff --git a/docs/development/v4-notes/change-register.mdx b/docs/development/v4-notes/change-register.mdx index 2ad00173a..81fb21359 100644 --- a/docs/development/v4-notes/change-register.mdx +++ b/docs/development/v4-notes/change-register.mdx @@ -166,7 +166,7 @@ The `fastmcp.Client` public API is largely preserved. The client stays a wrapper `Client(mode=...)` now defaults to `"auto"` instead of `"legacy"`. The client probes `server/discover` and adopts the modern (`2026-07-28`) era when the server responds, denylist-falling-back to the initialize handshake for any server that is not positive evidence of a modern peer. Against a FastMCP server (which serves both eras), an ordinary `Client(url)` now negotiates the modern era by default, where the legacy-only Context push features are unavailable per the per-feature era matrix (see the *Protocol eras* section below) — server-initiated sampling/elicitation/roots, `ping`, session ids, and FastMCP task submission all require the legacy era. The one-line revert is `Client(..., mode="legacy")`, which restores byte-identical pre-v4 negotiation. -The SSE transport is legacy-only (it cannot carry the sessionless modern era), so a client connecting over SSE negotiates the legacy handshake even under `mode="auto"` — expressed by a `ClientTransport.legacy_only` flag set on `SSETransport`. +The SSE transport is legacy-only (it cannot carry the sessionless modern era), so a client connecting over SSE negotiates the legacy handshake even under `mode="auto"` — expressed by a `ClientTransport.legacy_only` flag set on `SSETransport` and `MCPConfigTransport`. Two internal library seams that are inherently handshake-based are pinned to legacy so the flip does not break them: the `ProxyClient` backend (which forwards the initialize handshake and server-initiated features) defaults to `mode="legacy"`, and the `inspect` utility (which reads the full `server_info` only the handshake carries) connects legacy. ```python from fastmcp import Client @@ -175,7 +175,7 @@ client = Client("https://example.com/mcp") # now negotiates "au client = Client("https://example.com/mcp", mode="legacy") # opt back into the handshake ``` -*Verify:* `fastmcp_slim/fastmcp/client/client.py` (`mode` default, `_negotiate` `legacy_only` shortcut), `fastmcp_slim/fastmcp/client/transports/{base,sse}.py` (`legacy_only`), `tests/client/client/test_mode_negotiation.py` (default, clean discover-rejection fallback, legacy-only transport), `docs/clients/client.mdx`. +*Verify:* `fastmcp_slim/fastmcp/client/client.py` (`mode` default, `_negotiate` `legacy_only` shortcut), `fastmcp_slim/fastmcp/client/transports/{base,sse,config}.py` (`legacy_only`), `fastmcp_slim/fastmcp/server/providers/proxy.py` (`ProxyClient` legacy default), `fastmcp_slim/fastmcp/mcp_config.py` and `fastmcp_slim/fastmcp/utilities/inspect.py` (legacy inner clients), `tests/client/client/test_mode_negotiation.py` (default, clean discover-rejection fallback, legacy-only transport), `docs/clients/client.mdx`. ### `extensions=` / `result_claims=` surfaced — New (opt-in feature) diff --git a/fastmcp_slim/fastmcp/client/transports/config.py b/fastmcp_slim/fastmcp/client/transports/config.py index 472e94b86..6a71ea83c 100644 --- a/fastmcp_slim/fastmcp/client/transports/config.py +++ b/fastmcp_slim/fastmcp/client/transports/config.py @@ -72,6 +72,11 @@ class MCPConfigTransport(ClientTransport): ``` """ + # This transport fronts a proxy whose backend ProxyClient is legacy-only + # (proxy forwarding relies on the handshake era), so the composite server it + # exposes is legacy-era; a client with mode="auto" negotiates the handshake. + legacy_only = True + def __init__(self, config: MCPConfig | dict, name_as_prefix: bool = True): if isinstance(config, dict): config = MCPConfig.from_dict(config) diff --git a/fastmcp_slim/fastmcp/mcp_config.py b/fastmcp_slim/fastmcp/mcp_config.py index 286e34c56..9737b48b1 100644 --- a/fastmcp_slim/fastmcp/mcp_config.py +++ b/fastmcp_slim/fastmcp/mcp_config.py @@ -137,7 +137,9 @@ class _TransformingMCPServerMixin(BaseModel): ) from exc transport = cast("ClientTransport", super().to_transport()) # ty: ignore[unresolved-attribute] - client = Client(transport=transport, name=client_name) + # The proxy that wraps this client forwards the initialize handshake and + # server-initiated features, which require the legacy era. + client = Client(transport=transport, name=client_name, mode="legacy") wrapped_mcp_server = create_proxy(client, name=server_name) if self.include_tags is not None: diff --git a/fastmcp_slim/fastmcp/server/providers/proxy.py b/fastmcp_slim/fastmcp/server/providers/proxy.py index fd0817af1..1126ce3a0 100644 --- a/fastmcp_slim/fastmcp/server/providers/proxy.py +++ b/fastmcp_slim/fastmcp/server/providers/proxy.py @@ -1116,6 +1116,12 @@ class ProxyClient(Client[ClientTransportT]): ): if "name" not in kwargs: kwargs["name"] = self.generate_name() + # Proxy forwarding is built on the legacy handshake era: it relays + # server-initiated roots/sampling/elicitation/logging (unavailable on + # the sessionless modern era) and forwards the backend's initialize + # result. Default the backend connection to legacy unless the caller + # explicitly opts into another mode. + kwargs.setdefault("mode", "legacy") # Install context-restoring handler wrappers BEFORE super().__init__ # registers them with the Client's session kwargs. self._proxy_rc_ref = [None] diff --git a/fastmcp_slim/fastmcp/utilities/inspect.py b/fastmcp_slim/fastmcp/utilities/inspect.py index b9e3d0827..1ce403643 100644 --- a/fastmcp_slim/fastmcp/utilities/inspect.py +++ b/fastmcp_slim/fastmcp/utilities/inspect.py @@ -257,8 +257,9 @@ async def inspect_fastmcp_v1(mcp: SDKServer) -> FastMCPInfo: Returns: FastMCPInfo dataclass containing the extracted information """ - # Use a client to interact with the SDK's high-level MCPServer - async with Client(mcp) as client: + # Inspection reads the full server_info (icons, website_url) that only the + # legacy initialize handshake carries, so pin the handshake era. + async with Client(mcp, mode="legacy") as client: # Get components via client calls (these return MCP objects) mcp_tools = await client.list_tools() mcp_prompts = await client.list_prompts() @@ -467,7 +468,9 @@ async def format_mcp_info(mcp: FastMCP[Any] | SDKServer) -> bytes: Uses Client to get the standard MCP protocol format with camelCase fields. Includes version metadata at the top level. """ - async with Client(mcp) as client: + # Inspection reads the full server_info that only the legacy initialize + # handshake carries, so pin the handshake era. + async with Client(mcp, mode="legacy") as client: # Get all the MCP protocol objects tools_result = await client.list_tools_mcp() prompts_result = await client.list_prompts_mcp() diff --git a/tests/client/test_openapi.py b/tests/client/test_openapi.py index 236e6efb3..eaaecb8a8 100644 --- a/tests/client/test_openapi.py +++ b/tests/client/test_openapi.py @@ -196,7 +196,11 @@ async def test_client_headers_proxy(proxy_server: str): """ Test that client headers are passed through the proxy to the remove server. """ - async with Client(transport=StreamableHttpTransport(proxy_server)) as client: + # The proxy backend forwards over the legacy handshake, so align the outer + # client's era with it. + async with Client( + transport=StreamableHttpTransport(proxy_server), mode="legacy" + ) as client: result = await client.read_resource("resource://get_headers_headers_get") assert isinstance(result[0], TextResourceContents) headers = json.loads(result[0].text) diff --git a/tests/deprecated/test_elicitation.py b/tests/deprecated/test_elicitation.py index d86e549d8..cbb35541c 100644 --- a/tests/deprecated/test_elicitation.py +++ b/tests/deprecated/test_elicitation.py @@ -25,5 +25,7 @@ async def test_elicitation_none_response_type_warns_deprecation(): async def elicitation_handler(message, response_type, params, ctx): return ElicitResult(action="accept", content={}) - async with Client(mcp, elicitation_handler=elicitation_handler) as client: + async with Client( + mcp, mode="legacy", elicitation_handler=elicitation_handler + ) as client: await client.call_tool("my_tool", {}) diff --git a/tests/server/middleware/test_initialization_middleware.py b/tests/server/middleware/test_initialization_middleware.py index 3a077b367..a05727d04 100644 --- a/tests/server/middleware/test_initialization_middleware.py +++ b/tests/server/middleware/test_initialization_middleware.py @@ -123,7 +123,7 @@ async def test_simple_initialization_hook(): server.add_middleware(middleware) # Connect client - async with Client(server): + async with Client(server, mode="legacy"): # Middleware should have been called assert middleware.called is True, "on_initialize was not called" @@ -139,7 +139,7 @@ async def test_middleware_receives_initialization(): return f"Result: {x}" # Connect client - async with Client(server) as client: + async with Client(server, mode="legacy") as client: # Middleware should have been called during initialization assert middleware.initialized is True @@ -160,7 +160,7 @@ async def test_client_detection_middleware(): return "example" # Connect with a client - async with Client(server) as client: + async with Client(server, mode="legacy") as client: # Middleware should have been called during initialization assert middleware.initialization_called is True assert middleware.is_test_client is True @@ -190,7 +190,7 @@ async def test_multiple_middleware_initialization(): def test_tool() -> str: return "test" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: # Both middleware should have processed initialization assert init_mw.initialized is True assert detect_mw.initialization_called is True @@ -241,7 +241,7 @@ async def test_session_state_persists_across_tool_calls(): def test_tool() -> str: return "success" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: # First call - state should be None initially result = await client.call_tool("test_tool", {}) assert isinstance(result.content[0], TextContent) @@ -287,7 +287,7 @@ async def test_middleware_can_access_initialize_result(): middleware = ResponseCapturingMiddleware() server.add_middleware(middleware) - async with Client(server): + async with Client(server, mode="legacy"): # Middleware should have captured the InitializeResult assert middleware.initialize_result is not None assert isinstance(middleware.initialize_result, mt.InitializeResult) @@ -315,7 +315,7 @@ async def test_middleware_mcp_error_during_initialization(): server.add_middleware(ErrorThrowingMiddleware()) with pytest.raises(MCPError) as exc_info: - async with Client(server): + async with Client(server, mode="legacy"): pass assert exc_info.value.error.message == "Invalid initialization parameters" @@ -337,7 +337,7 @@ async def test_middleware_mcp_error_before_call_next(): server.add_middleware(EarlyErrorMiddleware()) with pytest.raises(MCPError) as exc_info: - async with Client(server): + async with Client(server, mode="legacy"): pass assert exc_info.value.error.message == "Request validation failed" @@ -370,7 +370,7 @@ async def test_middleware_mcp_error_after_call_next(): server.add_middleware(middleware) # Error is logged but not re-raised to prevent duplicate response - async with Client(server): + async with Client(server, mode="legacy"): pass assert middleware.error_raised is True @@ -403,7 +403,7 @@ async def test_state_isolation_between_streamable_http_clients(): # Client 1 stores its value transport1 = StreamableHttpTransport(url=url) - async with Client(transport=transport1) as client1: + async with Client(transport=transport1, mode="legacy") as client1: result1 = await client1.call_tool( "store_and_read", {"value": "client1-value"} ) @@ -414,7 +414,7 @@ async def test_state_isolation_between_streamable_http_clients(): # Client 2 should have completely isolated state transport2 = StreamableHttpTransport(url=url) - async with Client(transport=transport2) as client2: + async with Client(transport=transport2, mode="legacy") as client2: result2 = await client2.call_tool( "store_and_read", {"value": "client2-value"} ) diff --git a/tests/server/middleware/test_middleware.py b/tests/server/middleware/test_middleware.py index f58736437..8518845b7 100644 --- a/tests/server/middleware/test_middleware.py +++ b/tests/server/middleware/test_middleware.py @@ -173,7 +173,7 @@ class TestMiddlewareHooks: async def test_call_tool( self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware ): - async with Client(mcp_server) as client: + async with Client(mcp_server, mode="legacy") as client: await client.call_tool("add", {"a": 1, "b": 2}) assert recording_middleware.assert_called(at_least=9) @@ -299,7 +299,7 @@ class TestMiddlewareHooks: async def test_initialize( self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware ): - async with Client(mcp_server) as client: + async with Client(mcp_server, mode="legacy") as client: await client.ping() assert recording_middleware.assert_called(at_least=1) diff --git a/tests/server/middleware/test_middleware_nested.py b/tests/server/middleware/test_middleware_nested.py index cc2eea6c0..a67b78830 100644 --- a/tests/server/middleware/test_middleware_nested.py +++ b/tests/server/middleware/test_middleware_nested.py @@ -477,7 +477,7 @@ class TestProxyServer: # proxy server will have its tools listed as well as called in order to # apply transforms and filters prior to the call. proxy_server = create_proxy(mcp_server, name="Proxy Server") - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: await client.call_tool("add", {"a": 1, "b": 2}) assert recording_middleware.assert_called(at_least=6) diff --git a/tests/server/middleware/test_ping.py b/tests/server/middleware/test_ping.py index b35616fc1..4713ca7a5 100644 --- a/tests/server/middleware/test_ping.py +++ b/tests/server/middleware/test_ping.py @@ -193,7 +193,7 @@ class TestPingMiddlewareIntegration: assert len(middleware._active_sessions) == 0 - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: result = await client.call_tool("hello") assert result.content[0].text == "Hello!" @@ -214,7 +214,7 @@ class TestPingMiddlewareIntegration: def hello() -> str: return "Hello!" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: await client.call_tool("hello") # Should have one active session assert len(middleware._active_sessions) == 1 diff --git a/tests/server/providers/proxy/test_proxy_client.py b/tests/server/providers/proxy/test_proxy_client.py index 216d283e8..2fca6fd51 100644 --- a/tests/server/providers/proxy/test_proxy_client.py +++ b/tests/server/providers/proxy/test_proxy_client.py @@ -97,7 +97,7 @@ class TestProxyClient: """ Test that the proxy client correctly forwards the `echo` tool meta. """ - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: tools = await client.list_tools() echo_tool = next(t for t in tools if t.name == "echo") assert echo_tool.meta == {"fastmcp": {"tags": ["echo"]}} @@ -106,7 +106,7 @@ class TestProxyClient: """ Test that the proxy client correctly forwards an error response. """ - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: with pytest.raises(ToolError, match="Elicitation not supported"): await client.call_tool("elicit", {}) @@ -121,7 +121,7 @@ class TestProxyClient: roots_handler_called = True return [] - async with Client(proxy_server, roots=roots_handler) as client: + async with Client(proxy_server, mode="legacy", roots=roots_handler) as client: await client.call_tool("list_roots", {}) assert roots_handler_called @@ -130,7 +130,9 @@ class TestProxyClient: """ Test that the proxy client correctly forwards the `list_roots` response. """ - async with Client(proxy_server, roots=["file://x/y/z"]) as client: + async with Client( + proxy_server, mode="legacy", roots=["file://x/y/z"] + ) as client: result = await client.call_tool("list_roots", {}) assert result.data == ["file://x/y/z"] @@ -161,7 +163,9 @@ class TestProxyClient: ) return "" - async with Client(proxy_server, sampling_handler=sampling_handler) as client: + async with Client( + proxy_server, mode="legacy", sampling_handler=sampling_handler + ) as client: await client.call_tool("sampling", {}) assert sampling_handler_called @@ -171,7 +175,7 @@ class TestProxyClient: Test that the proxy client correctly forwards the `sampling` response. """ async with Client( - proxy_server, sampling_handler=lambda *args: "I love FastMCP" + proxy_server, mode="legacy", sampling_handler=lambda *args: "I love FastMCP" ) as client: result = await client.call_tool("sampling", {}) assert result.data == "I love FastMCP" @@ -199,7 +203,7 @@ class TestProxyClient: return ElicitResult(action="accept", content=response_type(name="Alice")) async with Client( - proxy_server, elicitation_handler=elicitation_handler + proxy_server, mode="legacy", elicitation_handler=elicitation_handler ) as client: await client.call_tool("elicit", {}) @@ -217,6 +221,7 @@ class TestProxyClient: async with Client( proxy_server, + mode="legacy", elicitation_handler=elicitation_handler, ) as client: result = await client.call_tool("elicit", {}) @@ -233,7 +238,7 @@ class TestProxyClient: return ElicitResult(action="decline") async with Client( - proxy_server, elicitation_handler=elicitation_handler + proxy_server, mode="legacy", elicitation_handler=elicitation_handler ) as client: result = await client.call_tool("elicit", {}) assert result.data == "No name provided." @@ -251,7 +256,9 @@ class TestProxyClient: assert message.level == "info" assert message.logger == "test" - async with Client(proxy_server, log_handler=log_handler) as client: + async with Client( + proxy_server, mode="legacy", log_handler=log_handler + ) as client: await client.call_tool( "log", {"message": "Hello, world!", "level": "info", "logger": "test"} ) @@ -277,7 +284,9 @@ class TestProxyClient: dict(progress=progress, total=total, message=message) ) - async with Client(proxy_server, progress_handler=progress_handler) as client: + async with Client( + proxy_server, mode="legacy", progress_handler=progress_handler + ) as client: await client.call_tool("report_progress", {}) assert PROGRESS_MESSAGES == EXPECTED_PROGRESS_MESSAGES @@ -293,8 +302,8 @@ class TestProxyClient: results["logger_b"] = message async with ( - Client(proxy_server, log_handler=log_handler_a) as client_a, - Client(proxy_server, log_handler=log_handler_b) as client_b, + Client(proxy_server, mode="legacy", log_handler=log_handler_a) as client_a, + Client(proxy_server, mode="legacy", log_handler=log_handler_b) as client_b, ): async with create_task_group() as tg: tg.start_soon( @@ -336,8 +345,12 @@ class TestProxyClient: results[name] = result.data async with ( - Client(proxy_server, elicitation_handler=elicitation_handler_a) as client_a, - Client(proxy_server, elicitation_handler=elicitation_handler_b) as client_b, + Client( + proxy_server, mode="legacy", elicitation_handler=elicitation_handler_a + ) as client_a, + Client( + proxy_server, mode="legacy", elicitation_handler=elicitation_handler_b + ) as client_b, ): async with create_task_group() as tg: tg.start_soon( @@ -396,7 +409,7 @@ class TestProxyClient: return {"content": "Test content", "acknowledge": True} async with Client( - proxy_server, elicitation_handler=elicitation_handler + proxy_server, mode="legacy", elicitation_handler=elicitation_handler ) as client: result = await client.call_tool("elicit_with_defaults", {}) assert result.data == "Content: Test content, Acknowledge: True" @@ -406,7 +419,7 @@ class TestProxyClient: from fastmcp.server.providers.proxy import FastMCPProxy # Create a disconnected client (should use fresh sessions per request) - base_client = Client(fastmcp_server) + base_client = Client(fastmcp_server, mode="legacy") # Test both create_proxy convenience function and direct client_factory usage proxy_via_create_proxy = create_proxy(base_client) @@ -488,7 +501,9 @@ class TestProxyServerInitiatedForwardingNonTool: roots_handler_called = True return ["file://from/client"] - async with Client(roots_proxy_server, roots=roots_handler) as client: + async with Client( + roots_proxy_server, mode="legacy", roots=roots_handler + ) as client: result = await client.read_resource("data://roots") assert roots_handler_called @@ -504,7 +519,9 @@ class TestProxyServerInitiatedForwardingNonTool: roots_handler_called = True return ["file://from/client"] - async with Client(roots_proxy_server, roots=roots_handler) as client: + async with Client( + roots_proxy_server, mode="legacy", roots=roots_handler + ) as client: result = await client.read_resource("data://roots/abc") assert roots_handler_called @@ -520,7 +537,9 @@ class TestProxyServerInitiatedForwardingNonTool: roots_handler_called = True return ["file://from/client"] - async with Client(roots_proxy_server, roots=roots_handler) as client: + async with Client( + roots_proxy_server, mode="legacy", roots=roots_handler + ) as client: result = await client.get_prompt("roots_prompt") assert roots_handler_called diff --git a/tests/server/providers/proxy/test_proxy_server.py b/tests/server/providers/proxy/test_proxy_server.py index 762a09abc..6d85f9712 100644 --- a/tests/server/providers/proxy/test_proxy_server.py +++ b/tests/server/providers/proxy/test_proxy_server.py @@ -176,7 +176,7 @@ async def test_create_proxy_with_client(fastmcp_server): async def test_create_proxy_with_server(fastmcp_server): """create_proxy should accept a FastMCP instance.""" proxy = create_proxy(fastmcp_server) - async with Client(proxy) as client: + async with Client(proxy, mode="legacy") as client: result = await client.call_tool("greet", {"name": "Test"}) assert result.data == "Hello, Test!" @@ -184,7 +184,7 @@ async def test_create_proxy_with_server(fastmcp_server): async def test_create_proxy_with_transport(fastmcp_server): """create_proxy should accept a ClientTransport.""" proxy = create_proxy(FastMCPTransport(fastmcp_server)) - async with Client(proxy) as client: + async with Client(proxy, mode="legacy") as client: result = await client.call_tool("greet", {"name": "Test"}) assert result.data == "Hello, Test!" @@ -218,7 +218,7 @@ async def test_proxy_with_async_client_factory(): async def test_proxy_ping_forwards_to_remote_server(fastmcp_server): proxy = create_proxy(fastmcp_server) - async with Client(proxy) as client: + async with Client(proxy, mode="legacy") as client: assert await client.ping() is True @@ -230,7 +230,7 @@ async def test_proxy_ping_surfaces_wrong_remote_path(): # SDK v2 surfaces a wrong remote path as an HTTP "Not Found" rather than # the v1 "Session terminated" message. with pytest.raises(MCPError, match="Not Found"): - async with Client(proxy): + async with Client(proxy, mode="legacy"): pass @@ -242,7 +242,7 @@ async def test_proxy_initialize_forwards_remote_connection_error(): ) with pytest.raises(MCPError, match="Client failed to connect"): - async with Client(proxy): + async with Client(proxy, mode="legacy"): pass @@ -265,7 +265,7 @@ async def test_proxy_list_tools_client_surfaces_remote_connection_error(): ) with pytest.raises(MCPError, match="Client failed to connect"): - async with Client(proxy) as client: + async with Client(proxy, mode="legacy") as client: await client.list_tools() @@ -322,7 +322,7 @@ class TestTools: ) proxy = create_proxy(server) - async with Client(proxy) as client: + async with Client(proxy, mode="legacy") as client: result = await client.call_tool("add_transformed", {"a": 1, "b": 2}) assert result.data == 3 @@ -332,31 +332,31 @@ class TestTools: assert tool.description is None async def test_list_tools_same_as_original(self, fastmcp_server, proxy_server): - async with Client(fastmcp_server) as original_client: + async with Client(fastmcp_server, mode="legacy") as original_client: original = await original_client.list_tools() - async with Client(proxy_server) as proxy_client: + async with Client(proxy_server, mode="legacy") as proxy_client: proxied = await proxy_client.list_tools() assert proxied == original async def test_call_tool_result_same_as_original( self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy ): - async with Client(fastmcp_server) as original_client: + async with Client(fastmcp_server, mode="legacy") as original_client: result = await original_client.call_tool("greet", {"name": "Alice"}) - async with Client(proxy_server) as proxy_client: + async with Client(proxy_server, mode="legacy") as proxy_client: proxy_result = await proxy_client.call_tool("greet", {"name": "Alice"}) assert result.content == proxy_result.content assert result.data == proxy_result.data async def test_call_tool_calls_tool(self, proxy_server): - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: proxy_result = await client.call_tool("add", {"a": 1, "b": 2}) assert proxy_result.data == 3 async def test_error_tool_raises_error(self, proxy_server): with pytest.raises(ToolError, match="This is a test error"): - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: await client.call_tool("error_tool", {}) async def test_error_tool_with_image_content(self, proxy_server): @@ -373,7 +373,7 @@ class TestTools: Client, "call_tool_mcp", new_callable=AsyncMock, return_value=error_result ): with pytest.raises(ToolError): - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: await client.call_tool("error_tool", {}) async def test_error_tool_with_empty_content(self, proxy_server): @@ -386,7 +386,7 @@ class TestTools: Client, "call_tool_mcp", new_callable=AsyncMock, return_value=error_result ): with pytest.raises(ToolError): - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: await client.call_tool("error_tool", {}) async def test_error_tool_passthrough_preserves_content(self, proxy_server): @@ -403,7 +403,7 @@ class TestTools: with patch.object( Client, "call_tool_mcp", new_callable=AsyncMock, return_value=error_result ): - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.call_tool("error_tool", {}, raise_on_error=False) assert result.is_error is True @@ -422,7 +422,7 @@ class TestTools: meta={"custom_key": "custom_value", "processed": True}, ) - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.call_tool("tool_with_meta", {"value": "test"}) assert isinstance(result.content[0], TextContent) @@ -438,7 +438,7 @@ class TestTools: def greet(name: str, extra: str = "extra") -> str: return f"Overwritten, {name}! {extra}" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.call_tool("greet", {"name": "Marvin", "extra": "abc"}) assert result.data == "Overwritten, Marvin! abc" @@ -451,7 +451,7 @@ class TestTools: def greet(name: str, extra: str = "extra") -> str: return f"Overwritten, {name}! {extra}" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: tools = await client.list_tools() greet_tool = next(t for t in tools if t.name == "greet") assert "extra" in greet_tool.input_schema["properties"] @@ -474,27 +474,27 @@ class TestResources: assert wave_resource.icons == [Icon(src="https://example.com/wave-icon.png")] async def test_list_resources_same_as_original(self, fastmcp_server, proxy_server): - async with Client(fastmcp_server) as original_client: + async with Client(fastmcp_server, mode="legacy") as original_client: original = await original_client.list_resources() - async with Client(proxy_server) as proxy_client: + async with Client(proxy_server, mode="legacy") as proxy_client: proxied = await proxy_client.list_resources() assert proxied == original async def test_read_resource(self, proxy_server: FastMCPProxy): - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.read_resource("resource://wave") assert isinstance(result[0], TextResourceContents) assert result[0].text == "👋" async def test_read_resource_same_as_original(self, fastmcp_server, proxy_server): - async with Client(fastmcp_server) as client: + async with Client(fastmcp_server, mode="legacy") as client: result = await client.read_resource("resource://wave") - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: proxy_result = await client.read_resource("resource://wave") assert proxy_result == result async def test_read_json_resource(self, proxy_server: FastMCPProxy): - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.read_resource("data://users") assert len(result) == 1 assert isinstance(result[0], TextResourceContents) @@ -507,11 +507,11 @@ class TestResources: ): """Test that proxy correctly returns all resource contents, not just the first one.""" # Read from original server - async with Client(fastmcp_server) as client: + async with Client(fastmcp_server, mode="legacy") as client: original_result = await client.read_resource("data://multi") # Read from proxy server - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: proxy_result = await client.read_resource("data://multi") # Both should return the same number of contents @@ -540,7 +540,7 @@ class TestResources: with pytest.raises( MCPError, match="Resource not found: 'resource://nonexistent'" ): - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: await client.read_resource("resource://nonexistent") async def test_proxy_can_overwrite_proxied_resource(self, proxy_server): @@ -552,7 +552,7 @@ class TestResources: def overwritten_wave() -> str: return "Overwritten wave! 🌊" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.read_resource("resource://wave") assert isinstance(result[0], TextResourceContents) assert result[0].text == "Overwritten wave! 🌊" @@ -566,7 +566,7 @@ class TestResources: def overwritten_wave() -> str: return "Overwritten wave! 🌊" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: resources = await client.list_resources() wave_resource = next( r for r in resources if str(r.uri) == "resource://wave" @@ -593,15 +593,15 @@ class TestResourceTemplates: async def test_list_resource_templates_same_as_original( self, fastmcp_server, proxy_server ): - async with Client(fastmcp_server) as original_client: + async with Client(fastmcp_server, mode="legacy") as original_client: result = await original_client.list_resource_templates() - async with Client(proxy_server) as proxy_client: + async with Client(proxy_server, mode="legacy") as proxy_client: proxy_result = await proxy_client.list_resource_templates() assert proxy_result == result @pytest.mark.parametrize("id", [1, 2, 3]) async def test_read_resource_template(self, proxy_server: FastMCPProxy, id: int): - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.read_resource(f"data://user/{id}") assert isinstance(result[0], TextResourceContents) assert json.loads(result[0].text) == USERS[id - 1] @@ -609,9 +609,9 @@ class TestResourceTemplates: async def test_read_resource_template_same_as_original( self, fastmcp_server, proxy_server ): - async with Client(fastmcp_server) as client: + async with Client(fastmcp_server, mode="legacy") as client: result = await client.read_resource("data://user/1") - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: proxy_result = await client.read_resource("data://user/1") assert proxy_result == result @@ -620,11 +620,11 @@ class TestResourceTemplates: ): """Test that proxy template correctly returns all resource contents.""" # Read from original server - async with Client(fastmcp_server) as client: + async with Client(fastmcp_server, mode="legacy") as client: original_result = await client.read_resource("data://multi/test123") # Read from proxy server - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: proxy_result = await client.read_resource("data://multi/test123") # Both should return the same number of contents @@ -662,7 +662,7 @@ class TestResourceTemplates: } ) - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.read_resource("data://user/1") assert isinstance(result[0], TextResourceContents) user_data = json.loads(result[0].text) @@ -678,7 +678,7 @@ class TestResourceTemplates: def overwritten_get_user(user_id: str) -> dict[str, Any]: return {"id": user_id, "name": "Overwritten User", "active": True} - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: templates = await client.list_resource_templates() user_template = next( t for t in templates if t.uri_template == "data://user/{user_id}" @@ -696,8 +696,8 @@ class TestResourceTemplateQueryParams: def get_data(id: str, format: str = "json") -> str: return f"id={id} format={format}" - proxy = create_proxy(Client(remote)) - async with Client(proxy) as client: + proxy = create_proxy(Client(remote, mode="legacy")) + async with Client(proxy, mode="legacy") as client: result = await client.read_resource("data://123?format=xml") assert isinstance(result[0], TextResourceContents) assert result[0].text == "id=123 format=xml" @@ -709,8 +709,8 @@ class TestResourceTemplateQueryParams: def get_data(id: str, format: str = "json") -> str: return f"id={id} format={format}" - proxy = create_proxy(Client(remote)) - async with Client(proxy) as client: + proxy = create_proxy(Client(remote, mode="legacy")) + async with Client(proxy, mode="legacy") as client: result = await client.read_resource("data://123") assert isinstance(result[0], TextResourceContents) assert result[0].text == "id=123 format=json" @@ -722,8 +722,8 @@ class TestResourceTemplateQueryParams: def get_data(id: str, limit: int = 10, offset: int = 0) -> str: return f"id={id} limit={limit} offset={offset}" - proxy = create_proxy(Client(remote)) - async with Client(proxy) as client: + proxy = create_proxy(Client(remote, mode="legacy")) + async with Client(proxy, mode="legacy") as client: result = await client.read_resource("data://abc?limit=5&offset=20") assert isinstance(result[0], TextResourceContents) assert result[0].text == "id=abc limit=5 offset=20" @@ -735,8 +735,8 @@ class TestResourceTemplateQueryParams: def get_data(id: str) -> str: return f"id={id}" - proxy = create_proxy(Client(remote)) - async with Client(proxy) as client: + proxy = create_proxy(Client(remote, mode="legacy")) + async with Client(proxy, mode="legacy") as client: result = await client.read_resource("data://a%2Fb") assert isinstance(result[0], TextResourceContents) assert result[0].text == "id=a/b" @@ -748,8 +748,8 @@ class TestResourceTemplateQueryParams: def get_data(id: str, api_version: str = "v1") -> str: return f"id={id} api_version={api_version}" - proxy = create_proxy(Client(remote)) - async with Client(proxy) as client: + proxy = create_proxy(Client(remote, mode="legacy")) + async with Client(proxy, mode="legacy") as client: result = await client.read_resource("data://123?api-version=v2") assert isinstance(result[0], TextResourceContents) assert result[0].text == "id=123 api_version=v2" @@ -769,8 +769,8 @@ class TestResourceTemplateQueryParams: def get_data(id: str, api_version: str = "v1") -> str: return f"id={id} api_version={api_version}" - proxy = create_proxy(Client(remote)) - async with Client(proxy) as client: + proxy = create_proxy(Client(remote, mode="legacy")) + async with Client(proxy, mode="legacy") as client: result = await client.read_resource("data://123?api-version=a%2Fb") assert isinstance(result[0], TextResourceContents) assert result[0].text == "id=123 api_version=a/b" @@ -791,23 +791,23 @@ class TestPrompts: ] async def test_list_prompts_same_as_original(self, fastmcp_server, proxy_server): - async with Client(fastmcp_server) as client: + async with Client(fastmcp_server, mode="legacy") as client: result = await client.list_prompts() - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: proxy_result = await client.list_prompts() assert proxy_result == result async def test_render_prompt_same_as_original( self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy ): - async with Client(fastmcp_server) as client: + async with Client(fastmcp_server, mode="legacy") as client: result = await client.get_prompt("welcome", {"name": "Alice"}) - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: proxy_result = await client.get_prompt("welcome", {"name": "Alice"}) assert proxy_result == result async def test_render_prompt_calls_prompt(self, proxy_server): - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.get_prompt("welcome", {"name": "Alice"}) assert result.messages[0].role == "user" assert isinstance(result.messages[0].content, TextContent) @@ -822,7 +822,7 @@ class TestPrompts: def welcome(name: str, extra: str = "friend") -> str: return f"Overwritten welcome, {name}! You are my {extra}." - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.get_prompt( "welcome", {"name": "Alice", "extra": "colleague"} ) @@ -842,7 +842,7 @@ class TestPrompts: def welcome(name: str, extra: str = "friend") -> str: return f"Overwritten welcome, {name}! You are my {extra}." - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: prompts = await client.list_prompts() welcome_prompt = next(p for p in prompts if p.name == "welcome") # Check that the overwritten prompt has the additional 'extra' parameter @@ -853,9 +853,9 @@ class TestPrompts: self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy ): """Test that ProxyPrompt preserves ImageContent without lossy conversion.""" - async with Client(fastmcp_server) as client: + async with Client(fastmcp_server, mode="legacy") as client: result = await client.get_prompt("image_prompt") - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: proxy_result = await client.get_prompt("image_prompt") # The proxy result should match the original exactly diff --git a/tests/server/providers/proxy/test_stateful_proxy_client.py b/tests/server/providers/proxy/test_stateful_proxy_client.py index 3ebb9d536..f9389d608 100644 --- a/tests/server/providers/proxy/test_stateful_proxy_client.py +++ b/tests/server/providers/proxy/test_stateful_proxy_client.py @@ -95,8 +95,12 @@ class TestStatefulProxyClient: results["logger_b"] = message async with ( - Client(stateful_proxy_server, log_handler=log_handler_a) as client_a, - Client(stateful_proxy_server, log_handler=log_handler_b) as client_b, + Client( + stateful_proxy_server, mode="legacy", log_handler=log_handler_a + ) as client_a, + Client( + stateful_proxy_server, mode="legacy", log_handler=log_handler_b + ) as client_b, ): async with create_task_group() as tg: tg.start_soon( @@ -115,7 +119,7 @@ class TestStatefulProxyClient: async def test_stateful_proxy(self, stateful_proxy_server: FastMCP): """Test that the state shared across multiple calls for the same client (fixes #959).""" - async with Client(stateful_proxy_server) as client: + async with Client(stateful_proxy_server, mode="legacy") as client: with pytest.raises(ToolError, match="Value not found"): await client.call_tool("stateful_get", {}) @@ -126,7 +130,7 @@ class TestStatefulProxyClient: async def test_stateless_proxy(self, stateless_server: str): """Test that the state will not be shared across different calls, even if they are from the same client.""" - async with Client(stateless_server) as client: + async with Client(stateless_server, mode="legacy") as client: await client.call_tool("stateful_put", {"value": 1}) with pytest.raises(ToolError, match="Value not found"): @@ -154,7 +158,7 @@ class TestStatefulProxyClient: multi_proxy_mcp.mount(proxy_mcp_a, namespace="a") multi_proxy_mcp.mount(proxy_mcp_b, namespace="b") - async with Client(multi_proxy_mcp) as client: + async with Client(multi_proxy_mcp, mode="legacy") as client: result_a = await client.call_tool("a_tool_a", {}) result_b = await client.call_tool("b_tool_b", {}) assert result_a.data == "a" @@ -202,7 +206,7 @@ class TestStatefulProxyClient: # related_request_id routing for server-initiated messages. async with run_server_async(proxy) as proxy_url: async with Client( - proxy_url, elicitation_handler=elicitation_handler + proxy_url, mode="legacy", elicitation_handler=elicitation_handler ) as client: result1 = await client.call_tool("ask_name", {}) assert result1.data == "Hello, Alice!" diff --git a/tests/server/tasks/test_concurrent_dependencies.py b/tests/server/tasks/test_concurrent_dependencies.py index eb5af1bb1..edc3b6a6a 100644 --- a/tests/server/tasks/test_concurrent_dependencies.py +++ b/tests/server/tasks/test_concurrent_dependencies.py @@ -30,7 +30,7 @@ async def test_concurrent_foreground_tools_with_context(): results.append(name) return f"done:{name}" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: tasks = [client.call_tool("slow_tool", {"name": f"task-{i}"}) for i in range(4)] outcomes = await asyncio.gather(*tasks) @@ -56,7 +56,7 @@ async def test_concurrent_foreground_tools_with_progress(): await progress.increment() return f"done:{name}" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: tasks = [ client.call_tool( "variable_tool", {"name": f"t-{i}", "delay": 0.01 * (i + 1)} @@ -80,7 +80,7 @@ async def test_concurrent_background_tasks_with_context(): await asyncio.sleep(0.05) return f"bg:{name}" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task_handles = [ await client.call_tool("bg_tool", {"name": f"bg-{i}"}, task=True) for i in range(4) @@ -109,7 +109,7 @@ async def test_concurrent_background_tasks_with_progress(): await progress.increment() return f"bg:{name}" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task_handles = [ await client.call_tool( "bg_progress_tool", @@ -137,7 +137,7 @@ async def test_dependency_aenter_returns_fresh_instances(): instances.append(ctx) return "ok" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: await asyncio.gather( client.call_tool("capture_context", {}), client.call_tool("capture_context", {}), @@ -161,7 +161,7 @@ async def test_progress_aenter_returns_fresh_instances(): await progress.increment() return "ok" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: await asyncio.gather( client.call_tool("capture_progress", {}), client.call_tool("capture_progress", {}), @@ -187,7 +187,7 @@ async def test_sync_context_functions_work_in_background_without_deps(): headers = get_http_headers() return {"has_headers": str(bool(headers))} - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("bare_sync_access", {}, task=True) result = await task.result() assert result.data == {"has_headers": "False"} @@ -207,7 +207,7 @@ async def test_sync_context_functions_work_in_background_with_context(): "is_background": str(ctx.is_background_task), } - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("context_sync_access", {}, task=True) result = await task.result() assert result.data["is_background"] == "True" diff --git a/tests/server/tasks/test_context_background_task.py b/tests/server/tasks/test_context_background_task.py index e0b8b3dc3..a81a293f7 100644 --- a/tests/server/tasks/test_context_background_task.py +++ b/tests/server/tasks/test_context_background_task.py @@ -1,7 +1,7 @@ """Tests for Context background task support (SEP-1686). Tests Context API surface (unit) and background task elicitation (integration). -Integration tests use Client(mcp) with the real memory:// Docket backend — +Integration tests use Client(mcp, mode="legacy") with the real memory:// Docket backend — no mocking of Redis, Docket, or session internals. """ @@ -272,7 +272,7 @@ class TestElicitFailFast: "fastmcp.server.tasks.notifications.push_notification", side_effect=ConnectionError("Redis queue unavailable"), ): - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("failfast_tool", {}, task=True) await asyncio.wait_for(elicit_started.wait(), timeout=5.0) await task.wait(timeout=10.0) @@ -304,14 +304,14 @@ class TestContextDocumentation: # ============================================================================= -# Integration tests: Client(mcp) + memory:// Docket backend +# Integration tests: Client(mcp, mode="legacy") + memory:// Docket backend # ============================================================================= class TestBackgroundTaskIntegration: """Integration tests for background task context using real Docket memory backend. - These tests use Client(mcp) with the memory:// broker — no mocking. + These tests use Client(mcp, mode="legacy") with the memory:// broker — no mocking. The memory:// backend provides a fully functional in-memory Redis store that Docket uses automatically when running tests. """ @@ -329,7 +329,7 @@ class TestBackgroundTaskIntegration: progress_reported.set() return "done" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("progress_tool", {}, task=True) await asyncio.wait_for(progress_reported.wait(), timeout=5.0) await task.wait(timeout=5.0) @@ -350,7 +350,7 @@ class TestBackgroundTaskIntegration: task_completed.set() return "ok" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("verify_wiring", {}, task=True) await asyncio.wait_for(task_completed.wait(), timeout=5.0) await task.wait(timeout=5.0) @@ -394,7 +394,7 @@ class TestBackgroundTaskIntegration: assert snapshot["origin_request_id"] == origin return "ok" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("check_origin_request_id", {}, task=True) result = await task.result() assert result.data == "ok" @@ -429,7 +429,9 @@ class TestBackgroundTaskIntegration: stop_reason="endTurn", ) - async with Client(mcp, sampling_handler=sampling_handler) as client: + async with Client( + mcp, mode="legacy", sampling_handler=sampling_handler + ) as client: task = await client.call_tool("ask_client", {}, task=True) result = await task.result() @@ -450,7 +452,7 @@ class TestBackgroundTaskIntegration: async def handler(message, response_type, params, ctx): return ElicitResult(action="accept", content={"value": "Bob"}) - async with Client(mcp, elicitation_handler=handler) as client: + async with Client(mcp, mode="legacy", elicitation_handler=handler) as client: task = await client.call_tool("ask_name", {}, task=True) await task.wait(timeout=10.0) result = await task.result() @@ -472,7 +474,7 @@ class TestBackgroundTaskIntegration: async def handler(message, response_type, params, ctx): return ElicitResult(action="decline") - async with Client(mcp, elicitation_handler=handler) as client: + async with Client(mcp, mode="legacy", elicitation_handler=handler) as client: task = await client.call_tool("optional_input", {}, task=True) await task.wait(timeout=10.0) result = await task.result() @@ -498,7 +500,7 @@ class TestBackgroundTaskIntegration: async def handler(message, response_type, params, ctx): return ElicitResult(action="accept", content={"name": "Alice", "age": 30}) - async with Client(mcp, elicitation_handler=handler) as client: + async with Client(mcp, mode="legacy", elicitation_handler=handler) as client: task = await client.call_tool("get_user_info", {}, task=True) await task.wait(timeout=10.0) result = await task.result() @@ -512,7 +514,7 @@ class TestBackgroundTaskIntegration: async def simple_tool() -> str: return "done" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("simple_tool", {}, task=True) await task.wait(timeout=5.0) @@ -530,7 +532,7 @@ class TestBackgroundTaskIntegration: class TestAccessTokenInBackgroundTasks: """Tests for access token availability in background tasks (#3095). - Integration tests use Client(mcp) with the real memory:// Docket backend. + Integration tests use Client(mcp, mode="legacy") with the real memory:// Docket backend. The token snapshot/restore round-trip flows through actual Redis (fakeredis). Note: async tests run in isolated asyncio tasks, so ContextVar changes @@ -556,7 +558,7 @@ class TestAccessTokenInBackgroundTasks: ) auth_context_var.set(AuthenticatedUser(test_token)) - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("check_token", {}, task=True) result = await task.result() assert result.data == "roundtrip-jwt|test-client" @@ -570,7 +572,7 @@ class TestAccessTokenInBackgroundTasks: token = get_access_token() return "no-token" if token is None else token.token - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("check_token", {}, task=True) result = await task.result() assert result.data == "no-token" diff --git a/tests/server/tasks/test_custom_subclass_tasks.py b/tests/server/tasks/test_custom_subclass_tasks.py index 2077e066c..80233e8fc 100644 --- a/tests/server/tasks/test_custom_subclass_tasks.py +++ b/tests/server/tasks/test_custom_subclass_tasks.py @@ -67,14 +67,14 @@ def custom_tool_server(): async def test_custom_tool_sync_execution(custom_tool_server): """Custom tool executes synchronously when no task metadata.""" - async with Client(custom_tool_server) as client: + async with Client(custom_tool_server, mode="legacy") as client: result = await client.call_tool("custom_tool", {}) assert "Custom tool executed" in str(result) async def test_custom_tool_background_execution(custom_tool_server): """Custom tool executes as background task when task=True.""" - async with Client(custom_tool_server) as client: + async with Client(custom_tool_server, mode="legacy") as client: task = await client.call_tool("custom_tool", {}, task=True) assert task is not None @@ -88,7 +88,7 @@ async def test_custom_tool_background_execution(custom_tool_server): async def test_custom_tool_with_arguments(custom_tool_server): """Custom tool receives arguments correctly in background execution.""" - async with Client(custom_tool_server) as client: + async with Client(custom_tool_server, mode="legacy") as client: task = await client.call_tool("custom_logic", {"duration": 1}, task=True) assert task is not None @@ -98,7 +98,7 @@ async def test_custom_tool_with_arguments(custom_tool_server): async def test_custom_tool_forbidden_sync_only(custom_tool_server): """Custom tool with forbidden mode executes sync only.""" - async with Client(custom_tool_server) as client: + async with Client(custom_tool_server, mode="legacy") as client: # Sync execution works result = await client.call_tool("custom_forbidden", {}) assert "Sync only" in str(result) @@ -106,7 +106,7 @@ async def test_custom_tool_forbidden_sync_only(custom_tool_server): async def test_custom_tool_forbidden_rejects_task(custom_tool_server): """Custom tool with forbidden mode returns error for task request.""" - async with Client(custom_tool_server) as client: + async with Client(custom_tool_server, mode="legacy") as client: task = await client.call_tool("custom_forbidden", {}, task=True) # Should return immediately with error diff --git a/tests/server/tasks/test_notifications.py b/tests/server/tasks/test_notifications.py index ca7abfe1a..a2908c0b2 100644 --- a/tests/server/tasks/test_notifications.py +++ b/tests/server/tasks/test_notifications.py @@ -1,7 +1,7 @@ """Tests for distributed notification queue (SEP-1686). Integration tests verify that the notification queue works end-to-end -using Client(mcp) with the real memory:// Docket backend. +using Client(mcp, mode="legacy") with the real memory:// Docket backend. No mocking of Redis, sessions, or Docket internals. """ @@ -53,6 +53,7 @@ class TestNotificationIntegration: async with Client( mcp, + mode="legacy", elicitation_handler=elicitation_handler, ) as client: task = await client.call_tool("elicit_tool", {}, task=True) @@ -107,7 +108,7 @@ class TestNotificationIntegration: count_before = get_subscriber_count() - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("lifecycle_tool", {}, task=True) await asyncio.wait_for(tool_started.wait(), timeout=5.0) diff --git a/tests/server/tasks/test_progress_dependency.py b/tests/server/tasks/test_progress_dependency.py index 6cc35996a..2370b6e34 100644 --- a/tests/server/tasks/test_progress_dependency.py +++ b/tests/server/tasks/test_progress_dependency.py @@ -16,7 +16,7 @@ async def test_progress_in_immediate_execution(): await progress.set_message("Testing") return "done" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: result = await client.call_tool("test_tool", {}) from mcp_types import TextContent @@ -35,7 +35,7 @@ async def test_progress_in_background_task(): await progress.set_message("Step 1") return "done" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("test_task", {}, task=True) result = await task.result() from mcp_types import TextContent @@ -55,7 +55,7 @@ async def test_progress_tracks_multiple_increments(): await progress.increment() return "counted" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: result = await client.call_tool("count_to_ten", {}) from mcp_types import TextContent @@ -86,7 +86,7 @@ async def test_progress_status_message_in_background_task(): await progress.increment() return "done" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("task_with_progress", {}, task=True) # Wait for first step to start @@ -141,7 +141,7 @@ async def test_inmemory_progress_state(): "message": progress.message, } - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: result = await client.call_tool("test_tool", {}) from mcp_types import TextContent diff --git a/tests/server/tasks/test_resource_task_meta_parameter.py b/tests/server/tasks/test_resource_task_meta_parameter.py index 86d36b3a4..1a5caeffd 100644 --- a/tests/server/tasks/test_resource_task_meta_parameter.py +++ b/tests/server/tasks/test_resource_task_meta_parameter.py @@ -124,7 +124,7 @@ class TestResourceTaskMetaClientIntegration: async def immediate_resource() -> str: return "hello" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: result = await client.read_resource("data://test") # Should get ReadResourceResult directly @@ -138,7 +138,7 @@ class TestResourceTaskMetaClientIntegration: async def task_resource() -> str: return "hello" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: from fastmcp.client.tasks import ResourceTask task = await client.read_resource("data://test", task=True) @@ -157,7 +157,7 @@ class TestResourceTaskMetaClientIntegration: async def get_item(id: str) -> str: return f"Item {id}" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: from fastmcp.client.tasks import ResourceTask task = await client.read_resource("item://42", task=True) @@ -187,7 +187,7 @@ class TestResourceTaskMetaDirectServerCall: # Should get CreateTaskResult since we provided task_meta return f"Created task: {result.task.task_id}" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: result = await client.call_tool("outer_tool", {}) assert "Created task:" in str(result) @@ -206,7 +206,7 @@ class TestResourceTaskMetaDirectServerCall: # Should get ResourceResult directly return f"Got result: {result.contents[0].content}" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: result = await client.call_tool("outer_tool", {}) assert "Got result: inner data" in str(result) @@ -223,7 +223,7 @@ class TestResourceTaskMetaDirectServerCall: result = await server.read_resource("item://99", task_meta=TaskMeta()) return f"Created task: {result.task.task_id}" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: result = await client.call_tool("outer_tool", {}) assert "Created task:" in str(result) @@ -243,7 +243,7 @@ class TestResourceTaskMetaDirectServerCall: ) return f"Task TTL: {result.task.ttl}" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: result = await client.call_tool("outer_tool", {}) assert "Task TTL: 45000" in str(result) @@ -274,7 +274,7 @@ class TestResourceTaskMetaTypeNarrowing: async def task_resource() -> str: return "hello" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: # Need to use client to get full task infrastructure from fastmcp.client.tasks import ResourceTask diff --git a/tests/server/tasks/test_server_tasks_parameter.py b/tests/server/tasks/test_server_tasks_parameter.py index acb30811e..45b777d6a 100644 --- a/tests/server/tasks/test_server_tasks_parameter.py +++ b/tests/server/tasks/test_server_tasks_parameter.py @@ -35,7 +35,7 @@ async def test_server_tasks_true_defaults_all_components(): async def my_resource() -> str: return "resource result" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Verify all task-enabled components are registered with docket # Components use prefixed keys: tool:name, prompt:name, resource:uri docket = mcp.docket @@ -82,7 +82,7 @@ async def test_server_tasks_false_defaults_all_components(): async def my_resource() -> str: return "resource result" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Tool with mode="forbidden" returns error when called with task=True tool_task = await client.call_tool("my_tool", task=True, raise_on_error=False) assert tool_task.returned_immediately @@ -107,7 +107,7 @@ async def test_server_tasks_none_defaults_to_false(): async def my_tool() -> str: return "tool result" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Tool should NOT support background execution (mode="forbidden" from default) tool_task = await client.call_tool("my_tool", task=True, raise_on_error=False) assert tool_task.returned_immediately @@ -128,7 +128,7 @@ async def test_component_explicit_false_overrides_server_true(): async def default_tool() -> str: return "background result" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Verify docket registration matches task settings (prefixed keys) docket = mcp.docket assert docket is not None @@ -163,7 +163,7 @@ async def test_component_explicit_true_overrides_server_false(): async def default_tool() -> str: return "immediate result" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Verify docket registration matches task settings (prefixed keys) docket = mcp.docket assert docket is not None @@ -224,7 +224,7 @@ async def test_mixed_explicit_and_inherited(): async def explicit_false_resource() -> str: return "explicit False" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Verify docket registration matches task settings # Components use prefixed keys: tool:name, prompt:name, resource:uri docket = mcp.docket @@ -282,7 +282,7 @@ async def test_server_tasks_parameter_sets_component_defaults(): async def tool_inherits_true() -> str: return "tool result" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Tool inherits tasks=True from server tool_task = await client.call_tool("tool_inherits_true", task=True) assert not tool_task.returned_immediately @@ -294,7 +294,7 @@ async def test_server_tasks_parameter_sets_component_defaults(): async def tool_inherits_false() -> str: return "tool result" - async with Client(mcp2) as client: + async with Client(mcp2, mode="legacy") as client: # Tool inherits tasks=False (mode="forbidden") - returns error tool_task = await client.call_tool( "tool_inherits_false", task=True, raise_on_error=False @@ -318,7 +318,7 @@ async def test_resource_template_inherits_server_tasks_default(): async def templated_resource(item_id: str) -> str: return f"resource {item_id}" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Template should support background execution resource_task = await client.read_resource("test://123", task=True) assert not resource_task.returned_immediately @@ -345,7 +345,7 @@ async def test_multiple_components_same_name_different_tasks(): async def shared_name_prompt() -> str: return "prompt result" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Tool with explicit True should support background execution tool_task = await client.call_tool("shared_name", task=True) assert not tool_task.returned_immediately @@ -368,7 +368,7 @@ async def test_task_with_custom_tool_name(): mcp.tool(my_function, name="custom-tool-name") - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Verify the tool is registered with its custom name in Docket (prefixed key) docket = mcp.docket assert docket is not None @@ -398,7 +398,7 @@ async def test_task_with_custom_resource_name(): async def my_resource_func() -> str: return "result from custom-named resource" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Verify the resource is registered with its key (prefixed URI) in Docket docket = mcp.docket assert docket is not None @@ -428,7 +428,7 @@ async def test_task_with_custom_template_name(): async def my_template_func(item_id: str) -> str: return f"result for {item_id}" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Verify the template is registered with its key (prefixed uri_template) in Docket docket = mcp.docket assert docket is not None diff --git a/tests/server/tasks/test_snapshot_restore.py b/tests/server/tasks/test_snapshot_restore.py index 946e1bf1b..09a5241d1 100644 --- a/tests/server/tasks/test_snapshot_restore.py +++ b/tests/server/tasks/test_snapshot_restore.py @@ -39,7 +39,7 @@ async def test_snapshot_restored_before_user_code_runs(): seen_cached.append(_recall_snapshot(info.task_id) is not None) return "ok" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("bare_tool", {}, task=True) await task.result() @@ -66,7 +66,7 @@ async def test_get_access_token_in_bg_task_without_context_dep(): ) auth_context_var.set(AuthenticatedUser(test_token)) - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("bare_tool", {}, task=True) await task.result() @@ -89,7 +89,7 @@ async def test_restore_failure_is_nonfatal(): def boom(*_args, **_kwargs): raise RuntimeError("simulated deserialization failure") - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: with patch.object(TaskContextSnapshot, "from_json", boom): task = await client.call_tool("bare_tool", {}, task=True) result = await task.result() diff --git a/tests/server/tasks/test_task_capabilities.py b/tests/server/tasks/test_task_capabilities.py index 3999c5dbe..e504cee53 100644 --- a/tests/server/tasks/test_task_capabilities.py +++ b/tests/server/tasks/test_task_capabilities.py @@ -18,7 +18,7 @@ async def test_capabilities_include_tasks(): async def test_tool() -> str: return "test" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Get server initialization result which includes capabilities init_result = client.initialize_result @@ -59,7 +59,7 @@ async def test_client_uses_task_capable_session(): async def test_tool() -> str: return "test" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Client should have connected successfully with task capabilities assert client.initialize_result is not None # Session should be a ClientSession (task-capable init uses standard session) diff --git a/tests/server/tasks/test_task_config.py b/tests/server/tasks/test_task_config.py index d6e095509..e10d7cff2 100644 --- a/tests/server/tasks/test_task_config.py +++ b/tests/server/tasks/test_task_config.py @@ -110,7 +110,7 @@ class TestToolModeEnforcement: async def test_required_mode_without_task_returns_error(self, server): """Required mode raises error when called without task metadata.""" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: with pytest.raises(ToolError) as exc_info: await client.call_tool("required_tool", {}) @@ -118,7 +118,7 @@ class TestToolModeEnforcement: async def test_required_mode_with_task_succeeds(self, server): """Required mode succeeds when called with task metadata.""" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: task = await client.call_tool("required_tool", {}, task=True) assert task is not None result = await task.result() @@ -126,7 +126,7 @@ class TestToolModeEnforcement: async def test_forbidden_mode_with_task_returns_error(self, server): """Forbidden mode returns error when called with task metadata.""" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: # Call with task=True should fail task = await client.call_tool( "forbidden_tool", {}, task=True, raise_on_error=False @@ -140,19 +140,19 @@ class TestToolModeEnforcement: async def test_forbidden_mode_without_task_succeeds(self, server): """Forbidden mode succeeds when called without task metadata.""" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: result = await client.call_tool("forbidden_tool", {}) assert "forbidden result" in str(result) async def test_optional_mode_without_task_succeeds(self, server): """Optional mode succeeds when called without task metadata.""" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: result = await client.call_tool("optional_tool", {}) assert "optional result" in str(result) async def test_optional_mode_with_task_succeeds(self, server): """Optional mode succeeds when called with task metadata.""" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: task = await client.call_tool("optional_tool", {}, task=True) assert task is not None result = await task.result() @@ -188,7 +188,7 @@ class TestResourceModeEnforcement: """Required mode returns error when read without task metadata.""" from mcp_types import METHOD_NOT_FOUND - async with Client(server) as client: + async with Client(server, mode="legacy") as client: with pytest.raises(MCPError) as exc_info: await client.read_resource("resource://required") @@ -203,7 +203,7 @@ class TestResourceModeEnforcement: ) async def test_required_resource_with_task_succeeds(self, server): """Required mode succeeds when read with task metadata.""" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: task = await client.read_resource("resource://required", task=True) assert task is not None result = await task.result() @@ -212,7 +212,7 @@ class TestResourceModeEnforcement: async def test_forbidden_resource_without_task_succeeds(self, server): """Forbidden mode succeeds when read without task metadata.""" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: result = await client.read_resource("resource://forbidden") assert "forbidden content" in str(result) @@ -246,7 +246,7 @@ class TestPromptModeEnforcement: """Required mode returns error when called without task metadata.""" from mcp_types import METHOD_NOT_FOUND - async with Client(server) as client: + async with Client(server, mode="legacy") as client: with pytest.raises(MCPError) as exc_info: await client.get_prompt("required_prompt") @@ -261,7 +261,7 @@ class TestPromptModeEnforcement: ) async def test_required_prompt_with_task_succeeds(self, server): """Required mode succeeds when called with task metadata.""" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: task = await client.get_prompt("required_prompt", task=True) assert task is not None result = await task.result() @@ -270,7 +270,7 @@ class TestPromptModeEnforcement: async def test_forbidden_prompt_without_task_succeeds(self, server): """Forbidden mode succeeds when called without task metadata.""" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: result = await client.get_prompt("forbidden_prompt") assert isinstance(result.messages[0].content, TextContent) assert "forbidden message" in str(result.messages[0].content) @@ -287,7 +287,7 @@ class TestToolExecutionMetadata: async def my_tool() -> str: return "ok" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: tools = await client.list_tools() tool = next(t for t in tools if t.name == "my_tool") assert isinstance(tool, MCPTool) @@ -302,7 +302,7 @@ class TestToolExecutionMetadata: async def my_tool() -> str: return "ok" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: tools = await client.list_tools() tool = next(t for t in tools if t.name == "my_tool") assert isinstance(tool, MCPTool) @@ -317,7 +317,7 @@ class TestToolExecutionMetadata: async def my_tool() -> str: return "ok" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: tools = await client.list_tools() tool = next(t for t in tools if t.name == "my_tool") assert tool.execution is None diff --git a/tests/server/tasks/test_task_dependencies.py b/tests/server/tasks/test_task_dependencies.py index 0aef545ee..1745f5e49 100644 --- a/tests/server/tasks/test_task_dependencies.py +++ b/tests/server/tasks/test_task_dependencies.py @@ -75,7 +75,7 @@ async def dependency_server(): async def test_background_tool_receives_docket_dependency(dependency_server): """Background tools can use CurrentDocket() and it resolves correctly.""" - async with Client(dependency_server) as client: + async with Client(dependency_server, mode="legacy") as client: task = await client.call_tool("tool_with_docket_dependency", {}, task=True) # Verify it's background @@ -96,7 +96,7 @@ async def test_background_tool_receives_server_dependency(dependency_server): """Background tools can use CurrentFastMCP() and get the actual FastMCP server.""" dependency_server._injected_values.clear() - async with Client(dependency_server) as client: + async with Client(dependency_server, mode="legacy") as client: task = await client.call_tool("tool_with_server_dependency", {}, task=True) # Verify background execution @@ -116,7 +116,7 @@ async def test_background_tool_receives_custom_depends(dependency_server): """Background tools can use Depends() with custom functions.""" dependency_server._injected_values.clear() - async with Client(dependency_server) as client: + async with Client(dependency_server, mode="legacy") as client: task = await client.call_tool( "tool_with_custom_dependency", {"value": 5}, task=True ) @@ -137,7 +137,7 @@ async def test_background_tool_with_multiple_dependencies(dependency_server): """Background tools can have multiple dependencies injected simultaneously.""" dependency_server._injected_values.clear() - async with Client(dependency_server) as client: + async with Client(dependency_server, mode="legacy") as client: task = await client.call_tool( "tool_with_multiple_dependencies", {"name": "test"}, task=True ) @@ -170,7 +170,7 @@ async def test_background_prompt_receives_dependencies(dependency_server): """Background prompts can use dependency injection.""" dependency_server._injected_values.clear() - async with Client(dependency_server) as client: + async with Client(dependency_server, mode="legacy") as client: task = await client.get_prompt( "prompt_with_server_dependency", {"topic": "AI"}, task=True ) @@ -196,7 +196,7 @@ async def test_background_resource_receives_dependencies(dependency_server): """Background resources can use dependency injection.""" dependency_server._injected_values.clear() - async with Client(dependency_server) as client: + async with Client(dependency_server, mode="legacy") as client: task = await client.read_resource("file://data.txt", task=True) assert not task.returned_immediately @@ -219,7 +219,7 @@ async def test_foreground_tool_dependencies_unaffected(dependency_server): dependency_server._injected_values.append(("sync_server", server)) return f"Sync: {server.name}" - async with Client(dependency_server) as client: + async with Client(dependency_server, mode="legacy") as client: await client.call_tool("sync_tool", {}) # Should execute immediately @@ -248,7 +248,7 @@ async def test_dependency_context_managers_cleaned_up_in_background(): assert "exit" not in cleanup_called # Still open during execution return f"Used: {conn}" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("use_connection", {"name": "test"}, task=True) result = await task @@ -270,7 +270,7 @@ async def test_dependency_errors_propagate_to_task_failure(): ) -> str: return f"Got: {dep}" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool( "tool_with_failing_dep", {"value": "test"}, task=True ) diff --git a/tests/server/tasks/test_task_elicitation_relay.py b/tests/server/tasks/test_task_elicitation_relay.py index 42362edd2..bf8d6e8b9 100644 --- a/tests/server/tasks/test_task_elicitation_relay.py +++ b/tests/server/tasks/test_task_elicitation_relay.py @@ -7,7 +7,7 @@ an elicitation/create request to the client session. The client's elicitation_handler fires, and the relay pushes the response to Redis for the blocked worker. -These tests use Client(mcp) with the real memory:// Docket backend. +These tests use Client(mcp, mode="legacy") with the real memory:// Docket backend. """ import asyncio @@ -44,7 +44,7 @@ class TestElicitationRelay: assert message == "What is your name?" return ElicitResult(action="accept", content={"value": "Alice"}) - async with Client(mcp, elicitation_handler=handler) as client: + async with Client(mcp, mode="legacy", elicitation_handler=handler) as client: task = await client.call_tool("ask_name", {}, task=True) result = await task.result() assert result.data == "Hello, Alice!" @@ -65,7 +65,7 @@ class TestElicitationRelay: async def handler(message, response_type, params, ctx): return ElicitResult(action="decline") - async with Client(mcp, elicitation_handler=handler) as client: + async with Client(mcp, mode="legacy", elicitation_handler=handler) as client: task = await client.call_tool("optional_input", {}, task=True) result = await task.result() assert result.data == "User declined" @@ -84,7 +84,7 @@ class TestElicitationRelay: async def handler(message, response_type, params, ctx): return ElicitResult(action="cancel") - async with Client(mcp, elicitation_handler=handler) as client: + async with Client(mcp, mode="legacy", elicitation_handler=handler) as client: task = await client.call_tool("cancellable", {}, task=True) result = await task.result() assert result.data == "Cancelled" @@ -109,7 +109,7 @@ class TestElicitationRelay: async def handler(message, response_type, params, ctx): return ElicitResult(action="accept", content={"name": "Bob", "age": 30}) - async with Client(mcp, elicitation_handler=handler) as client: + async with Client(mcp, mode="legacy", elicitation_handler=handler) as client: task = await client.call_tool("get_user", {}, task=True) result = await task.result() assert result.data == "Bob is 30" @@ -135,7 +135,7 @@ class TestElicitationRelay: action="accept", content={"host": "localhost", "port": 8080} ) - async with Client(mcp, elicitation_handler=handler) as client: + async with Client(mcp, mode="legacy", elicitation_handler=handler) as client: task = await client.call_tool("get_config", {}, task=True) result = await task.result() assert result.data == "localhost:8080" @@ -166,7 +166,7 @@ class TestElicitationRelay: assert message == "Last name?" return ElicitResult(action="accept", content={"value": "Doe"}) - async with Client(mcp, elicitation_handler=handler) as client: + async with Client(mcp, mode="legacy", elicitation_handler=handler) as client: task = await client.call_tool("two_questions", {}, task=True) result = await task.result() assert result.data == "Jane Doe" @@ -185,7 +185,7 @@ class TestElicitationRelay: return f"Got: {result.data}" return "Other" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: task = await client.call_tool("needs_input", {}, task=True) result = await asyncio.wait_for(task.result(), timeout=15.0) assert result.data == "Cancelled as expected" diff --git a/tests/server/tasks/test_task_meta_parameter.py b/tests/server/tasks/test_task_meta_parameter.py index e76930637..bea973aba 100644 --- a/tests/server/tasks/test_task_meta_parameter.py +++ b/tests/server/tasks/test_task_meta_parameter.py @@ -76,7 +76,7 @@ class TestTaskMetaParameter: # call_tool enriches the task_meta before passing to _run # We test this via the client integration path - async with Client(server) as client: + async with Client(server, mode="legacy") as client: result = await client.call_tool("auto_key_tool", {}, task=True) # Should succeed because fn_key was auto-populated from fastmcp.client.tasks import ToolTask @@ -105,7 +105,7 @@ class TestTaskMetaTTL: custom_ttl_ms = 30000 # 30 seconds - async with Client(server) as client: + async with Client(server, mode="legacy") as client: # Use client.call_tool with task=True and ttl task = await client.call_tool("ttl_tool", {}, task=True, ttl=custom_ttl_ms) @@ -125,7 +125,7 @@ class TestTaskMetaTTL: async def default_ttl_tool() -> str: return "done" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: # Use client.call_tool with task=True, default ttl task = await client.call_tool("default_ttl_tool", {}, task=True) @@ -169,7 +169,7 @@ class TestTaskMetaMiddleware: server.add_middleware(TrackingMiddleware(middleware_saw_request)) - async with Client(server) as client: + async with Client(server, mode="legacy") as client: # Use client to trigger the middleware chain task = await client.call_tool("middleware_test_tool", {}, task=True) @@ -193,7 +193,7 @@ class TestTaskMetaClientIntegration: async def client_test_tool(x: int) -> int: return x * 2 - async with Client(server) as client: + async with Client(server, mode="legacy") as client: # Client passes task=True, server receives as task_meta task = await client.call_tool("client_test_tool", {"x": 5}, task=True) @@ -214,7 +214,7 @@ class TestTaskMetaClientIntegration: async def immediate_tool(x: int) -> int: return x * 2 - async with Client(server) as client: + async with Client(server, mode="legacy") as client: # No task=True, should execute synchronously result = await client.call_tool("immediate_tool", {"x": 5}) @@ -231,7 +231,7 @@ class TestTaskMetaClientIntegration: custom_ttl_ms = 60000 # 60 seconds - async with Client(server) as client: + async with Client(server, mode="legacy") as client: task = await client.call_tool( "custom_ttl_tool", {}, task=True, ttl=custom_ttl_ms ) @@ -265,7 +265,7 @@ class TestTaskMetaDirectServerCall: # Should get CreateTaskResult since we're in server context return f"Created task: {result.task.task_id}" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: # Call outer_tool which internally calls inner_tool with task_meta result = await client.call_tool("outer_tool", {"x": 5}) # The outer tool should have successfully created a background task @@ -288,7 +288,7 @@ class TestTaskMetaDirectServerCall: assert isinstance(first_content, mcp_types.TextContent) return f"Got result: {first_content.text}" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: result = await client.call_tool("outer_tool", {"x": 5}) assert "Got result: 10" in str(result) @@ -308,7 +308,7 @@ class TestTaskMetaDirectServerCall: ) return f"Task TTL: {result.task.ttl}" - async with Client(server) as client: + async with Client(server, mode="legacy") as client: result = await client.call_tool("outer_tool", {"x": 5}) # The inner tool task should have the custom TTL assert "Task TTL: 45000" in str(result) diff --git a/tests/server/tasks/test_task_metadata.py b/tests/server/tasks/test_task_metadata.py index 0d8935d36..2bfdf4b13 100644 --- a/tests/server/tasks/test_task_metadata.py +++ b/tests/server/tasks/test_task_metadata.py @@ -25,7 +25,7 @@ async def metadata_server(): async def test_tasks_get_includes_related_task_metadata(metadata_server: FastMCP): """tasks/get response includes io.modelcontextprotocol/related-task in _meta.""" - async with Client(metadata_server) as client: + async with Client(metadata_server, mode="legacy") as client: # Submit a task task = await client.call_tool("test_tool", {"value": 5}, task=True) task_id = task.task_id @@ -41,7 +41,7 @@ async def test_tasks_get_includes_related_task_metadata(metadata_server: FastMCP async def test_tasks_result_includes_related_task_metadata(metadata_server: FastMCP): """tasks/result response includes io.modelcontextprotocol/related-task in _meta.""" - async with Client(metadata_server) as client: + async with Client(metadata_server, mode="legacy") as client: # Submit and complete a task task = await client.call_tool("test_tool", {"value": 7}, task=True) result = await task.result() @@ -54,7 +54,7 @@ async def test_tasks_result_includes_related_task_metadata(metadata_server: Fast async def test_tasks_list_includes_related_task_metadata(metadata_server: FastMCP): """tasks/list response includes io.modelcontextprotocol/related-task in _meta.""" - async with Client(metadata_server) as client: + async with Client(metadata_server, mode="legacy") as client: # List tasks via client (which uses protocol properly) result = await client.list_tasks() diff --git a/tests/server/tasks/test_task_methods.py b/tests/server/tasks/test_task_methods.py index 493bc9167..4e8832ef6 100644 --- a/tests/server/tasks/test_task_methods.py +++ b/tests/server/tasks/test_task_methods.py @@ -39,7 +39,7 @@ async def endpoint_server(): async def test_tasks_get_endpoint_returns_status(endpoint_server): """POST /tasks/get returns task status.""" - async with Client(endpoint_server) as client: + async with Client(endpoint_server, mode="legacy") as client: # Submit a task task = await client.call_tool("quick_tool", {"value": 21}, task=True) @@ -58,7 +58,7 @@ async def test_tasks_get_endpoint_returns_status(endpoint_server): async def test_tasks_get_endpoint_includes_poll_interval(endpoint_server): """Task status includes pollFrequency hint.""" - async with Client(endpoint_server) as client: + async with Client(endpoint_server, mode="legacy") as client: task = await client.call_tool("quick_tool", {"value": 42}, task=True) status = await task.status() @@ -68,7 +68,7 @@ async def test_tasks_get_endpoint_includes_poll_interval(endpoint_server): async def test_tasks_result_endpoint_returns_result_when_completed(endpoint_server): """POST /tasks/result returns the tool result when completed.""" - async with Client(endpoint_server) as client: + async with Client(endpoint_server, mode="legacy") as client: task = await client.call_tool("quick_tool", {"value": 21}, task=True) # Wait for completion and get result @@ -86,7 +86,7 @@ async def test_tasks_result_endpoint_errors_if_not_completed(endpoint_server): await completion_signal.wait() return "done" - async with Client(endpoint_server) as client: + async with Client(endpoint_server, mode="legacy") as client: task = await client.call_tool("blocked_tool", task=True) # Try to get result immediately (task still running) @@ -99,7 +99,7 @@ async def test_tasks_result_endpoint_errors_if_not_completed(endpoint_server): async def test_tasks_result_endpoint_errors_if_task_not_found(endpoint_server): """POST /tasks/result returns error for non-existent task.""" - async with Client(endpoint_server) as client: + async with Client(endpoint_server, mode="legacy") as client: # Try to get result for non-existent task with pytest.raises(Exception): await client.get_task_result("non-existent-task-id") @@ -107,7 +107,7 @@ async def test_tasks_result_endpoint_errors_if_task_not_found(endpoint_server): async def test_tasks_result_endpoint_returns_error_for_failed_task(endpoint_server): """POST /tasks/result returns error information for failed tasks.""" - async with Client(endpoint_server) as client: + async with Client(endpoint_server, mode="legacy") as client: task = await client.call_tool("error_tool", task=True) # Wait for task to fail @@ -126,7 +126,7 @@ async def test_tasks_result_endpoint_returns_error_for_failed_task(endpoint_serv async def test_tasks_list_endpoint_session_isolation(endpoint_server): """list_tasks returns only tasks submitted by this client.""" # Since client tracks tasks locally, this tests client-side tracking - async with Client(endpoint_server) as client: + async with Client(endpoint_server, mode="legacy") as client: # Submit multiple tasks (server generates IDs) tasks = [] for i in range(3): @@ -147,7 +147,7 @@ async def test_tasks_list_endpoint_session_isolation(endpoint_server): async def test_get_status_nonexistent_task_raises_error(endpoint_server): """Getting status for nonexistent task raises MCP error (per SEP-1686 SDK behavior).""" - async with Client(endpoint_server) as client: + async with Client(endpoint_server, mode="legacy") as client: # Try to get status for task that was never created # Per SDK implementation: raises ValueError which becomes JSON-RPC error with pytest.raises(MCPError, match="Task nonexistent-task-id not found"): @@ -156,7 +156,7 @@ async def test_get_status_nonexistent_task_raises_error(endpoint_server): async def test_task_cancellation_workflow(endpoint_server): """Task can be cancelled, transitioning to cancelled state.""" - async with Client(endpoint_server) as client: + async with Client(endpoint_server, mode="legacy") as client: # Submit slow task task = await client.call_tool("slow_tool", {}, task=True) @@ -199,7 +199,7 @@ async def test_task_cancellation_interrupts_running_coroutine(endpoint_server): was_interrupted.set() raise - async with Client(endpoint_server) as client: + async with Client(endpoint_server, mode="legacy") as client: task = await client.call_tool("interruptible_tool", {}, task=True) # Wait for the tool to actually start executing diff --git a/tests/server/tasks/test_task_mount.py b/tests/server/tasks/test_task_mount.py index 0401259c1..218ef1a47 100644 --- a/tests/server/tasks/test_task_mount.py +++ b/tests/server/tasks/test_task_mount.py @@ -109,7 +109,7 @@ class TestMountedToolTasks: async def test_mounted_tool_task_returns_task_object(self, parent_server): """Mounted tool called with task=True returns a task object.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: # Tool name is prefixed: child_multiply task = await client.call_tool("child_multiply", {"a": 6, "b": 7}, task=True) @@ -120,7 +120,7 @@ class TestMountedToolTasks: async def test_mounted_tool_task_executes_in_background(self, parent_server): """Mounted tool task executes in background.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: task = await client.call_tool("child_multiply", {"a": 3, "b": 4}, task=True) # Should execute in background @@ -130,7 +130,7 @@ class TestMountedToolTasks: self, parent_server: FastMCP ): """Mounted tool task returns correct result.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: task = await client.call_tool("child_multiply", {"a": 8, "b": 9}, task=True) result = await task.result() @@ -138,7 +138,7 @@ class TestMountedToolTasks: async def test_mounted_tool_task_status(self, parent_server): """Can poll task status for mounted tool.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: task = await client.call_tool( "child_slow_child_tool", {"duration": 0.5}, task=True ) @@ -157,7 +157,7 @@ class TestMountedToolTasks: @pytest.mark.timeout(10) async def test_mounted_tool_task_cancellation(self, parent_server): """Can cancel a mounted tool task.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: task = await client.call_tool( "child_slow_child_tool", {"duration": 10.0}, task=True ) @@ -174,7 +174,7 @@ class TestMountedToolTasks: async def test_graceful_degradation_sync_mounted_tool(self, parent_server): """Sync-only mounted tool returns error with task=True.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: task = await client.call_tool( "child_sync_child_tool", {"message": "hello"}, @@ -190,7 +190,7 @@ class TestMountedToolTasks: async def test_parent_and_mounted_tools_both_work(self, parent_server): """Both parent and mounted tools work as tasks.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: # Parent tool parent_task = await client.call_tool("parent_tool", {"value": 5}, task=True) # Mounted tool @@ -212,7 +212,7 @@ class TestMountedToolTasksNoPrefix: self, parent_server_no_prefix ): """Mounted tool without prefix works as task.""" - async with Client(parent_server_no_prefix) as client: + async with Client(parent_server_no_prefix, mode="legacy") as client: # No prefix, so tool keeps original name task = await client.call_tool("multiply", {"a": 5, "b": 6}, task=True) @@ -227,7 +227,7 @@ class TestMountedPromptTasks: async def test_mounted_prompt_task_returns_task_object(self, parent_server): """Mounted prompt called with task=True returns a task object.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: # Prompt name is prefixed: child_child_prompt task = await client.get_prompt( "child_child_prompt", {"topic": "FastMCP"}, task=True @@ -245,7 +245,7 @@ class TestMountedPromptTasks: ) async def test_mounted_prompt_task_executes_in_background(self, parent_server): """Mounted prompt task executes in background.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: task = await client.get_prompt( "child_child_prompt", {"topic": "testing"}, task=True ) @@ -256,7 +256,7 @@ class TestMountedPromptTasks: self, parent_server: FastMCP ): """Mounted prompt task returns correct result.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: task = await client.get_prompt( "child_child_prompt", {"topic": "MCP protocol"}, task=True ) @@ -271,7 +271,7 @@ class TestMountedResourceTasks: async def test_mounted_resource_task_returns_task_object(self, parent_server): """Mounted resource read with task=True returns a task object.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: # Resource URI is prefixed: child://child/data.txt task = await client.read_resource("child://child/data.txt", task=True) @@ -287,14 +287,14 @@ class TestMountedResourceTasks: ) async def test_mounted_resource_task_executes_in_background(self, parent_server): """Mounted resource task executes in background.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: task = await client.read_resource("child://child/data.txt", task=True) assert not task.returned_immediately async def test_mounted_resource_task_returns_correct_result(self, parent_server): """Mounted resource task returns correct result.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: task = await client.read_resource("child://child/data.txt", task=True) result = await task.result() @@ -309,7 +309,7 @@ class TestMountedResourceTasks: ) async def test_mounted_resource_template_task(self, parent_server): """Mounted resource template with task=True works.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: task = await client.read_resource("child://child/item/99.json", task=True) assert not task.returned_immediately @@ -335,7 +335,7 @@ class TestMountedTaskDependencies: parent = FastMCP("dep-parent") parent.mount(child, namespace="child") - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: task = await client.call_tool("child_tool_with_docket", {}, task=True) result = await task.result() @@ -356,7 +356,7 @@ class TestMountedTaskDependencies: parent = FastMCP("server-dep-parent") parent.mount(child, namespace="child") - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: task = await client.call_tool("child_tool_with_server", {}, task=True) await task.result() @@ -380,7 +380,7 @@ class TestMountedTaskServerContext: parent = FastMCP("parent") parent.mount(child, namespace="child") - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: task = await client.call_tool("child_whoami", {}, task=True) result = await task.result() @@ -403,7 +403,7 @@ class TestMountedTaskServerContext: parent = FastMCP("parent") parent.mount(child, namespace="child") - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: task = await client.call_tool("child_whoami_ctx", {}, task=True) result = await task.result() @@ -427,7 +427,7 @@ class TestMountedTaskServerContext: parent = FastMCP("parent") parent.mount(child, namespace="child") - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: task = await client.call_tool("child_gc_deep_whoami", {}, task=True) result = await task.result() @@ -456,7 +456,7 @@ class TestMultipleMounts: parent.mount(child1, namespace="math1") parent.mount(child2, namespace="math2") - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: task1 = await client.call_tool("math1_add", {"a": 10, "b": 5}, task=True) task2 = await client.call_tool( "math2_subtract", {"a": 10, "b": 5}, task=True @@ -489,7 +489,7 @@ class TestMountedFunctionNameCollisions: parent.mount(child1, namespace="c1") parent.mount(child2, namespace="c2") - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: # Both should execute their own implementation task1 = await client.call_tool("c1_process", {"value": 10}, task=True) task2 = await client.call_tool("c2_process", {"value": 10}, task=True) @@ -517,7 +517,7 @@ class TestMountedFunctionNameCollisions: parent.mount(child1) # No prefix parent.mount(child2) # No prefix - overwrites child1's "process" - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: # Last mount wins - child2's process should execute task = await client.call_tool("process", {"value": 10}, task=True) result = await task.result() @@ -536,7 +536,7 @@ class TestMountedFunctionNameCollisions: child.mount(grandchild, namespace="gc") parent.mount(child, namespace="child") - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: # Tool should be accessible and execute correctly task = await client.call_tool("child_gc_deep_tool", {}, task=True) result = await task.result() @@ -548,7 +548,7 @@ class TestMountedTaskList: async def test_list_tasks_includes_mounted_tasks(self, parent_server): """Task list includes tasks from mounted server tools.""" - async with Client(parent_server) as client: + async with Client(parent_server, mode="legacy") as client: # Create tasks on both parent and mounted tools parent_task = await client.call_tool("parent_tool", {"value": 1}, task=True) child_task = await client.call_tool( @@ -645,13 +645,13 @@ class TestMountedTaskConfigModes: async def test_optional_mode_sync_through_mount(self, parent_with_modes): """Optional mode tool works without task through mount.""" - async with Client(parent_with_modes) as client: + async with Client(parent_with_modes, mode="legacy") as client: result = await client.call_tool("child_optional_tool", {}) assert "optional result" in str(result) async def test_optional_mode_task_through_mount(self, parent_with_modes): """Optional mode tool works with task through mount.""" - async with Client(parent_with_modes) as client: + async with Client(parent_with_modes, mode="legacy") as client: task = await client.call_tool("child_optional_tool", {}, task=True) assert task is not None result = await task.result() @@ -659,7 +659,7 @@ class TestMountedTaskConfigModes: async def test_required_mode_with_task_through_mount(self, parent_with_modes): """Required mode tool succeeds with task through mount.""" - async with Client(parent_with_modes) as client: + async with Client(parent_with_modes, mode="legacy") as client: task = await client.call_tool("child_required_tool", {}, task=True) assert task is not None result = await task.result() @@ -669,7 +669,7 @@ class TestMountedTaskConfigModes: """Required mode tool errors without task through mount.""" from fastmcp.exceptions import ToolError - async with Client(parent_with_modes) as client: + async with Client(parent_with_modes, mode="legacy") as client: with pytest.raises(ToolError) as exc_info: await client.call_tool("child_required_tool", {}) @@ -677,13 +677,13 @@ class TestMountedTaskConfigModes: async def test_forbidden_mode_sync_through_mount(self, parent_with_modes): """Forbidden mode tool works without task through mount.""" - async with Client(parent_with_modes) as client: + async with Client(parent_with_modes, mode="legacy") as client: result = await client.call_tool("child_forbidden_tool", {}) assert "forbidden result" in str(result) async def test_forbidden_mode_with_task_through_mount(self, parent_with_modes): """Forbidden mode tool degrades gracefully with task through mount.""" - async with Client(parent_with_modes) as client: + async with Client(parent_with_modes, mode="legacy") as client: task = await client.call_tool( "child_forbidden_tool", {}, task=True, raise_on_error=False ) @@ -787,7 +787,7 @@ class TestMiddlewareWithMountedTasks: parent.mount(child, namespace="c") parent.add_middleware(ToolTracingMiddleware("parent", calls)) - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: task = await client.call_tool("c_gc_compute", {"x": 5}, task=True) result = await task.result() assert result.data == 10 @@ -831,7 +831,7 @@ class TestMiddlewareWithMountedTasks: parent.mount(child, namespace="c") parent.add_middleware(ResourceTracingMiddleware("parent", calls)) - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: task = await client.read_resource("data://c/gc/value", task=True) result = await task.result() assert result[0].text == "result" @@ -874,7 +874,7 @@ class TestMiddlewareWithMountedTasks: parent.mount(child, namespace="c") parent.add_middleware(PromptTracingMiddleware("parent", calls)) - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: task = await client.get_prompt("c_gc_greet", {"name": "World"}, task=True) result = await task.result() assert result.messages[0].content.text == "Hello, World!" @@ -917,7 +917,7 @@ class TestMiddlewareWithMountedTasks: parent.mount(child, namespace="c") parent.add_middleware(ResourceTracingMiddleware("parent", calls)) - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: task = await client.read_resource("item://c/gc/42", task=True) result = await task.result() assert result[0].text == "item-42" @@ -966,7 +966,7 @@ class TestMountedTasksWithTaskMetaParameter: ) return f"task:{result.task.task_id}" - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: result = await client.call_tool("outer", {}) assert "task:" in str(result) @@ -990,7 +990,7 @@ class TestMountedTasksWithTaskMetaParameter: ) return f"task:{result.task.task_id}" - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: result = await client.call_tool("outer", {}) assert "task:" in str(result) @@ -1014,7 +1014,7 @@ class TestMountedTasksWithTaskMetaParameter: ) return f"task:{result.task.task_id}" - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: result = await client.call_tool("outer", {}) assert "task:" in str(result) @@ -1041,7 +1041,7 @@ class TestMountedTasksWithTaskMetaParameter: ) return f"task:{result.task.task_id}" - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: result = await client.call_tool("outer", {}) assert "task:" in str(result) @@ -1068,7 +1068,7 @@ class TestMountedTasksWithTaskMetaParameter: ) return f"task:{result.task.task_id}" - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: result = await client.call_tool("outer", {}) assert "task:" in str(result) @@ -1092,7 +1092,7 @@ class TestMountedTasksWithTaskMetaParameter: ) return f"task:{result.task.task_id}" - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: result = await client.call_tool("outer", {}) assert "task:" in str(result) @@ -1119,6 +1119,6 @@ class TestMountedTasksWithTaskMetaParameter: ) return f"task:{result.task.task_id}" - async with Client(parent) as client: + async with Client(parent, mode="legacy") as client: result = await client.call_tool("outer", {}) assert "task:" in str(result) diff --git a/tests/server/tasks/test_task_prompts.py b/tests/server/tasks/test_task_prompts.py index ca62a3a0f..1054d3cee 100644 --- a/tests/server/tasks/test_task_prompts.py +++ b/tests/server/tasks/test_task_prompts.py @@ -31,7 +31,7 @@ async def prompt_server(): async def test_synchronous_prompt_unchanged(prompt_server): """Prompts without task metadata execute synchronously as before.""" - async with Client(prompt_server) as client: + async with Client(prompt_server, mode="legacy") as client: # Regular call without task metadata result = await client.get_prompt("simple_prompt", {"topic": "AI"}) @@ -41,7 +41,7 @@ async def test_synchronous_prompt_unchanged(prompt_server): async def test_prompt_with_task_metadata_returns_immediately(prompt_server): """Prompts with task metadata return immediately with PromptTask object.""" - async with Client(prompt_server) as client: + async with Client(prompt_server, mode="legacy") as client: # Call with task metadata task = await client.get_prompt("background_prompt", {"topic": "AI"}, task=True) @@ -59,7 +59,7 @@ async def test_prompt_with_task_metadata_returns_immediately(prompt_server): ) async def test_prompt_task_executes_in_background(prompt_server): """Prompt task executes via Docket in background.""" - async with Client(prompt_server) as client: + async with Client(prompt_server, mode="legacy") as client: task = await client.get_prompt( "background_prompt", {"topic": "Machine Learning", "depth": "comprehensive"}, @@ -89,7 +89,7 @@ async def test_forbidden_mode_prompt_rejects_task_calls(prompt_server): async def sync_only_prompt(topic: str) -> str: return f"Sync prompt: {topic}" - async with Client(prompt_server) as client: + async with Client(prompt_server, mode="legacy") as client: # Calling with task=True when task=False should raise MCPError import pytest diff --git a/tests/server/tasks/test_task_protocol.py b/tests/server/tasks/test_task_protocol.py index 9fd2fd7a6..f47648618 100644 --- a/tests/server/tasks/test_task_protocol.py +++ b/tests/server/tasks/test_task_protocol.py @@ -31,7 +31,7 @@ async def task_enabled_server(): async def test_task_metadata_includes_task_id_and_ttl(task_enabled_server): """Task metadata properly includes server-generated taskId and ttl.""" - async with Client(task_enabled_server) as client: + async with Client(task_enabled_server, mode="legacy") as client: # Submit with specific ttl (server generates task ID) task = await client.call_tool( "simple_tool", @@ -54,7 +54,7 @@ async def test_task_notification_sent_after_submission(task_enabled_server): async def background_tool(message: str) -> str: return f"Processed: {message}" - async with Client(task_enabled_server) as client: + async with Client(task_enabled_server, mode="legacy") as client: task = await client.call_tool("background_tool", {"message": "test"}, task=True) assert task assert not task.returned_immediately @@ -71,7 +71,7 @@ async def test_failed_task_stores_error(task_enabled_server): async def failing_task_tool() -> str: raise ValueError("This tool always fails") - async with Client(task_enabled_server) as client: + async with Client(task_enabled_server, mode="legacy") as client: task = await client.call_tool("failing_task_tool", task=True) assert task assert not task.returned_immediately diff --git a/tests/server/tasks/test_task_proxy.py b/tests/server/tasks/test_task_proxy.py index c272a4444..c8abd5c5d 100644 --- a/tests/server/tasks/test_task_proxy.py +++ b/tests/server/tasks/test_task_proxy.py @@ -68,13 +68,13 @@ class TestProxyToolsSyncExecution: async def test_tool_sync_execution_works(self, proxy_server: FastMCP): """Tool called without task=True works through proxy.""" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.call_tool("add_numbers", {"a": 5, "b": 3}) assert "8" in str(result) async def test_sync_only_tool_works(self, proxy_server: FastMCP): """Sync-only tool works through proxy.""" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.call_tool("sync_only_tool", {"message": "test"}) assert "sync: test" in str(result) @@ -84,7 +84,7 @@ class TestProxyToolsTaskForbidden: async def test_tool_task_returns_error_immediately(self, proxy_server: FastMCP): """Tool called with task=True through proxy returns error immediately.""" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: task = await client.call_tool( "add_numbers", {"a": 5, "b": 3}, task=True, raise_on_error=False ) @@ -100,7 +100,7 @@ class TestProxyToolsTaskForbidden: self, proxy_server: FastMCP ): """Sync-only tool with task=True also returns error immediately.""" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: task = await client.call_tool( "sync_only_tool", {"message": "test"}, @@ -118,7 +118,7 @@ class TestProxyPromptsSyncExecution: async def test_prompt_sync_execution_works(self, proxy_server: FastMCP): """Prompt called without task=True works through proxy.""" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.get_prompt("greeting_prompt", {"name": "Alice"}) assert isinstance(result.messages[0].content, TextContent) assert "Hello, Alice!" in result.messages[0].content.text @@ -135,7 +135,7 @@ class TestProxyPromptsTaskForbidden: ) async def test_prompt_task_raises_mcp_error(self, proxy_server: FastMCP): """Prompt called with task=True through proxy raises MCPError.""" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: with pytest.raises(MCPError) as exc_info: await client.get_prompt("greeting_prompt", {"name": "Alice"}, task=True) @@ -147,14 +147,14 @@ class TestProxyResourcesSyncExecution: async def test_resource_sync_execution_works(self, proxy_server: FastMCP): """Resource read without task=True works through proxy.""" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.read_resource("data://info.txt") assert isinstance(result[0], TextResourceContents) assert "Important information from the backend" in result[0].text async def test_resource_template_sync_execution_works(self, proxy_server: FastMCP): """Resource template without task=True works through proxy.""" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: result = await client.read_resource("data://user/42.json") assert isinstance(result[0], TextResourceContents) assert '"id": "42"' in result[0].text @@ -171,7 +171,7 @@ class TestProxyResourcesTaskForbidden: ) async def test_resource_task_raises_mcp_error(self, proxy_server: FastMCP): """Resource read with task=True through proxy raises MCPError.""" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: with pytest.raises(MCPError) as exc_info: await client.read_resource("data://info.txt", task=True) @@ -185,7 +185,7 @@ class TestProxyResourcesTaskForbidden: ) async def test_resource_template_task_raises_mcp_error(self, proxy_server: FastMCP): """Resource template with task=True through proxy raises MCPError.""" - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: with pytest.raises(MCPError) as exc_info: await client.read_resource("data://user/42.json", task=True) diff --git a/tests/server/tasks/test_task_resources.py b/tests/server/tasks/test_task_resources.py index f7768adc7..acb136281 100644 --- a/tests/server/tasks/test_task_resources.py +++ b/tests/server/tasks/test_task_resources.py @@ -36,7 +36,7 @@ async def resource_server(): async def test_synchronous_resource_unchanged(resource_server): """Resources without task metadata execute synchronously as before.""" - async with Client(resource_server) as client: + async with Client(resource_server, mode="legacy") as client: # Regular call without task metadata result = await client.read_resource("file://data.txt") @@ -46,7 +46,7 @@ async def test_synchronous_resource_unchanged(resource_server): async def test_resource_with_task_metadata_returns_immediately(resource_server): """Resources with task metadata return immediately with ResourceTask object.""" - async with Client(resource_server) as client: + async with Client(resource_server, mode="legacy") as client: # Call with task metadata task = await client.read_resource("file://large.txt", task=True) @@ -64,7 +64,7 @@ async def test_resource_with_task_metadata_returns_immediately(resource_server): ) async def test_resource_task_executes_in_background(resource_server): """Resource task executes via Docket in background.""" - async with Client(resource_server) as client: + async with Client(resource_server, mode="legacy") as client: task = await client.read_resource("file://large.txt", task=True) # Verify background execution @@ -84,7 +84,7 @@ async def test_resource_task_executes_in_background(resource_server): ) async def test_resource_template_with_task(resource_server): """Resource templates with task=True execute in background.""" - async with Client(resource_server) as client: + async with Client(resource_server, mode="legacy") as client: task = await client.read_resource("file://user/123/data.json", task=True) # Verify background execution @@ -113,7 +113,7 @@ async def test_forbidden_mode_resource_rejects_task_calls(resource_server): async def sync_only_resource() -> str: return "Sync content" - async with Client(resource_server) as client: + async with Client(resource_server, mode="legacy") as client: # Calling with task=True when task=False should raise MCPError with pytest.raises(MCPError) as exc_info: await client.read_resource("file://sync.txt", task=True) diff --git a/tests/server/tasks/test_task_return_types.py b/tests/server/tasks/test_task_return_types.py index 15452c79f..a8255a5a7 100644 --- a/tests/server/tasks/test_task_return_types.py +++ b/tests/server/tasks/test_task_return_types.py @@ -96,7 +96,7 @@ async def test_task_basic_types( expected_value: Any, ): """Task mode returns basic types correctly.""" - async with Client(return_type_server) as client: + async with Client(return_type_server, mode="legacy") as client: task = await client.call_tool(tool_name, task=True) result = await task assert isinstance(result.data, expected_type) @@ -105,7 +105,7 @@ async def test_task_basic_types( async def test_task_model_return(return_type_server): """Task mode returns same BaseModel (as dict) as immediate mode.""" - async with Client(return_type_server) as client: + async with Client(return_type_server, mode="legacy") as client: task = await client.call_tool("return_model", task=True) result = await task @@ -118,7 +118,7 @@ async def test_task_model_return(return_type_server): async def test_task_vs_immediate_equivalence(return_type_server): """Verify task mode and immediate mode return identical results.""" - async with Client(return_type_server) as client: + async with Client(return_type_server, mode="legacy") as client: # Test a few types to verify equivalence tools_to_test = ["return_string", "return_int", "return_dict"] @@ -160,7 +160,7 @@ async def prompt_return_server(): async def test_prompt_task_single_message(prompt_return_server): """Prompt task returns single message correctly.""" - async with Client(prompt_return_server) as client: + async with Client(prompt_return_server, mode="legacy") as client: task = await client.get_prompt("single_message_prompt", task=True) result = await task @@ -170,7 +170,7 @@ async def test_prompt_task_single_message(prompt_return_server): async def test_prompt_task_multiple_messages(prompt_return_server): """Prompt task returns multiple messages correctly.""" - async with Client(prompt_return_server) as client: + async with Client(prompt_return_server, mode="legacy") as client: task = await client.get_prompt("multi_message_prompt", task=True) result = await task @@ -202,7 +202,7 @@ async def resource_return_server(): async def test_resource_task_text_content(resource_return_server): """Resource task returns text content correctly.""" - async with Client(resource_return_server) as client: + async with Client(resource_return_server, mode="legacy") as client: task = await client.read_resource("text://simple", task=True) contents = await task @@ -212,7 +212,7 @@ async def test_resource_task_text_content(resource_return_server): async def test_resource_task_json_content(resource_return_server): """Resource task returns structured content correctly.""" - async with Client(resource_return_server) as client: + async with Client(resource_return_server, mode="legacy") as client: task = await client.read_resource("data://json", task=True) contents = await task @@ -287,7 +287,7 @@ async def test_task_binary_types( assertion_fn: Any, ): """Task mode handles binary and special types.""" - async with Client(binary_type_server) as client: + async with Client(binary_type_server, mode="legacy") as client: task = await client.call_tool(tool_name, task=True) result = await task assert isinstance(result.data, expected_type) @@ -338,7 +338,7 @@ async def test_task_collection_types( expected_value: Any, ): """Task mode handles collection types.""" - async with Client(collection_server) as client: + async with Client(collection_server, mode="legacy") as client: task = await client.call_tool(tool_name, task=True) result = await task assert isinstance(result.data, expected_type) @@ -347,7 +347,7 @@ async def test_task_collection_types( async def test_task_empty_dict_return(collection_server): """Task mode handles empty dict return.""" - async with Client(collection_server) as client: + async with Client(collection_server, mode="legacy") as client: task = await client.call_tool("return_empty_dict", task=True) result = await task # Empty structured content becomes None in data @@ -426,7 +426,7 @@ async def test_task_media_types( assertion_fn: Any, ): """Task mode handles media types (Image, Audio, File).""" - async with Client(media_server) as client: + async with Client(media_server, mode="legacy") as client: task = await client.call_tool(tool_name, task=True) result = await task assert assertion_fn(result) @@ -498,7 +498,7 @@ async def test_task_structured_dict_types( expected_age: int, ): """Task mode handles TypedDict and dataclass returns.""" - async with Client(structured_type_server) as client: + async with Client(structured_type_server, mode="legacy") as client: task = await client.call_tool(tool_name, task=True) result = await task # Both deserialize to dynamic Root class @@ -520,7 +520,7 @@ async def test_task_union_types( expected_value: Any, ): """Task mode handles union type branches.""" - async with Client(structured_type_server) as client: + async with Client(structured_type_server, mode="legacy") as client: task = await client.call_tool(tool_name, task=True) result = await task assert isinstance(result.data, expected_type) @@ -541,7 +541,7 @@ async def test_task_optional_types( expected_value: Any, ): """Task mode handles Optional types.""" - async with Client(structured_type_server) as client: + async with Client(structured_type_server, mode="legacy") as client: task = await client.call_tool(tool_name, task=True) result = await task assert isinstance(result.data, expected_type) @@ -650,7 +650,7 @@ async def test_task_mcp_content_types( assertion_fn: Any, ): """Task mode handles MCP content block types.""" - async with Client(mcp_content_server) as client: + async with Client(mcp_content_server, mode="legacy") as client: task = await client.call_tool(tool_name, task=True) result = await task assert assertion_fn(result) @@ -658,7 +658,7 @@ async def test_task_mcp_content_types( async def test_task_mixed_content_return(mcp_content_server): """Task mode handles mixed content list return.""" - async with Client(mcp_content_server) as client: + async with Client(mcp_content_server, mode="legacy") as client: task = await client.call_tool("return_mixed_content", task=True) result = await task assert len(result.content) == 3 diff --git a/tests/server/tasks/test_task_security.py b/tests/server/tasks/test_task_security.py index 5d3b16ffa..605382894 100644 --- a/tests/server/tasks/test_task_security.py +++ b/tests/server/tasks/test_task_security.py @@ -38,7 +38,7 @@ async def test_same_client_can_access_all_its_tasks(task_server: FastMCP): ) reset = auth_context_var.set(AuthenticatedUser(token)) try: - async with Client(task_server) as client: + async with Client(task_server, mode="legacy") as client: task1 = await client.call_tool( "secret_tool", {"data": "first"}, task=True, task_id="task-1" ) @@ -60,7 +60,7 @@ async def test_same_client_can_access_all_its_tasks(task_server: FastMCP): async def test_unauthenticated_client_can_access_its_tasks(task_server: FastMCP): """An unauthenticated client can access tasks it created (by task ID).""" - async with Client(task_server) as client: + async with Client(task_server, mode="legacy") as client: task = await client.call_tool( "secret_tool", {"data": "hello"}, task=True, task_id="my-task" ) @@ -95,14 +95,14 @@ async def test_distinct_clients_cannot_access_each_others_tasks( a peer's task id returns 'not found'.""" reset = _set_auth("client-a") try: - async with Client(task_server) as client_a: + async with Client(task_server, mode="legacy") as client_a: task_id = await _submit_task_id(client_a, "client-a-secret") finally: auth_context_var.reset(reset) reset = _set_auth("client-b") try: - async with Client(task_server) as client_b: + async with Client(task_server, mode="legacy") as client_b: with pytest.raises(Exception, match="not found"): await client_b.get_task_status(task_id) finally: @@ -118,14 +118,14 @@ async def test_distinct_subs_same_client_id_cannot_access_each_others_tasks( reset = _set_auth(shared_client, sub="user-alice") try: - async with Client(task_server) as alice: + async with Client(task_server, mode="legacy") as alice: task_id = await _submit_task_id(alice, "alice-secret") finally: auth_context_var.reset(reset) reset = _set_auth(shared_client, sub="user-bob") try: - async with Client(task_server) as bob: + async with Client(task_server, mode="legacy") as bob: with pytest.raises(Exception, match="not found"): await bob.get_task_status(task_id) finally: @@ -139,11 +139,11 @@ async def test_authenticated_and_anonymous_keyspaces_are_disjoint( tasks (and vice versa) even when colliding on task id.""" reset = _set_auth("client-a") try: - async with Client(task_server) as authed: + async with Client(task_server, mode="legacy") as authed: authed_task_id = await _submit_task_id(authed, "authed-secret") finally: auth_context_var.reset(reset) - async with Client(task_server) as anon: + async with Client(task_server, mode="legacy") as anon: with pytest.raises(Exception, match="not found"): await anon.get_task_status(authed_task_id) diff --git a/tests/server/tasks/test_task_status_notifications.py b/tests/server/tasks/test_task_status_notifications.py index 98d333ca9..1b292bc73 100644 --- a/tests/server/tasks/test_task_status_notifications.py +++ b/tests/server/tasks/test_task_status_notifications.py @@ -55,7 +55,7 @@ async def notification_server(): async def test_subscription_spawned_for_tool_task(notification_server: FastMCP): """Subscription task is spawned when tool task is created.""" - async with Client(notification_server) as client: + async with Client(notification_server, mode="legacy") as client: # Create task - should spawn subscription task = await client.call_tool("quick_task", {"value": 5}, task=True) @@ -69,7 +69,7 @@ async def test_subscription_spawned_for_tool_task(notification_server: FastMCP): async def test_subscription_handles_task_completion(notification_server: FastMCP): """Subscription properly handles task completion and cleanup.""" - async with Client(notification_server) as client: + async with Client(notification_server, mode="legacy") as client: # Multiple tasks should each get their own subscription task1 = await client.call_tool("quick_task", {"value": 1}, task=True) task2 = await client.call_tool("quick_task", {"value": 2}, task=True) @@ -91,7 +91,7 @@ async def test_subscription_handles_task_completion(notification_server: FastMCP async def test_subscription_handles_task_failure(notification_server: FastMCP): """Subscription properly handles task failure.""" - async with Client(notification_server) as client: + async with Client(notification_server, mode="legacy") as client: task = await client.call_tool("failing_task", {}, task=True) # Task should fail @@ -104,7 +104,7 @@ async def test_subscription_handles_task_failure(notification_server: FastMCP): async def test_subscription_for_prompt_tasks(notification_server: FastMCP): """Subscriptions work for prompt tasks.""" - async with Client(notification_server) as client: + async with Client(notification_server, mode="legacy") as client: task = await client.get_prompt("test_prompt", {"name": "World"}, task=True) result = await task @@ -117,7 +117,7 @@ async def test_subscription_for_prompt_tasks(notification_server: FastMCP): async def test_subscription_for_resource_tasks(notification_server: FastMCP): """Subscriptions work for resource tasks.""" - async with Client(notification_server) as client: + async with Client(notification_server, mode="legacy") as client: task = await client.read_resource("test://resource", task=True) result = await task @@ -132,7 +132,7 @@ async def test_subscriptions_cleanup_on_session_disconnect( ): """Subscriptions are cleaned up when session disconnects.""" # Start session and create task - async with Client(notification_server) as client: + async with Client(notification_server, mode="legacy") as client: task = await client.call_tool("slow_task", {"duration": 1.0}, task=True) task_id = task.task_id # Disconnect before task completes (session __aexit__ cancels subscriptions) @@ -145,7 +145,7 @@ async def test_subscriptions_cleanup_on_session_disconnect( async def test_multiple_concurrent_subscriptions(notification_server: FastMCP): """Multiple concurrent tasks each have their own subscription.""" - async with Client(notification_server) as client: + async with Client(notification_server, mode="legacy") as client: # Start many tasks concurrently tasks = [] for i in range(10): diff --git a/tests/server/tasks/test_task_tools.py b/tests/server/tasks/test_task_tools.py index 1515504d1..c9d269bf7 100644 --- a/tests/server/tasks/test_task_tools.py +++ b/tests/server/tasks/test_task_tools.py @@ -59,7 +59,7 @@ async def test_task_tool_validates_model_arguments(): arguments = {"item": {"value": "a"}, "items": [{"value": "b"}]} expected = {"item": "_Item", "element": "_Item"} - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: sync_result = await client.call_tool("inspect_items", arguments) task = await client.call_tool("inspect_items", arguments, task=True) task_result = await task.result() @@ -92,7 +92,7 @@ async def test_task_tool_invalid_arguments_fail_before_task_state(): return item.value recorder = _Recorder() - async with Client(server, message_handler=recorder) as client: + async with Client(server, mode="legacy", message_handler=recorder) as client: # `item` is missing its required `value` field. task = await client.call_tool("needs_item", {"item": {}}, task=True) assert task.returned_immediately @@ -126,7 +126,7 @@ async def test_task_submission_honors_strict_input_validation(): return n * n recorder = _Recorder() - async with Client(server, message_handler=recorder) as client: + async with Client(server, mode="legacy", message_handler=recorder) as client: # Sync path rejects the string-for-int coercion under strict validation. with pytest.raises(ToolError): await client.call_tool("square", {"n": "1"}) @@ -149,7 +149,7 @@ async def test_task_submission_valid_argument_under_strict_validation(): async def square(n: int) -> int: return n * n - async with Client(server) as client: + async with Client(server, mode="legacy") as client: task = await client.call_tool("square", {"n": 4}, task=True) assert not task.returned_immediately result = await task.result() @@ -174,7 +174,7 @@ def test_resolve_param_hints_handles_partials(): async def test_synchronous_tool_call_unchanged(tool_server): """Tools without task metadata execute synchronously as before.""" - async with Client(tool_server) as client: + async with Client(tool_server, mode="legacy") as client: # Regular call without task metadata result = await client.call_tool("simple_tool", {"message": "hello"}) @@ -184,7 +184,7 @@ async def test_synchronous_tool_call_unchanged(tool_server): async def test_tool_with_task_metadata_returns_immediately(tool_server): """Tools with task metadata return immediately with ToolTask object.""" - async with Client(tool_server) as client: + async with Client(tool_server, mode="legacy") as client: # Call with task metadata task = await client.call_tool("simple_tool", {"message": "test"}, task=True) assert task @@ -207,7 +207,7 @@ async def test_tool_task_executes_in_background(tool_server): await execution_completed.wait() return "completed" - async with Client(tool_server) as client: + async with Client(tool_server, mode="legacy") as client: task = await client.call_tool("coordinated_tool", task=True) assert task assert not task.returned_immediately @@ -229,7 +229,7 @@ async def test_tool_task_executes_in_background(tool_server): async def test_forbidden_mode_tool_rejects_task_calls(tool_server): """Tools with task=False (mode=forbidden) reject task-augmented calls.""" - async with Client(tool_server) as client: + async with Client(tool_server, mode="legacy") as client: # Calling with task=True when task=False should return error task = await client.call_tool( "sync_only_tool", {"message": "test"}, task=True, raise_on_error=False diff --git a/tests/server/tasks/test_task_ttl.py b/tests/server/tasks/test_task_ttl.py index 769fafffa..5760e4f99 100644 --- a/tests/server/tasks/test_task_ttl.py +++ b/tests/server/tasks/test_task_ttl.py @@ -32,7 +32,7 @@ async def keepalive_server(): async def test_keepalive_returned_in_submitted_state(keepalive_server: FastMCP): """ttl is returned in tasks/get even when task is submitted/working.""" - async with Client(keepalive_server) as client: + async with Client(keepalive_server, mode="legacy") as client: # Submit task with explicit ttl task = await client.call_tool( "slow_task", @@ -55,7 +55,7 @@ async def test_keepalive_returned_in_submitted_state(keepalive_server: FastMCP): async def test_keepalive_returned_in_completed_state(keepalive_server: FastMCP): """ttl is returned in tasks/get after task completes.""" - async with Client(keepalive_server) as client: + async with Client(keepalive_server, mode="legacy") as client: # Submit and complete task task = await client.call_tool( "quick_task", @@ -77,7 +77,7 @@ async def test_keepalive_returned_in_completed_state(keepalive_server: FastMCP): async def test_default_keepalive_when_not_specified(keepalive_server: FastMCP): """Default ttl is used when client doesn't specify.""" - async with Client(keepalive_server) as client: + async with Client(keepalive_server, mode="legacy") as client: # Submit without explicit ttl task = await client.call_tool("quick_task", {"value": 3}, task=True) await task.wait(timeout=2.0) diff --git a/tests/server/telemetry/test_sampling_tracing.py b/tests/server/telemetry/test_sampling_tracing.py index db02b13bc..af9392344 100644 --- a/tests/server/telemetry/test_sampling_tracing.py +++ b/tests/server/telemetry/test_sampling_tracing.py @@ -43,7 +43,9 @@ class TestSamplingCreateMessageSpan: result = await context.sample(messages=question) return result.text or "" - async with Client(mcp, sampling_handler=sampling_handler) as client: + async with Client( + mcp, mode="legacy", sampling_handler=sampling_handler + ) as client: await client.call_tool("ask", {"question": "hi"}) spans = _spans_named(trace_exporter, "sampling create_message") @@ -78,7 +80,9 @@ class TestSamplingCreateMessageSpan: return result.text or "" with pytest.raises(Exception): - async with Client(mcp, sampling_handler=sampling_handler) as client: + async with Client( + mcp, mode="legacy", sampling_handler=sampling_handler + ) as client: await client.call_tool("ask", {"question": "hi"}) spans = _spans_named(trace_exporter, "sampling create_message") diff --git a/tests/server/telemetry/test_server_tracing.py b/tests/server/telemetry/test_server_tracing.py index c04f49fd6..a9bc92334 100644 --- a/tests/server/telemetry/test_server_tracing.py +++ b/tests/server/telemetry/test_server_tracing.py @@ -447,7 +447,7 @@ class TestSeamServerSpan: ): mcp = FastMCP("test-server") - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: await client.set_logging_level("info") spans = trace_exporter.get_finished_spans() @@ -468,7 +468,7 @@ class TestSeamServerSpan: """A seam-spanned method must produce exactly one SERVER span, not two.""" mcp = FastMCP("test-server") - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: await client.set_logging_level("info") spans = trace_exporter.get_finished_spans() diff --git a/tests/server/test_icons.py b/tests/server/test_icons.py index 9fba37b9b..3bbf0c527 100644 --- a/tests/server/test_icons.py +++ b/tests/server/test_icons.py @@ -35,7 +35,7 @@ class TestServerIcons: ) # Verify that icons and website_url are passed to the underlying server - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: server_info = client.initialize_result.server_info assert server_info.website_url == "https://example.com" assert server_info.icons == icons @@ -44,7 +44,7 @@ class TestServerIcons: """Test that server works without icons and websiteUrl.""" mcp = FastMCP(name="TestServer") - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: server_info = client.initialize_result.server_info assert server_info.website_url is None assert server_info.icons is None @@ -289,7 +289,7 @@ class TestIconTypes: mcp = FastMCP("TestServer", icons=icons) - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: server_info = client.initialize_result.server_info assert len(server_info.icons) == 3 assert server_info.icons == icons @@ -318,7 +318,7 @@ class TestIconTypes: mcp = FastMCP("TestServer", icons=icons) - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: server_info = client.initialize_result.server_info assert server_info.icons[0].src == "https://example.com/icon.png" assert server_info.icons[0].mime_type is None diff --git a/tests/server/test_session_visibility.py b/tests/server/test_session_visibility.py index d98a0d72f..6a6f9b105 100644 --- a/tests/server/test_session_visibility.py +++ b/tests/server/test_session_visibility.py @@ -70,7 +70,7 @@ class TestSessionVisibility: assert rules[0]["tags"] == ["finance"] return "activated" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: result = await client.call_tool("activate_finance", {}) assert result.data == "activated" @@ -94,7 +94,7 @@ class TestSessionVisibility: assert rules[0]["tags"] == ["internal"] return "deactivated" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: result = await client.call_tool("deactivate_internal", {}) assert result.data == "deactivated" @@ -116,7 +116,7 @@ class TestSessionVisibility: # Globally disable finance tools mcp.disable(tags={"finance"}) - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Before activation, finance tool should not be visible tools_before = await client.list_tools() assert not any(t.name == "finance_tool" for t in tools_before) @@ -151,7 +151,7 @@ class TestSessionVisibility: # Globally disable finance tools mcp.disable(tags={"finance"}) - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Activate finance await client.call_tool("activate_finance", {}) @@ -182,13 +182,13 @@ class TestSessionVisibility: mcp.disable(tags={"finance"}) # Session A activates finance - async with Client(mcp) as client_a: + async with Client(mcp, mode="legacy") as client_a: await client_a.call_tool("activate_finance", {}) tools_a = await client_a.list_tools() assert any(t.name == "finance_tool" for t in tools_a) # Session B should not see finance tool (different session) - async with Client(mcp) as client_b: + async with Client(mcp, mode="legacy") as client_b: tools_b = await client_b.list_tools() assert not any(t.name == "finance_tool" for t in tools_b) @@ -220,7 +220,7 @@ class TestSessionVisibility: # Globally disable all versioned tools mcp.disable(names={"old_tool", "new_tool"}) - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Enable v2 tools await client.call_tool("enable_v2_only", {}) @@ -254,7 +254,7 @@ class TestSessionVisibility: # Globally disable finance tools mcp.disable(tags={"finance"}) - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Activate finance await client.call_tool("activate_finance", {}) tools_after_activate = await client.list_tools() @@ -292,7 +292,7 @@ class TestSessionVisibility: # Globally disable finance and admin tools mcp.disable(tags={"finance", "admin"}) - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Activate both await client.call_tool("activate_multiple", {}) @@ -318,7 +318,7 @@ class TestSessionVisibility: await ctx.disable_components(tags={"test"}) return "toggled" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Toggle (enable then disable) await client.call_tool("toggle_test", {}) @@ -344,7 +344,7 @@ class TestSessionVisibility: # Globally disable finance resources mcp.disable(tags={"finance"}) - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Before activation, finance resource should not be visible resources_before = await client.list_resources() assert not any(str(r.uri) == "resource://finance" for r in resources_before) @@ -374,7 +374,7 @@ class TestSessionVisibility: # Globally disable finance prompts mcp.disable(tags={"finance"}) - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Before activation, finance prompt should not be visible prompts_before = await client.list_prompts() assert not any(p.name == "finance_prompt" for p in prompts_before) @@ -402,7 +402,7 @@ class TestSessionVisibilityNotifications: return "activated" handler = RecordingMessageHandler() - async with Client(mcp, message_handler=handler) as client: + async with Client(mcp, mode="legacy", message_handler=handler) as client: handler.reset() await client.call_tool("activate", {}) @@ -432,7 +432,7 @@ class TestSessionVisibilityNotifications: return "deactivated" handler = RecordingMessageHandler() - async with Client(mcp, message_handler=handler) as client: + async with Client(mcp, mode="legacy", message_handler=handler) as client: handler.reset() await client.call_tool("deactivate", {}) @@ -461,7 +461,7 @@ class TestSessionVisibilityNotifications: return "cleared" handler = RecordingMessageHandler() - async with Client(mcp, message_handler=handler) as client: + async with Client(mcp, mode="legacy", message_handler=handler) as client: handler.reset() await client.call_tool("clear", {}) @@ -491,7 +491,7 @@ class TestSessionVisibilityNotifications: return "activated" handler = RecordingMessageHandler() - async with Client(mcp, message_handler=handler) as client: + async with Client(mcp, mode="legacy", message_handler=handler) as client: handler.reset() await client.call_tool("activate_tools_only", {}) @@ -537,7 +537,7 @@ class TestConcurrentSessionIsolation: async def session_a(): nonlocal session_a_sees_finance - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Activate finance for this session await client.call_tool("activate_finance", {}) @@ -556,7 +556,7 @@ class TestConcurrentSessionIsolation: # Wait for session A to activate await ready_event.wait() - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Session B should NOT see finance tool tools = await client.list_tools() session_b_sees_finance = any(t.name == "finance_tool" for t in tools) @@ -590,13 +590,13 @@ class TestConcurrentSessionIsolation: results: dict[str, bool] = {} async def activated_session(session_id: str): - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: await client.call_tool("activate_premium", {}) tools = await client.list_tools() results[session_id] = any(t.name == "premium_tool" for t in tools) async def non_activated_session(session_id: str): - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: tools = await client.list_tools() results[session_id] = any(t.name == "premium_tool" for t in tools) @@ -644,7 +644,7 @@ class TestSessionVisibilityResetBug: await ctx.reset_visibility() return "exited" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Tool visible initially tools = await client.list_tools() assert any(t.name == "my_tool" for t in tools) @@ -681,7 +681,7 @@ class TestSessionVisibilityResetBug: await ctx.reset_visibility() return "exited" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: for i in range(3): # create_project should be visible tools = await client.list_tools() @@ -719,7 +719,7 @@ class TestSessionVisibilityResetBug: check_done = anyio.Event() async def session_a(): - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: await client.call_tool("disable_system", {}) ready.set() await check_done.wait() @@ -727,7 +727,7 @@ class TestSessionVisibilityResetBug: async def session_b(): nonlocal session_b_sees_tool await ready.wait() - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: tools = await client.list_tools() session_b_sees_tool = any(t.name == "shared_tool" for t in tools) check_done.set() @@ -756,13 +756,13 @@ class TestSessionVisibilityResetBug: return "disabled" # Session A disables the tool (no reset) - async with Client(mcp) as client_a: + async with Client(mcp, mode="legacy") as client_a: await client_a.call_tool("disable_system", {}) tools = await client_a.list_tools() assert not any(t.name == "shared_tool" for t in tools) # Session B should see it fresh - async with Client(mcp) as client_b: + async with Client(mcp, mode="legacy") as client_b: tools = await client_b.list_tools() assert any(t.name == "shared_tool" for t in tools), ( "New session should see shared_tool regardless of previous session" diff --git a/tests/server/test_tool_annotations.py b/tests/server/test_tool_annotations.py index 096fd451d..46bb44e5e 100644 --- a/tests/server/test_tool_annotations.py +++ b/tests/server/test_tool_annotations.py @@ -229,7 +229,7 @@ async def test_task_execution_auto_populated_for_task_enabled_tool(): """A tool that runs in background.""" return f"Processed: {data}" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: tools_result = await client.list_tools() assert len(tools_result) == 1 assert tools_result[0].name == "background_tool" diff --git a/tests/server/transforms/test_search.py b/tests/server/transforms/test_search.py index 981d044f2..241743ce2 100644 --- a/tests/server/transforms/test_search.py +++ b/tests/server/transforms/test_search.py @@ -188,7 +188,7 @@ class TestBaseTransformBehavior: await ctx.disable_components(names={"delete_record"}) return "disabled" - async with Client(mcp) as client: + async with Client(mcp, mode="legacy") as client: # Before disabling, search should find delete_record result = await client.call_tool("search_tools", {"pattern": "delete"}) found = _parse_tool_result(result) diff --git a/tests/test_apps.py b/tests/test_apps.py index dac7276cc..6aa65afed 100644 --- a/tests/test_apps.py +++ b/tests/test_apps.py @@ -417,14 +417,14 @@ class TestExtensionAdvertisement: experimental_capabilities={"file_exchange": {"version": "0.3"}}, ) - async with Client(server) as client: + async with Client(server, mode="legacy") as client: experimental = client.initialize_result.capabilities.experimental or {} assert experimental.get("file_exchange") == {"version": "0.3"} async def test_experimental_capabilities_default_empty(self): server = FastMCP("test") - async with Client(server) as client: + async with Client(server, mode="legacy") as client: experimental = client.initialize_result.capabilities.experimental assert not experimental diff --git a/tests/test_compat.py b/tests/test_compat.py index bb2c1233a..ec38cd6f6 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -231,7 +231,7 @@ class TestClientBehaviorCompat: assert result.data == "hi" async def test_ping_returns_bool(self, server): - client = Client(transport=FastMCPTransport(server)) + client = Client(transport=FastMCPTransport(server), mode="legacy") async with client: result = await client.ping() assert result is True diff --git a/tests/tools/tool_transform/test_tool_transform.py b/tests/tools/tool_transform/test_tool_transform.py index f9e50df72..8e3f79350 100644 --- a/tests/tools/tool_transform/test_tool_transform.py +++ b/tests/tools/tool_transform/test_tool_transform.py @@ -736,7 +736,7 @@ class TestProxy: ) proxy_server.add_tool(new_add_tool) - async with Client(proxy_server) as client: + async with Client(proxy_server, mode="legacy") as client: # The tool should be registered with its transformed name result = await client.call_tool("add_transformed", {"new_x": 1, "old_y": 2}) assert isinstance(result.content[0], TextContent)