mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
Fix client meta forwarding, ClientNotification construction, and v1 test idioms for SDK v2
This commit is contained in:
parent
59234364dd
commit
7389e294af
6 changed files with 60 additions and 18 deletions
|
|
@ -809,14 +809,12 @@ class Client(
|
|||
reason: str | None = None,
|
||||
) -> None:
|
||||
"""Send a cancellation notification for an in-progress request."""
|
||||
notification = mcp_types.ClientNotification(
|
||||
root=mcp_types.CancelledNotification(
|
||||
method="notifications/cancelled",
|
||||
params=mcp_types.CancelledNotificationParams(
|
||||
request_id=request_id,
|
||||
reason=reason,
|
||||
),
|
||||
)
|
||||
notification = mcp_types.CancelledNotification(
|
||||
method="notifications/cancelled",
|
||||
params=mcp_types.CancelledNotificationParams(
|
||||
request_id=request_id,
|
||||
reason=reason,
|
||||
),
|
||||
)
|
||||
await self.session.send_notification(notification)
|
||||
|
||||
|
|
|
|||
|
|
@ -160,8 +160,14 @@ class ClientToolsMixin:
|
|||
) as span:
|
||||
logger.debug(f"[{self.name}] called call_tool: {name}")
|
||||
|
||||
# Inject trace context into meta for propagation to server
|
||||
# Inject trace context into meta for propagation to server.
|
||||
# SDK v2: request `_meta` is `RequestParamsMeta` (a TypedDict), not
|
||||
# the old `RequestParams.Meta` nested model.
|
||||
propagated_meta = inject_trace_context(meta)
|
||||
request_meta = cast(
|
||||
"mcp_types.RequestParamsMeta | None",
|
||||
propagated_meta if propagated_meta else None,
|
||||
)
|
||||
|
||||
result = await self._await_with_session_monitoring(
|
||||
self.session.call_tool(
|
||||
|
|
@ -169,7 +175,7 @@ class ClientToolsMixin:
|
|||
arguments=arguments,
|
||||
read_timeout_seconds=normalize_timeout_to_seconds(timeout),
|
||||
progress_callback=progress_handler or self._progress_handler,
|
||||
meta=propagated_meta if propagated_meta else None,
|
||||
meta=request_meta,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -365,7 +371,7 @@ class ClientToolsMixin:
|
|||
# Use RootModel with Union to handle both response types (SDK calls model_validate)
|
||||
wrapped_result = await self._await_with_session_monitoring(
|
||||
self.session.send_request(
|
||||
request=request, # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
||||
request=request, # type: ignore[arg-type]
|
||||
result_type=ToolTaskResponseUnion,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -94,12 +94,13 @@ async def test_call_tool_with_meta():
|
|||
assert context.request_context is not None
|
||||
meta = context.request_context.meta
|
||||
|
||||
# Return the metadata as a dict
|
||||
# Return the metadata as a dict. Under SDK v2 the lifted request meta
|
||||
# is a plain dict, so custom fields are read by key.
|
||||
if meta is not None:
|
||||
return {
|
||||
"has_meta": True,
|
||||
"user_id": getattr(meta, "user_id", None),
|
||||
"trace_id": getattr(meta, "trace_id", None),
|
||||
"user_id": meta.get("user_id"),
|
||||
"trace_id": meta.get("trace_id"),
|
||||
}
|
||||
return {"has_meta": False}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ from mcp_types import (
|
|||
ToolResultContent,
|
||||
ToolUseContent,
|
||||
)
|
||||
from pydantic import AnyUrl
|
||||
|
||||
from fastmcp.client.sampling.handlers.anthropic import (
|
||||
AnthropicSamplingHandler,
|
||||
|
|
@ -387,7 +386,7 @@ def test_convert_messages_raises_on_unsupported_content_type():
|
|||
embedded = EmbeddedResource(
|
||||
type="resource",
|
||||
resource=TextResourceContents(
|
||||
uri=AnyUrl("file:///test.txt"), text="hello", mime_type="text/plain"
|
||||
uri="file:///test.txt", text="hello", mime_type="text/plain"
|
||||
),
|
||||
)
|
||||
# Must be inside a list content — single-content messages hit a
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ from openai.types.chat import (
|
|||
ChatCompletionUserMessageParam,
|
||||
)
|
||||
from openai.types.chat.chat_completion import Choice
|
||||
from pydantic import AnyUrl
|
||||
|
||||
from fastmcp.client.sampling.handlers.openai import (
|
||||
OpenAISamplingHandler,
|
||||
|
|
@ -331,7 +330,7 @@ def test_convert_messages_raises_on_unsupported_content_type():
|
|||
embedded = EmbeddedResource(
|
||||
type="resource",
|
||||
resource=TextResourceContents(
|
||||
uri=AnyUrl("file:///test.txt"), text="hello", mime_type="text/plain"
|
||||
uri="file:///test.txt", text="hello", mime_type="text/plain"
|
||||
),
|
||||
)
|
||||
msg = SamplingMessage.model_construct(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Tests for the MCP SDK v2 camelCase compatibility bridge (fastmcp._compat)."""
|
||||
|
||||
import datetime
|
||||
import warnings
|
||||
|
||||
import mcp_types
|
||||
|
|
@ -7,6 +8,8 @@ import pytest
|
|||
from mcp import MCPError as SDKMCPError
|
||||
|
||||
import fastmcp._compat as _compat
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.client.transports import FastMCPTransport
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning, MCPError, McpError
|
||||
|
||||
|
||||
|
|
@ -184,3 +187,39 @@ class TestExceptionAlias:
|
|||
def test_except_mcp_error_catches_sdk_raised(self):
|
||||
with pytest.raises(McpError):
|
||||
raise SDKMCPError(code=-32000, message="boom")
|
||||
|
||||
|
||||
class TestClientBehaviorCompat:
|
||||
"""Behavior compat checklist (design decision D)."""
|
||||
|
||||
@pytest.fixture
|
||||
def server(self):
|
||||
srv = FastMCP("BehaviorServer")
|
||||
|
||||
@srv.tool
|
||||
def echo(x: str) -> str:
|
||||
return x
|
||||
|
||||
return srv
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"timeout",
|
||||
[5, 5.0, datetime.timedelta(seconds=5), None],
|
||||
)
|
||||
async def test_client_accepts_timedelta_or_float_timeout(self, server, timeout):
|
||||
client = Client(transport=FastMCPTransport(server), timeout=timeout)
|
||||
async with client:
|
||||
result = await client.call_tool("echo", {"x": "hi"})
|
||||
assert result.data == "hi"
|
||||
|
||||
async def test_ping_returns_bool(self, server):
|
||||
client = Client(transport=FastMCPTransport(server))
|
||||
async with client:
|
||||
result = await client.ping()
|
||||
assert result is True
|
||||
|
||||
async def test_session_id_none_safe(self, server):
|
||||
# In-memory transport has no HTTP session id; must return None, not raise.
|
||||
client = Client(transport=FastMCPTransport(server))
|
||||
async with client:
|
||||
assert client.transport.get_session_id() is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue