"use client";
import { PencilIcon, Trash2 } from "lucide-react";
import { type ReactNode, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { useI18n } from "@/core/i18n/hooks";
import { MCPConfigRequestError } from "@/core/mcp/api";
import {
useEnableMCPServer,
useMCPConfig,
useMCPServerMutation,
} from "@/core/mcp/hooks";
import {
formatMCPServerDefinition,
MCPServerDefinitionError,
parseMCPServerDefinition,
} from "@/core/mcp/parse";
import type { MCPServerConfig } from "@/core/mcp/types";
import { env } from "@/env";
import { CapabilityCard, CapabilityIcon } from "./capability-card";
type MCPPluginManagerProps = {
query?: string;
children?: ReactNode;
toolbar?: ReactNode;
};
export function MCPPluginManager(props: MCPPluginManagerProps) {
const { t } = useI18n();
const { config, isLoading, error } = useMCPConfig();
if (isLoading || error) {
return (
{props.toolbar}
{isLoading ? (
{t.common.loading}
) : (
{error instanceof MCPConfigRequestError && error.isAdminRequired
? t.settings.tools.adminRequired
: `${t.common.error} ${error?.message}`}
)}
{props.children}
);
}
return ;
}
function MCPServerList({
servers,
query = "",
children,
toolbar,
}: MCPPluginManagerProps & {
servers?: Record;
}) {
const { t } = useI18n();
const { isPending, mutate: enableMCPServer } = useEnableMCPServer();
const { isPending: isWriting, mutate: mutateServer } = useMCPServerMutation();
const [editor, setEditor] = useState<
{ mode: "add" } | { mode: "edit"; name: string } | null
>(null);
const [definition, setDefinition] = useState("");
const [definitionError, setDefinitionError] = useState(null);
const [pendingRemoval, setPendingRemoval] = useState(null);
const readOnly = env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true";
const current = servers ?? {};
const entries = Object.entries(current).filter(([name, config]) =>
`${name} ${config.description ?? ""}`
.toLowerCase()
.includes(query.trim().toLowerCase()),
);
const isMutating = isPending || isWriting;
function displayServerName(name: string | null) {
return name === null || name.length === 0
? t.settings.tools.unnamedServer
: name;
}
function closeEditor() {
setEditor(null);
setDefinition("");
setDefinitionError(null);
}
function openAddEditor() {
setDefinition("");
setDefinitionError(null);
setEditor({ mode: "add" });
}
function openEditEditor(name: string, config: MCPServerConfig) {
setDefinition(formatMCPServerDefinition(name, config));
setDefinitionError(null);
setEditor({ mode: "edit", name });
}
function handleSaveDefinition() {
if (editor === null) {
return;
}
let parsed: Record;
try {
parsed = parseMCPServerDefinition(definition);
} catch (parseError) {
if (parseError instanceof MCPServerDefinitionError) {
const messages = {
emptyDefinition: t.settings.tools.definitionEmpty,
invalidJson: t.settings.tools.definitionInvalidJson,
rootNotObject: t.settings.tools.definitionRootNotObject,
emptyServerMap: t.settings.tools.definitionNoServers,
serverConfigNotObject:
t.settings.tools.definitionServerNotObject.replace(
"{name}",
parseError.serverName ?? "",
),
};
setDefinitionError(messages[parseError.code]);
} else {
setDefinitionError(t.settings.tools.definitionInvalidJson);
}
return;
}
if (editor.mode === "add") {
const duplicate = Object.keys(parsed).find((name) =>
Object.hasOwn(current, name),
);
if (duplicate !== undefined) {
setDefinitionError(
t.settings.tools.serverAlreadyExists.replace("{name}", duplicate),
);
return;
}
setDefinitionError(null);
mutateServer(
{ operation: "create", servers: parsed },
{ onSuccess: closeEditor },
);
} else {
const editedEntries = Object.entries(parsed);
if (editedEntries.length !== 1) {
setDefinitionError(t.settings.tools.editSingleServer);
return;
}
const [editedName, editedConfig] = editedEntries[0]!;
if (editedName !== editor.name) {
setDefinitionError(
t.settings.tools.editServerNameMismatch.replace(
"{name}",
editor.name,
),
);
return;
}
setDefinitionError(null);
mutateServer(
{
operation: "update",
serverName: editor.name,
server: editedConfig,
},
{ onSuccess: closeEditor },
);
}
}
function handleRemove(name: string) {
mutateServer(
{ operation: "delete", serverName: name },
{ onSuccess: () => setPendingRemoval(null) },
);
}
return (
{toolbar ?? }
{entries.length === 0 && !children ? (
{query ? t.capabilities.noResults : t.settings.tools.empty}
) : (
{children}
{entries.map(([name, config]) => {
const displayName = displayServerName(name);
const actions = (
<>
enableMCPServer({ serverName: name, enabled: checked })
}
/>
>
);
return (
}
status={
<>
{config.enabled
? t.capabilities.enabled
: t.capabilities.disabled}
>
}
onDetails={
readOnly || isMutating
? undefined
: () => openEditEditor(name, config)
}
detailsLabel={`${t.capabilities.details} ${displayName}`}
>
{actions}
);
})}
)}
);
}