diff --git a/src/components/mcp/McpImportDialog.tsx b/src/components/mcp/McpImportDialog.tsx new file mode 100644 index 0000000..6c1572c --- /dev/null +++ b/src/components/mcp/McpImportDialog.tsx @@ -0,0 +1,178 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { Download, FileJson, FileCode, Sparkles } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { mcpApi } from "@/lib/api"; +import type { AppId } from "@/lib/api/types"; + +interface McpImportDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onImportComplete?: () => void; +} + +/** + * MCP 导入对话框 + * 支持从 Claude/Codex/Gemini 的 live 配置导入 MCP 服务器 + */ +const McpImportDialog: React.FC = ({ + open, + onOpenChange, + onImportComplete, +}) => { + const { t } = useTranslation(); + const [importing, setImporting] = useState(false); + const [selectedSource, setSelectedSource] = useState(null); + + const handleImport = async (source: AppId) => { + setImporting(true); + setSelectedSource(source); + + try { + let count = 0; + + switch (source) { + case "claude": + count = await mcpApi.importFromClaude(); + break; + case "codex": + count = await mcpApi.importFromCodex(); + break; + case "gemini": + count = await mcpApi.importFromGemini(); + break; + } + + if (count > 0) { + toast.success(t("mcp.unifiedPanel.import.success", { count })); + onImportComplete?.(); + onOpenChange(false); + } else { + toast.info(t("mcp.unifiedPanel.import.noServersFound")); + } + } catch (error) { + toast.error(t("common.error"), { + description: String(error), + }); + } finally { + setImporting(false); + setSelectedSource(null); + } + }; + + const importSources = [ + { + id: "claude" as AppId, + name: t("mcp.unifiedPanel.apps.claude"), + description: t("mcp.unifiedPanel.import.fromClaudeDesc"), + icon: FileJson, + color: "text-blue-600 dark:text-blue-400", + bgColor: "bg-blue-100 dark:bg-blue-900/30", + }, + { + id: "codex" as AppId, + name: t("mcp.unifiedPanel.apps.codex"), + description: t("mcp.unifiedPanel.import.fromCodexDesc"), + icon: FileCode, + color: "text-green-600 dark:text-green-400", + bgColor: "bg-green-100 dark:bg-green-900/30", + }, + { + id: "gemini" as AppId, + name: t("mcp.unifiedPanel.apps.gemini"), + description: t("mcp.unifiedPanel.import.fromGeminiDesc"), + icon: Sparkles, + color: "text-purple-600 dark:text-purple-400", + bgColor: "bg-purple-100 dark:bg-purple-900/30", + }, + ]; + + return ( + + + + {t("mcp.unifiedPanel.import.title")} + + +
+

+ {t("mcp.unifiedPanel.import.description")} +

+ +
+ {importSources.map((source) => { + const Icon = source.icon; + const isImporting = importing && selectedSource === source.id; + + return ( + + ); + })} +
+
+ + + + +
+
+ ); +}; + +export default McpImportDialog; diff --git a/src/components/mcp/UnifiedMcpPanel.tsx b/src/components/mcp/UnifiedMcpPanel.tsx index fed80ed..6afdb96 100644 --- a/src/components/mcp/UnifiedMcpPanel.tsx +++ b/src/components/mcp/UnifiedMcpPanel.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Plus, Server, Check, RefreshCw } from "lucide-react"; +import { Plus, Server, Check, RefreshCw, Download } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -14,6 +14,7 @@ import { useAllMcpServers, useToggleMcpApp, useSyncAllMcpServers } from "@/hooks 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"; @@ -36,6 +37,7 @@ const UnifiedMcpPanel: React.FC = ({ }) => { const { t } = useTranslation(); const [isFormOpen, setIsFormOpen] = useState(false); + const [isImportOpen, setIsImportOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [confirmDialog, setConfirmDialog] = useState<{ isOpen: boolean; @@ -45,7 +47,7 @@ const UnifiedMcpPanel: React.FC = ({ } | null>(null); // Queries and Mutations - const { data: serversMap, isLoading } = useAllMcpServers(); + const { data: serversMap, isLoading, refetch } = useAllMcpServers(); const toggleAppMutation = useToggleMcpApp(); const deleteServerMutation = useDeleteMcpServer(); const syncAllMutation = useSyncAllMcpServers(); @@ -126,6 +128,11 @@ const UnifiedMcpPanel: React.FC = ({ setEditingId(null); }; + const handleImportComplete = () => { + // Refresh the servers list after import + refetch(); + }; + return ( <> @@ -134,6 +141,15 @@ const UnifiedMcpPanel: React.FC = ({
{t("mcp.unifiedPanel.title")}
+