From 32102021325f00bb3f79fd96dc7b02b7b0d58b00 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 10 Nov 2025 12:03:15 +0800 Subject: [PATCH] fix(mcp): preserve custom fields in Codex TOML config editor Fixed an issue where custom/extension fields (e.g., timeout_ms, retry_count) were silently dropped when editing Codex MCP server configurations in TOML format. Root cause: The TOML parser functions only extracted known fields (type, command, args, env, cwd, url, headers), discarding any additional fields during normalization. Changes: - mcpServerToToml: Now uses spread operator to copy all fields before stringification - normalizeServerConfig: Added logic to preserve unknown fields after processing known ones - Both stdio and http server types now retain custom configuration fields This fix enables forward compatibility with future MCP protocol extensions and allows users to add custom configurations without code changes. --- .../providers/forms/ProviderForm.tsx | 7 +--- src/lib/schemas/common.ts | 1 - src/utils/tomlUtils.ts | 42 +++++++++++++------ 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 4922be5..99384a1 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -3,12 +3,7 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; -import { - Form, - FormField, - FormItem, - FormMessage, -} from "@/components/ui/form"; +import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form"; import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider"; import type { AppId } from "@/lib/api"; import type { ProviderCategory, ProviderMeta } from "@/types"; diff --git a/src/lib/schemas/common.ts b/src/lib/schemas/common.ts index d197bb2..08d5cd5 100644 --- a/src/lib/schemas/common.ts +++ b/src/lib/schemas/common.ts @@ -93,4 +93,3 @@ export const tomlConfigSchema = z.string().superRefine((value, ctx) => { }); } }); - diff --git a/src/utils/tomlUtils.ts b/src/utils/tomlUtils.ts index 8d9333b..47a711b 100644 --- a/src/utils/tomlUtils.ts +++ b/src/utils/tomlUtils.ts @@ -23,21 +23,11 @@ export const validateToml = (text: string): string => { /** * 将 McpServerSpec 对象转换为 TOML 字符串 * 使用 @iarna/toml 的 stringify,自动处理转义与嵌套表 + * 保留所有字段(包括扩展字段如 timeout_ms) */ export const mcpServerToToml = (server: McpServerSpec): string => { - const obj: any = {}; - if (server.type) obj.type = server.type; - - if (server.type === "stdio") { - if (server.command !== undefined) obj.command = server.command; - if (server.args && Array.isArray(server.args)) obj.args = server.args; - if (server.cwd !== undefined) obj.cwd = server.cwd; - if (server.env && typeof server.env === "object") obj.env = server.env; - } else if (server.type === "http") { - if (server.url !== undefined) obj.url = server.url; - if (server.headers && typeof server.headers === "object") - obj.headers = server.headers; - } + // 先复制所有字段(保留扩展字段) + const obj: any = { ...server }; // 去除未定义字段,确保输出更干净 for (const k of Object.keys(obj)) { @@ -103,6 +93,7 @@ export const tomlToMcpServer = (tomlText: string): McpServerSpec => { /** * 规范化服务器配置对象为 McpServer 格式 + * 保留所有字段(包括扩展字段如 timeout_ms) */ function normalizeServerConfig(config: any): McpServerSpec { if (!config || typeof config !== "object") { @@ -111,6 +102,9 @@ function normalizeServerConfig(config: any): McpServerSpec { const type = (config.type as string) || "stdio"; + // 已知字段列表(用于后续排除) + const knownFields = new Set(); + if (type === "stdio") { if (!config.command || typeof config.command !== "string") { throw new Error("stdio 类型的 MCP 服务器必须包含 command 字段"); @@ -120,10 +114,13 @@ function normalizeServerConfig(config: any): McpServerSpec { type: "stdio", command: config.command, }; + knownFields.add("type"); + knownFields.add("command"); // 可选字段 if (config.args && Array.isArray(config.args)) { server.args = config.args.map((arg: any) => String(arg)); + knownFields.add("args"); } if (config.env && typeof config.env === "object") { const env: Record = {}; @@ -131,9 +128,18 @@ function normalizeServerConfig(config: any): McpServerSpec { env[k] = String(v); } server.env = env; + knownFields.add("env"); } if (config.cwd && typeof config.cwd === "string") { server.cwd = config.cwd; + knownFields.add("cwd"); + } + + // 保留所有未知字段(如 timeout_ms 等扩展字段) + for (const key of Object.keys(config)) { + if (!knownFields.has(key)) { + server[key] = config[key]; + } } return server; @@ -146,6 +152,8 @@ function normalizeServerConfig(config: any): McpServerSpec { type: "http", url: config.url, }; + knownFields.add("type"); + knownFields.add("url"); // 可选字段 if (config.headers && typeof config.headers === "object") { @@ -154,6 +162,14 @@ function normalizeServerConfig(config: any): McpServerSpec { headers[k] = String(v); } server.headers = headers; + knownFields.add("headers"); + } + + // 保留所有未知字段 + for (const key of Object.keys(config)) { + if (!knownFields.has(key)) { + server[key] = config[key]; + } } return server;