"""Multi-server MCP client using fastmcp composition. Composes the three domain-scoped MCP servers into namespaced configurations that agents connect to as a single client. """ import json from contextlib import asynccontextmanager from typing import Any from fastmcp import Client def _env() -> dict: """Forward LLM-related env vars and active scenario to MCP server subprocesses.""" import os from mcp_servers.data.scenarios.manager import scenario_manager env = {} for key in ( "LLM_PROVIDER", "GROQ_API_KEY", "GROQ_MODEL", "ANTHROPIC_API_KEY", "ANTHROPIC_MODEL", "OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_MODEL", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_DEFAULT_REGION", "BEDROCK_MODEL_ID", "USE_BEDROCK", "PATH", ): val = os.getenv(key) if val: env[key] = val env["ACTIVE_SCENARIO"] = scenario_manager.active_id return env def _server_config(module: str) -> dict: """Build server config with current env vars (called at connect time, not import time).""" return { "command": "uv", "args": ["run", "python", "-m", module], "env": _env(), } SERVER_MODULES = { "shared": "mcp_servers.shared", "ops": "mcp_servers.ops", "passenger": "mcp_servers.passenger", } # Agent profiles — which servers each agent connects to AGENT_PROFILES = { "fce": ["shared", "ops", "passenger"], "handover": ["shared", "ops"], } class MCPMultiClient: """Manages connections to multiple MCP servers via fastmcp Client.""" def __init__(self) -> None: self._clients: dict[str, Client] = {} async def connect(self, server_names: list[str]) -> None: """Connect to the specified MCP servers.""" for name in server_names: if name not in SERVER_MODULES: raise ValueError(f"Unknown server: {name}. Available: {list(SERVER_MODULES.keys())}") config = {"mcpServers": {"default": _server_config(SERVER_MODULES[name])}} client = Client(config) await client.__aenter__() self._clients[name] = client async def close(self) -> None: """Close all server connections.""" for client in self._clients.values(): try: await client.__aexit__(None, None, None) except (Exception, BaseException): pass self._clients.clear() async def call_tool(self, server: str, tool_name: str, arguments: dict) -> Any: """Call a tool on a specific server. Returns parsed result.""" client = self._clients.get(server) if not client: raise ValueError(f"Not connected to server: {server}") result = await client.call_tool(tool_name, arguments) # Parse the result content if isinstance(result, list): texts = [c.text for c in result if hasattr(c, "text")] elif hasattr(result, "content"): texts = [c.text for c in result.content if hasattr(c, "text")] else: return result if len(texts) == 1: try: return json.loads(texts[0]) except (json.JSONDecodeError, TypeError): return texts[0] elif len(texts) > 1: parsed = [] for t in texts: try: parsed.append(json.loads(t)) except (json.JSONDecodeError, TypeError): parsed.append(t) return parsed return None async def read_resource(self, server: str, uri: str) -> Any: """Read a resource from a specific server.""" client = self._clients.get(server) if not client: raise ValueError(f"Not connected to server: {server}") result = await client.read_resource(uri) if isinstance(result, str): try: return json.loads(result) except (json.JSONDecodeError, TypeError): return result return result async def get_prompt(self, server: str, prompt_name: str, arguments: dict) -> str: """Get a rendered prompt from a specific server.""" client = self._clients.get(server) if not client: raise ValueError(f"Not connected to server: {server}") result = await client.get_prompt(prompt_name, arguments) if isinstance(result, str): return result # Handle structured prompt response texts = [] if hasattr(result, "messages"): for msg in result.messages: if hasattr(msg.content, "text"): texts.append(msg.content.text) elif isinstance(msg.content, list): for c in msg.content: if hasattr(c, "text"): texts.append(c.text) return "\n".join(texts) if texts else str(result) @asynccontextmanager async def connect_servers(server_names: list[str]): """Context manager for multi-server MCP connections.""" client = MCPMultiClient() try: await client.connect(server_names) yield client finally: await client.close()