import { useState } from "react"; import { Link } from "react-router"; import { useSWRConfig } from "swr"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { ChevronDownIcon, PlusIcon } from "@heroicons/react/16/solid"; import { EllipsisVerticalIcon } from "@heroicons/react/20/solid"; import type { McpServer } from "@qltysh/fabro-api-client"; import { Badge, Muted, Panel, PanelSkeleton, SettingsPageIntro, } from "../components/settings-panel"; import { ConfirmDialog } from "../components/ui"; import { useToast } from "../components/toast"; import { ApiError, apiData, mcpServersApi } from "../lib/api-client"; import { removeMcpServerFromList } from "../lib/mcp-server-cache"; import { MCP_TRANSPORT_KINDS, type McpTransportKind } from "../lib/mcp-transport-kinds"; import { queryKeys } from "../lib/query-keys"; import { useMcpServers } from "../lib/queries"; const MENU_ITEM_CLASS = "flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-fg-3 transition-colors data-focus:bg-overlay data-focus:text-fg data-focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-60"; const MENU_ITEM_DANGER_CLASS = "flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-coral transition-colors data-focus:bg-coral/10 data-focus:text-coral data-focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-60"; const NEW_BUTTON_CLASS = "inline-flex items-center gap-1.5 rounded-md border border-line bg-panel/80 px-2.5 py-1 text-sm font-medium text-fg-3 transition-colors hover:border-line-strong hover:bg-panel hover:text-fg disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-line disabled:hover:bg-panel/80 disabled:hover:text-fg-3"; const DESCRIPTION = "MCP servers are server-managed tool providers stored on this Fabro server. Workflows can enable a stored server by name without embedding connection details in each run."; export function meta() { return [{ title: "MCP servers — Fabro" }]; } export default function SettingsMcps() { const query = useMcpServers(); return (
} /> {query.data ? ( ) : query.error ? (
Couldn't load MCP servers. Please try again.
) : ( )}
); } function NewMcpServerMenu() { return ( {MCP_TRANSPORT_KINDS.map((kind) => ( {transportLabel(kind)} ))} ); } function McpServersPanel({ servers }: { servers: McpServer[] }) { const { mutate } = useSWRConfig(); const toast = useToast(); const [pendingDelete, setPendingDelete] = useState(null); const [deleting, setDeleting] = useState(false); async function confirmDelete() { if (!pendingDelete) return; const target = pendingDelete; setDeleting(true); try { await apiData(() => mcpServersApi.deleteMcpServer(target.id, target.revision)); await mutate( queryKeys.mcpServers.list(), (current) => removeMcpServerFromList(current, target.id), { revalidate: false }, ); toast.push({ message: `MCP server “${target.id}” deleted.` }); setPendingDelete(null); void mutate(queryKeys.mcpServers.list()); } catch (cause) { if (cause instanceof ApiError && cause.status === 409) { await mutate(queryKeys.mcpServers.list()); toast.push({ tone: "error", message: "This MCP server changed before it could be deleted. Refresh and try again.", }); } else { toast.push({ tone: "error", message: cause instanceof ApiError && cause.message ? cause.message : "Couldn't delete the MCP server. Please try again.", }); } } finally { setDeleting(false); } } return ( <> {servers.length === 0 ? (
No MCP servers defined yet.
) : ( servers.map((server) => ( setPendingDelete(server)} /> )) )}
Delete {pendingDelete?.id}? Workflows that enable this server will fail until it is recreated. } confirmLabel="Delete" pendingLabel="Deleting…" pending={deleting} onConfirm={confirmDelete} onCancel={() => { if (!deleting) setPendingDelete(null); }} /> ); } function McpServerRow({ server, disabled, onDelete, }: { server: McpServer; disabled: boolean; onDelete: () => void; }) { const summary = transportSummary(server); return (
{server.display_name} {server.transport.type}
{server.id} {server.description ? {server.description} : null}
{summary ?? No transport details}
); } function transportSummary(server: McpServer): string | null { switch (server.transport.type) { case "stdio": return server.transport.command.join(" ") || null; case "http": return server.transport.url; case "sandbox": { const command = server.transport.command.join(" "); return command ? `${command} · port ${server.transport.port}` : `port ${server.transport.port}`; } } } function transportLabel(kind: McpTransportKind): string { return kind.charAt(0).toUpperCase() + kind.slice(1); } function RowMenu({ server, disabled, onDelete, }: { server: McpServer; disabled: boolean; onDelete: () => void; }) { return ( Edit
); }