"use client"; import { useState, useMemo } from "react"; import { MCPServerConfig, MCPRemoteConfigZodSchema, MCPStdioConfigZodSchema, } from "app-types/mcp"; import { Input } from "./ui/input"; import { Button } from "./ui/button"; import { Label } from "./ui/label"; import { Textarea } from "./ui/textarea"; import JsonView from "./ui/json-view"; import { toast } from "sonner"; import { safe } from "ts-safe"; import { useRouter } from "next/navigation"; import { createDebounce, fetcher, isNull, safeJSONParse } from "lib/utils"; import { handleErrorWithToast } from "ui/shared-toast"; import { mutate } from "swr"; import { Loader } from "lucide-react"; import { isMaybeMCPServerConfig, isMaybeRemoteConfig, } from "lib/ai/mcp/is-mcp-config"; import { Alert, AlertDescription, AlertTitle } from "ui/alert"; import { z } from "zod"; import { useTranslations } from "next-intl"; import { existMcpClientByServerNameAction } from "@/app/api/mcp/actions"; interface MCPEditorProps { initialConfig?: MCPServerConfig; name?: string; id?: string; } const STDIO_ARGS_ENV_PLACEHOLDER = `/** STDIO Example */ { "command": "node", "args": ["index.js"], "env": { "OPENAI_API_KEY": "sk-...", } } /** SSE,Streamable HTTP Example */ { "url": "https://api.example.com", "headers": { "Authorization": "Bearer sk-..." } }`; export default function MCPEditor({ initialConfig, name: initialName, id, }: MCPEditorProps) { const t = useTranslations(); const shouldInsert = useMemo(() => isNull(id), [id]); const [isLoading, setIsLoading] = useState(false); const [jsonError, setJsonError] = useState(null); const [nameError, setNameError] = useState(null); const errorDebounce = useMemo(() => createDebounce(), []); // State for form fields const [name, setName] = useState(initialName ?? ""); const router = useRouter(); const [config, setConfig] = useState( initialConfig as MCPServerConfig, ); const [jsonString, setJsonString] = useState( initialConfig ? JSON.stringify(initialConfig, null, 2) : "", ); // Name validation schema const nameSchema = z.string().regex(/^[a-zA-Z0-9\-]+$/, { message: t("MCP.nameMustContainOnlyAlphanumericCharactersAndHyphens"), }); const validateName = (nameValue: string): boolean => { const result = nameSchema.safeParse(nameValue); if (!result.success) { setNameError( t("MCP.nameMustContainOnlyAlphanumericCharactersAndHyphens"), ); return false; } setNameError(null); return true; }; const saveDisabled = useMemo(() => { return ( name.trim() === "" || isLoading || !!jsonError || !!nameError || !isMaybeMCPServerConfig(config) ); }, [isLoading, jsonError, nameError, config, name]); // Validate const validateConfig = (jsonConfig: unknown): boolean => { const result = isMaybeRemoteConfig(jsonConfig) ? MCPRemoteConfigZodSchema.safeParse(jsonConfig) : MCPStdioConfigZodSchema.safeParse(jsonConfig); if (!result.success) { handleErrorWithToast(result.error, "mcp-editor-error"); } return result.success; }; // Handle save button click const handleSave = async () => { // Perform validation if (!validateConfig(config)) return; if (!name) { return handleErrorWithToast( new Error(t("MCP.nameIsRequired")), "mcp-editor-error", ); } if (!validateName(name)) { return handleErrorWithToast( new Error(t("MCP.nameMustContainOnlyAlphanumericCharactersAndHyphens")), "mcp-editor-error", ); } safe(() => setIsLoading(true)) .map(async () => { if (shouldInsert) { const exist = await existMcpClientByServerNameAction(name); if (exist) { throw new Error(t("MCP.nameAlreadyExists")); } } }) .map(() => fetcher("/api/mcp", { method: "POST", body: JSON.stringify({ name, config, id, }), }), ) .ifOk(() => { toast.success(t("MCP.configurationSavedSuccessfully")); mutate("/api/mcp/list"); router.push("/mcp"); }) .ifFail(handleErrorWithToast) .watch(() => setIsLoading(false)); }; const handleConfigChange = (data: string) => { setJsonString(data); const result = safeJSONParse(data); errorDebounce.clear(); if (result.success) { setConfig(result.value as MCPServerConfig); setJsonError(null); } else if (data.trim() !== "") { errorDebounce(() => { setJsonError( (result.error as Error)?.message ?? JSON.stringify(result.error, null, 2), ); }, 1000); } }; return ( <>
{/* Name field */}
{ setName(e.target.value); if (e.target.value) validateName(e.target.value); }} placeholder={t("MCP.enterMcpServerName")} className={nameError ? "border-destructive" : ""} /> {nameError &&

{nameError}

}
{/* Split view for config editor */}
{/* Left side: Textarea for editing */}