// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // SPDX-License-Identifier: MIT import { motion } from "framer-motion"; import { Blocks, Edit2, PencilRuler, RefreshCw, Trash } from "lucide-react"; import { useTranslations } from "next-intl"; import { useCallback, useState } from "react"; import { Tooltip } from "~/components/deer-flow/tooltip"; import { Button } from "~/components/ui/button"; import { Switch } from "~/components/ui/switch"; import { queryMCPServerMetadata } from "~/core/api"; import type { MCPServerMetadata } from "~/core/mcp"; import { cn } from "~/lib/utils"; import { AddMCPServerDialog } from "../dialogs/add-mcp-server-dialog"; import { EditMCPServerDialog } from "../dialogs/edit-mcp-server-dialog"; import type { Tab } from "./types"; export const MCPTab: Tab = ({ settings, onChange }) => { const t = useTranslations("settings.mcp"); const [servers, setServers] = useState( settings.mcp.servers, ); const [newlyAdded, setNewlyAdded] = useState(false); const [editingServer, setEditingServer] = useState(null); const handleAddServers = useCallback( (servers: MCPServerMetadata[]) => { const merged = mergeServers(settings.mcp.servers, servers); setServers(merged); onChange({ ...settings, mcp: { ...settings.mcp, servers: merged } }); setNewlyAdded(true); setTimeout(() => { setNewlyAdded(false); }, 1000); setTimeout(() => { document.getElementById("settings-content-scrollable")?.scrollTo({ top: 0, behavior: "smooth", }); }, 100); }, [onChange, settings], ); const handleDeleteServer = useCallback( (name: string) => { const merged = settings.mcp.servers.filter( (server) => server.name !== name, ); setServers(merged); onChange({ ...settings, mcp: { ...settings.mcp, servers: merged } }); }, [onChange, settings], ); const handleEditServer = useCallback(async (config: string) => { if (!editingServer) return false; try { const parsedConfig = JSON.parse(config) as { mcpServers?: Record }; if (!parsedConfig.mcpServers || typeof parsedConfig.mcpServers !== 'object') { console.error('Invalid configuration format: mcpServers not found'); return false; } const serverEntries = Object.entries(parsedConfig.mcpServers); if (serverEntries.length === 0) { console.error('No server configuration found in mcpServers'); return false; } const firstEntry = serverEntries[0]; if (!firstEntry) { console.error('Failed to get server configuration'); return false; } const [serverName, serverConfig] = firstEntry; // Update the server configuration const updatedServers = settings.mcp.servers.map(server => server.name === editingServer.name ? { ...server, ...serverConfig, name: serverName, // Allow renaming the server updatedAt: Date.now(), } : server ); setServers(updatedServers); onChange({ ...settings, mcp: { ...settings.mcp, servers: updatedServers } }); return true; } catch (error) { console.error('Failed to update server configuration:', error); return false; } }, [editingServer, onChange, settings]); const handleRefreshServers = useCallback(async (serverName?: string) => { try { // Create a new array with the updated server const updatedServers = await Promise.all( settings.mcp.servers.map(async (server) => { // Skip if this is not the server we want to refresh if (serverName && server.name !== serverName) { return server; } // Skip disabled servers unless explicitly requested if (!server.enabled && server.name !== serverName) { return server; } try { // Get the latest metadata const metadata = await queryMCPServerMetadata(server); // Create a new server object with preserved properties return { ...server, // Keep all existing properties ...metadata, // Apply metadata updates name: server.name, // Ensure name is preserved enabled: server.enabled, // Preserve the enabled state createdAt: server.createdAt, // Keep the original creation time updatedAt: Date.now(), // Update the last updated time }; } catch (error) { console.error(`Failed to refresh server ${server.name}:`, error); // Return the original server if refresh fails return server; } }) ); // Update the servers list setServers(updatedServers); onChange({ ...settings, mcp: { ...settings.mcp, servers: updatedServers } }); } catch (error) { console.error('Failed to refresh MCP servers:', error); } }, [onChange, settings]); const handleToggleServer = useCallback( async (name: string, enabled: boolean) => { const merged = settings.mcp.servers.map((server) => server.name === name ? { ...server, enabled } : server, ); setServers(merged); onChange({ ...settings, mcp: { ...settings.mcp, servers: merged } }); // Refresh server metadata when enabling a server if (enabled) { try { const server = merged.find(s => s.name === name); if (server) { const metadata = await queryMCPServerMetadata(server); const updatedServers = merged.map(s => s.name === name ? { ...s, ...metadata, name: s.name, enabled: true, createdAt: s.createdAt, updatedAt: Date.now(), } : s ); setServers(updatedServers); onChange({ ...settings, mcp: { ...settings.mcp, servers: updatedServers } }); } } catch (error) { console.error(`Failed to refresh server ${name}:`, error); } } }, [onChange, settings], ); const animationProps = { initial: { backgroundColor: "gray" }, animate: { backgroundColor: "transparent" }, transition: { duration: 1 }, style: { transition: "background-color 1s ease-out", }, }; return (

{t("title")}

{t("description")} {t("learnMore")}
    {servers.map((server) => { const isNew = server.createdAt && server.createdAt > Date.now() - 1000 * 60 * 60 * 1; return (
    { void handleToggleServer(server.name, checked); }} />
    {server.name}
    {!server.enabled && (
    {t("disabled")}
    )}
    {server.transport}
    {isNew && (
    {t("new")}
    )}
      {server.tools.map((tool) => (
    • {tool.name}
    • ))}
    ); })}
{editingServer && ( !open && setEditingServer(null)} onSave={async (config) => { const success = await handleEditServer(config); if (success) { setEditingServer(null); } return success; }} /> )}
); }; MCPTab.icon = Blocks; MCPTab.displayName = "MCP"; MCPTab.badge = "Beta"; MCPTab.displayName = "MCP"; function mergeServers( existing: MCPServerMetadata[], added: MCPServerMetadata[], ): MCPServerMetadata[] { const serverMap = new Map(existing.map((server) => [server.name, server])); for (const addedServer of added) { addedServer.createdAt = Date.now(); addedServer.updatedAt = Date.now(); serverMap.set(addedServer.name, addedServer); } const result = Array.from(serverMap.values()); result.sort((a, b) => b.createdAt - a.createdAt); return result; }