多AI编程代理MCP与Skills统一同步方案:从配置割据到单一事实来源
先说个很真实的场景某天我需要在本机的几个 AI 编程代理里加同一个 MCP 服务器那个服务器能抓取网页正文给 Claude Code、Codex、Cursor、Hermes Agent 同时用。我原以为这是十分钟的小事结果打开四个配置文件逐个粘贴、调格式、改路径折腾了快一个小时。更气人的是一周后我发现其中一个配置里的参数和其他三个已经不完全一样了——当初复制的时候漏改了一个超时时间。从那以后我就在琢磨一件事这些本地 agent 的 MCP 工具和技能到底有没有办法做到零配置统一同步。这篇文章就是我把这套方案跑通之后的完整记录。核心结论是不要试图在四个工具里各维护一份配置而是把本机所有 agent 的 MCP server 和 skills 收敛到一个“单一事实来源”用一层很薄的适配逻辑去做格式转换和路径分发最终做到“改一处全家桶生效”。这套做法适合所有装了多个 AI 编码工具、又不想反复维护重复配置的开发者也适合那些经常重装环境、需要在两台电脑间快速迁移工具链的人。如果你只有一个 agent本文的收益不大但只要你本机同时装着两个及以上 AI 编程代理而且已经开始往里面堆 MCP server 和自定义技能这篇应该能帮你省下大量重复劳动。1. 多 agent 文件割据的本质配置格式差异比你想的更大1.1 四份配置、四种格式、四种命运先说清楚问题到底长什么样。每个 AI 编程代理都不是凭空运行的它们需要知道自己能调用哪些 MCP 服务器、能读取哪些技能文件。问题在于不同工具对这两类东西的读取方式差别很大Claude Code项目的 MCP 配置写在项目根目录的.mcp.json用户级作用域则通过claude mcp add --scope user写入全局配置。技能目录在当前版本常见路径是~/.claude/skills/每个技能是一个带SKILL.md的文件夹。CursorMCP 配置在项目级.cursor/mcp.json用户级则是~/.cursor/mcp.json都是 JSON 格式的{mcpServers: {...}}。技能则是放在.cursor/skills/或用户级 skills 目录。Codex配置走的是 TOML通常在~/.codex/config.tomlMCP server 需要按 TOML 语法写进[mcp_servers.xxx]段技能或指令则依赖AGENTS.md这类文件。Hermes Agent以及类似的新兴本地 agent 框架配置又常常是另一种 YAML 或 JSON schema技能目录的层级、元数据字段也各自为政。同样一个filesystem服务器你在 Claude Code 里写成 JSON在 Codex 里要改成 TOML在 Cursor 里可能还要补上enabled之类的可选字段。语法差异本身不难难的是你每次新增或修改一个服务器都要记得去四个地方做同样的操作而且这些配置之间没有任何自动校验关系。1.2 复制粘贴维护带来的真实成本很多朋友一开始都像我一样觉得“无非是复制粘贴四次”。但用久了你会发现这事的隐性成本很高第一是漏改。人的注意力是有限的同一时间改四个文件总有一次会漏掉其中一个而且漏掉的那个工具往往是你当时最需要用的。等到真要用的时候agent 一脸茫然地说找不到这个工具你才回去检查一来一回就是十几分钟。第二是静默漂移。复制的时候参数一样但后来你只给 Cursor 加了timeout只给 Codex 换了args几个文件慢慢就长得不一样了。哪天你想统一排查所有 MCP 的调用行为根本分不清哪一个才是“标准版本”。第三是技能类的配置更隐蔽。技能不像 MCP server 那样集中在某个 JSON 数组里而是一整个目录有SKILL.md、有辅助脚本、有参考文档。四个 agent 的技能目录各放一份副本的话你更新了某一个技能包的脚本另外三个目录还是旧版本这种“版本分裂”比 MCP 配置更难发现因为它不会有语法报错只会让你某天觉得“这个 agent 怎么这么笨”。我当时被逼到临界点是在一次重装系统之后。备份习惯不好配置文件全得重来对着历史命令和记忆一个个重建。那时我才确定必须有一套本机范围内的统一管理方案。2. 先想清楚“0 配置”真正应该做到什么2.1 单一事实来源Single Source of Truth要设计一套方案第一步不是急着写脚本而是想明白“0 配置”到底指什么。我的理解是对你的日常工作来说不需要再逐个打开各 agent 的配置文件。你只需要维护一个唯一的源头其他东西由工具自动完成。所以方案的核心是建立一个本机级目录当作所有 agent 配置的单一事实来源。我把它放在~/.config/agent-sync/里面有mcp.json本机所有 MCP server 的唯一注册表统一用 JSON 声明skills/所有技能包的唯一存储目录每种技能一个子目录targets/生成后的目标配置暂存区可以被安全覆盖和重新生成agentsync一个本地同步脚本负责读取mcp.json和skills/生成各 agent 需要的格式并分发到它们各自的配置路径。这个方案的关键点在于你不是去学习四个工具的四种配置语法你只学一种——自己定义的那一种。其他格式由脚本替你转换。2.2 薄适配层而不是硬编码我在设计阶段考虑过几个思路其中有个看起来很美但实际走不通的方案“写一个超集配置文件然后让每个 agent 启动前去读它。”问题在于大多数 agent 启动时并不会执行你自定义的 hook也不支持指定一个外部 JSON 作为所有配置的来源。它们只认自己的默认路径。所以必须接受一个现实本机需要一个“分发动作”。这个动作不是让你手动复制粘贴而是脚本自动完成的。脚本就是一层薄适配层输入是统一格式的mcp.json和标准化的技能目录输出是四个工具各自认识的文件和目录。用工程的话说这叫 Adapter 模式。你没有改变任何 agent 的行为只是用一层很薄的本地逻辑把“你的规范”翻译成“工具的规范”。2.3 为什么不用一个“超集文件”走天下有人会问既然 Claude Code 支持.mcp.jsonCursor 也支持mcp.json能不能我直接维护一个.mcp.json然后让 Cursor 去读同一个文件答案是某些项目级的路径确实可以但用户级的全局路径各有各的格式你无法让 Codex 老老实实去读一个 JSON——它默认读 TOML。即使有工具支持 JSON它对字段名称、嵌套结构的要求也可能不完全一致。另一个“伪方案”是统一用符号链接把同一个mcp.json链到四个工具的配置路径。这个思路对skills 目录是可行的因为多数工具技能目录就是读一堆文件夹但对MCP 配置文件基本行不通因为 Claude Code 的全局配置文件和 Codex 的config.toml根本不是同一种格式无法共享同一个文件。所以最终的架构必须是统一格式 适配生成 按需分发。这也是下面落地部分要展开的做法。3. 落地统一注册表 分发生效的完整做法3.1 建立统一的 MCP 注册表我定义的mcp.json格式很简单只保留必要字段{ version: 1, servers: { filesystem: { type: stdio, command: npx, args: [-y, some/mcp-server-filesystem, /tmp], env: {}, enabled: true }, docs-search: { type: http, url: http://127.0.0.1:8989/mcp, headers: { Authorization: Bearer ${DOCS_TOKEN} }, enabled: true }, legacy-tool: { type: stdio, command: python, args: [/opt/scripts/legacy_mcp.py], env: {}, enabled: false } } }有几个设计决策值得说明type字段区分stdio和http。stdio server 需要commandargsHTTP server 需要urlheaders。这个区分很重要因为不同 agent 对两类服务的配置写法不一样。enabled字段让你可以暂时停用一个 server 而不是删掉它。脚本只分发enabled: true的项。env字段保留环境变量注入位。注意我建议把密钥这类敏感信息用${DOCS_TOKEN}的形式引用而不是明文写入mcp.json。因为这份文件将来可能被git管理别把密钥提交进去。3.2 同步脚本的核心逻辑我写的是一个 Python 脚本原因很简单要同时生成 JSON、TOML、YAMLPython 的标准库和第三方库都齐全处理起来比 shell 脚本稳得多。脚本的主体逻辑只有四步读取~/.config/agent-sync/mcp.json校验 JSON schema过滤出enabled的 server按目标 agent 分组生成对应格式的配置把生成结果写到各 agent 的默认配置路径或用符号链接指向技能目录打印同步报告标明哪些文件写了、哪些 server 被停用。简化版的核心代码如下#!/usr/bin/env python3 import json import os from pathlib import Path BASE Path.home() / .config / agent-sync MCP_REGISTRY BASE / mcp.json def load_registry(): with open(MCP_REGISTRY, r, encodingutf-8) as f: data json.load(f) return { name: cfg for name, cfg in data[servers].items() if cfg.get(enabled, True) } def write_claude_mcp(servers, target: Path): # Claude Code 项目级 .mcp.json 格式 payload {mcpServers: {}} for name, cfg in servers.items(): if cfg[type] stdio: payload[mcpServers][name] { command: cfg[command], args: cfg[args], env: cfg.get(env, {}), } else: payload[mcpServers][name] { url: cfg[url], headers: cfg.get(headers, {}), } target.write_text(json.dumps(payload, indent2, ensure_asciiFalse)) def write_cursor_mcp(servers, target: Path): # Cursor 和 Claude 的格式非常接近但路径不同 write_claude_mcp(servers, target) def write_codex_mcp(servers, target: Path): # Codex 需要 TOML 格式 lines [] for name, cfg in servers.items(): lines.append(f[mcp_servers.{name}]) lines.append(fcommand {cfg[command]}) args , .join(f{a} for a in cfg.get(args, [])) lines.append(fargs [{args}]) env cfg.get(env, {}) if env: env_str , .join(f{k} {v} for k, v in env.items()) lines.append(fenv {{ {env_str} }}) lines.append() target.write_text(\n.join(lines), encodingutf-8) def main(): servers load_registry() write_claude_mcp(servers, Path.cwd() / .mcp.json) write_cursor_mcp(servers, Path.home() / .cursor / mcp.json) write_codex_mcp(servers, Path.home() / .codex / config.toml) print(fSynced {len(servers)} MCP servers.) if __name__ __main__: main()这段代码只展示了核心骨架实际使用中你还需要处理“不要覆盖已有其他配置”的问题。我的做法是在脚本里维护一个区块标记比如写 Codex 的config.toml时只替换[mcp_servers]段保留用户手写的其他字段。如果你直接用整文件覆盖会把辛辛苦苦配好的其他参数冲掉。这是个非常容易踩的坑。3.3 各 agent 的目标路径与格式对照如果你要在自己机器上复刻这套做法建议先用下面这个表格确认各工具当前版本默认读取的位置工具用户级/全局配置项目级配置MCP 格式技能目录Claude Codeclaude mcp add --scope user管理.mcp.jsonJSON{mcpServers: {...}}~/.claude/skills/Cursor~/.cursor/mcp.json.cursor/mcp.jsonJSON{mcpServers: {...}}~/.cursor/skills/Codex~/.codex/config.toml项目级AGENTS.mdTOML[mcp_servers.xxx]指令类用AGENTS.mdHermes Agent~/.hermes/config.yaml等按框架而定YAML/JSON 不定按框架而定常在~/.hermes/skills/注意这里有一个细节版本不同路径可能不同。以我本机安装的版本为例Claude Code 和 Cursor 会默认读上述路径但如果你用的版本较新、或者装了 beta 通道最好先用工具自带的命令行查一下实际生效路径。不要盲目相信我文里的路径要学会自己验证。4. Skills 同步比 MCP 更隐蔽目录约定和格式转换4.1 各工具对“技能”的真实定义做完 MCP 同步后我发现真正的硬骨头是 skills。因为 MCP 好歹是“一个服务器 一组工具”大家的概念基本一致但不同 agent 对“技能”的定义完全不同Claude Code的技能是一个目录里面必须有SKILL.md可以用 frontmatter 写name、description还可以带辅助脚本。它追求的是“一个技能包 一份说明 可执行脚本”。Cursor的技能概念更接近“规则或工作流”除了SKILL.md还可以用特定目录组织知识和脚手架。部分版本里技能和 Rules 是打通的。Codex不太强调单独技能包它更依赖AGENTS.md这种指令文件让 agent 根据项目说明自主决定行为。严格说Codex 对“技能”的支持程度取决于版本和配置方式。Hermes Agent这类框架通常会把技能当成一种插件单元有固定的目录约定甚至要求写metadata.yaml来描述技能入口。这意味着你不能天真地以为“一个技能目录复制给所有工具就行”。某些工具对技能目录的嵌套层级、frontmatter 字段有严格要求差一个字段就会直接无视整个技能。4.2 一份技能包如何分发到不同格式我的做法是在~/.config/agent-sync/skills/下维护标准技能包产物的最一般形式然后通过符号链接把它们放到各工具的技能目录里同时对那些要求元数据的工具生成对应的元数据文件。技能包的标准结构skills/ git-review/ SKILL.md scripts/ review.py assets/ prompt_template.md然后像这样建立符号链接ln -s ~/.config/agent-sync/skills/git-review ~/.claude/skills/git-review ln -s ~/.config/agent-sync/skills/git-review ~/.cursor/skills/git-review ln -s ~/.config/agent-sync/skills/git-review ~/.hermes/skills/git-review有两点必须提前说清楚第一symlink 对 Claude Code 和 Cursor 通常有效但对 Codex 不一定有意义因为 Codex 的技能/指令体系不一样。我在实际环境中通常把 Codex 的AGENTS.md当作一个汇总入口里面通过文字引导 agent 去~/.config/agent-sync/skills/读取对应技能。这本质上是“技能分发”的另一种形态不是复制文件而是让 agent 知道去哪里读。第二有些工具更新版本后可能改用复制而非跟随符号链接。我遇到过某个版本更新后工具启动时会把符号链接目录“实体化”为实际目录导致链接断裂。如果你发现链接结构总在变化建议改成脚本复制而不是链接虽然牺牲了“改一处全局生效”的便利但换来的是稳定性。4.3 技能更新后 agent 何时生效每个人都会问我改了技能agent 那边要多久才认实测下来不同 agent 的刷新策略差异很大工具技能生效时机备注Claude Code重开会话后当前会话不会热加载CursorReload 窗口 / 重开 Composer部分技能可热生效Codex重新启动进程AGENTS.md通常在进程启动时读取Hermes Agent重载配置文件或重启服务依赖框架实现这个时间差很容易让人误判“同步脚本没生效”。建议每次改完技能后不要盯着当前已经打开的会话要看效果先把 agent 重启一下。 提示写完技能包后第一件事不是“测试调用”而是先确认 agent 是否能在技能列表里看到它。看不到就排查路径、目录层级、frontmatter 三个地方而不是反复修改技能内容。5. 验证与排错怎么确认同步真的生效了5.1 每个 agent 分别怎么验证工具链这东西最怕的就是“你觉得配好了实际没加载”。所以我把验证方法单独列出来Claude Code在会话里直接输入/mcp它会列出当前已连接和可用的 MCP server。如果列表里出现了你同步进去的 server说明分发成功。技能方面可以用/skills或让 agent 列出可用技能。Cursor打开 MCP 配置面板看 server 是否出现在列表中并处于 enabled 状态技能则可以在 settings 或 AI 面板里查看技能列表。Codex根据你使用的具体版本在交互界面里询问“你可以使用哪些 MCP 工具”或者查看启动时日志里是否有加载 MCP server 的记录。命令行工具通常会有类似codex mcp list的调试入口。Hermes Agent一般通过日志输出或框架自带的 CLI 查询命令来确认。如果你用的是可视化管理界面直接在配置页看 MCP server 状态和技能列表即可。我个人的准则是每一项配置改动都要在对应 agent 里看到证据才算完成不能只停留在“脚本报错成功”这一步。5.2 常见故障排查表这个排查表是我这一个月里反复用到的按出现频率排序症状可能原因处理方式某个 server 在所有 agent 里都看不到enabled设成了 false或 JSON 语法错误先python -m json.tool mcp.json校验再把enabled改为 trueClaude Code 能看到 server但调用工具报错stdio server 的命令在 PATH 中找不到用绝对路径写command或先手动执行一次该命令看是否能启动Cursor 能加载 server但 HTTP 类型一直连不上headers里的 token 失效或 URL 写成了 SSE 格式先用 curl 手动请求 url 验证再改配置Codex 启动后提示 mcp 配置解析失败TOML 里写了 JSON 风格的字符串或转义不当用编辑器打开config.toml确认引号与括号匹配技能在 Claude Code 里不出现目录里缺少SKILL.md或 frontmatter 字段不对检查技能目录结构确认SKILL.md存在且 frontmatter 有name技能目录用符号链接后 agent 拒绝读取该工具版本不支持跟随符号链接改成复制目录或用脚本每次同步时强制重建目录改完配置agent 说还是旧行为agent 缓存了旧配置重启 agent 进程必要时删除工具 profile 相关缓存目录5.3 日志和环境变量的坑最后一个很容易被忽略的坑是环境变量。很多 MCP server 是 stdio 类型的它们在stderr里打日志但 agent 在启动 server 时会把stderr当作协议通道的一部分来解析。如果服务器在启动阶段输出了无关文本agent 可能直接判定协议握手失败server 就“神秘消失”了。遇到这种情况先用命令行单独跑一遍 server确认启动过程没有多余输出再交给 agent 托管。环境变量的第二层坑是密钥管理。我用${DOCS_TOKEN}这种占位符形式写在mcp.json里然后在同步脚本分发时去读当前 shell 环境变量、替换成实际值。这样做的好处是配置文件本身可以安全入库坏处是你必须确保 agent 是以同一个环境变量的上下文启动的。如果你用 launchd、systemd user service 或桌面快捷方式启动 agent环境变量可能和你 shell 里不一样那就需要在服务定义文件里显式补充变量。最后的经验是脚本里一定要加 diff 模式。我后面实际用的时候发现每次手动跑同步总会提心吊胆——这次会不会覆盖了什么后来我给脚本加了个--diff参数只输出将要改变的差异、不真正写文件确认无误后再用--apply执行。这就像git diff一样多花五秒钟却避免了很多次误操作。6. 一个月实测下来的真话6.1 这套方案在什么场景下收益最大运行一个月后我明显感觉这套方案帮我省下的时间集中在三个时刻一是日常加新 MCP server的时候。以前改四个文件加五个验证步骤现在改一个mcp.json跑一次脚本逐个 agent 重启验证五分钟内全部搞定。二是技能迭代的时候。我以前经常改进一个技能的提示词或脚本然后忘记同步到其他 agent导致不同 agent 表现不一致。现在所有技能都来自~/.config/agent-sync/skills/符号链接指向同一个地方改完就是全局生效再也不会出现“这个 agent 会、那个 agent 不会”的割裂感。三是换机和重装环境的时候。之前每次重装都要凭记忆重建配置现在只需要把~/.config/agent-sync/这个目录备份走新机器上跑一次同步脚本四套 agent 的 MCP 和技能就都回来了。6.2 别硬套这套方案的三类场景当然它也有边界。远程容器和 CI 环境我建议别用这套本机 symlink 方案。容器内路径和宿主机不一致agent 运行在不同的文件系统里这时候更合适的是把 MCP 配置直接写进项目的.mcp.json提交到仓库里去而不是依赖宿主机全局目录。团队多人协作的项目也不要过度依赖本机脚本。因为同事的机器上未必有你的agent-sync目录。这种场景我会把统一注册表文件放进项目仓库再配一个最小化的引导脚本让大家 clone 后自动生成各自 agent 的配置。还有一种情况是某工具官方开始内建 MCP 聚合或配置中心的时候。如果某个 agent 官方提供了跨项目、跨作用域的统一配置能力优先用官方的毕竟版本升级兼容性更稳。我的脚本只是为了弥合当下各工具各管各的空白期。6.3 我后来加的两个小优化如果你决定自己也搭一套最后分享两个让我体验明显提升的小优化。第一个优化是用 git 管理整个~/.config/agent-sync/目录。这样你不仅能回滚某次同步前的状态还能在一个地方看到 MCP 和技能的历史变更记录。特别是技能提示词的调整用 git 记录后可以对比不同版本的 prompt 差异对调试 agent 行为特别有用。第二个优化是在脚本里加一个“软清理”步骤每次同步时对目标技能目录里那些存在于 agent 侧但不存在于agent-sync源目录的技能给出警告而不是直接删除。因为你可能在某个 agent 里单独加了只有它能用的临时技能直接删掉就误伤了。这套方案不是什么魔法本质上就是把“多个目标、多个格式”的问题收敛成“一个源头、一个脚本”的问题。它不需要你停止写配置文件而是帮你把写配置文件的次数从四次减少到一次。至少在我这里这一个月下来我再也没有因为“哪个 agent 少配了什么工具”而半夜骂人了。