diff --git a/src/wf_sources_mcp/client/source_client.py b/src/wf_sources_mcp/client/source_client.py index dd904b4f..d3f9d2f1 100644 --- a/src/wf_sources_mcp/client/source_client.py +++ b/src/wf_sources_mcp/client/source_client.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Protocol -from mcp import ClientResult from mcp.types import ( CallToolResult, ClientNotification, @@ -13,8 +12,12 @@ from mcp.types import ( ListResourcesResult, ListToolsResult, ReadResourceResult, + ServerResult, + client_notification_adapter, + client_request_adapter, + server_result_adapter, ) -from pydantic import AnyUrl +from pydantic import TypeAdapter from wf_sources_mcp.connections import McpSourceConnection @@ -43,7 +46,7 @@ class McpClientSession(Protocol): async def list_prompts(self) -> ListPromptsResult: ... - async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: ... + async def read_resource(self, uri: str) -> ReadResourceResult: ... async def get_prompt( self, @@ -56,8 +59,8 @@ class McpClientSession(Protocol): async def send_request( self, request: ClientRequest, - result_type: type[ClientResult], - ) -> ClientResult: ... + result_type: type[ServerResult] | TypeAdapter[ServerResult], + ) -> ServerResult: ... async def send_notification(self, notification: ClientNotification) -> None: ... @@ -118,7 +121,7 @@ class McpSourceClient: } async def read_resource(self, uri: str) -> dict[str, Any]: - result = await self.session.read_resource(AnyUrl(uri)) + result = await self.session.read_resource(str(uri)) return result.model_dump(by_alias=True, mode="json", exclude_none=True) async def get_prompt( @@ -135,8 +138,10 @@ class McpSourceClient: params: dict[str, Any] | None = None, ) -> dict[str, Any]: result = await self.session.send_request( - ClientRequest.model_validate({"method": method, "params": params}), - ClientResult, + client_request_adapter.validate_python( + {"method": method, "params": params} + ), + server_result_adapter, ) return result.model_dump(by_alias=True, mode="json", exclude_none=True) @@ -146,7 +151,9 @@ class McpSourceClient: params: dict[str, Any] | None = None, ) -> None: await self.session.send_notification( - ClientNotification.model_validate({"method": method, "params": params}) + client_notification_adapter.validate_python( + {"method": method, "params": params} + ) ) async def call_tool( diff --git a/src/wf_sources_mcp/runtime/session.py b/src/wf_sources_mcp/runtime/session.py index 946ef671..1675d1ec 100644 --- a/src/wf_sources_mcp/runtime/session.py +++ b/src/wf_sources_mcp/runtime/session.py @@ -5,7 +5,6 @@ from dataclasses import dataclass from typing import Any from mcp.client.session import ClientSession -from pydantic import AnyUrl from wf_sources_mcp.auth import AuthRecord from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool @@ -66,7 +65,7 @@ class PersistentMcpSession: if self.read_resource_callback is not None: return await self.read_resource_callback(uri) if self.client is not None: - result = await self.client.read_resource(AnyUrl(uri)) + result = await self.client.read_resource(str(uri)) return result.model_dump(by_alias=True, mode="json", exclude_none=True) raise RuntimeError("persistent MCP session has no resource read transport") @@ -135,12 +134,13 @@ class PersistentMcpSession: if self.invoke_method_callback is not None: return await self.invoke_method_callback(method, params) if self.client is not None: - from mcp import ClientResult - from mcp.types import ClientRequest + from mcp.types import client_request_adapter, server_result_adapter result = await self.client.send_request( - ClientRequest.model_validate({"method": method, "params": params}), - ClientResult, + client_request_adapter.validate_python( + {"method": method, "params": params} + ), + server_result_adapter, ) return result.model_dump(by_alias=True, mode="json", exclude_none=True) raise RuntimeError("persistent MCP session has no method invoke transport") @@ -155,10 +155,12 @@ class PersistentMcpSession: await self.send_notification_callback(method, params) return if self.client is not None: - from mcp.types import ClientNotification + from mcp.types import client_notification_adapter await self.client.send_notification( - ClientNotification.model_validate({"method": method, "params": params}) + client_notification_adapter.validate_python( + {"method": method, "params": params} + ) ) return raise RuntimeError("persistent MCP session has no notification send transport") diff --git a/tests/wf_mcp/test_sdk_adapter.py b/tests/wf_mcp/test_sdk_adapter.py index 0362761c..1037ab18 100644 --- a/tests/wf_mcp/test_sdk_adapter.py +++ b/tests/wf_mcp/test_sdk_adapter.py @@ -3,7 +3,7 @@ from __future__ import annotations from pathlib import Path import pytest -from mcp import McpError +from mcp import MCPError from mcp.types import ErrorData from wf_mcp.broker import WfMcpService @@ -33,10 +33,10 @@ class _ToolsOnlyAdapter: ] async def list_resources(self, connection, auth): - raise McpError(ErrorData(code=-32601, message="Method not found")) + raise MCPError.from_error_data(ErrorData(code=-32601, message="Method not found")) async def list_prompts(self, connection, auth): - raise McpError(ErrorData(code=-32601, message="Method not found")) + raise MCPError.from_error_data(ErrorData(code=-32601, message="Method not found")) async def get_connection_metadata(self, connection, auth): return { @@ -65,7 +65,7 @@ class _WrappedToolsOnlyAdapter(_ToolsOnlyAdapter): async def list_resources(self, connection, auth): raise ExceptionGroup( "unhandled errors in a TaskGroup", - [McpError(ErrorData(code=-32601, message="Method not found"))], + [MCPError.from_error_data(ErrorData(code=-32601, message="Method not found"))], ) diff --git a/tests/wf_mcp/test_support.py b/tests/wf_mcp/test_support.py index ea81e623..b3cb4a96 100644 --- a/tests/wf_mcp/test_support.py +++ b/tests/wf_mcp/test_support.py @@ -50,10 +50,14 @@ def finalize_tool( @deprecated("Use pytests tmp_path fixture instead") @overload + + def local_temp_root() -> Path: ... @overload + + def local_temp_root(root_path: Path) -> Path: ... diff --git a/tests/wf_sources_mcp/test_runtime.py b/tests/wf_sources_mcp/test_runtime.py index d05aa258..2267c62d 100644 --- a/tests/wf_sources_mcp/test_runtime.py +++ b/tests/wf_sources_mcp/test_runtime.py @@ -5,7 +5,7 @@ from contextlib import AsyncExitStack from typing import Any import pytest -from mcp import ClientResult +from mcp import GetPromptResult, ReadResourceResult, ServerResult from mcp.client.session import ClientSession from mcp.types import CallToolResult as RawCallToolResult from mcp.types import ( @@ -18,8 +18,9 @@ from mcp.types import ( Resource, TextContent, Tool, + server_result_adapter, ) -from pydantic import AnyUrl +from pydantic import AnyUrl, TypeAdapter from wf_sources_mcp.auth import AuthRecord from wf_sources_mcp.connections import McpSourceConnection @@ -96,7 +97,7 @@ class _FakeFactory(PersistentSessionFactory): self.calls.append((tool_name, payload)) return RawCallToolResult( content=[TextContent(type="text", text="ok")], - structuredContent={"echoed": payload["text"]}, + structured_content={"echoed": payload["text"]}, ) async def _create_with_stack( @@ -115,48 +116,38 @@ class _FakeFactory(PersistentSessionFactory): return await factory._call_tool(tool_name, payload) async def read_resource(self, uri: AnyUrl): - return type( - "ReadResourceResult", - (), - { - "model_dump": lambda _self, **_kwargs: { - "contents": [{"uri": str(uri), "text": "resource text"}] - } - }, - )() + return ReadResourceResult.model_validate( + {"contents": [{"uri": str(uri), "text": "resource text"}]} + ) async def get_prompt( self, prompt_name: str, arguments: dict[str, str] | None = None, - ): - return type( - "GetPromptResult", - (), + ) -> GetPromptResult: + return GetPromptResult.model_validate( { - "model_dump": lambda _self, **_kwargs: { - "messages": [ - { - "role": "user", - "content": { - "type": "text", - "text": f"{prompt_name}:{arguments or {}}", - }, - } - ] - } - }, - )() + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": f"{prompt_name}:{arguments or {}}", + }, + } + ] + } + ) async def list_resources(self) -> ListResourcesResult: return ListResourcesResult( resources=[ Resource( - uri=AnyUrl("fixture://docs/runtime"), + uri=("fixture://docs/runtime"), name="resource.runtime", title="Runtime Resource", description="Runtime-scoped resource.", - mimeType="text/plain", + mime_type="text/plain", ) ] ) @@ -180,7 +171,7 @@ class _FakeFactory(PersistentSessionFactory): name="tool.runtime", title="Runtime Tool", description="Runtime-scoped tool.", - inputSchema={"type": "object"}, + input_schema={"type": "object"}, ) ] ) @@ -205,9 +196,9 @@ class _FakeFactory(PersistentSessionFactory): async def send_request( self, request: ClientRequest, - result_type: type[ClientResult], - ) -> ClientResult: - return ClientResult.model_validate( + result_type: type[ServerResult], + ) -> ServerResult: + return server_result_adapter.validate_python( {"jsonrpc": "2.0", "id": 1, "result": {}} ) @@ -360,11 +351,9 @@ async def test_persistent_session_factory_routes_resource_reads_through_owner() assert factory.created_connections == [connection] assert factory.calls == [("echo", {"text": "one"})] - assert resource_payload == { - "contents": [ - {"uri": "fixture://docs/welcome", "text": "resource text"}, - ] - } + assert resource_payload["contents"] == [ + {"uri": "fixture://docs/welcome", "text": "resource text"} + ] @pytest.mark.asyncio @@ -564,7 +553,7 @@ async def test_persistent_session_list_tools_client_fallback() -> None: Tool( name="client_tool", description="Client tool", - inputSchema={"type": "object"}, + input_schema={"type": "object"}, ) ] ) @@ -582,17 +571,13 @@ async def test_persistent_session_list_tools_client_fallback() -> None: @pytest.mark.asyncio async def test_persistent_session_invoke_method_client_fallback() -> None: - from mcp import ClientResult - class _MinimalClient: async def send_request( self, request: ClientRequest, - result_type: type[ClientResult], - ) -> ClientResult: - return ClientResult.model_validate( - {"jsonrpc": "2.0", "id": 1, "result": {"tools": []}} - ) + result_type: type[ServerResult] | TypeAdapter[ServerResult], + ) -> ServerResult: + return server_result_adapter.validate_python({"tools": []}) session = PersistentMcpSession( connection=_connection(), @@ -601,8 +586,7 @@ async def test_persistent_session_invoke_method_client_fallback() -> None: ) result = await session.invoke_method("tools/list") - - assert result["result"]["tools"] == [] + assert result["tools"] == [] @pytest.mark.asyncio diff --git a/tests/wf_sources_mcp/test_sdk_adapter.py b/tests/wf_sources_mcp/test_sdk_adapter.py index e99b05da..0b7df355 100644 --- a/tests/wf_sources_mcp/test_sdk_adapter.py +++ b/tests/wf_sources_mcp/test_sdk_adapter.py @@ -5,7 +5,7 @@ from contextlib import asynccontextmanager from typing import Any import pytest -from mcp import ClientResult +from mcp import GetPromptResult, ReadResourceResult, ServerResult from mcp.types import ( CallToolResult, ClientNotification, @@ -18,7 +18,7 @@ from mcp.types import ( TextContent, Tool, ) -from pydantic import AnyUrl +from pydantic import TypeAdapter from wf_sources_mcp.client import McpSourceClient from wf_sources_mcp.connections import McpSourceConnection @@ -47,7 +47,7 @@ class _FakeSession: name="echo", title="Echo", description="Echo text.", - inputSchema={"type": "object", "properties": {}}, + input_schema={"type": "object", "properties": {}}, ) ] ) @@ -56,11 +56,11 @@ class _FakeSession: return ListResourcesResult( resources=[ Resource( - uri=AnyUrl("fixture://docs/welcome"), + uri="fixture://docs/welcome", name="resource.welcome", title="Welcome", description="Welcome resource.", - mimeType="text/plain", + mime_type="text/plain", ) ] ) @@ -77,49 +77,39 @@ class _FakeSession: ] ) - async def read_resource(self, uri: AnyUrl) -> Any: - return type( - "ReadResourceResult", - (), - { - "model_dump": lambda _self, **_kwargs: { - "contents": [{"uri": str(uri), "text": "hello"}] - } - }, - )() + async def read_resource(self, uri: str) -> ReadResourceResult: + return ReadResourceResult.model_validate( + {"contents": [{"uri": str(uri), "text": "hello"}]} + ) async def get_prompt( self, prompt_name: str, arguments: dict[str, str] | None = None, - ) -> Any: - return type( - "GetPromptResult", - (), + ) -> GetPromptResult: + return GetPromptResult.model_validate( { - "model_dump": lambda _self, **_kwargs: { - "messages": [ - { - "role": "user", - "content": { - "type": "text", - "text": f"{prompt_name}:{arguments or {}}", - }, - } - ] - } - }, - )() + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": f"{prompt_name}:{arguments or {}}", + }, + } + ] + } + ) async def send_request( self, request: ClientRequest, - result_type: type[ClientResult], + result_type: type[ServerResult] | TypeAdapter[ServerResult], ) -> Any: - assert result_type is ClientResult + assert isinstance(result_type, TypeAdapter) self.requests.append(request) return type( - "ClientResultModel", + "ServerResultModel", (), {"model_dump": lambda _self, **_kwargs: {"ok": True}}, )() @@ -134,7 +124,7 @@ class _FakeSession: ) -> CallToolResult: return CallToolResult( content=[TextContent(type="text", text="ok")], - structuredContent={"tool": tool_name, "payload": payload}, + structured_content={"tool": tool_name, "payload": payload}, ) diff --git a/tests/wf_sources_mcp/test_source_client.py b/tests/wf_sources_mcp/test_source_client.py index 3d9814d5..ee74907f 100644 --- a/tests/wf_sources_mcp/test_source_client.py +++ b/tests/wf_sources_mcp/test_source_client.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any import pytest -from mcp import ClientResult +from mcp import GetPromptResult, ReadResourceResult, ServerResult from mcp.types import ( CallToolResult, ClientNotification, @@ -16,7 +16,7 @@ from mcp.types import ( TextContent, Tool, ) -from pydantic import AnyUrl +from pydantic import TypeAdapter from wf_sources_mcp.client import McpSourceClient from wf_sources_mcp.connections import McpSourceConnection @@ -44,7 +44,7 @@ class _FakeSession: name="echo", title="Echo", description="Echo text.", - inputSchema={"type": "object", "properties": {}}, + input_schema={"type": "object", "properties": {}}, ) ] ) @@ -53,11 +53,11 @@ class _FakeSession: return ListResourcesResult( resources=[ Resource( - uri=AnyUrl("fixture://docs/welcome"), + uri=("fixture://docs/welcome"), name="resource.welcome", title="Welcome", description="Welcome resource.", - mimeType="text/plain", + mime_type="text/plain", ) ] ) @@ -74,46 +74,37 @@ class _FakeSession: ] ) - async def read_resource(self, uri: AnyUrl) -> Any: - return type( - "ReadResourceResult", - (), - { - "model_dump": lambda _self, **_kwargs: { - "contents": [{"uri": str(uri), "text": "hello"}] - } - }, - )() + # TODO investigate why these are uses type/3 for object creation + async def read_resource(self, uri: str) -> ReadResourceResult: + return ReadResourceResult.model_validate( + {"contents": [{"uri": str(uri), "text": "hello"}]} + ) async def get_prompt( self, prompt_name: str, arguments: dict[str, str] | None = None, - ) -> Any: - return type( - "GetPromptResult", - (), + ) -> GetPromptResult: + return GetPromptResult.model_validate( { - "model_dump": lambda _self, **_kwargs: { - "messages": [ - { - "role": "user", - "content": { - "type": "text", - "text": f"{prompt_name}:{arguments or {}}", - }, - } - ] - } - }, - )() + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": f"{prompt_name}:{arguments or {}}", + }, + } + ] + } + ) async def send_request( self, request: ClientRequest, - result_type: type[ClientResult], + result_type: type[ServerResult] | TypeAdapter[ServerResult], ) -> Any: - assert result_type is ClientResult + assert isinstance(result_type, TypeAdapter) self.requests.append(request) return type( "ClientResultModel", @@ -131,7 +122,7 @@ class _FakeSession: ) -> CallToolResult: return CallToolResult( content=[TextContent(type="text", text="ok")], - structuredContent={"tool": tool_name, "payload": payload}, + structured_content={"tool": tool_name, "payload": payload}, )