diff --git a/backend/main.py b/backend/main.py index 6201970..80c795c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -623,6 +623,100 @@ async def auth_logout(): return {"status": "error", "message": str(e)} +# ============ MCP Server Management ============ + +@app.get("/api/mcp/servers") +async def list_mcp_servers(): + """List configured MCP servers""" + try: + proc = await asyncio.create_subprocess_exec( + "claude", "mcp", "list", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=10) + output = stdout.decode(errors="replace").strip() + + # Parse the output — claude mcp list shows servers in a table or JSON + servers = [] + if "No MCP servers configured" in output: + return {"servers": [], "raw": output} + + # Try JSON parse first + try: + import json as _json + data = _json.loads(output) + if isinstance(data, list): + servers = data + elif isinstance(data, dict): + servers = list(data.values()) if data else [] + except Exception: + # Parse text output line by line + for line in output.split("\n"): + line = line.strip() + if line and not line.startswith("-") and not line.startswith("Name"): + parts = line.split() + if len(parts) >= 1: + servers.append({"name": parts[0], "details": " ".join(parts[1:])}) + + return {"servers": servers, "raw": output} + except Exception as e: + return {"servers": [], "error": str(e)} + + +class McpServerAdd(BaseModel): + name: str + server_type: str = "sse" # sse | stdio + url: Optional[str] = None + command: Optional[str] = None + args: Optional[List[str]] = None + + +@app.post("/api/mcp/servers") +async def add_mcp_server(server: McpServerAdd): + """Add an MCP server""" + try: + cmd = ["claude", "mcp", "add", server.name] + if server.server_type == "sse" and server.url: + cmd.extend(["--transport", "sse", server.url]) + elif server.server_type == "stdio" and server.command: + cmd.extend(["--transport", "stdio", server.command]) + if server.args: + cmd.extend(server.args) + + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=15) + output = stdout.decode(errors="replace") + stderr.decode(errors="replace") + + return { + "status": "ok" if proc.returncode == 0 else "error", + "message": output.strip(), + "name": server.name + } + except Exception as e: + return {"status": "error", "message": str(e)} + + +@app.delete("/api/mcp/servers/{name}") +async def remove_mcp_server(name: str): + """Remove an MCP server""" + try: + proc = await asyncio.create_subprocess_exec( + "claude", "mcp", "remove", name, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=10) + output = stdout.decode(errors="replace") + stderr.decode(errors="replace") + return {"status": "ok" if proc.returncode == 0 else "error", "message": output.strip()} + except Exception as e: + return {"status": "error", "message": str(e)} + + # Serve static frontend app.mount("/", StaticFiles(directory="/app/frontend/dist", html=True), name="static")