import React, { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Plus, Server, Check, RefreshCw, Download } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Checkbox } from "@/components/ui/checkbox"; import { useAllMcpServers, useToggleMcpApp, useSyncAllMcpServers } from "@/hooks/useMcp"; import type { McpServer } from "@/types"; import type { AppId } from "@/lib/api/types"; import McpFormModal from "./McpFormModal"; import McpImportDialog from "./McpImportDialog"; import { ConfirmDialog } from "../ConfirmDialog"; import { useDeleteMcpServer } from "@/hooks/useMcp"; import { Edit3, Trash2 } from "lucide-react"; import { settingsApi } from "@/lib/api"; import { mcpPresets } from "@/config/mcpPresets"; import { toast } from "sonner"; interface UnifiedMcpPanelProps { open: boolean; onOpenChange: (open: boolean) => void; } /** * 统一 MCP 管理面板 * v3.7.0 新架构:所有 MCP 服务器统一管理,每个服务器通过复选框控制应用到哪些客户端 */ const UnifiedMcpPanel: React.FC = ({ open, onOpenChange, }) => { const { t } = useTranslation(); const [isFormOpen, setIsFormOpen] = useState(false); const [isImportOpen, setIsImportOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [confirmDialog, setConfirmDialog] = useState<{ isOpen: boolean; title: string; message: string; onConfirm: () => void; } | null>(null); // Queries and Mutations const { data: serversMap, isLoading, refetch } = useAllMcpServers(); const toggleAppMutation = useToggleMcpApp(); const deleteServerMutation = useDeleteMcpServer(); const syncAllMutation = useSyncAllMcpServers(); // Convert serversMap to array for easier rendering const serverEntries = useMemo((): Array<[string, McpServer]> => { if (!serversMap) return []; return Object.entries(serversMap); }, [serversMap]); // Count enabled servers per app const enabledCounts = useMemo(() => { const counts = { claude: 0, codex: 0, gemini: 0 }; serverEntries.forEach(([_, server]) => { if (server.apps.claude) counts.claude++; if (server.apps.codex) counts.codex++; if (server.apps.gemini) counts.gemini++; }); return counts; }, [serverEntries]); const handleToggleApp = async ( serverId: string, app: AppId, enabled: boolean, ) => { try { await toggleAppMutation.mutateAsync({ serverId, app, enabled }); } catch (error) { toast.error(t("common.error"), { description: String(error), }); } }; const handleEdit = (id: string) => { setEditingId(id); setIsFormOpen(true); }; const handleAdd = () => { setEditingId(null); setIsFormOpen(true); }; const handleDelete = (id: string) => { setConfirmDialog({ isOpen: true, title: t("mcp.unifiedPanel.deleteServer"), message: t("mcp.unifiedPanel.deleteConfirm", { id }), onConfirm: async () => { try { await deleteServerMutation.mutateAsync(id); setConfirmDialog(null); toast.success(t("common.success")); } catch (error) { toast.error(t("common.error"), { description: String(error), }); } }, }); }; const handleSyncAll = async () => { try { await syncAllMutation.mutateAsync(); toast.success(t("mcp.unifiedPanel.syncAllSuccess")); } catch (error) { toast.error(t("common.error"), { description: String(error), }); } }; const handleCloseForm = () => { setIsFormOpen(false); setEditingId(null); }; const handleImportComplete = () => { // Refresh the servers list after import refetch(); }; return ( <>
{t("mcp.unifiedPanel.title")}
{/* Info Section */}
{t("mcp.serverCount", { count: serverEntries.length })} ·{" "} {t("mcp.unifiedPanel.apps.claude")}: {enabledCounts.claude} ·{" "} {t("mcp.unifiedPanel.apps.codex")}: {enabledCounts.codex} ·{" "} {t("mcp.unifiedPanel.apps.gemini")}: {enabledCounts.gemini}
{/* Content - Scrollable */}
{isLoading ? (
{t("mcp.loading")}
) : serverEntries.length === 0 ? (

{t("mcp.unifiedPanel.noServers")}

{t("mcp.emptyDescription")}

) : (
{serverEntries.map(([id, server]) => ( ))}
)}
{/* Form Modal */} {isFormOpen && ( { setIsFormOpen(false); setEditingId(null); }} onClose={handleCloseForm} unified /> )} {/* Import Dialog */} {/* Confirm Dialog */} {confirmDialog && ( setConfirmDialog(null)} /> )} ); }; /** * 统一 MCP 列表项组件 * 展示服务器名称、描述,以及三个应用的复选框 */ interface UnifiedMcpListItemProps { id: string; server: McpServer; onToggleApp: (serverId: string, app: AppId, enabled: boolean) => void; onEdit: (id: string) => void; onDelete: (id: string) => void; } const UnifiedMcpListItem: React.FC = ({ id, server, onToggleApp, onEdit, onDelete, }) => { const { t } = useTranslation(); const name = server.name || id; const description = server.description || ""; // 匹配预设元信息 const meta = mcpPresets.find((p) => p.id === id); const docsUrl = server.docs || meta?.docs; const homepageUrl = server.homepage || meta?.homepage; const tags = server.tags || meta?.tags; const openDocs = async () => { const url = docsUrl || homepageUrl; if (!url) return; try { await settingsApi.openExternal(url); } catch { // ignore } }; return (
{/* 左侧:服务器信息 */}

{name}

{description && (

{description}

)} {!description && tags && tags.length > 0 && (

{tags.join(", ")}

)}
{/* 中间:应用复选框 */}
onToggleApp(id, "claude", checked === true) } />
onToggleApp(id, "codex", checked === true) } />
onToggleApp(id, "gemini", checked === true) } />
{/* 右侧:操作按钮 */}
{docsUrl && ( )}
); }; export default UnifiedMcpPanel;