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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user