MCP 配置校验拦住了哪些写法
把一份 MCP 服务器配置贴进 CC Switch,它没收下,报了一句错。这时候你想知道的其实只有一件事:是哪一层拒的。是那个叫 validation 的模块,是解析深链的解析器,还是最后往 config.toml 落盘的写入函数?这三层的报错文案完全不同,改法也完全不同。
先说结论里最反直觉的一处:名字里带 validation 的那一层,是四层里最薄的。src-tauri/src/mcp/validation.rs 全文 69 行,只有两个公开函数,规则加起来四条,文件里 #[test] 数量是 0(grep -c "#\[test\]" src-tauri/src/mcp/validation.rs 结果为 0)。你遇到的大部分”这么写不行”,都不是它拦的。
以下全部基于我们本地 clone 的 cc-switch 仓库快照 c39c903(提交日期 2026-08-10),仓库内版本号 3.19.2。我们只读源码文本,没有安装也没有运行过这个桌面应用。
第一层:validation.rs 只管四件事
validation.rs 的两个公开函数是 validate_server_spec 与 extract_server_spec(src-tauri/src/mcp/validation.rs:8、54)。
validate_server_spec 的规则一共四条:
| 条件 | 不满足时的报错原文 | 位置 |
|---|---|---|
| spec 必须是 JSON 对象 | MCP 服务器连接定义必须为 JSON 对象 | validation.rs:9-13 |
type 只允许 stdio / http / sse | 见源码原文,省略 type 按 stdio 处理 | validation.rs:14-24 |
stdio 必须有非空 command | stdio 类型的 MCP 服务器缺少 command 字段 | validation.rs:26-33 |
http 与 sse 各自必须有非空 url | 对应类型缺少 url 字段 | validation.rs:34-49 |
type 那一条里有个细节值得单独拎出来:缺省 type 不会被拒,而是按 stdio 处理,源码注释给的理由是”与社区常见 .mcp.json 一致”(validation.rs:14-24)。所以一份没写 type、也没写 command 的配置,你收到的报错不是”缺少 type”,而是”stdio 类型的 MCP 服务器缺少 command 字段”——它先把你当成 stdio,再抱怨你没有 command。看到这句报错先别急着去补 type,多半是 command 那一行的问题。
另一个函数 extract_server_spec 管的是外面那一层壳:条目本身必须是对象,必须含 server 字段,而且 server 本身也必须是对象(validation.rs:54-68)。这一条最容易撞,因为它要求的是嵌套结构:条目里裹着一个 server,server 里才是 type / command / args 那套东西。你把一份扁平的服务器定义直接当条目递进去,卡住的位置是 extract_server_spec 而不是 validate_server_spec,报错指向的是缺少 server 字段,而不是里面那套 type / command 写得对不对。这两个函数的分工要分清:外壳不对,里面写得再规范也走不到 validate_server_spec。
这个结构在前端预设里也能对上。src/config/mcpPresets.ts 里的每条预设,字段是 id、name、tags、server(内含 type / command / args)、homepage、docs(src/config/mcpPresets.ts:32-43)——server 是一个独立的嵌套对象,而不是和 id、name 平铺在一起。类型定义写成 McpPreset = Omit<McpServer, "enabled" | "description">,即预设本身不带 enabled 与 description(src/config/mcpPresets.ts:4)。这批预设我们采集时数出来是 5 条,server.type 全部是 "stdio"(src/config/mcpPresets.ts:31-90、:37、:49、:60、:70、:84)。源码注释自己给了定位:“仅包含最常用、可快速落地的 stdio 模式示例”、“不涉及分类/模板/测速等复杂逻辑,默认以 disabled 形式”回种”到 config.json”(src/config/mcpPresets.ts:26-30)。要照着一份能过校验的最小结构改自己的配置,这五条是仓库里现成的参照。
这一层不校验什么,同样重要:它不检查 command 指向的程序是否存在、args 是否合理、env 里的变量是否有值、url 是否能连通。过了 validate_server_spec 只意味着结构合法,不意味着这个服务器起得来。
第二层:深链解析器,编码与外层键
如果你的配置是通过 ccswitch:// 深链导进来的,那么在这份配置被写进统一库之前,先有一串独立于 validation.rs 的解析规则会拦你——两者的报错文案完全不重叠,看一眼报错就能分清是哪一边。
MCP 类型的深链必填 apps 与 config 两个参数,config_format 被硬编码为 "json",注释是 “MCP config is always JSON”(src-tauri/src/deeplink/parser.rs:255-293)。而 config 的处理链是:Base64 解码 → UTF-8 → JSON → 必须含 mcpServers 对象,为空时报 “No MCP servers found in config”(src-tauri/src/deeplink/mcp.rs:62-89)。
也就是说,深链这条路要求的最外层键是 mcpServers,比第一层多了一层壳。这里有一处文档与代码对不上的地方:用户手册 docs/user-manual/zh/5-faq/5.3-deeplink.md 把 config 说明为”MCP 服务器配置(JSON 格式)“,示例给的是 URL 编码的裸 JSON {"command":"uvx","args":[...]}(5.3-deeplink.md:78-83、:110);而代码要求先 Base64 解码,且解出来的 JSON 必须含 mcpServers 对象,否则报 “MCP config must contain ‘mcpServers’ object”(deeplink/mcp.rs:68-83)。两处不一致,以我们实读的仓库状态为准,本文不推断原因。
Base64 这一段是有容错的:解码时会尝试把空格还原成 +、补齐 = padding,并在 STANDARD / STANDARD_NO_PAD / URL_SAFE / URL_SAFE_NO_PAD 四种引擎间轮询(src-tauri/src/deeplink/utils.rs:24-74)。所以链接在传递过程中被换掉 + 这类常见损坏,未必会当场失败。
深链的 apps 白名单是 7 个可选值,额外接受别名 grok(等价 grokbuild)(parser.rs:261-273);而 parse_mcp_apps 对 "openclaw" 分支只写一条 debug 日志,注释是 “OpenClaw doesn’t support MCP, ignore silently”(src-tauri/src/deeplink/mcp.rs:171-174)——这条不会报错,是静默忽略。你把 openclaw 写进 apps 里,导入不会失败,但那一项不生效。关于 ccswitch:// 四类载荷的完整解析边界,我们另有一篇专门讲,这里只取与 MCP 配置写法直接相关的这几条。
第三层:转换器,有些写法是格式本身不接受
配置过了校验,接下来要按目标应用的格式改写。Codex 侧走的是 JSON→TOML,源码里明确列出了不支持的三类值:null、深度嵌套对象、混合类型数组(src-tauri/src/mcp/codex.rs:511-514)。
这三类在 JSON 里都是完全合法的写法,第一层的四条规则一条都拦不住,但它们到 TOML 这一步过不去。所以「同一份 JSON,同步到 A 应用没事、同步到 B 应用出问题」这种现象,第一个要看的不是校验规则,而是目标格式。
同一个文件里还有一张 21 项的 Codex 扩展字段白名单(codex.rs:625-649),字段全表留给专讲各应用格式差异的那一篇;与本篇相关的只是它说明了一件事——「合法的 JSON 字段」与「Codex 认的字段」是两个集合,前者不蕴含后者。白名单之外的字段会被怎么处理,我们没有核实,不写。另有一处字段名改写:统一结构用 headers,写进 Codex TOML 时叫 http_headers(codex.rs:87-89、687-695)。
要分清「拒绝」和「改写」。 各应用之间还有一批不会报错、但会让你写下去的东西变样的转换:写 Gemini 之前会移除 type/enabled/source/id/name/description/tags/homepage/docs 这批 UI 辅助字段(src-tauri/src/gemini_mcp.rs:117-126);OpenCode 写出时会额外加 enabled: true(src-tauri/src/mcp/opencode.rs:76、96),反向转换时 remote 一律回落成 sse,注释写的是 “Convert to “sse” type (default remote protocol)“(opencode.rs:152-154);Hermes 走 merge-on-write,核心字段来自新 spec,Hermes 专有字段从已存在的条目里保留,注释理由是 “prevents CC Switch from overwriting user customizations”(src-tauri/src/mcp/hermes.rs:204-209)。这些都是转换而不是校验——你在一处写下的字段,到另一处未必还在。各应用的配置文件路径与格式差异我们另有一篇在讲,这里只强调它对”你的写法算不算合法”的影响。
第四层:写入前的防御,问题可能不在你这份配置里
最后一层在落盘。Codex 同步前会先读现有的 config.toml,若语法无效则直接报错,不尝试覆盖(src-tauri/src/mcp/codex.rs:283)。这一条意味着:你新加的这份 MCP 配置本身可能完全没问题,失败的原因是目标文件里早就有别的语法错误。
同一处还有两个防御写得很直白:upsert_mcp_server_table 专门处理 mcp_servers 存在但不是表的情况(比如文件里被写成 mcp_servers = "x"),否则 toml_edit 的 IndexMut 会 panic;remove_mcp_server_from_doc 用 as_table_like_mut 处理 inline table,否则删除会”静默失效”(codex.rs:351-390)。第二条尤其值得记一下:inline table 写法下的删除失败是不报错的。
Codex 这边还有一个历史遗留的写法问题:正确格式是顶层 [mcp_servers] 表,同步时会”自动清理错误格式 [mcp.servers]”(codex.rs:278-286);导入侧则两种都读,注释是”错误格式 [mcp.servers.*](容错读取,用于迁移错误写入的配置)“(codex.rs:45-51)。所以 [mcp.servers] 这个写法在读的时候被容忍、在写的时候被清理。
Windows 侧那条单独的写法
Windows 上用 npx 起 MCP 服务器,写法和 Linux/macOS 不一样,这一点仓库里做了两处处理。
后端侧:Windows 上 npx 类命令会被包成 cmd /c,注释说明这是为解决 Claude Code /doctor 报的 “Windows requires ‘cmd /c’ wrapper to execute npx” 警告(src-tauri/src/claude_mcp.rs:15-16)。前端预设侧:createNpxCommand 在 Windows 下产出 command: "cmd"、args: ["/c","npx", ...extraArgs, packageName],非 Windows 直接 command: "npx"(src/config/mcpPresets.ts:9-24)。
还有一条边界写在注释里:WSL 网络路径(\\wsl$\... / \\wsl.localhost\...)不做 cmd 包装,且注明”仅检测直接 UNC 路径,映射磁盘符(如 Z:)无法检测”(src-tauri/src/claude_mcp.rs:68-70)。也就是说,你把 WSL 路径映射成盘符再用,这个检测认不出来。
出问题时的核查顺序
按报错文案定位是最快的路子,因为四层的文案互不重叠:
- 报错是中文短句、且指向的是”必须为 JSON 对象""缺少 command 字段”或缺少
server字段这类结构问题,那是第一层,去读src-tauri/src/mcp/validation.rs全文——69 行,两分钟能读完,四条规则全在里面。 - 报错里出现 “No MCP servers found in config” 或 “MCP config must contain ‘mcpServers’ object”,是深链层,去对
src-tauri/src/deeplink/mcp.rs:62-89,重点核你的config是不是 Base64、外层键是不是mcpServers。 - 配置在别的应用能用、在 Codex 出问题,先对
codex.rs:511-514那三类不支持的值(null / 深度嵌套对象 / 混合类型数组)逐个排掉。 - 上面三步都对得上却还是失败,去看目标文件本身:
~/.codex/config.toml的语法是否有效、mcp_servers是不是被写成了字符串而不是表。
处置之后怎么验证:把改后的结构和 src/config/mcpPresets.ts:31-90 里那 5 条预设的字段形状对一遍(server 是不是独立嵌套对象、type 是不是 stdio/http/sse 之一),再确认目标应用对应的配置文件能被正常解析。
什么情况说明不是校验的问题:如果配置根本没被写进目标文件,先看那个应用的目录在不在——每个应用都有一个 should_sync_*_mcp() 守卫,Claude 判断 ~/.claude 目录或 ~/.claude.json 任一存在(src-tauri/src/mcp/claude.rs:11-15),Codex 判断 ~/.codex 目录存在(codex.rs:16-20),守卫注释写明”按用户偏好:此时跳过写入/删除,不创建任何文件或目录”(claude.rs:12-14)。这种情况下你不会收到校验报错,因为压根没走到写入。同步机制本身另有一篇在讲。
顺手可查的两个数
想自己确认这层校验有多薄,跑这一条就够了:
grep -c "#\[test\]" src-tauri/src/mcp/*.rs
我们采集时得到的分布是:codex.rs 5、grokbuild.rs 2、hermes.rs 12、opencode.rs 4,claude.rs / gemini.rs / mod.rs / validation.rs 各 0。测试最多的是 hermes.rs,而 validation.rs 是 0——这是静态计数,我们没有运行过任何构建或测试,也不由这个数推导任何质量结论。
另一个数是模块体量:src-tauri/src/mcp/ 共 8 个文件,claude.rs 149、codex.rs 847、gemini.rs 144、grokbuild.rs 251、hermes.rs 575、mod.rs 40、opencode.rs 356、validation.rs 69,合计 2431 行。2431 行里,专门叫”校验”的那 69 行占不到 3%。 剩下的 97% 在做转换、同步和防御——这也正是”你那份配置为什么不行”的答案更常落在那 97% 里的原因。
最后提一句边界:CC Switch 会读写 ~/.claude.json、~/.codex/config.toml 这类你本机上的真实 CLI 配置文件,MCP 配置里也可能带上鉴权头。改动前留一份自己的副本是通用做法,不是这个项目文档里的指引。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册与发布说明、
src/config/ 的预设定义与 src-tauri/src/ 的后端源码整理,核对日 2026-08-10,对应仓库快照 c39c903。
本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,
因此不涉及界面外观、操作手感与切换速度的任何描述。
文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。
该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。