diff --git a/fastmcp_slim/fastmcp/client/mixins/prompts.py b/fastmcp_slim/fastmcp/client/mixins/prompts.py index 9fb4db2f0..21f234777 100644 --- a/fastmcp_slim/fastmcp/client/mixins/prompts.py +++ b/fastmcp_slim/fastmcp/client/mixins/prompts.py @@ -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 diff --git a/fastmcp_slim/fastmcp/client/mixins/resources.py b/fastmcp_slim/fastmcp/client/mixins/resources.py index 36099caee..850fd3192 100644 --- a/fastmcp_slim/fastmcp/client/mixins/resources.py +++ b/fastmcp_slim/fastmcp/client/mixins/resources.py @@ -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 ) diff --git a/fastmcp_slim/fastmcp/server/middleware/authorization.py b/fastmcp_slim/fastmcp/server/middleware/authorization.py index 41b45e1b6..2040778e3 100644 --- a/fastmcp_slim/fastmcp/server/middleware/authorization.py +++ b/fastmcp_slim/fastmcp/server/middleware/authorization.py @@ -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 diff --git a/fastmcp_slim/fastmcp/server/server.py b/fastmcp_slim/fastmcp/server/server.py index febdba2c0..2580e729d 100644 --- a/fastmcp_slim/fastmcp/server/server.py +++ b/fastmcp_slim/fastmcp/server/server.py @@ -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" diff --git a/tests/client/tasks/test_client_resource_tasks.py b/tests/client/tasks/test_client_resource_tasks.py index be7c4ddd3..0dda0366f 100644 --- a/tests/client/tasks/test_client_resource_tasks.py +++ b/tests/client/tasks/test_client_resource_tasks.py @@ -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: