"""MCP Client — calls external MCP servers to list and execute tools. Uses httpx for async HTTP calls. External MCP servers are expected to expose: GET /tools → { tools: [{ name, description, parameters }] } POST /tools/{name}/execute → { result: ... } """ from __future__ import annotations import logging from typing import Any import httpx from app.plugins.builtins.mcp_client.schemas import ( McpServerExecuteRequest, McpServerExecuteResponse, McpServerToolInfo, McpServerToolsResponse, ) logger = logging.getLogger(__name__) class McpClient: """Async client for communicating with external MCP servers.""" def __init__(self, base_url: str, api_token: str | None = None, timeout: float = 30.0) -> None: self.base_url = base_url.rstrip("/") self.api_token = api_token self.timeout = timeout def _headers(self) -> dict[str, str]: headers = {"Content-Type": "application/json"} if self.api_token: headers["Authorization"] = f"Bearer {self.api_token}" return headers async def list_tools(self) -> McpServerToolsResponse: """List available tools from the external MCP server.""" async with httpx.AsyncClient(timeout=self.timeout) as client: resp = await client.get(f"{self.base_url}/tools", headers=self._headers()) resp.raise_for_status() data = resp.json() tools = [McpServerToolInfo(**t) for t in data.get("tools", [])] return McpServerToolsResponse( server_name=data.get("server_name", "unknown"), server_url=self.base_url, tools=tools, count=len(tools), ) async def execute_tool(self, tool_name: str, arguments: dict[str, Any]) -> McpServerExecuteResponse: """Execute a tool on the external MCP server.""" req = McpServerExecuteRequest(tool_name=tool_name, arguments=arguments) async with httpx.AsyncClient(timeout=self.timeout) as client: resp = await client.post( f"{self.base_url}/tools/{tool_name}/execute", json=req.model_dump(), headers=self._headers(), ) resp.raise_for_status() data = resp.json() return McpServerExecuteResponse( server_name=data.get("server_name", "unknown"), tool=tool_name, success=data.get("success", True), result=data.get("result"), error=data.get("error"), ) async def health_check(self) -> bool: """Check if the external MCP server is reachable.""" try: async with httpx.AsyncClient(timeout=5.0) as client: resp = await client.get(f"{self.base_url}/tools", headers=self._headers()) return resp.status_code == 200 except Exception: return False