""" MCP server API handlers. - GET /api/mcp/servers list - POST /api/mcp/servers add - POST /api/mcp/servers//connect - POST /api/mcp/servers//disconnect - DELETE /api/mcp/servers/ remove - PATCH /api/mcp/servers/ update exposure/display_name """ from __future__ import annotations from typing import Any from .api_helpers import ( connect_and_discover, count_mcp_tools, disconnect_server, load_config, mcp_status, save_config, ) def list_mcp_servers() -> dict: """GET /api/mcp/servers""" cfg = load_config() servers = cfg.get("mcp_servers", {}) out = {} for sid, srv in servers.items(): entry = dict(srv) entry["status"] = mcp_status(sid) entry["tools_count"] = count_mcp_tools(cfg, sid) out[sid] = entry return out def add_mcp_server(body: dict) -> dict: """POST /api/mcp/servers""" sid = body.get("server_id", "").strip() if not sid: return {"error": "server_id is required"}, 400 cfg = load_config() servers = cfg.setdefault("mcp_servers", {}) if sid in servers: return {"error": "Server already exists"}, 409 transport = body.get("transport", "sse") mode = body.get("mode", "toolset") # "toolset" or "individual" exposure = body.get("exposure", "secondary") display_name = body.get("display_name", "").strip() or sid srv: dict[str, Any] = { "transport": transport, "enabled": body.get("enabled", True), "auto_connect": body.get("auto_connect", True), "exposure": exposure, "display_name": display_name, "mode": mode, } # When mode is "toolset", individual tools are hidden; server-level # exposure controls visibility of the whole server as a grouped toolset. # When mode is "individual", each tool has its own exposure and the # server-level exposure is irrelevant. tool_exposure = "hidden" if mode == "toolset" else exposure if transport == "stdio": srv["command"] = body.get("command", "") srv["args"] = body.get("args", []) elif transport == "folder": srv["folder"] = body.get("folder", "") srv["sub_transport"] = body.get("sub_transport", "sse") srv["command"] = body.get("command", "") srv["args"] = body.get("args", []) srv["url"] = body.get("url", "") else: srv["url"] = body.get("url", "") env = body.get("env") if env: srv["env"] = env servers[sid] = srv save_config(cfg) tools: list[dict] = [] conn_err = None if srv.get("auto_connect", True): try: tools = connect_and_discover(sid, srv) for t in tools: tn = t["name"] cfg["tools"][tn] = { "source": f"mcp:{sid}", "enabled": True, "exposure": tool_exposure, "parallel_safe": False, "subagent_safe": False, "description": t.get("description", ""), } save_config(cfg) except Exception as exc: conn_err = str(exc) return { "success": True, "server_id": sid, "tools_discovered": len(tools), "tools": tools, "connection_error": conn_err, }, 200 def connect_mcp_server(sid: str) -> dict: """POST /api/mcp/servers//connect""" cfg = load_config() servers = cfg.get("mcp_servers", {}) if sid not in servers: return {"error": "Server not found"}, 404 srv = servers[sid] tools = connect_and_discover(sid, srv) mode = srv.get("mode", "toolset") exposure = srv.get("exposure", "secondary") tool_exposure = "hidden" if mode == "toolset" else exposure for t in tools: tn = t["name"] if tn not in cfg.get("tools", {}): cfg["tools"][tn] = { "source": f"mcp:{sid}", "enabled": True, "exposure": tool_exposure, "parallel_safe": False, "subagent_safe": False, "description": t.get("description", ""), } save_config(cfg) return {"success": True, "tools": tools}, 200 def disconnect_mcp_server(sid: str) -> dict: """POST /api/mcp/servers//disconnect""" disconnect_server(sid) return {"success": True}, 200 def remove_mcp_server(sid: str) -> dict: """DELETE /api/mcp/servers/""" cfg = load_config() if sid not in cfg.get("mcp_servers", {}): return {"error": "Server not found"}, 404 disconnect_server(sid) del cfg["mcp_servers"][sid] prefix = f"mcp:{sid}" cfg["tools"] = {k: v for k, v in cfg.get("tools", {}).items() if v.get("source") != prefix} save_config(cfg) return {"success": True}, 200 def patch_mcp_server(server_id: str, body: dict) -> dict: """PATCH /api/mcp/servers/""" cfg = load_config() servers = cfg.get("mcp_servers", {}) if server_id not in servers: return {"error": "MCP server not found"}, 404 srv = servers[server_id] updated = {} if "mode" in body: new_mode = body["mode"] if new_mode not in ("toolset", "individual"): return {"error": "mode must be 'toolset' or 'individual'"}, 400 old_mode = srv.get("mode", "toolset") srv["mode"] = new_mode updated["mode"] = new_mode prefix = f"mcp:{server_id}" if new_mode == "toolset" and old_mode != "toolset": # Switching to toolset: hide all individual tools for tn, ti in cfg.get("tools", {}).items(): if isinstance(ti, dict) and ti.get("source") == prefix: ti["exposure"] = "hidden" elif new_mode == "individual" and old_mode != "individual": # Switching to individual: restore tools to server's exposure exp = srv.get("exposure", "secondary") for tn, ti in cfg.get("tools", {}).items(): if isinstance(ti, dict) and ti.get("source") == prefix: ti["exposure"] = exp if "exposure" in body: srv["exposure"] = body["exposure"] updated["exposure"] = body["exposure"] prefix = f"mcp:{server_id}" # Only sync tool exposures when mode is "individual" if srv.get("mode", "toolset") == "individual": synced = 0 for tn, ti in cfg.get("tools", {}).items(): if isinstance(ti, dict) and ti.get("source") == prefix: ti["exposure"] = body["exposure"] synced += 1 updated["tools_synced"] = synced if "display_name" in body: srv["display_name"] = body["display_name"].strip() updated["display_name"] = srv["display_name"] save_config(cfg) return {"success": True, "server": server_id, **updated}, 200