diff --git a/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx b/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx new file mode 100644 index 00000000000..f8125fd18f5 --- /dev/null +++ b/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx @@ -0,0 +1,275 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { Switch, Spin, Input, Button } from "antd"; +import { SearchOutlined, ArrowLeftOutlined, RightOutlined } from "@ant-design/icons"; +import { fetchMCPServers, listMCPTools } from "../networking"; +import { MCPServer } from "../mcp_tools/types"; +import { message } from "antd"; + +interface Props { + accessToken: string; + selectedServers: string[]; + onChange: (servers: string[]) => void; +} + +const AVATAR_COLORS = [ + "#1677ff", "#52c41a", "#fa8c16", "#eb2f96", "#722ed1", + "#13c2c2", "#fa541c", "#2f54eb", "#a0d911", "#faad14", +]; + +function getAvatarColor(name: string): string { + let hash = 0; + for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash); + return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length]; +} + +type TabKey = "all" | "connected"; + +const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange }) => { + const [servers, setServers] = useState([]); + const [loading, setLoading] = useState(true); + const [query, setQuery] = useState(""); + const [activeTab, setActiveTab] = useState("all"); + const [togglingOn, setTogglingOn] = useState>(new Set()); + const [detailServer, setDetailServer] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + fetchMCPServers(accessToken) + .then((data) => { + if (cancelled) return; + const list: MCPServer[] = Array.isArray(data) ? data : (data?.data ?? []); + setServers(list); + }) + .catch(() => { + if (!cancelled) setServers([]); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { cancelled = true; }; + }, [accessToken]); + + const handleToggle = async (serverName: string, checked: boolean) => { + if (!checked) { + onChange(selectedServers.filter((s) => s !== serverName)); + return; + } + setTogglingOn((prev) => new Set(prev).add(serverName)); + try { + const result = await listMCPTools(accessToken, serverName); + if (result?.error) { + message.warning(`Could not load tools for ${serverName}`); + return; + } + onChange([...selectedServers, serverName]); + } catch { + message.warning(`Could not load tools for ${serverName}`); + } finally { + setTogglingOn((prev) => { + const next = new Set(prev); + next.delete(serverName); + return next; + }); + } + }; + + const nameOf = (s: MCPServer) => s.server_name ?? s.alias ?? s.server_id; + + const filtered = servers.filter((s) => { + const name = nameOf(s); + const matchesQuery = !query.trim() || + name.toLowerCase().includes(query.toLowerCase()) || + (s.description ?? "").toLowerCase().includes(query.toLowerCase()); + const matchesTab = activeTab === "all" || selectedServers.includes(name); + return matchesQuery && matchesTab; + }); + + const connectedCount = servers.filter((s) => selectedServers.includes(nameOf(s))).length; + + // ── Detail view ── + if (detailServer) { + const name = nameOf(detailServer); + const isConnected = selectedServers.includes(name); + const isTogglingOn = togglingOn.has(name); + const color = getAvatarColor(name); + + return ( +
+ {/* Back */} + + + {/* Avatar + name + connect */} +
+
+ {name.charAt(0).toUpperCase()} +
+
+

{name}

+

{detailServer.description ?? "MCP server"}

+
+ +
+ + {/* Info table */} +

Information

+
+ {[ + ["Server ID", detailServer.server_id], + ["Transport", (detailServer as MCPServer & { mcp_info?: { server_url?: string } }).mcp_info?.server_url ? "HTTP" : "stdio"], + ["Status", isConnected ? "Connected" : "Not connected"], + ].filter(([, v]) => v).map(([label, value], i, arr) => ( +
+ {label} + {value} +
+ ))} +
+
+ ); + } + + // ── List view ── + return ( +
+ + {/* Header row */} +
+
+
+

MCP Servers

+ Beta +
+

+ Connect tools to your chat. +

+
+ } + placeholder="Search servers..." + value={query} + onChange={(e) => setQuery(e.target.value)} + allowClear + style={{ width: 220, borderRadius: 8, fontSize: 13 }} + size="middle" + /> +
+ + {/* Tabs */} +
+ {(["all", "connected"] as TabKey[]).map((tab) => ( + + ))} +
+ + {/* Grid */} + {loading ? ( +
+ +
+ ) : filtered.length === 0 ? ( +
+ {servers.length === 0 + ? "No MCP servers configured. Add servers in Tools → MCP Servers." + : activeTab === "connected" ? "No servers connected yet." : "No servers match your search."} +
+ ) : ( +
+ {filtered.map((server, idx) => { + const name = nameOf(server); + const isConnected = selectedServers.includes(name); + const color = getAvatarColor(name); + const isEvenRow = Math.floor(idx / 2) % 2 === 0; + const isLeftCol = idx % 2 === 0; + + return ( +
setDetailServer(server)} + style={{ + display: "flex", alignItems: "center", gap: 12, + padding: "14px 16px", background: "#fff", + borderRight: isLeftCol ? "1px solid #f3f4f6" : "none", + borderBottom: idx < filtered.length - 2 ? "1px solid #f3f4f6" : "none", + cursor: "pointer", minWidth: 0, + transition: "background 0.1s", + }} + onMouseEnter={(e) => { (e.currentTarget as HTMLDivElement).style.background = "#fafafa"; }} + onMouseLeave={(e) => { (e.currentTarget as HTMLDivElement).style.background = "#fff"; }} + > +
+ {name.charAt(0).toUpperCase()} +
+
+
+ {name} +
+
+ {server.description ?? "MCP server"} +
+
+ {isConnected && ( + + )} + +
+ ); + })} +
+ )} +
+ ); +}; + +export default MCPAppsPanel;