222 lines
7.6 KiB
Python
222 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from mcp import MCPError
|
|
from mcp.types import ErrorData
|
|
|
|
from wf_mcp.broker import WfMcpService
|
|
from wf_mcp.capabilities import DiscoveredTool
|
|
from wf_mcp.models import ConnectionConfig
|
|
from wf_mcp.sdk import McpSdkAdapter
|
|
from wf_mcp.storage import FileStore
|
|
from wf_sources_mcp.connections import mcp_source_connection_from_connection_config
|
|
|
|
from .test_support import (
|
|
everything_server_connection,
|
|
fixture_server_path,
|
|
sys,
|
|
)
|
|
|
|
|
|
class _ToolsOnlyAdapter:
|
|
async def list_tools(self, connection, auth):
|
|
return [
|
|
DiscoveredTool(
|
|
name="echo_tool",
|
|
title="Echo",
|
|
description="Echo text.",
|
|
input_schema={"type": "object", "properties": {}},
|
|
output_schema={"type": "object", "properties": {}},
|
|
)
|
|
]
|
|
|
|
async def list_resources(self, connection, auth):
|
|
raise MCPError.from_error_data(ErrorData(code=-32601, message="Method not found"))
|
|
|
|
async def list_prompts(self, connection, auth):
|
|
raise MCPError.from_error_data(ErrorData(code=-32601, message="Method not found"))
|
|
|
|
async def get_connection_metadata(self, connection, auth):
|
|
return {
|
|
"server": getattr(
|
|
connection, "provider", getattr(connection, "server", None)
|
|
)
|
|
}
|
|
|
|
async def read_resource(self, connection, auth, uri):
|
|
raise NotImplementedError
|
|
|
|
async def get_prompt(self, connection, auth, prompt_name, arguments=None):
|
|
raise NotImplementedError
|
|
|
|
async def invoke_method(self, connection, auth, method, params=None):
|
|
raise NotImplementedError
|
|
|
|
async def send_notification(self, connection, auth, method, params=None):
|
|
raise NotImplementedError
|
|
|
|
async def call_tool(self, connection, auth, tool_name, payload):
|
|
raise NotImplementedError
|
|
|
|
|
|
class _WrappedToolsOnlyAdapter(_ToolsOnlyAdapter):
|
|
async def list_resources(self, connection, auth):
|
|
raise ExceptionGroup(
|
|
"unhandled errors in a TaskGroup",
|
|
[MCPError.from_error_data(ErrorData(code=-32601, message="Method not found"))],
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mcp_sdk_adapter_lists_and_calls_stdio_tool(tmp_path: Path) -> None:
|
|
service = WfMcpService(store=FileStore(tmp_path / "sdk_adapter_store"))
|
|
service.register_connection(
|
|
ConnectionConfig(
|
|
id="fixture.personal",
|
|
server="fixture",
|
|
account="personal",
|
|
metadata={
|
|
"transport": "stdio",
|
|
"command": sys.executable,
|
|
"args": [fixture_server_path()],
|
|
},
|
|
)
|
|
)
|
|
service.register_adapter("fixture", McpSdkAdapter())
|
|
|
|
try:
|
|
await service.refresh_connection_catalog("fixture.personal")
|
|
except PermissionError as exc:
|
|
pytest.skip(f"stdio MCP transport is not permitted in this environment: {exc}")
|
|
|
|
payload = service.get_catalog().as_payload()
|
|
assert payload["nodes"][0]["qualified_name"] == "fixture.personal.echo_tool"
|
|
assert payload["resources"] == [
|
|
{
|
|
"qualified_name": "fixture.personal.resource.welcome",
|
|
"connection_id": "fixture.personal",
|
|
"local_name": "resource.welcome",
|
|
"uri": "fixture://docs/welcome",
|
|
"title": "Resource Welcome",
|
|
"description": "Welcome text resource for fixture tests.",
|
|
"mime_type": "text/plain",
|
|
"metadata": payload["resources"][0]["metadata"],
|
|
}
|
|
]
|
|
assert payload["prompts"] == [
|
|
{
|
|
"qualified_name": "fixture.personal.prompt.summarize",
|
|
"connection_id": "fixture.personal",
|
|
"local_name": "prompt.summarize",
|
|
"title": "Prompt Summarize",
|
|
"description": "Summarize an input text for fixture tests.",
|
|
"arguments": payload["prompts"][0]["arguments"],
|
|
"metadata": payload["prompts"][0]["metadata"],
|
|
}
|
|
]
|
|
|
|
resource_result = await service.read_resource("fixture.personal.resource.welcome")
|
|
prompt_result = await service.render_prompt(
|
|
"fixture.personal.prompt.summarize",
|
|
arguments={"text": "hello"},
|
|
)
|
|
assert (
|
|
resource_result["contents"][0]["text"] == "Welcome from the fixture MCP server."
|
|
)
|
|
assert (
|
|
prompt_result["messages"][0]["content"]["text"]
|
|
== "Summarize this text:\n\nhello"
|
|
)
|
|
ping_result = await service.invoke_method("fixture.personal", "ping")
|
|
assert ping_result == {}
|
|
|
|
adapter = McpSdkAdapter()
|
|
try:
|
|
source_connection = mcp_source_connection_from_connection_config(
|
|
service.connections.get("fixture.personal")
|
|
)
|
|
result = await adapter.call_tool(
|
|
connection=source_connection,
|
|
auth=None,
|
|
tool_name="echo_tool",
|
|
payload={"text": "hello"},
|
|
)
|
|
except PermissionError as exc:
|
|
pytest.skip(f"stdio MCP transport is not permitted in this environment: {exc}")
|
|
assert result.outcome == "ok"
|
|
assert result.output == {"echoed": "hello"}
|
|
|
|
|
|
async def test_mcp_sdk_adapter_can_probe_everything_server(tmp_path: Path) -> None:
|
|
connection = everything_server_connection()
|
|
if connection is None:
|
|
pytest.skip(
|
|
"set MCP_EVERYTHING_COMMAND to enable the live everything-server integration test"
|
|
)
|
|
|
|
service = WfMcpService(store=FileStore(tmp_path / "everything_server_store"))
|
|
service.register_connection(connection)
|
|
service.register_adapter("everything", McpSdkAdapter())
|
|
|
|
try:
|
|
await service.refresh_connection_catalog("everything.default")
|
|
except PermissionError as exc:
|
|
pytest.skip(f"live MCP transport is not permitted in this environment: {exc}")
|
|
|
|
payload = service.get_catalog().as_payload()
|
|
assert payload["nodes"], "everything-server should expose at least one tool"
|
|
assert all(
|
|
node["qualified_name"].startswith("everything.default.")
|
|
for node in payload["nodes"]
|
|
)
|
|
assert "resources" in payload
|
|
assert "prompts" in payload
|
|
|
|
|
|
async def test_refresh_catalog_keeps_tools_when_optional_lists_are_unsupported(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service = WfMcpService(store=FileStore(tmp_path / "tools_only_server_store"))
|
|
service.register_connection(
|
|
ConnectionConfig(
|
|
id="tools_only.personal",
|
|
server="tools_only",
|
|
account="personal",
|
|
metadata={"transport": "stdio", "command": "fake-tools-only"},
|
|
)
|
|
)
|
|
service.register_adapter("tools_only", _ToolsOnlyAdapter())
|
|
|
|
await service.refresh_connection_catalog("tools_only.personal")
|
|
|
|
payload = service.get_catalog().as_payload()
|
|
assert payload["nodes"][0]["qualified_name"] == "tools_only.personal.echo_tool"
|
|
assert payload["resources"] == []
|
|
assert payload["prompts"] == []
|
|
|
|
|
|
async def test_refresh_catalog_unwraps_taskgroup_method_not_found(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service = WfMcpService(
|
|
store=FileStore(tmp_path / "wrapped_tools_only_server_store")
|
|
)
|
|
service.register_connection(
|
|
ConnectionConfig(
|
|
id="wrapped_tools_only.personal",
|
|
server="wrapped_tools_only",
|
|
account="personal",
|
|
metadata={"transport": "stdio", "command": "fake-tools-only"},
|
|
)
|
|
)
|
|
service.register_adapter("wrapped_tools_only", _WrappedToolsOnlyAdapter())
|
|
|
|
await service.refresh_connection_catalog("wrapped_tools_only.personal")
|
|
|
|
payload = service.get_catalog().as_payload()
|
|
assert payload["nodes"][0]["qualified_name"] == (
|
|
"wrapped_tools_only.personal.echo_tool"
|
|
)
|