mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 14:34:17 +02:00
Migrate _meta to plain dict (v2), normalize resource task URIs, xfail wire-unexpressible resource tasks
This commit is contained in:
parent
0b7bcdcf85
commit
ee6ddcc4e8
5 changed files with 48 additions and 22 deletions
|
|
@ -280,9 +280,14 @@ class ClientPromptsMixin:
|
|||
PromptTask: Future-like object for accessing task status and results
|
||||
"""
|
||||
# Per SEP-1686 final spec: client sends only ttl, server generates taskId
|
||||
# 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.RequestParams.Meta | None, propagated_meta)
|
||||
request_meta = cast(
|
||||
"mcp_types.RequestParamsMeta | None",
|
||||
propagated_meta if propagated_meta else None,
|
||||
)
|
||||
|
||||
# Serialize arguments for MCP protocol
|
||||
serialized_arguments: dict[str, str] | None = None
|
||||
|
|
|
|||
|
|
@ -213,7 +213,10 @@ class ClientResourcesMixin:
|
|||
RuntimeError: If called while the client is not connected.
|
||||
MCPError: If the request results in a TimeoutError | JSONRPCError
|
||||
"""
|
||||
uri_str = str(uri)
|
||||
# SDK v2: the wire `uri` is a plain string, but resources are stored
|
||||
# under the AnyUrl-normalized form (e.g. a trailing slash for authority
|
||||
# URIs), so normalize through AnyUrl to keep server-side lookups aligned.
|
||||
uri_str = str(AnyUrl(uri)) if isinstance(uri, str) else str(uri)
|
||||
with client_span(
|
||||
"resources/read",
|
||||
"resources/read",
|
||||
|
|
@ -223,9 +226,6 @@ class ClientResourcesMixin:
|
|||
):
|
||||
logger.debug(f"[{self.name}] called read_resource: {uri}")
|
||||
|
||||
if isinstance(uri, str):
|
||||
uri = AnyUrl(uri) # Ensure AnyUrl
|
||||
|
||||
# Inject trace context into meta for propagation to server
|
||||
propagated_meta = inject_trace_context(meta)
|
||||
request_meta = cast("mcp_types.RequestParamsMeta | None", propagated_meta)
|
||||
|
|
@ -233,9 +233,10 @@ class ClientResourcesMixin:
|
|||
# If meta provided, use send_request for SEP-1686 task support
|
||||
if propagated_meta:
|
||||
task_dict = propagated_meta.get("modelcontextprotocol.io/task")
|
||||
# SDK v2: ReadResourceRequestParams.uri is a plain string.
|
||||
request = mcp_types.ReadResourceRequest(
|
||||
params=mcp_types.ReadResourceRequestParams(
|
||||
uri=uri,
|
||||
uri=uri_str,
|
||||
task=mcp_types.TaskMetadata(**task_dict) if task_dict else None,
|
||||
_meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
|
||||
)
|
||||
|
|
@ -248,7 +249,7 @@ class ClientResourcesMixin:
|
|||
)
|
||||
else:
|
||||
result = await self._await_with_session_monitoring(
|
||||
self.session.read_resource(str(uri))
|
||||
self.session.read_resource(uri_str)
|
||||
)
|
||||
return result
|
||||
|
||||
|
|
@ -349,16 +350,22 @@ class ClientResourcesMixin:
|
|||
ResourceTask: Future-like object for accessing task status and results
|
||||
"""
|
||||
# Per SEP-1686 final spec: client sends only ttl, server generates taskId
|
||||
# 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.RequestParams.Meta | None, propagated_meta)
|
||||
request_meta = cast(
|
||||
"mcp_types.RequestParamsMeta | None",
|
||||
propagated_meta if propagated_meta else None,
|
||||
)
|
||||
|
||||
if isinstance(uri, str):
|
||||
uri = AnyUrl(uri)
|
||||
# SDK v2: ReadResourceRequestParams.uri is a plain string, but resources
|
||||
# are stored under the AnyUrl-normalized form, so normalize to match.
|
||||
uri_str = str(AnyUrl(uri)) if isinstance(uri, str) else str(uri)
|
||||
|
||||
request = mcp_types.ReadResourceRequest(
|
||||
params=mcp_types.ReadResourceRequestParams(
|
||||
uri=uri,
|
||||
uri=uri_str,
|
||||
task=mcp_types.TaskMetadata(ttl=ttl),
|
||||
_meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ Example:
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
|
||||
import mcp_types as mt
|
||||
|
||||
|
|
@ -49,12 +50,13 @@ from fastmcp.utilities.versions import VersionSpec
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _requested_version(meta: mt.RequestParams.Meta | None) -> VersionSpec | None:
|
||||
if meta is None:
|
||||
def _requested_version(meta: Mapping[str, Any] | None) -> VersionSpec | None:
|
||||
# SDK v2: request `_meta` is a plain dict (the `Meta` type alias), not the
|
||||
# old `RequestParams.Meta` nested model.
|
||||
if not meta:
|
||||
return None
|
||||
|
||||
meta_dict = meta.model_dump(exclude_none=True)
|
||||
fastmcp_meta = meta_dict.get("fastmcp")
|
||||
fastmcp_meta = meta.get("fastmcp")
|
||||
if not isinstance(fastmcp_meta, dict):
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ logger = get_logger(__name__)
|
|||
|
||||
def _version_request_meta(
|
||||
version: VersionSpec | None,
|
||||
) -> mcp_types.RequestParams.Meta | None:
|
||||
) -> dict[str, Any] | None:
|
||||
if version is None:
|
||||
return None
|
||||
|
||||
|
|
@ -122,9 +122,9 @@ def _version_request_meta(
|
|||
if not version_value:
|
||||
return None
|
||||
|
||||
return mcp_types.RequestParams.Meta.model_validate(
|
||||
{"fastmcp": {"version": version_value}}
|
||||
)
|
||||
# SDK v2: request `_meta` is a plain dict (the `Meta` type alias), not the
|
||||
# old `RequestParams.Meta` nested model.
|
||||
return {"fastmcp": {"version": version_value}}
|
||||
|
||||
|
||||
# The MCP SDK warns "Tool X not listed, no validation will be performed"
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ async def test_resource_task_server_generated_id(resource_server):
|
|||
assert "-" in task.task_id
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="SDK v2 has no `task` field on ReadResourceRequestParams, so "
|
||||
"resource reads cannot be submitted as background tasks over the wire and "
|
||||
"always graceful-degrade to immediate execution (sdk-feedback #3).",
|
||||
strict=True,
|
||||
)
|
||||
async def test_resource_task_result_returns_read_resource_result(resource_server):
|
||||
"""ResourceTask.result() returns list of ReadResourceContents."""
|
||||
async with Client(resource_server) as client:
|
||||
|
|
@ -77,6 +83,12 @@ async def test_resource_task_await_syntax(resource_server):
|
|||
assert result[0].text == "Document content here"
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="SDK v2 has no `task` field on ReadResourceRequestParams, so "
|
||||
"resource reads cannot be submitted as background tasks over the wire and "
|
||||
"always graceful-degrade to immediate execution (sdk-feedback #3).",
|
||||
strict=True,
|
||||
)
|
||||
async def test_resource_template_task(resource_server):
|
||||
"""Resource templates work with task support."""
|
||||
async with Client(resource_server) as client:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue