"use client"; /* External MCP - register MCP servers, discover their tools, toggle which are live. Server-scoped: agents and workflow nodes consume a server's *enabled* tools. */ import { useCallback, useEffect, useState } from "react"; import { Icon } from "../icons"; import { Field, Modal, Tile, Toggle } from "../primitives"; import { api, McpClientT } from "@/lib/api"; export function McpClientsScreen({ project }: { project: any }) { const [rows, setRows] = useState([]); const [selId, setSelId] = useState(null); const [addOpen, setAddOpen] = useState(false); // Session cache of discovered tools per server, so re-selecting one is instant // (the server is the source of truth - "Re-discover" forces a fresh fetch). const [toolCache, setToolCache] = useState>({}); const reload = useCallback(() => { if (!project?.id) return; api.listMcpClients(project.id).then((r) => { setRows(r); setSelId((s) => (s && r.some((x) => x.id === s) ? s : (r[0]?.id ?? null))); }).catch(() => {}); }, [project?.id]); useEffect(() => { reload(); }, [reload]); const sel = rows.find((r) => r.id === selId) || null; return (
{/* LEFT list */}
External MCP
{rows.length === 0 &&
No MCP servers yet. Click + to connect one (e.g. GitHub).
} {rows.map((m) => { const on = selId === m.id; return ( ); })}
{/* RIGHT detail */}
{sel ? setToolCache((c) => ({ ...c, [sel.id]: list }))} /> : (
Connect an MCP server
)}
setAddOpen(false)} onAdded={(id) => { setAddOpen(false); reload(); setSelId(id); }} />
); } function ServerDetail({ project, server, onChanged, cached, onLoaded }: { project: any; server: McpClientT; onChanged: () => void; cached: { name: string; description?: string }[] | null; onLoaded: (list: { name: string; description?: string }[]) => void }) { const [tools, setTools] = useState<{ name: string; description?: string }[] | null>(cached); const [err, setErr] = useState(null); const [loading, setLoading] = useState(false); const [disabled, setDisabled] = useState>(new Set(server.disabled_tools || [])); const [saving, setSaving] = useState(false); const discover = useCallback(() => { setLoading(true); setErr(null); api.discoverMcpTools(project.id, server.id) .then((r) => { if (r.ok) { const list = r.tools || []; setTools(list); onLoaded(list); } else setErr(r.error || "Could not list tools from that server."); }) .catch((e) => setErr(String(e?.message || e))) .finally(() => setLoading(false)); }, [project.id, server.id, onLoaded]); // Only fetch when this server's tools aren't already cached this session; the server is // the source of truth, so the "Re-discover" button forces a fresh fetch on demand. useEffect(() => { if (cached === null) discover(); }, [server.id]); // eslint-disable-line react-hooks/exhaustive-deps async function toggle(name: string) { const next = new Set(disabled); if (next.has(name)) next.delete(name); else next.add(name); setDisabled(next); setSaving(true); try { await api.updateMcpClient(project.id, server.id, { disabled_tools: [...next] }); onChanged(); } catch { setDisabled(new Set(server.disabled_tools || [])); } finally { setSaving(false); } } const enabledCount = tools ? tools.filter((t) => !disabled.has(t.name)).length : 0; return (
{server.name}
{server.transport} · {server.url}
Tools{tools ? ` · ${enabledCount}/${tools.length} enabled` : ""}
{saving && Saving…}
{err &&
{err}
} {!err && tools === null &&
Connecting to the server…
} {!err && tools && tools.length === 0 &&
This server exposes no tools.
} {!err && tools && tools.length > 0 && (
{tools.map((t) => { const on = !disabled.has(t.name); return (
{t.name} {t.description && {t.description}}
toggle(t.name)} />
); })}
)}
Disabled tools stay hidden from agents and workflow nodes that use this server.
); } function AddServerModal({ open, onClose, project, onAdded }: { open: boolean; onClose: () => void; project: any; onAdded: (id: string) => void }) { const [form, setForm] = useState({ name: "github", transport: "streamable_http", url: "", token: "" }); const [busy, setBusy] = useState(false); const [err, setErr] = useState(null); useEffect(() => { if (open) { setForm({ name: "github", transport: "streamable_http", url: "", token: "" }); setErr(null); } }, [open]); async function add() { if (!form.url.trim()) { setErr("Enter the server URL."); return; } setBusy(true); setErr(null); try { const name = (form.name || "mcp_server").trim().replace(/[^a-zA-Z0-9_-]/g, "_"); let headers_ref: string | undefined; if (form.token.trim()) { const secName = `${name}_mcp_headers`; await api.createSecret(project.id, { name: secName, value: { Authorization: `Bearer ${form.token.trim()}` }, kind: "mcp_headers" }); headers_ref = `secret://proj/${secName}`; } const created = await api.createMcpClient(project.id, { name, transport: form.transport, url: form.url.trim(), headers_ref }); onAdded(created.id); } catch (e: any) { setErr(String(e?.message || e)); } finally { setBusy(false); } } return ( }>
setForm((f) => ({ ...f, name: e.target.value }))} placeholder="github" />
setForm((f) => ({ ...f, url: e.target.value }))} placeholder="https://api.githubcopilot.com/mcp/" /> setForm((f) => ({ ...f, token: e.target.value }))} placeholder="ghp_…" /> {err &&
{err}
}
Forge connects and lists the server's tools; toggle which ones agents and workflows can use.
); }