"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} ); })}
)} !open && !isWriting && closeEditor()} > {editor?.mode === "edit" ? t.settings.tools.editServer : t.settings.tools.addServer} {editor?.mode === "edit" ? t.settings.tools.editServerDescription.replace( "{name}", editor.name, ) : t.settings.tools.addServerDescription}