refactor(mcp): complete v3.7.0 cleanup - remove legacy code and warnings
This commit finalizes the v3.7.0 unified MCP architecture migration by removing all deprecated code paths and eliminating compiler warnings. Frontend Changes (~950 lines removed): - Remove deprecated components: McpPanel, McpListItem, McpToggle - Remove deprecated hook: useMcpActions - Remove unused API methods: importFrom*, syncEnabledTo*, syncAllServers - Simplify McpFormModal by removing dual-mode logic (unified/legacy) - Remove syncOtherSide checkbox and conflict detection - Clean up unused imports and state variables - Delete associated test files Backend Changes (~400 lines cleaned): - Remove unused Tauri commands: import_mcp_from_*, sync_enabled_mcp_to_* - Delete unused Gemini MCP functions: get_mcp_status, upsert/delete_mcp_server - Add #[allow(deprecated)] to compatibility layer commands - Add #[allow(dead_code)] to legacy helper functions for future migration - Simplify boolean expression in mcp.rs per Clippy suggestion API Deprecation: - Mark legacy APIs with @deprecated JSDoc (getConfig, upsertServerInConfig, etc.) - Preserve backward compatibility for v3.x, planned removal in v4.0 Verification: - ✅ Zero TypeScript errors (pnpm typecheck) - ✅ Zero Clippy warnings (cargo clippy) - ✅ All code formatted (prettier + cargo fmt) - ✅ Builds successfully Total cleanup: ~1,350 lines of code removed/marked Breaking changes: None (all legacy APIs still functional)
This commit is contained in:
@@ -302,10 +302,7 @@ function App() {
|
||||
appId={activeApp}
|
||||
/>
|
||||
|
||||
<UnifiedMcpPanel
|
||||
open={isMcpOpen}
|
||||
onOpenChange={setIsMcpOpen}
|
||||
/>
|
||||
<UnifiedMcpPanel open={isMcpOpen} onOpenChange={setIsMcpOpen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import React, { useMemo, useState, useEffect } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Save,
|
||||
Plus,
|
||||
AlertCircle,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
import { Save, Plus, AlertCircle, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -19,7 +12,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { mcpApi, type AppId } from "@/lib/api";
|
||||
import type { AppId } from "@/lib/api/types";
|
||||
import { McpServer, McpServerSpec } from "@/types";
|
||||
import { mcpPresets, getMcpPresetWithDescription } from "@/config/mcpPresets";
|
||||
import McpWizardModal from "./McpWizardModal";
|
||||
@@ -40,14 +33,9 @@ interface McpFormModalProps {
|
||||
appId: AppId;
|
||||
editingId?: string;
|
||||
initialData?: McpServer;
|
||||
onSave: (
|
||||
id: string,
|
||||
server: McpServer,
|
||||
options?: { syncOtherSide?: boolean },
|
||||
) => Promise<void>;
|
||||
onSave: () => Promise<void>; // v3.7.0: 简化为仅用于关闭表单的回调
|
||||
onClose: () => void;
|
||||
existingIds?: string[];
|
||||
unified?: boolean; // 统一模式:使用 useUpsertMcpServer 而非传统回调
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,13 +50,11 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
onSave,
|
||||
onClose,
|
||||
existingIds = [],
|
||||
unified = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { formatTomlError, validateTomlConfig, validateJsonConfig } =
|
||||
useMcpValidation();
|
||||
|
||||
// 统一模式下使用 mutation
|
||||
const upsertMutation = useUpsertMcpServer();
|
||||
|
||||
const [formId, setFormId] = useState(
|
||||
@@ -112,38 +98,9 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const [idError, setIdError] = useState("");
|
||||
const [syncOtherSide, setSyncOtherSide] = useState(false);
|
||||
const [otherSideHasConflict, setOtherSideHasConflict] = useState(false);
|
||||
|
||||
// 判断是否使用 TOML 格式
|
||||
const useToml = appId === "codex";
|
||||
const syncTargetLabel = appId === "claude" ? "Codex" : "Claude";
|
||||
const otherAppType: AppId = appId === "claude" ? "codex" : "claude";
|
||||
const syncCheckboxId = useMemo(() => `sync-other-side-${appId}`, [appId]);
|
||||
|
||||
// 检测另一侧是否有同名 MCP
|
||||
useEffect(() => {
|
||||
const checkOtherSide = async () => {
|
||||
const currentId = formId.trim();
|
||||
if (!currentId) {
|
||||
setOtherSideHasConflict(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const otherConfig = await mcpApi.getConfig(otherAppType);
|
||||
const hasConflict = Object.keys(otherConfig.servers || {}).includes(
|
||||
currentId,
|
||||
);
|
||||
setOtherSideHasConflict(hasConflict);
|
||||
} catch (error) {
|
||||
console.error("检查另一侧 MCP 配置失败:", error);
|
||||
setOtherSideHasConflict(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkOtherSide();
|
||||
}, [formId, otherAppType]);
|
||||
|
||||
const wizardInitialSpec = useMemo(() => {
|
||||
const fallback = initialData?.server;
|
||||
@@ -377,22 +334,13 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
name: finalName,
|
||||
server: serverSpec,
|
||||
// 确保 apps 字段始终存在(v3.7.0 新架构必需)
|
||||
apps: initialData?.apps || { claude: false, codex: false, gemini: false },
|
||||
apps: initialData?.apps || {
|
||||
claude: false,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
},
|
||||
};
|
||||
|
||||
// 统一模式下无需再初始化 apps(上面已经处理)
|
||||
// 传统模式需要设置 enabled 字段
|
||||
if (!unified) {
|
||||
// 传统模式:新增 MCP 时默认启用(enabled=true)
|
||||
// 编辑模式下保留原有的 enabled 状态
|
||||
if (initialData?.enabled !== undefined) {
|
||||
entry.enabled = initialData.enabled;
|
||||
} else {
|
||||
// 新增模式:默认启用
|
||||
entry.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
const descriptionTrimmed = formDescription.trim();
|
||||
if (descriptionTrimmed) {
|
||||
entry.description = descriptionTrimmed;
|
||||
@@ -424,16 +372,10 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
delete entry.tags;
|
||||
}
|
||||
|
||||
// 显式等待保存流程
|
||||
if (unified) {
|
||||
// 统一模式:调用 useUpsertMcpServer mutation
|
||||
await upsertMutation.mutateAsync(entry);
|
||||
toast.success(t("common.success"));
|
||||
onClose();
|
||||
} else {
|
||||
// 传统模式:调用父组件回调
|
||||
await onSave(trimmedId, entry, { syncOtherSide });
|
||||
}
|
||||
// 保存到统一配置
|
||||
await upsertMutation.mutateAsync(entry);
|
||||
toast.success(t("common.success"));
|
||||
await onSave(); // 通知父组件关闭表单
|
||||
} catch (error: any) {
|
||||
const detail = extractErrorMessage(error);
|
||||
const mapped = translateMcpBackendError(detail, t);
|
||||
@@ -646,58 +588,24 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<DialogFooter className="flex-col sm:flex-row sm:justify-between gap-3 pt-4">
|
||||
{/* 双端同步选项 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id={syncCheckboxId}
|
||||
type="checkbox"
|
||||
className="h-4 w-4 rounded border-border-default text-emerald-600 focus:ring-emerald-500 dark:bg-gray-800"
|
||||
checked={syncOtherSide}
|
||||
onChange={(event) => setSyncOtherSide(event.target.checked)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={syncCheckboxId}
|
||||
className="text-sm text-gray-700 dark:text-gray-300 cursor-pointer select-none"
|
||||
title={t("mcp.form.syncOtherSideHint", {
|
||||
target: syncTargetLabel,
|
||||
})}
|
||||
>
|
||||
{t("mcp.form.syncOtherSide", { target: syncTargetLabel })}
|
||||
</label>
|
||||
</div>
|
||||
{syncOtherSide && otherSideHasConflict && (
|
||||
<div className="flex items-center gap-1.5 text-amber-600 dark:text-amber-400">
|
||||
<AlertTriangle size={14} />
|
||||
<span className="text-xs font-medium">
|
||||
{t("mcp.form.willOverwriteWarning", {
|
||||
target: syncTargetLabel,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-end gap-3 pt-4">
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={saving || (!isEditing && !!idError)}
|
||||
variant="mcp"
|
||||
>
|
||||
{isEditing ? <Save size={16} /> : <Plus size={16} />}
|
||||
{saving
|
||||
? t("common.saving")
|
||||
: isEditing
|
||||
? t("common.save")
|
||||
: t("common.add")}
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={saving || (!isEditing && !!idError)}
|
||||
variant="mcp"
|
||||
>
|
||||
{isEditing ? <Save size={16} /> : <Plus size={16} />}
|
||||
{saving
|
||||
? t("common.saving")
|
||||
: isEditing
|
||||
? t("common.save")
|
||||
: t("common.add")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Edit3, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { settingsApi } from "@/lib/api";
|
||||
import { McpServer } from "@/types";
|
||||
import { mcpPresets } from "@/config/mcpPresets";
|
||||
import McpToggle from "./McpToggle";
|
||||
|
||||
interface McpListItemProps {
|
||||
id: string;
|
||||
server: McpServer;
|
||||
onToggle: (id: string, enabled: boolean) => void;
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP 列表项组件
|
||||
* 每个 MCP 占一行,左侧是 Toggle 开关,中间是名称和详细信息,右侧是编辑和删除按钮
|
||||
*/
|
||||
const McpListItem: React.FC<McpListItemProps> = ({
|
||||
id,
|
||||
server,
|
||||
onToggle,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 仅当显式为 true 时视为启用;避免 undefined 被误判为启用
|
||||
const enabled = server.enabled === true;
|
||||
const name = server.name || id;
|
||||
|
||||
// 只显示 description,没有则留空
|
||||
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 (
|
||||
<div className="h-16 rounded-lg border border-border-default bg-card p-4 transition-[border-color,box-shadow] duration-200 hover:border-border-hover hover:shadow-sm">
|
||||
<div className="flex items-center gap-4 h-full">
|
||||
{/* 左侧:Toggle 开关 */}
|
||||
<div className="flex-shrink-0">
|
||||
<McpToggle
|
||||
enabled={enabled}
|
||||
onChange={(newEnabled) => onToggle(id, newEnabled)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 中间:名称和详细信息 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-gray-900 dark:text-gray-100 mb-1">
|
||||
{name}
|
||||
</h3>
|
||||
{description && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 truncate">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{!description && tags && tags.length > 0 && (
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 truncate">
|
||||
{tags.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
{/* 预设标记已移除 */}
|
||||
</div>
|
||||
|
||||
{/* 右侧:操作按钮 */}
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{docsUrl && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={openDocs}
|
||||
title={t("mcp.presets.docs")}
|
||||
>
|
||||
{t("mcp.presets.docs")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onEdit(id)}
|
||||
title={t("common.edit")}
|
||||
>
|
||||
<Edit3 size={16} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onDelete(id)}
|
||||
className="hover:text-red-500 hover:bg-red-100 dark:hover:text-red-400 dark:hover:bg-red-500/10"
|
||||
title={t("common.delete")}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default McpListItem;
|
||||
@@ -1,236 +0,0 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Plus, Server, Check } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { type AppId } from "@/lib/api";
|
||||
import { McpServer } from "@/types";
|
||||
import { useMcpActions } from "@/hooks/useMcpActions";
|
||||
import McpListItem from "./McpListItem";
|
||||
import McpFormModal from "./McpFormModal";
|
||||
import { ConfirmDialog } from "../ConfirmDialog";
|
||||
|
||||
interface McpPanelProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
appId: AppId;
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP 管理面板
|
||||
* 采用与主界面一致的设计风格,右上角添加按钮,每个 MCP 占一行
|
||||
*/
|
||||
const McpPanel: React.FC<McpPanelProps> = ({ open, onOpenChange, appId }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [confirmDialog, setConfirmDialog] = useState<{
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
onConfirm: () => void;
|
||||
} | null>(null);
|
||||
|
||||
// Use MCP actions hook
|
||||
const { servers, loading, reload, toggleEnabled, saveServer, deleteServer } =
|
||||
useMcpActions(appId);
|
||||
|
||||
useEffect(() => {
|
||||
const setup = async () => {
|
||||
try {
|
||||
// Initialize: only import existing MCPs from corresponding client
|
||||
if (appId === "claude") {
|
||||
const mcpApi = await import("@/lib/api").then((m) => m.mcpApi);
|
||||
await mcpApi.importFromClaude();
|
||||
} else if (appId === "codex") {
|
||||
const mcpApi = await import("@/lib/api").then((m) => m.mcpApi);
|
||||
await mcpApi.importFromCodex();
|
||||
} else if (appId === "gemini") {
|
||||
const mcpApi = await import("@/lib/api").then((m) => m.mcpApi);
|
||||
await mcpApi.importFromGemini();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("MCP initialization import failed (ignored)", e);
|
||||
} finally {
|
||||
await reload();
|
||||
}
|
||||
};
|
||||
setup();
|
||||
// Re-initialize when appId changes
|
||||
}, [appId, reload]);
|
||||
|
||||
const handleEdit = (id: string) => {
|
||||
setEditingId(id);
|
||||
setIsFormOpen(true);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingId(null);
|
||||
setIsFormOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setConfirmDialog({
|
||||
isOpen: true,
|
||||
title: t("mcp.confirm.deleteTitle"),
|
||||
message: t("mcp.confirm.deleteMessage", { id }),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await deleteServer(id);
|
||||
setConfirmDialog(null);
|
||||
} catch (e) {
|
||||
// Error already handled by useMcpActions
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async (
|
||||
id: string,
|
||||
server: McpServer,
|
||||
options?: { syncOtherSide?: boolean },
|
||||
) => {
|
||||
await saveServer(id, server, options);
|
||||
setIsFormOpen(false);
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
const handleCloseForm = () => {
|
||||
setIsFormOpen(false);
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
const serverEntries = useMemo(
|
||||
() => Object.entries(servers) as Array<[string, McpServer]>,
|
||||
[servers],
|
||||
);
|
||||
|
||||
const enabledCount = useMemo(
|
||||
() => serverEntries.filter(([_, server]) => server.enabled).length,
|
||||
[serverEntries],
|
||||
);
|
||||
|
||||
const panelTitle =
|
||||
appId === "claude"
|
||||
? t("mcp.claudeTitle")
|
||||
: appId === "codex"
|
||||
? t("mcp.codexTitle")
|
||||
: t("mcp.geminiTitle");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[85vh] min-h-[600px] flex flex-col">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between pr-8">
|
||||
<DialogTitle>{panelTitle}</DialogTitle>
|
||||
<Button type="button" variant="mcp" onClick={handleAdd}>
|
||||
<Plus size={16} />
|
||||
{t("mcp.add")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="flex-shrink-0 px-6 py-4">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t("mcp.serverCount", { count: Object.keys(servers).length })} ·{" "}
|
||||
{t("mcp.enabledCount", { count: enabledCount })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content - Scrollable */}
|
||||
<div className="flex-1 overflow-y-auto px-6 pb-4">
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
{t("mcp.loading")}
|
||||
</div>
|
||||
) : (
|
||||
(() => {
|
||||
const hasAny = serverEntries.length > 0;
|
||||
if (!hasAny) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
|
||||
<Server
|
||||
size={24}
|
||||
className="text-gray-400 dark:text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
{t("mcp.empty")}
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
||||
{t("mcp.emptyDescription")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* 已安装 */}
|
||||
{serverEntries.map(([id, server]) => (
|
||||
<McpListItem
|
||||
key={`installed-${id}`}
|
||||
id={id}
|
||||
server={server}
|
||||
onToggle={toggleEnabled}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* 预设已移至"新增 MCP"面板中展示与套用 */}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="mcp"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
<Check size={16} />
|
||||
{t("common.done")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Form Modal */}
|
||||
{isFormOpen && (
|
||||
<McpFormModal
|
||||
appId={appId}
|
||||
editingId={editingId || undefined}
|
||||
initialData={editingId ? servers[editingId] : undefined}
|
||||
existingIds={Object.keys(servers)}
|
||||
onSave={handleSave}
|
||||
onClose={handleCloseForm}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Confirm Dialog */}
|
||||
{confirmDialog && (
|
||||
<ConfirmDialog
|
||||
isOpen={confirmDialog.isOpen}
|
||||
title={confirmDialog.title}
|
||||
message={confirmDialog.message}
|
||||
onConfirm={confirmDialog.onConfirm}
|
||||
onCancel={() => setConfirmDialog(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default McpPanel;
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
interface McpToggleProps {
|
||||
enabled: boolean;
|
||||
onChange: (enabled: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle 开关组件
|
||||
* 启用时为淡绿色,禁用时为灰色
|
||||
*/
|
||||
const McpToggle: React.FC<McpToggleProps> = ({
|
||||
enabled,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(!enabled)}
|
||||
className={`
|
||||
relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500/20
|
||||
${enabled ? "bg-emerald-500 dark:bg-emerald-600" : "bg-gray-300 dark:bg-gray-600"}
|
||||
${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
|
||||
${enabled ? "translate-x-6" : "translate-x-1"}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default McpToggle;
|
||||
@@ -193,14 +193,15 @@ const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({
|
||||
<McpFormModal
|
||||
appId="claude" // Default to claude for unified panel
|
||||
editingId={editingId || undefined}
|
||||
initialData={editingId && serversMap ? serversMap[editingId] : undefined}
|
||||
initialData={
|
||||
editingId && serversMap ? serversMap[editingId] : undefined
|
||||
}
|
||||
existingIds={serversMap ? Object.keys(serversMap) : []}
|
||||
onSave={async () => {
|
||||
setIsFormOpen(false);
|
||||
setEditingId(null);
|
||||
}}
|
||||
onClose={handleCloseForm}
|
||||
unified
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
@@ -12,7 +12,7 @@ const Checkbox = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -22,7 +22,7 @@ const Checkbox = React.forwardRef<
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox }
|
||||
export { Checkbox };
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { mcpApi } from '@/lib/api/mcp';
|
||||
import type { McpServer } from '@/types';
|
||||
import type { AppId } from '@/lib/api/types';
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { mcpApi } from "@/lib/api/mcp";
|
||||
import type { McpServer } from "@/types";
|
||||
import type { AppId } from "@/lib/api/types";
|
||||
|
||||
/**
|
||||
* 查询所有 MCP 服务器(统一管理)
|
||||
*/
|
||||
export function useAllMcpServers() {
|
||||
return useQuery({
|
||||
queryKey: ['mcp', 'all'],
|
||||
queryKey: ["mcp", "all"],
|
||||
queryFn: () => mcpApi.getAllServers(),
|
||||
});
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export function useUpsertMcpServer() {
|
||||
return useMutation({
|
||||
mutationFn: (server: McpServer) => mcpApi.upsertUnifiedServer(server),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['mcp', 'all'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["mcp", "all"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export function useToggleMcpApp() {
|
||||
enabled: boolean;
|
||||
}) => mcpApi.toggleApp(serverId, app, enabled),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['mcp', 'all'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["mcp", "all"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export function useDeleteMcpServer() {
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => mcpApi.deleteUnifiedServer(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['mcp', 'all'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["mcp", "all"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { mcpApi, type AppId } from "@/lib/api";
|
||||
import type { McpServer } from "@/types";
|
||||
import {
|
||||
extractErrorMessage,
|
||||
translateMcpBackendError,
|
||||
} from "@/utils/errorUtils";
|
||||
|
||||
export interface UseMcpActionsResult {
|
||||
servers: Record<string, McpServer>;
|
||||
loading: boolean;
|
||||
reload: () => Promise<void>;
|
||||
toggleEnabled: (id: string, enabled: boolean) => Promise<void>;
|
||||
saveServer: (
|
||||
id: string,
|
||||
server: McpServer,
|
||||
options?: { syncOtherSide?: boolean },
|
||||
) => Promise<void>;
|
||||
deleteServer: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* useMcpActions - MCP management business logic
|
||||
* Responsibilities:
|
||||
* - Load MCP servers
|
||||
* - Toggle enable/disable status
|
||||
* - Save server configuration
|
||||
* - Delete server
|
||||
* - Error handling and toast notifications
|
||||
*/
|
||||
export function useMcpActions(appId: AppId): UseMcpActionsResult {
|
||||
const { t } = useTranslation();
|
||||
const [servers, setServers] = useState<Record<string, McpServer>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const cfg = await mcpApi.getConfig(appId);
|
||||
setServers(cfg.servers || {});
|
||||
} catch (error) {
|
||||
console.error("[useMcpActions] Failed to load MCP config", error);
|
||||
const detail = extractErrorMessage(error);
|
||||
const mapped = translateMcpBackendError(detail, t);
|
||||
toast.error(mapped || detail || t("mcp.error.loadFailed"), {
|
||||
duration: mapped || detail ? 6000 : 5000,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [appId, t]);
|
||||
|
||||
const toggleEnabled = useCallback(
|
||||
async (id: string, enabled: boolean) => {
|
||||
// Optimistic update
|
||||
const previousServers = servers;
|
||||
setServers((prev) => ({
|
||||
...prev,
|
||||
[id]: {
|
||||
...prev[id],
|
||||
enabled,
|
||||
},
|
||||
}));
|
||||
|
||||
try {
|
||||
await mcpApi.setEnabled(appId, id, enabled);
|
||||
toast.success(enabled ? t("mcp.msg.enabled") : t("mcp.msg.disabled"), {
|
||||
duration: 1500,
|
||||
});
|
||||
} catch (error) {
|
||||
// Rollback on failure
|
||||
setServers(previousServers);
|
||||
const detail = extractErrorMessage(error);
|
||||
const mapped = translateMcpBackendError(detail, t);
|
||||
toast.error(mapped || detail || t("mcp.error.saveFailed"), {
|
||||
duration: mapped || detail ? 6000 : 5000,
|
||||
});
|
||||
}
|
||||
},
|
||||
[appId, servers, t],
|
||||
);
|
||||
|
||||
const saveServer = useCallback(
|
||||
async (
|
||||
id: string,
|
||||
server: McpServer,
|
||||
options?: { syncOtherSide?: boolean },
|
||||
) => {
|
||||
try {
|
||||
const payload: McpServer = { ...server, id };
|
||||
await mcpApi.upsertServerInConfig(appId, id, payload, {
|
||||
syncOtherSide: options?.syncOtherSide,
|
||||
});
|
||||
await reload();
|
||||
toast.success(t("mcp.msg.saved"), { duration: 1500 });
|
||||
} catch (error) {
|
||||
const detail = extractErrorMessage(error);
|
||||
const mapped = translateMcpBackendError(detail, t);
|
||||
const msg = mapped || detail || t("mcp.error.saveFailed");
|
||||
toast.error(msg, { duration: mapped || detail ? 6000 : 5000 });
|
||||
// Re-throw to allow form-level error handling
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[appId, reload, t],
|
||||
);
|
||||
|
||||
const deleteServer = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await mcpApi.deleteServerInConfig(appId, id);
|
||||
await reload();
|
||||
toast.success(t("mcp.msg.deleted"), { duration: 1500 });
|
||||
} catch (error) {
|
||||
const detail = extractErrorMessage(error);
|
||||
const mapped = translateMcpBackendError(detail, t);
|
||||
toast.error(mapped || detail || t("mcp.error.deleteFailed"), {
|
||||
duration: mapped || detail ? 6000 : 5000,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[appId, reload, t],
|
||||
);
|
||||
|
||||
return {
|
||||
servers,
|
||||
loading,
|
||||
reload,
|
||||
toggleEnabled,
|
||||
saveServer,
|
||||
deleteServer,
|
||||
};
|
||||
}
|
||||
@@ -32,18 +32,16 @@ export const mcpApi = {
|
||||
return await invoke("validate_mcp_command", { cmd });
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated 使用 getAllServers() 代替(v3.7.0+)
|
||||
*/
|
||||
async getConfig(app: AppId = "claude"): Promise<McpConfigResponse> {
|
||||
return await invoke("get_mcp_config", { app });
|
||||
},
|
||||
|
||||
async importFromClaude(): Promise<number> {
|
||||
return await invoke("import_mcp_from_claude");
|
||||
},
|
||||
|
||||
async importFromCodex(): Promise<number> {
|
||||
return await invoke("import_mcp_from_codex");
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated 使用 upsertUnifiedServer() 代替(v3.7.0+)
|
||||
*/
|
||||
async upsertServerInConfig(
|
||||
app: AppId,
|
||||
id: string,
|
||||
@@ -61,6 +59,9 @@ export const mcpApi = {
|
||||
return await invoke("upsert_mcp_server_in_config", payload);
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated 使用 deleteUnifiedServer() 代替(v3.7.0+)
|
||||
*/
|
||||
async deleteServerInConfig(
|
||||
app: AppId,
|
||||
id: string,
|
||||
@@ -76,26 +77,13 @@ export const mcpApi = {
|
||||
return await invoke("delete_mcp_server_in_config", payload);
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated 使用 toggleApp() 代替(v3.7.0+)
|
||||
*/
|
||||
async setEnabled(app: AppId, id: string, enabled: boolean): Promise<boolean> {
|
||||
return await invoke("set_mcp_enabled", { app, id, enabled });
|
||||
},
|
||||
|
||||
async syncEnabledToClaude(): Promise<boolean> {
|
||||
return await invoke("sync_enabled_mcp_to_claude");
|
||||
},
|
||||
|
||||
async syncEnabledToCodex(): Promise<boolean> {
|
||||
return await invoke("sync_enabled_mcp_to_codex");
|
||||
},
|
||||
|
||||
async syncEnabledToGemini(): Promise<boolean> {
|
||||
return await invoke("sync_enabled_mcp_to_gemini");
|
||||
},
|
||||
|
||||
async importFromGemini(): Promise<number> {
|
||||
return await invoke("import_mcp_from_gemini");
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// v3.7.0 新增:统一 MCP 管理 API
|
||||
// ========================================================================
|
||||
@@ -131,11 +119,4 @@ export const mcpApi = {
|
||||
): Promise<void> {
|
||||
return await invoke("toggle_mcp_app", { serverId, app, enabled });
|
||||
},
|
||||
|
||||
/**
|
||||
* 手动同步所有启用的 MCP 服务器到对应的应用
|
||||
*/
|
||||
async syncAllServers(): Promise<void> {
|
||||
return await invoke("sync_all_mcp_servers");
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user