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.
This commit is contained in:
Jason
2025-11-10 12:03:15 +08:00
parent 7b52c44a9d
commit 3210202132
3 changed files with 30 additions and 20 deletions

View File

@@ -3,12 +3,7 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form";
Form,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider"; import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
import type { AppId } from "@/lib/api"; import type { AppId } from "@/lib/api";
import type { ProviderCategory, ProviderMeta } from "@/types"; import type { ProviderCategory, ProviderMeta } from "@/types";

View File

@@ -93,4 +93,3 @@ export const tomlConfigSchema = z.string().superRefine((value, ctx) => {
}); });
} }
}); });

View File

@@ -23,21 +23,11 @@ export const validateToml = (text: string): string => {
/** /**
* 将 McpServerSpec 对象转换为 TOML 字符串 * 将 McpServerSpec 对象转换为 TOML 字符串
* 使用 @iarna/toml 的 stringify自动处理转义与嵌套表 * 使用 @iarna/toml 的 stringify自动处理转义与嵌套表
* 保留所有字段(包括扩展字段如 timeout_ms
*/ */
export const mcpServerToToml = (server: McpServerSpec): string => { export const mcpServerToToml = (server: McpServerSpec): string => {
const obj: any = {}; // 先复制所有字段(保留扩展字段)
if (server.type) obj.type = server.type; const obj: any = { ...server };
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;
}
// 去除未定义字段,确保输出更干净 // 去除未定义字段,确保输出更干净
for (const k of Object.keys(obj)) { for (const k of Object.keys(obj)) {
@@ -103,6 +93,7 @@ export const tomlToMcpServer = (tomlText: string): McpServerSpec => {
/** /**
* 规范化服务器配置对象为 McpServer 格式 * 规范化服务器配置对象为 McpServer 格式
* 保留所有字段(包括扩展字段如 timeout_ms
*/ */
function normalizeServerConfig(config: any): McpServerSpec { function normalizeServerConfig(config: any): McpServerSpec {
if (!config || typeof config !== "object") { if (!config || typeof config !== "object") {
@@ -111,6 +102,9 @@ function normalizeServerConfig(config: any): McpServerSpec {
const type = (config.type as string) || "stdio"; const type = (config.type as string) || "stdio";
// 已知字段列表(用于后续排除)
const knownFields = new Set<string>();
if (type === "stdio") { if (type === "stdio") {
if (!config.command || typeof config.command !== "string") { if (!config.command || typeof config.command !== "string") {
throw new Error("stdio 类型的 MCP 服务器必须包含 command 字段"); throw new Error("stdio 类型的 MCP 服务器必须包含 command 字段");
@@ -120,10 +114,13 @@ function normalizeServerConfig(config: any): McpServerSpec {
type: "stdio", type: "stdio",
command: config.command, command: config.command,
}; };
knownFields.add("type");
knownFields.add("command");
// 可选字段 // 可选字段
if (config.args && Array.isArray(config.args)) { if (config.args && Array.isArray(config.args)) {
server.args = config.args.map((arg: any) => String(arg)); server.args = config.args.map((arg: any) => String(arg));
knownFields.add("args");
} }
if (config.env && typeof config.env === "object") { if (config.env && typeof config.env === "object") {
const env: Record<string, string> = {}; const env: Record<string, string> = {};
@@ -131,9 +128,18 @@ function normalizeServerConfig(config: any): McpServerSpec {
env[k] = String(v); env[k] = String(v);
} }
server.env = env; server.env = env;
knownFields.add("env");
} }
if (config.cwd && typeof config.cwd === "string") { if (config.cwd && typeof config.cwd === "string") {
server.cwd = config.cwd; 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; return server;
@@ -146,6 +152,8 @@ function normalizeServerConfig(config: any): McpServerSpec {
type: "http", type: "http",
url: config.url, url: config.url,
}; };
knownFields.add("type");
knownFields.add("url");
// 可选字段 // 可选字段
if (config.headers && typeof config.headers === "object") { if (config.headers && typeof config.headers === "object") {
@@ -154,6 +162,14 @@ function normalizeServerConfig(config: any): McpServerSpec {
headers[k] = String(v); headers[k] = String(v);
} }
server.headers = headers; server.headers = headers;
knownFields.add("headers");
}
// 保留所有未知字段
for (const key of Object.keys(config)) {
if (!knownFields.has(key)) {
server[key] = config[key];
}
} }
return server; return server;