mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Audit mode="legacy" pins in server tests and top-level stragglers
Removes pins added while making the auto-default suite pass that weren't actually testing older-protocol-only behavior, and keeps (with a stated reason) the ones that are. Along the way, fixes two real defects the audit surfaced in the modern protocol path: PingMiddleware could leak a _active_sessions entry when a connection's exit_stack closed before its keepalive task got its first scheduler turn, and FastMCP(experimental_ capabilities=...) was silently dropped from server/discover responses (it only ever reached the legacy initialize handshake).
This commit is contained in:
parent
bf352d8a4a
commit
de14ed1b7c
17 changed files with 135 additions and 50 deletions
|
|
@ -461,13 +461,9 @@ class LowLevelServer(_Server[LifespanResultT]):
|
|||
# ensure we use the FastMCP notification options
|
||||
if notification_options is None:
|
||||
notification_options = self.notification_options
|
||||
merged = {
|
||||
**self.fastmcp.experimental_capabilities,
|
||||
**(experimental_capabilities or {}),
|
||||
}
|
||||
return super().create_initialization_options(
|
||||
notification_options=notification_options,
|
||||
experimental_capabilities=merged or None,
|
||||
experimental_capabilities=experimental_capabilities,
|
||||
extensions=extensions,
|
||||
)
|
||||
|
||||
|
|
@ -483,13 +479,23 @@ class LowLevelServer(_Server[LifespanResultT]):
|
|||
and advertise the MCP Apps UI extension.
|
||||
|
||||
``ServerCapabilities.tasks`` and ``ServerCapabilities.extensions`` are
|
||||
real declared fields in v2, so we update them directly.
|
||||
real declared fields in v2, so we update them directly. The
|
||||
`FastMCP(experimental_capabilities=...)` merge also lives here rather
|
||||
than in `create_initialization_options`: the modern `server/discover`
|
||||
handler calls this directly, without going through
|
||||
`create_initialization_options` at all, so merging there only reached
|
||||
the handshake-era `initialize` response and silently dropped
|
||||
constructor-configured experimental capabilities from `discover`.
|
||||
"""
|
||||
from fastmcp.server.tasks.capabilities import get_task_capabilities
|
||||
|
||||
merged_experimental = {
|
||||
**self.fastmcp.experimental_capabilities,
|
||||
**(experimental_capabilities or {}),
|
||||
}
|
||||
capabilities = super().get_capabilities(
|
||||
notification_options,
|
||||
experimental_capabilities,
|
||||
merged_experimental or None,
|
||||
extensions,
|
||||
protocol_version=protocol_version,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -71,6 +71,15 @@ class PingMiddleware(Middleware):
|
|||
ping_task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await ping_task
|
||||
# `ping_task` may be cancelled before its first
|
||||
# scheduler turn (a connection can be built and torn
|
||||
# down within a single request on the modern,
|
||||
# per-request `Connection` path), in which case its
|
||||
# body - and the `finally` in `_ping_loop` that would
|
||||
# otherwise discard this entry - never runs. Discard
|
||||
# unconditionally here so a connection that closes
|
||||
# before the loop starts doesn't leak its entry.
|
||||
self._active_sessions.discard(connection_id)
|
||||
|
||||
connection.exit_stack.push_async_callback(_cancel_ping)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ async def test_elicitation_none_response_type_warns_deprecation():
|
|||
async def elicitation_handler(message, response_type, params, ctx):
|
||||
return ElicitResult(action="accept", content={})
|
||||
|
||||
# `ctx.elicit` sends a server-initiated request down the client's
|
||||
# back-channel, which only the older protocol has, so this pins that era.
|
||||
async with Client(
|
||||
mcp, mode="legacy", elicitation_handler=elicitation_handler
|
||||
) as client:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
"""Tests for middleware support during initialization."""
|
||||
"""Tests for middleware support during initialization.
|
||||
|
||||
`on_initialize` only fires for the `initialize` handshake, which is unique to
|
||||
the older protocol version; the modern version connects without it, so a
|
||||
default client never triggers this hook. Most tests below pin `mode="legacy"`
|
||||
for that reason. `test_session_state_persists_across_tool_calls` pins for a
|
||||
different reason: it exercises `ctx.set_state`/`get_state` persisting across
|
||||
multiple tool calls in the same client session, which requires the
|
||||
handshake-era's persistent session (see `test_session_visibility.py` for the
|
||||
same distinction applied to a different feature).
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
|
@ -401,8 +411,7 @@ async def test_state_isolation_between_streamable_http_clients():
|
|||
import json
|
||||
|
||||
# Client 1 stores its value
|
||||
# Session ids belong to the handshake era, so these pin the legacy era.
|
||||
async with running_server.client(mode="legacy") as client1:
|
||||
async with running_server.client() as client1:
|
||||
result1 = await client1.call_tool(
|
||||
"store_and_read", {"value": "client1-value"}
|
||||
)
|
||||
|
|
@ -412,7 +421,7 @@ async def test_state_isolation_between_streamable_http_clients():
|
|||
session_id_1 = data1["session_id"]
|
||||
|
||||
# Client 2 should have completely isolated state
|
||||
async with running_server.client(mode="legacy") as client2:
|
||||
async with running_server.client() as client2:
|
||||
result2 = await client2.call_tool(
|
||||
"store_and_read", {"value": "client2-value"}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from typing import Any
|
|||
|
||||
import mcp_types
|
||||
import pytest
|
||||
from mcp.shared.dispatcher import CallOptions
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp_types import ElicitRequest, ElicitRequestFormParams, InputRequiredResult
|
||||
|
||||
|
|
@ -65,6 +66,26 @@ def _adder() -> FastMCP:
|
|||
return server
|
||||
|
||||
|
||||
async def _raw_request(
|
||||
client: Client, method: str, params: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Send a bare JSON-RPC request through the dispatcher, bypassing the
|
||||
typed `send_request` that normally stamps the outgoing envelope.
|
||||
|
||||
The modern protocol version requires every request's `params._meta` to
|
||||
carry the protocol version, client info, and client capabilities (there is
|
||||
no handshake to establish them once, up front), so a raw request built by
|
||||
hand must stamp them the same way `send_request` would or the server
|
||||
rejects the envelope before dispatch ever sees it.
|
||||
"""
|
||||
data: dict[str, Any] = {"method": method, "params": params}
|
||||
opts: CallOptions = {}
|
||||
client.session._stamp(data, opts)
|
||||
return await client.session._dispatcher.send_raw_request(
|
||||
method, data.get("params"), opts
|
||||
)
|
||||
|
||||
|
||||
class TestNotificationVisibility:
|
||||
async def test_client_cancelled_notification_reaches_on_message(self):
|
||||
"""A ``notifications/cancelled`` from the client is observed by
|
||||
|
|
@ -116,11 +137,9 @@ class TestUnroutableAndMalformed:
|
|||
recorder = HookRecorder()
|
||||
server.add_middleware(recorder)
|
||||
|
||||
async with Client(server, mode="legacy") as client:
|
||||
async with Client(server) as client:
|
||||
with pytest.raises(MCPError):
|
||||
await client.session._dispatcher.send_raw_request(
|
||||
"does/not/exist", {}, {}
|
||||
)
|
||||
await _raw_request(client, "does/not/exist", {})
|
||||
|
||||
assert ("on_message", "does/not/exist") in recorder.records
|
||||
assert ("on_request", "does/not/exist") in recorder.records
|
||||
|
|
@ -133,11 +152,9 @@ class TestUnroutableAndMalformed:
|
|||
recorder = HookRecorder()
|
||||
server.add_middleware(recorder)
|
||||
|
||||
async with Client(server, mode="legacy") as client:
|
||||
async with Client(server) as client:
|
||||
with pytest.raises(MCPError):
|
||||
await client.session._dispatcher.send_raw_request(
|
||||
"tools/call", {"not_a_valid": "param"}, {}
|
||||
)
|
||||
await _raw_request(client, "tools/call", {"not_a_valid": "param"})
|
||||
|
||||
assert ("on_message", "tools/call") in recorder.records
|
||||
assert ("on_call_tool", "tools/call") not in recorder.records
|
||||
|
|
@ -216,6 +233,9 @@ class TestMessageModification:
|
|||
server = _adder()
|
||||
server.add_middleware(RewriteLevel())
|
||||
|
||||
# `logging/setLevel` was dropped from the method registry in the modern
|
||||
# protocol version (logging is opt-in per-request via `_meta` there),
|
||||
# so exercising it needs the older protocol.
|
||||
async with Client(server, mode="legacy") as client:
|
||||
await client.session._dispatcher.send_raw_request(
|
||||
"logging/setLevel", {"level": "not-a-valid-level"}, {}
|
||||
|
|
@ -227,6 +247,8 @@ class TestMessageModification:
|
|||
recorder = HookRecorder()
|
||||
server.add_middleware(recorder)
|
||||
|
||||
# `logging/setLevel` only exists on the older protocol; see the pin
|
||||
# note in `test_modified_message_reaches_sdk_dispatch` above.
|
||||
async with Client(server, mode="legacy") as client:
|
||||
await client.session._dispatcher.send_raw_request(
|
||||
"logging/setLevel", {"level": "debug"}, {}
|
||||
|
|
@ -254,6 +276,8 @@ class TestMessageModification:
|
|||
server.add_middleware(recorder)
|
||||
server.add_middleware(RewriteMethod())
|
||||
|
||||
# `ping` was removed from the modern protocol version, so this pins
|
||||
# the era where it's still a real method to rewrite away from.
|
||||
async with Client(server, mode="legacy") as client:
|
||||
await client.session._dispatcher.send_raw_request("ping", {}, {})
|
||||
|
||||
|
|
@ -284,11 +308,9 @@ class TestMessageModification:
|
|||
server.add_middleware(RepairAttempt())
|
||||
server.add_middleware(recorder)
|
||||
|
||||
async with Client(server, mode="legacy") as client:
|
||||
async with Client(server) as client:
|
||||
with pytest.raises(MCPError):
|
||||
await client.session._dispatcher.send_raw_request(
|
||||
"tools/call", {"not_a_valid": "param"}, {}
|
||||
)
|
||||
await _raw_request(client, "tools/call", {"not_a_valid": "param"})
|
||||
|
||||
calls = [r for r in recorder.records if r == ("on_message", "tools/call")]
|
||||
assert len(calls) == 1
|
||||
|
|
|
|||
|
|
@ -173,10 +173,13 @@ class TestMiddlewareHooks:
|
|||
async def test_call_tool(
|
||||
self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware
|
||||
):
|
||||
async with Client(mcp_server, mode="legacy") as client:
|
||||
async with Client(mcp_server) as client:
|
||||
await client.call_tool("add", {"a": 1, "b": 2})
|
||||
|
||||
assert recording_middleware.assert_called(at_least=9)
|
||||
# The floor is lower than a legacy connection's 11: the modern
|
||||
# `server/discover` negotiation fires 2 generic hooks, vs. 5 for the
|
||||
# older `initialize` request plus its `notifications/initialized`.
|
||||
assert recording_middleware.assert_called(at_least=8)
|
||||
assert recording_middleware.assert_called(method="tools/call", at_least=3)
|
||||
assert recording_middleware.assert_called(hook="on_message", at_least=1)
|
||||
assert recording_middleware.assert_called(hook="on_request", at_least=1)
|
||||
|
|
@ -299,6 +302,7 @@ class TestMiddlewareHooks:
|
|||
async def test_initialize(
|
||||
self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware
|
||||
):
|
||||
# `ping` only exists on the older protocol, so this pins that era.
|
||||
async with Client(mcp_server, mode="legacy") as client:
|
||||
await client.ping()
|
||||
|
||||
|
|
|
|||
|
|
@ -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, mode="legacy") as client:
|
||||
async with Client(proxy_server) as client:
|
||||
await client.call_tool("add", {"a": 1, "b": 2})
|
||||
|
||||
assert recording_middleware.assert_called(at_least=6)
|
||||
|
|
|
|||
|
|
@ -193,6 +193,13 @@ class TestPingMiddlewareIntegration:
|
|||
|
||||
assert len(middleware._active_sessions) == 0
|
||||
|
||||
# PingMiddleware keys its keepalive loop off the connection, which
|
||||
# persists for the life of a handshake-era session. On the modern
|
||||
# protocol version, a connection lives only for the single request
|
||||
# that built it, so `_active_sessions` never holds a mid-session
|
||||
# entry an outside observer can see — the register-and-clean-up
|
||||
# happens entirely within one call. That per-request lifecycle is
|
||||
# itself the reason this test pins the older era.
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
result = await client.call_tool("hello")
|
||||
assert result.content[0].text == "Hello!"
|
||||
|
|
@ -214,6 +221,9 @@ class TestPingMiddlewareIntegration:
|
|||
def hello() -> str:
|
||||
return "Hello!"
|
||||
|
||||
# See the pin note in `test_ping_middleware_registers_session`: a
|
||||
# mid-session `_active_sessions` entry is only observable when the
|
||||
# connection persists across requests, which is handshake-era only.
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
await client.call_tool("hello")
|
||||
# Should have one active session
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ Regression focus: the `sampling create_message` span is created with
|
|||
`record_exception=False, set_status_on_exception=False` and records the
|
||||
exception manually in its `except` block. A failed sampling call must
|
||||
therefore produce exactly ONE exception event, not two.
|
||||
|
||||
`ctx.sample` requires the server to send a request down to the client, which
|
||||
only the older protocol's back-channel supports, so every client below pins
|
||||
`mode="legacy"`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -449,6 +449,8 @@ class TestSeamServerSpan:
|
|||
):
|
||||
mcp = FastMCP("test-server")
|
||||
|
||||
# `logging/setLevel` was dropped from the modern protocol version
|
||||
# (SEP-2577), so exercising it needs the older protocol.
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
await client.set_logging_level("info")
|
||||
|
||||
|
|
@ -470,6 +472,8 @@ class TestSeamServerSpan:
|
|||
"""A seam-spanned method must produce exactly one SERVER span, not two."""
|
||||
mcp = FastMCP("test-server")
|
||||
|
||||
# `logging/setLevel` only exists on the older protocol; see the pin
|
||||
# note in `test_set_logging_level_emits_seam_span` above.
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
await client.set_logging_level("info")
|
||||
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ class TestServerIcons:
|
|||
)
|
||||
|
||||
# Verify that icons and website_url are passed to the underlying server
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
server_info = client.initialize_result.server_info
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.session.server_info
|
||||
assert server_info.website_url == "https://example.com"
|
||||
assert server_info.icons == icons
|
||||
|
||||
|
|
@ -45,8 +45,8 @@ class TestServerIcons:
|
|||
"""Test that server works without icons and websiteUrl."""
|
||||
mcp = FastMCP(name="TestServer")
|
||||
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
server_info = client.initialize_result.server_info
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.session.server_info
|
||||
assert server_info.website_url is None
|
||||
assert server_info.icons is None
|
||||
|
||||
|
|
@ -290,8 +290,8 @@ class TestIconTypes:
|
|||
|
||||
mcp = FastMCP("TestServer", icons=icons)
|
||||
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
server_info = client.initialize_result.server_info
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.session.server_info
|
||||
assert len(server_info.icons) == 3
|
||||
assert server_info.icons == icons
|
||||
|
||||
|
|
@ -319,8 +319,8 @@ class TestIconTypes:
|
|||
|
||||
mcp = FastMCP("TestServer", icons=icons)
|
||||
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
server_info = client.initialize_result.server_info
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.session.server_info
|
||||
assert server_info.icons[0].src == "https://example.com/icon.png"
|
||||
assert server_info.icons[0].mime_type is None
|
||||
assert server_info.icons[0].sizes is None
|
||||
|
|
@ -336,8 +336,8 @@ class TestIconTheme:
|
|||
|
||||
mcp = FastMCP("TestServer", icons=icons)
|
||||
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
server_info = client.initialize_result.server_info
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.session.server_info
|
||||
assert server_info.icons[0].theme == theme
|
||||
|
||||
async def test_icon_without_theme_is_none(self):
|
||||
|
|
@ -346,8 +346,8 @@ class TestIconTheme:
|
|||
|
||||
mcp = FastMCP("TestServer", icons=icons)
|
||||
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
server_info = client.initialize_result.server_info
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.session.server_info
|
||||
assert server_info.icons[0].theme is None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,16 @@ class RecordingMessageHandler(MessageHandler):
|
|||
|
||||
|
||||
class TestSessionVisibility:
|
||||
"""Test session-specific visibility control via Context."""
|
||||
"""Test session-specific visibility control via Context.
|
||||
|
||||
Session-scoped visibility rules are stored under `ctx.session_id`. The
|
||||
modern protocol version is stateless: each request gets a fresh
|
||||
connection identity, so a rule set in one request is gone by the next.
|
||||
Tests that only check state within a single tool call are era-neutral
|
||||
and stay unpinned; tests that activate a rule in one request and observe
|
||||
its effect in a later request are pinned to the handshake era, where the
|
||||
rule's persistence is the very thing under test.
|
||||
"""
|
||||
|
||||
async def test_enable_components_stores_rule_dict(self):
|
||||
"""Test that enable_components stores a rule dict in session state."""
|
||||
|
|
@ -70,7 +79,7 @@ class TestSessionVisibility:
|
|||
assert rules[0]["tags"] == ["finance"]
|
||||
return "activated"
|
||||
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("activate_finance", {})
|
||||
assert result.data == "activated"
|
||||
|
||||
|
|
@ -94,7 +103,7 @@ class TestSessionVisibility:
|
|||
assert rules[0]["tags"] == ["internal"]
|
||||
return "deactivated"
|
||||
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("deactivate_internal", {})
|
||||
assert result.data == "deactivated"
|
||||
|
||||
|
|
@ -402,7 +411,7 @@ class TestSessionVisibilityNotifications:
|
|||
return "activated"
|
||||
|
||||
handler = RecordingMessageHandler()
|
||||
async with Client(mcp, mode="legacy", message_handler=handler) as client:
|
||||
async with Client(mcp, message_handler=handler) as client:
|
||||
handler.reset()
|
||||
await client.call_tool("activate", {})
|
||||
|
||||
|
|
@ -432,7 +441,7 @@ class TestSessionVisibilityNotifications:
|
|||
return "deactivated"
|
||||
|
||||
handler = RecordingMessageHandler()
|
||||
async with Client(mcp, mode="legacy", message_handler=handler) as client:
|
||||
async with Client(mcp, message_handler=handler) as client:
|
||||
handler.reset()
|
||||
await client.call_tool("deactivate", {})
|
||||
|
||||
|
|
@ -461,7 +470,7 @@ class TestSessionVisibilityNotifications:
|
|||
return "cleared"
|
||||
|
||||
handler = RecordingMessageHandler()
|
||||
async with Client(mcp, mode="legacy", message_handler=handler) as client:
|
||||
async with Client(mcp, message_handler=handler) as client:
|
||||
handler.reset()
|
||||
await client.call_tool("clear", {})
|
||||
|
||||
|
|
@ -491,7 +500,7 @@ class TestSessionVisibilityNotifications:
|
|||
return "activated"
|
||||
|
||||
handler = RecordingMessageHandler()
|
||||
async with Client(mcp, mode="legacy", message_handler=handler) as client:
|
||||
async with Client(mcp, message_handler=handler) as client:
|
||||
handler.reset()
|
||||
await client.call_tool("activate_tools_only", {})
|
||||
|
||||
|
|
|
|||
|
|
@ -229,6 +229,8 @@ async def test_task_execution_auto_populated_for_task_enabled_tool():
|
|||
"""A tool that runs in background."""
|
||||
return f"Processed: {data}"
|
||||
|
||||
# `execution.task_support` (SEP-1686) is advertised in the handshake-era
|
||||
# tool listing only; the modern listing omits it.
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
tools_result = await client.list_tools()
|
||||
assert len(tools_result) == 1
|
||||
|
|
|
|||
|
|
@ -188,6 +188,9 @@ class TestBaseTransformBehavior:
|
|||
await ctx.disable_components(names={"delete_record"})
|
||||
return "disabled"
|
||||
|
||||
# Session visibility rules only persist across requests on the
|
||||
# handshake era (see `test_session_visibility.py`); the modern
|
||||
# protocol version has no session for them to persist in.
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
# Before disabling, search should find delete_record
|
||||
result = await client.call_tool("search_tools", {"pattern": "delete"})
|
||||
|
|
|
|||
|
|
@ -417,15 +417,15 @@ class TestExtensionAdvertisement:
|
|||
experimental_capabilities={"file_exchange": {"version": "0.3"}},
|
||||
)
|
||||
|
||||
async with Client(server, mode="legacy") as client:
|
||||
experimental = client.initialize_result.capabilities.experimental or {}
|
||||
async with Client(server) as client:
|
||||
experimental = client.server_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, mode="legacy") as client:
|
||||
experimental = client.initialize_result.capabilities.experimental
|
||||
async with Client(server) as client:
|
||||
experimental = client.server_capabilities.experimental
|
||||
assert not experimental
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@ class TestClientBehaviorCompat:
|
|||
assert result.data == "hi"
|
||||
|
||||
async def test_ping_returns_bool(self, server):
|
||||
# `ping` only exists on the older protocol, so this pins that era.
|
||||
client = Client(transport=FastMCPTransport(server), mode="legacy")
|
||||
async with client:
|
||||
result = await client.ping()
|
||||
|
|
|
|||
|
|
@ -753,7 +753,7 @@ class TestProxy:
|
|||
)
|
||||
proxy_server.add_tool(new_add_tool)
|
||||
|
||||
async with Client(proxy_server, mode="legacy") as client:
|
||||
async with Client(proxy_server) 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