Files
leocrm/app/plugins/builtins/mcp_client/client.py
T
Agent Zero 317d5c81f8 feat: MCP Client plugin (Task 5.17) - integrate external MCP servers for AI agents
- New plugin app/plugins/builtins/mcp_client/ with server config CRUD
- Models: McpServerConfig with TenantMixin (name, url, api_token, enabled)
- Routes: GET/POST/PATCH/DELETE /api/v1/mcp-client/servers
- Routes: GET /servers/{id}/tools, POST /servers/{id}/execute
- client.py: async MCP client using httpx for external server calls
- tool_registry_integration.py: registers external MCP tools in AI tool registry
- Migration: 0001_initial.sql for mcp_server_configs table
- Frontend: mcpClient.ts API client with React Query hooks
- Frontend: MCP Client settings UI in SettingsMcp.tsx (server CRUD, tool viewing)
- Tests: 8 tests covering CRUD, auth, tool registry integration
2026-07-23 23:02:07 +02:00

80 lines
2.9 KiB
Python

"""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