import { useState } from "react"; import Stack from "@moduletrace/twui/layout/Stack"; import { Plug } from "lucide-react"; import IconTile from "@/src/components/general/icon-tile"; import { McpTransports } from "@/src/dict/mcp-dict"; import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api"; import type { ExtensionTargets, McpServersOverviewPayload, PublicMcpServer } from "@/src/types"; import ExtensionStatusLine from "../../(partials)/extension-status-line"; import ExtensionAccessModal from "../../(partials)/extension-access-modal"; import SettingsListRow from "../../(partials)/settings-list-row"; import SettingsRowAction from "../../(partials)/settings-row-action"; import checkedAgo from "../../(utils)/checked-ago"; import extensionAccessLabel from "../../(utils)/extension-access-label"; import updatedAgo from "../../(utils)/updated-ago"; import McpServerModal from "./mcp-server-modal"; type Props = { server: PublicMcpServer; targets: ExtensionTargets; onChanged: (overview: McpServersOverviewPayload, msg: string) => void; onError: (msg: string) => void; }; /** * One MCP server: how it's reached and who gets it, and under it whether * it connected (when it was last checked, and why it failed). Edit and * Access open in a modal. */ export default function McpServerRow({ server, targets, onChanged, onError }: Props) { const [open, setOpen] = useState<"edit" | "access">(); const [busy, setBusy] = useState(false); const post = async ({ path, body, confirm }: { path: string; body: Record; confirm?: string }) => { if (busy || (confirm && !window.confirm(confirm))) return; setBusy(true); const res = await postAdminApi({ path, body }); setBusy(false); if (res.success && res.singleRes) onChanged(res.singleRes, res.msg || "Done."); else onError(res.msg || "That didn't work."); }; const saved = (overview: McpServersOverviewPayload, msg: string) => { setOpen(undefined); onChanged(overview, msg); }; const tools = `${server.tools.length} ${server.tools.length == 1 ? "tool" : "tools"}`; const when = server.last_synced_at ? ` · checked ${checkedAgo({ at: server.last_synced_at })}` : ""; const status = server.status == "ok" ? { text: `Connected · ${tools}${when}` } : server.status == "error" ? { text: `Couldn't connect${when}`, error: server.last_error } : { text: server.enabled ? "Not connected yet: Refresh to connect." : "Not connected yet." }; return ( } title={server.name} badge={server.enabled ? undefined : "Off"} subtitle={[ McpTransports.find((t) => t.value == server.transport)?.title, extensionAccessLabel({ access: server.access }), updatedAgo({ at: server.updated_at }), ] .filter(Boolean) .join(" · ")} > {targets.can_edit ? ( setOpen(open == "edit" ? undefined : "edit")} /> ) : null} setOpen(open == "access" ? undefined : "access")} /> {targets.can_edit ? ( post({ path: "mcps/sync-mcp", body: { mcp_server_id: server.id } })} /> ) : null} {targets.can_edit ? ( post({ path: "mcps/delete-mcp", body: { mcp_server_id: server.id }, confirm: `Delete ${server.name}? Bots lose its tools at once.`, }) } /> ) : null} {open == "edit" ? ( post({ path: "mcps/sync-mcp", body: { mcp_server_id: server.id, reinstall: true }, confirm: `Run ${server.name}'s setup command again?`, }) } onSaved={saved} onCancel={() => setOpen(undefined)} /> ) : null} {open == "access" ? ( id={server.id} name={server.name} access={server.access} targets={targets} path="mcps/set-mcp-access" onSaved={saved} onCancel={() => setOpen(undefined)} /> ) : null} ); }