Pin legacy for handshake-era features after mode=auto flip

This commit is contained in:
Jeremiah Lowin 2026-07-09 11:14:16 -04:00
commit 7d6c36a85d
No known key found for this signature in database
48 changed files with 423 additions and 371 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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:

View file

@ -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]

View file

@ -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()

View file

@ -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)

View file

@ -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", {})

View file

@ -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"}
)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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!"

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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
)

View file

@ -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"

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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

View file

@ -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)

View file

@ -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")

View file

@ -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()

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)