feat(frontend): implement unified MCP panel for v3.7.0

Complete Phase 3 (P0) frontend implementation for unified MCP management:

**New Files:**
- src/hooks/useMcp.ts: React Query hooks for unified MCP operations
- src/components/mcp/UnifiedMcpPanel.tsx: Unified MCP management panel
- src/components/ui/checkbox.tsx: Checkbox component from shadcn/ui

**Features:**
- Unified panel with three-column layout: server info + app checkboxes + actions
- Multi-app control: Claude/Codex/Gemini checkboxes for each server
- Real-time stats: Show enabled server counts per app
- Full CRUD operations: Add, edit, delete, sync all servers

**Integration:**
- Replace old app-specific McpPanel with UnifiedMcpPanel in App.tsx
- Update McpFormModal to support unified mode with apps field
- Add i18n support: mcp.unifiedPanel namespace (zh/en)

**Type Safety:**
- Ensure McpServer.apps field always initialized
- Fix all test files to include apps field
- TypeScript type check passes 

**Architecture:**
- Single source of truth: mcp.servers manages all MCP configs
- Per-server app control: apps.claude/codex/gemini boolean flags
- Backward compatible: McpFormModal supports both unified and legacy modes

Next: P1 tasks (import dialogs, sub-components, tests)
This commit is contained in:
Jason
2025-11-14 15:24:48 +08:00
parent 32a6de074c
commit 9e8abf5f26
10 changed files with 569 additions and 15 deletions

View File

@@ -34,6 +34,7 @@ import {
} from "@/utils/tomlUtils";
import { normalizeTomlText } from "@/utils/textNormalization";
import { useMcpValidation } from "./useMcpValidation";
import { useUpsertMcpServer } from "@/hooks/useMcp";
interface McpFormModalProps {
appId: AppId;
@@ -46,6 +47,7 @@ interface McpFormModalProps {
) => Promise<void>;
onClose: () => void;
existingIds?: string[];
unified?: boolean; // 统一模式:使用 useUpsertMcpServer 而非传统回调
}
/**
@@ -60,11 +62,15 @@ 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(
() => editingId || initialData?.id || "",
);
@@ -361,24 +367,32 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
setSaving(true);
try {
// 先处理 name 字段(必填)
const nameTrimmed = (formName || trimmedId).trim();
const finalName = nameTrimmed || trimmedId;
const entry: McpServer = {
...(initialData ? { ...initialData } : {}),
id: trimmedId,
name: finalName,
server: serverSpec,
// 确保 apps 字段始终存在v3.7.0 新架构必需)
apps: initialData?.apps || { claude: false, codex: false, gemini: false },
};
// 修复:新增 MCP 时默认启用enabled=true
// 编辑模式下保留原有的 enabled 状态
if (initialData?.enabled !== undefined) {
entry.enabled = initialData.enabled;
} else {
// 新增模式:默认启用
entry.enabled = true;
// 统一模式下无需再初始化 apps上面已经处理
// 传统模式需要设置 enabled 字段
if (!unified) {
// 传统模式:新增 MCP 时默认启用enabled=true
// 编辑模式下保留原有的 enabled 状态
if (initialData?.enabled !== undefined) {
entry.enabled = initialData.enabled;
} else {
// 新增模式:默认启用
entry.enabled = true;
}
}
const nameTrimmed = (formName || trimmedId).trim();
entry.name = nameTrimmed || trimmedId;
const descriptionTrimmed = formDescription.trim();
if (descriptionTrimmed) {
entry.description = descriptionTrimmed;
@@ -410,8 +424,16 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
delete entry.tags;
}
// 显式等待父组件保存流程
await onSave(trimmedId, entry, { syncOtherSide });
// 显式等待保存流程
if (unified) {
// 统一模式:调用 useUpsertMcpServer mutation
await upsertMutation.mutateAsync(entry);
toast.success(t("common.success"));
onClose();
} else {
// 传统模式:调用父组件回调
await onSave(trimmedId, entry, { syncOtherSide });
}
} catch (error: any) {
const detail = extractErrorMessage(error);
const mapped = translateMcpBackendError(detail, t);