"""Integrates external MCP server tools into the AI Assistant tool_registry. When the MCP Client plugin activates, it loads all enabled MCP server configs, fetches their tool lists, and registers each tool in the global ToolRegistry. Agents can then call external MCP tools like native tools. """ from __future__ import annotations import logging import uuid from typing import Any from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.plugins.builtins.ai_assistant.tool_registry import get_tool_registry from app.plugins.builtins.mcp_client.client import McpClient from app.plugins.builtins.mcp_client.models import McpServerConfig as McpServerConfigModel logger = logging.getLogger(__name__) PLUGIN_NAME = "mcp_client" def _make_tool_name(server_name: str, tool_name: str) -> str: """Generate a unique tool name: mcp__{server}__{tool}.""" safe_server = server_name.replace(" ", "_").replace("-", "_").lower() return f"mcp__{safe_server}__{tool_name}" def _make_handler(server_cfg: McpServerConfigModel, tool_name: str): """Create an async handler that calls the external MCP server.""" async def _handler(arguments: dict[str, Any], context: dict[str, Any]) -> str: client = McpClient(base_url=server_cfg.url, api_token=server_cfg.api_token) try: resp = await client.execute_tool(tool_name, arguments) if resp.success: import json result = resp.result if resp.result is not None else {} return json.dumps(result) if isinstance(result, dict) else str(result) return f"Error: {resp.error or 'Unknown error'}" except Exception as exc: logger.exception("MCP tool execution failed: %s/%s", server_cfg.name, tool_name) return f"Error: {exc}" return _handler async def sync_external_tools(db: AsyncSession, tenant_id: uuid.UUID) -> int: """Fetch tools from all enabled MCP servers and register them in the tool registry. Returns the number of tools registered. """ registry = get_tool_registry() # Unregister previous tools from this plugin registry.unregister_plugin(PLUGIN_NAME) stmt = select(McpServerConfigModel).where( McpServerConfigModel.tenant_id == tenant_id, McpServerConfigModel.enabled.is_(True), ) result = await db.execute(stmt) configs = result.scalars().all() count = 0 for cfg in configs: try: client = McpClient(base_url=cfg.url, api_token=cfg.api_token, timeout=10.0) tools_resp = await client.list_tools() for tool in tools_resp.tools: tool_name = _make_tool_name(cfg.name, tool.name) registry.register( name=tool_name, description=f"[MCP:{cfg.name}] {tool.description}", parameters=tool.parameters if isinstance(tool.parameters, dict) else {}, handler=_make_handler(cfg, tool.name), plugin_name=PLUGIN_NAME, required_permission="mcp-client:read", category="mcp-external", ) count += 1 logger.info("Registered %d tools from MCP server %s", len(tools_resp.tools), cfg.name) except Exception as exc: logger.warning("Failed to sync tools from MCP server %s: %s", cfg.name, exc) return count def unregister_all_external_tools() -> None: """Remove all MCP client tools from the registry.""" registry = get_tool_registry() registry.unregister_plugin(PLUGIN_NAME)