mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
Pin legacy for handshake-era features after mode=auto flip
This commit is contained in:
parent
93fe4e89e0
commit
7d6c36a85d
48 changed files with 423 additions and 371 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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", {})
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue