Migrate _meta to plain dict (v2), normalize resource task URIs, xfail wire-unexpressible resource tasks

This commit is contained in:
Jeremiah Lowin 2026-07-05 22:17:02 -04:00
commit ee6ddcc4e8
No known key found for this signature in database
5 changed files with 48 additions and 22 deletions

View file

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

View file

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

View file

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

View file

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

View file

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