"use client"; import { useState, useEffect, useCallback } from "react"; import { useCopilotChat } from "@copilotkit/react-core"; import { useMcpServers } from "./CopilotKitProvider"; import type { McpServerEntry } from "../constants/mcpServers"; import { triggerBlobDownload } from "@/lib/open-download"; import type { WorkspaceInfo } from "@/lib/workspace/types"; import type { ServerIntrospection } from "../hooks/useMcpIntrospect"; export type { McpServerEntry }; export { DEFAULT_SERVERS } from "../constants/mcpServers"; function AddServerForm({ onAdd, onCancel, }: { onAdd: (entry: McpServerEntry) => void; onCancel: () => void; }) { const [endpoint, setEndpoint] = useState(""); const [serverId, setServerId] = useState(""); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const url = endpoint.trim(); if (!url) return; onAdd({ endpoint: url, serverId: serverId.trim() || undefined }); setEndpoint(""); setServerId(""); }; return (
setEndpoint(e.target.value)} placeholder="MCP endpoint URL (e.g. http://localhost:3108/mcp)" className="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:border-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-400" required /> setServerId(e.target.value)} placeholder="Server ID (optional, e.g. threejs)" className="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:border-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-400" />
); } export function McpServerManager({ activeWorkspace, serverStatuses = [], onReconnect, globalLoading = false, }: { activeWorkspace?: WorkspaceInfo | null; /** Per-server introspection state (error, loading) from useMcpIntrospect */ serverStatuses?: ServerIntrospection[]; /** Called when user clicks Reconnect for a failed server */ onReconnect?: () => void; /** True when a full refresh is in progress */ globalLoading?: boolean; }) { const [downloading, setDownloading] = useState(false); const [downloadError, setDownloadError] = useState(null); // Single source of truth: React context owned by DynamicCopilotKitProvider const { servers, setServers } = useMcpServers(); const [showAddForm, setShowAddForm] = useState(false); const { setMcpServers } = useCopilotChat(); // Keep CopilotKit's runtime in sync whenever the list changes const syncToRuntime = useCallback( (list: McpServerEntry[]) => { console.log( "[McpServerManager] syncToRuntime called with", list.length, "server(s):", list.map((s) => s.endpoint), ); if (typeof setMcpServers === "function") { setMcpServers( list.map((s) => ({ endpoint: s.endpoint, ...(s.serverId ? { serverId: s.serverId } : {}), })), ); console.log( "[McpServerManager] setMcpServers called — agent server list updated", ); } else { console.warn( "[McpServerManager] setMcpServers is not available (not inside CopilotKit context?)", ); } }, [setMcpServers], ); useEffect(() => { syncToRuntime(servers); }, [servers, syncToRuntime]); const addServer = (entry: McpServerEntry) => { setServers([...servers, entry]); setShowAddForm(false); }; const removeServer = (index: number) => { setServers(servers.filter((_, i) => i !== index)); }; return (

MCP servers

{showAddForm && ( setShowAddForm(false)} /> )} {downloadError && (

{downloadError}

)}
    {servers.map((s, i) => { const isWorkspace = activeWorkspace?.endpoint === s.endpoint; const isProvisioning = isWorkspace && activeWorkspace?.status === "provisioning"; const isRunning = isWorkspace && activeWorkspace?.status === "running"; const status = serverStatuses.find( (st) => st.endpoint === s.endpoint, ); const hasError = Boolean(status?.error); const isConnecting = Boolean(status?.loading); const handleDownload = async () => { if (!activeWorkspace) return; setDownloadError(null); setDownloading(true); const wid = activeWorkspace.workspaceId; try { const res = await fetch("/api/workspace/download", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ workspaceId: wid, stream: true, fullKit: true, }), }); if (!res.ok) { const body = (await res.json().catch(() => ({}))) as { error?: string; }; setDownloadError( body.error || `Download failed (${res.status})`, ); return; } const blob = await res.blob(); const safeId = wid.replace(/[^\w-]/g, "").slice(0, 16) || "workspace"; const cd = res.headers.get("Content-Disposition"); const m = cd?.match(/filename="([^"]+)"/); const filename = m?.[1] ?? `workspace-${safeId}.tar.gz`; triggerBlobDownload(blob, filename); } catch (e) { setDownloadError( e instanceof Error ? e.message : "Download failed", ); } finally { setDownloading(false); } }; return (
  • {s.serverId || `Server ${i + 1}`} {isProvisioning && ( Setting up… )} {isConnecting && !hasError && ( Connecting… )} {isRunning && !hasError && ( Running )} {hasError && ( Error )}
    {s.endpoint}
    {hasError && onReconnect && ( )} {isRunning && ( )}
    {hasError && status?.error && (

    {status.error.length > 80 ? `${status.error.slice(0, 80)}…` : status.error}

    )}
  • ); })}
{servers.length === 0 && (

No servers. Add one to let the assistant use MCP tools.

)}
); }