fix: restore dict-at-boundary MCP source API for SDK v2
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user