"""MCP client adapters. The Agent only needs a synchronous `call_tool(name, arguments)` surface. This module adapts simple callables or SDK-like sessions to that surface without forcing the rest of the codebase to import a concrete MCP SDK. """ from __future__ import annotations import json from collections.abc import Callable from typing import Any class FunctionMcpToolClient: """Wrap a plain Python callable as an MCP tool client.""" def __init__(self, caller: Callable[[str, dict[str, Any]], Any]) -> None: self.caller = caller def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: return self.caller(tool_name, arguments) class SessionMcpToolClient: """Adapt SDK-like sessions exposing `call_tool`. The adapter accepts common result shapes: - raw dict/list/string - object with `structuredContent` - object with `content`, where text content may contain JSON """ def __init__(self, session: Any) -> None: if not hasattr(session, "call_tool"): raise TypeError("MCP session must expose call_tool") self.session = session def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: result = self.session.call_tool(tool_name, arguments) return normalize_mcp_sdk_result(result) def normalize_mcp_sdk_result(result: Any) -> Any: if hasattr(result, "structuredContent"): structured = getattr(result, "structuredContent") if structured is not None: return structured if hasattr(result, "content"): content = getattr(result, "content") text_parts: list[str] = [] for item in content or []: text = getattr(item, "text", None) if text is not None: text_parts.append(text) if text_parts: joined = "\n".join(text_parts) try: return json.loads(joined) except json.JSONDecodeError: return joined return result