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 (
);
}
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 (
);
}