refactor: add mcp source client facade
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .source_client import McpClientSession, McpSourceClient
|
||||
from .transport import open_mcp_session
|
||||
|
||||
__all__ = ["open_mcp_session"]
|
||||
__all__ = ["McpClientSession", "McpSourceClient", "open_mcp_session"]
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
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,
|
||||
ClientRequest,
|
||||
ListPromptsResult,
|
||||
ListResourcesResult,
|
||||
ListToolsResult,
|
||||
)
|
||||
from pydantic import AnyUrl
|
||||
|
||||
from wf_sources_mcp.connections import McpSourceConnection
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from wf_sources_mcp.catalog import (
|
||||
DiscoveredPrompt,
|
||||
DiscoveredResource,
|
||||
DiscoveredTool,
|
||||
)
|
||||
from wf_sources_mcp.sdk.protocols import ToolCallResult
|
||||
|
||||
|
||||
class McpClientSession(Protocol):
|
||||
"""Subset of MCP SDK ClientSession operations used by source clients."""
|
||||
|
||||
async def list_tools(self) -> ListToolsResult: ...
|
||||
|
||||
async def list_resources(self) -> ListResourcesResult: ...
|
||||
|
||||
async def list_prompts(self) -> ListPromptsResult: ...
|
||||
|
||||
async def read_resource(self, uri: AnyUrl) -> Any: ...
|
||||
|
||||
async def get_prompt(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, str] | None = None,
|
||||
/,
|
||||
) -> Any: ...
|
||||
|
||||
async def send_request(
|
||||
self,
|
||||
request: ClientRequest,
|
||||
result_type: type[ClientResult],
|
||||
) -> Any: ...
|
||||
|
||||
async def send_notification(self, notification: ClientNotification) -> None: ...
|
||||
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any],
|
||||
/,
|
||||
) -> CallToolResult: ...
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class McpSourceClient:
|
||||
"""Operation facade over an initialized MCP SDK ClientSession.
|
||||
|
||||
This class owns SDK operation calls and conversion to wf_sources_mcp DTOs.
|
||||
It does not own transport lifetime. One-shot callers enter it through
|
||||
`open_mcp_session`; persistent runtime owners may wrap the session inside
|
||||
their owner task in a later slice.
|
||||
"""
|
||||
|
||||
session: McpClientSession
|
||||
connection: McpSourceConnection
|
||||
|
||||
async def list_tools(self) -> list[DiscoveredTool]:
|
||||
from wf_sources_mcp.sdk.converters import tool_to_discovered
|
||||
|
||||
result: ListToolsResult = await self.session.list_tools()
|
||||
return [tool_to_discovered(tool) for tool in result.tools]
|
||||
|
||||
async def list_resources(self) -> list[DiscoveredResource]:
|
||||
from wf_sources_mcp.sdk.converters import resource_to_discovered
|
||||
|
||||
result: ListResourcesResult = await self.session.list_resources()
|
||||
return [resource_to_discovered(resource) for resource in result.resources]
|
||||
|
||||
async def list_prompts(self) -> list[DiscoveredPrompt]:
|
||||
from wf_sources_mcp.sdk.converters import prompt_to_discovered
|
||||
|
||||
result: ListPromptsResult = await self.session.list_prompts()
|
||||
return [prompt_to_discovered(prompt) for prompt in result.prompts]
|
||||
|
||||
async def get_connection_metadata(self) -> dict[str, Any]:
|
||||
transport = self.connection.transport
|
||||
return {
|
||||
"server": self.connection.provider,
|
||||
"transport": transport.kind if transport is not None else None,
|
||||
}
|
||||
|
||||
async def read_resource(self, uri: str) -> dict[str, Any]:
|
||||
result = await self.session.read_resource(AnyUrl(uri))
|
||||
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
|
||||
async def get_prompt(
|
||||
self,
|
||||
prompt_name: str,
|
||||
arguments: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
result = await self.session.get_prompt(prompt_name, arguments)
|
||||
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
|
||||
async def invoke_method(
|
||||
self,
|
||||
method: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
result = await self.session.send_request(
|
||||
ClientRequest.model_validate({"method": method, "params": params}),
|
||||
ClientResult,
|
||||
)
|
||||
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
|
||||
async def send_notification(
|
||||
self,
|
||||
method: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
await self.session.send_notification(
|
||||
ClientNotification.model_validate({"method": method, "params": params})
|
||||
)
|
||||
|
||||
async def call_tool(
|
||||
self,
|
||||
tool_name: str,
|
||||
payload: dict[str, Any],
|
||||
) -> ToolCallResult:
|
||||
from wf_sources_mcp.sdk.converters import tool_result_to_call_result
|
||||
|
||||
result = await self.session.call_tool(tool_name, payload)
|
||||
return tool_result_to_call_result(result)
|
||||
|
||||
|
||||
__all__ = ["McpClientSession", "McpSourceClient"]
|
||||
@@ -23,6 +23,10 @@ class PersistentMcpSession:
|
||||
inside an AnyIO cancel scope and must be called and closed by that same
|
||||
owner task. `client` remains available for simple injected/fake sessions in
|
||||
tests. `call_tool()` always normalizes SDK results for workflow nodes.
|
||||
|
||||
This runtime intentionally exposes only tool calls for now. Shared
|
||||
non-tool operations live on `McpSourceClient`; routing them through the
|
||||
owner task is a separate runtime-expansion slice.
|
||||
"""
|
||||
|
||||
connection: McpSourceConnection
|
||||
|
||||
@@ -1,86 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from mcp import ClientResult
|
||||
from mcp.types import (
|
||||
ClientNotification,
|
||||
ClientRequest,
|
||||
ListPromptsResult,
|
||||
ListResourcesResult,
|
||||
ListToolsResult,
|
||||
)
|
||||
from pydantic import AnyUrl
|
||||
|
||||
from wf_sources_mcp.auth import AuthRecord
|
||||
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
||||
from wf_sources_mcp.client import open_mcp_session
|
||||
from wf_sources_mcp.client import McpSourceClient, open_mcp_session
|
||||
from wf_sources_mcp.connections import McpSourceConnection
|
||||
|
||||
from .converters import (
|
||||
prompt_to_discovered,
|
||||
resource_to_discovered,
|
||||
tool_result_to_call_result,
|
||||
tool_to_discovered,
|
||||
)
|
||||
from .protocols import BackendAdapter, ToolCallResult
|
||||
|
||||
|
||||
class McpSdkAdapter(BackendAdapter):
|
||||
"""One-shot MCP client adapter for upstream MCP source operations.
|
||||
|
||||
This adapter intentionally opens a fresh SDK session per operation. Stateful
|
||||
workflow tool execution is handled by `wf_sources_mcp.runtime`; discovery
|
||||
and admin operations use this simpler one-shot path.
|
||||
This adapter intentionally opens a fresh SDK session per operation. It
|
||||
delegates all MCP operation/conversion details to `McpSourceClient` so the
|
||||
same operation facade can later be used inside persistent runtime owners.
|
||||
"""
|
||||
|
||||
@asynccontextmanager
|
||||
async def _session(
|
||||
async def _client(
|
||||
self,
|
||||
connection: McpSourceConnection,
|
||||
auth: AuthRecord | None,
|
||||
):
|
||||
) -> AsyncIterator[McpSourceClient]:
|
||||
async with open_mcp_session(connection, auth) as session:
|
||||
yield session
|
||||
yield McpSourceClient(session=session, connection=connection)
|
||||
|
||||
async def list_tools(
|
||||
self,
|
||||
connection: McpSourceConnection,
|
||||
auth: AuthRecord | None,
|
||||
) -> list[DiscoveredTool]:
|
||||
async with self._session(connection, auth) as session:
|
||||
result: ListToolsResult = await session.list_tools()
|
||||
return [tool_to_discovered(tool) for tool in result.tools]
|
||||
async with self._client(connection, auth) as client:
|
||||
return await client.list_tools()
|
||||
|
||||
async def list_resources(
|
||||
self,
|
||||
connection: McpSourceConnection,
|
||||
auth: AuthRecord | None,
|
||||
) -> list[DiscoveredResource]:
|
||||
async with self._session(connection, auth) as session:
|
||||
result: ListResourcesResult = await session.list_resources()
|
||||
return [resource_to_discovered(resource) for resource in result.resources]
|
||||
async with self._client(connection, auth) as client:
|
||||
return await client.list_resources()
|
||||
|
||||
async def list_prompts(
|
||||
self,
|
||||
connection: McpSourceConnection,
|
||||
auth: AuthRecord | None,
|
||||
) -> list[DiscoveredPrompt]:
|
||||
async with self._session(connection, auth) as session:
|
||||
result: ListPromptsResult = await session.list_prompts()
|
||||
return [prompt_to_discovered(prompt) for prompt in result.prompts]
|
||||
async with self._client(connection, auth) as client:
|
||||
return await client.list_prompts()
|
||||
|
||||
async def get_connection_metadata(
|
||||
self,
|
||||
connection: McpSourceConnection,
|
||||
auth: AuthRecord | None,
|
||||
) -> dict[str, Any]:
|
||||
transport = connection.transport
|
||||
return {
|
||||
"server": connection.provider,
|
||||
"transport": transport.kind if transport is not None else None,
|
||||
}
|
||||
async with self._client(connection, auth) as client:
|
||||
return await client.get_connection_metadata()
|
||||
|
||||
async def read_resource(
|
||||
self,
|
||||
@@ -88,9 +67,8 @@ class McpSdkAdapter(BackendAdapter):
|
||||
auth: AuthRecord | None,
|
||||
uri: str,
|
||||
) -> dict[str, Any]:
|
||||
async with self._session(connection, auth) as session:
|
||||
result = await session.read_resource(AnyUrl(uri))
|
||||
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
async with self._client(connection, auth) as client:
|
||||
return await client.read_resource(uri)
|
||||
|
||||
async def get_prompt(
|
||||
self,
|
||||
@@ -99,9 +77,8 @@ class McpSdkAdapter(BackendAdapter):
|
||||
prompt_name: str,
|
||||
arguments: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
async with self._session(connection, auth) as session:
|
||||
result = await session.get_prompt(prompt_name, arguments)
|
||||
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
async with self._client(connection, auth) as client:
|
||||
return await client.get_prompt(prompt_name, arguments)
|
||||
|
||||
async def invoke_method(
|
||||
self,
|
||||
@@ -110,12 +87,8 @@ class McpSdkAdapter(BackendAdapter):
|
||||
method: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
async with self._session(connection, auth) as session:
|
||||
result = await session.send_request(
|
||||
ClientRequest.model_validate({"method": method, "params": params}),
|
||||
ClientResult,
|
||||
)
|
||||
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
async with self._client(connection, auth) as client:
|
||||
return await client.invoke_method(method, params)
|
||||
|
||||
async def send_notification(
|
||||
self,
|
||||
@@ -124,10 +97,8 @@ class McpSdkAdapter(BackendAdapter):
|
||||
method: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
async with self._session(connection, auth) as session:
|
||||
await session.send_notification(
|
||||
ClientNotification.model_validate({"method": method, "params": params})
|
||||
)
|
||||
async with self._client(connection, auth) as client:
|
||||
await client.send_notification(method, params)
|
||||
|
||||
async def call_tool(
|
||||
self,
|
||||
@@ -136,9 +107,8 @@ class McpSdkAdapter(BackendAdapter):
|
||||
tool_name: str,
|
||||
payload: dict[str, Any],
|
||||
) -> ToolCallResult:
|
||||
async with self._session(connection, auth) as session:
|
||||
result = await session.call_tool(tool_name, payload)
|
||||
return tool_result_to_call_result(result)
|
||||
async with self._client(connection, auth) as client:
|
||||
return await client.call_tool(tool_name, payload)
|
||||
|
||||
|
||||
__all__ = ["McpSdkAdapter"]
|
||||
|
||||
Reference in New Issue
Block a user