一份YAML管所有AI代理:MCP配置同步方案全解析 1. 被重复配置逼疯后的自救我最近终于把电脑上的 AI 编程代理整理到了同一个武器库里。起因很朴素同一套 MCP 工具我给 Claude Code 配完又给 Codex 配了一遍接着是 Cursor最后新装的 Hermes Agent 也来一遍——光一个 filesystem 服务器的参数我就敲了四遍。如果你只用其中一个代理完全感觉不到这个问题。但像我这种按照任务切换工具的用法就很难受了。MCP 工具的定义一旦要改比如把可访问的目录从~/Documents换成~/Projects/work四个地方都要同步改。起初我还能靠记忆硬撑直到有一次 Codex 还挂着旧路径文件读不到我排查了十分钟才发现是配置没同步。那种感觉太蠢了。也就是从那时候起我开始做这套“0 配置”同步方案把 MCP 工具和技能的定义收到一个文件里用一个脚本自动分发到本机所有代理。今天这篇文章就是把整个思路和踩过的坑完整讲一遍。适合同时使用多个 AI 代理、不想在每个工具里重复维护配置、以及准备给团队推广一套统一 MCP 工具规范的读者。先说结论这套方案做下来之后我再也没有手工改过任何一个代理的 MCP 配置文件。新增一个工具只需要在中心 YAML 里加几行然后关掉终端重开所有代理就都有了。真正改变日常的是这个“一次性冗余”被消灭掉之后你会更愿意频繁调整工具集而不是因为改起来麻烦就凑合着用旧配置。2. 为什么“一份配置四处复用”能做到在写同步脚本之前我先花了两天确认一件事四个代理的 MCP 配置格式完全不同但底层语义到底是不是完全一致的答案是肯定的而且这种一致性是 MCP 协议天然带来的。2.1 MCP 本质上和客户端解耦MCPModel Context Protocol模型上下文协议里的“服务器”本质是一个独立进程。它可以是一个 npx 包一个 Python 脚本一个编译好的二进制甚至是一个远程接口。它与 Claude Code、Codex 这些客户端的关系是客户端启动时按照配置好的命令把服务器进程拉起来然后通过标准输入输出通信交换工具、资源、提示词。所以每个代理的配置文件其实只做了三件事告诉代理启动哪个命令、传什么参数、这个服务器叫什么名字。至于服务器内部如何实现、如何和远端交互客户端完全不管。这就意味着你可以完全共享一份语义相同的“启动配方”只为不同代理套上不同的客户端语法外壳。这是整个同步方案的地基。如果你的工具是那种“必须在某个客户端里装插件”的封闭格式那今天这套做法就不成立。但 MCP 走的是开放协议客户端只是个壳壳可以换内容不变。2.2 用 npx/uvx 做“裸运行”配置从装箱单变成配方看清楚这一点之后第二个问题来了同一套服务器在不同机器上路径可能完全不同。如果你把/usr/local/bin/xxx这种绝对路径写死在配置里那换台机器配置就要重写一遍。真正让“0 配置”成为可能的是 npx 和 uvx 这种“裸运行”机制。npx 允许你直接执行某个 npm 包里的可执行文件不需要先全局安装。市面上绝大多数 MCP 服务器都发布成了 npm 包或 Python 包于是配置里写的就不再是“我装在哪里的哪个程序”而是“这个包叫什么用什么运行时启动”。比如npx --yes modelcontextprotocol/server-filesystem这一行在任何一台装了 Node 的机器上都能跑起来客户端只需要关心这一行不需要关心包安装细节。这就是“从装箱单变成配方”的意思。装箱单记录的是“某个位置有一个东西”换个位置就失效配方记录的是“从哪里获取什么”只要来源还在任何环境都能复现。对于本机多代理同步来说配方粒度的配置天然适合生成和分发。2.3 技能的文本属性让共享变得容易技能Skill这个概念的载体也很有意思。它不像 MCP 服务器那样有一个需要编译、运行的进程技能本质上是一份给代理看的 Markdown 操作手册。以 Claude Code 为例一个技能就是某个目录下的SKILL.md里面写清步骤、检查清单、示例代理在需要的时候按这个手册执行。因为技能是纯文本所以它天然能被复制、软链接、被 git 版本管理甚至可以跨代理共享。但做的时候要意识到每个代理对“技能文件放哪里、后缀名是什么、要不要 frontmatter”的容忍度略有不同。你完全可以准备一份主目录的 Markdown再通过链接和小小的“指路牌”文件让每个代理各自读到。到这里方案的整体轮廓就清晰了用一份 YAML 统一声明所有 MCP 服务器和技能清单用 npx/uvx 保证“配方”在任何环境可执行用一个生成脚本把这份声明翻译成四个代理各自的配置格式用符号链接和指路牌文件让技能目录也实现单一真源。3. 搭建统一配置源一份 YAML 管所有工具动手之前先定好目录和文件的布局。这是整个方案里最需要设计的一步后面所有脚本逻辑都依赖它。3.1 目录布局我选在用户主目录下建一个.agent-sync目录和代理自身的配置目录~/.claude、~/.codex这些平级避免互相干扰~/.agent-sync/ ├── config.yaml ├── skills/ │ ├── code-review/ │ │ └── SKILL.md │ ├── plan-and-design/ │ │ └── SKILL.md │ └── web-research/ │ └── SKILL.md ├── generated/ │ ├── cursor-mcp.json │ ├── codex-mcp.toml │ └── hermes-mcp.json └── agent-sync.pyconfig.yaml是唯一需要手工维护的文件skills/是技能主目录后续所有代理的技能都会从这里的子目录链接或派生generated/放生成出来的中间文件方便排查问题agent-sync.py是同步脚本本体。这样一个目录的好处是整个方案可以作为一个独立仓库用 git 管理换机器时克隆下来就能用。3.2 config.yaml 怎么写下面是一份我实际在用的配置文件你可以直接复制改:profile: work mcp_servers: filesystem: command: npx args: - --yes - modelcontextprotocol/server-filesystem - ~/Documents - ~/Projects/work git: command: npx args: - --yes - modelcontextprotocol/server-git fetch: command: npx args: - --yes - mcp-server-fetch memory: command: npx args: - --yes - modelcontextprotocol/server-memory sqlite: command: npx args: - --yes - mcp-server-sqlite - ~/.local/share/agent-sync.db skills: - code-review - plan-and-design - web-researchmcp_servers字段的每一项就是一个 MCP 服务器command是启动命令args是启动参数。这几个服务器都是社区常用的filesystem把本地目录暴露给代理让它可以读写文件git让代理直接查询 git 状态、提交历史、变更内容fetch抓取网页内容给代理分析memory提供一份持久的键值存储代理可以把长期信息写进去sqlite直接查询本地 SQLite 数据库适合分析结构化数据。skills字段是一个列表列出本机所有代理都应该启用的技能名。名字要对应skills/目录下的子目录名。3.3 为什么选 YAML 而不是 JSON有人可能会问JSON 不是更通用吗为什么不用 JSON 做中心配置就我的实际体验来说有三个理由第一YAML 支持注释。MCP 服务器的参数往往是路径、密钥、目录白名单旁边写一行注释说明“这个目录是用来放临时文件的”一个月后回来看还能想起来当初为什么这么配置。JSON 想加注释就麻烦了。第二YAML 的多行列表可读性更好。看一个args列表的每一项占一行比在 JSON 里挤成一团舒服得多。尤其在服务器数量超过五六个之后这个差异会非常明显。第三YAML 和 JSON 本质上可以互相转换脚本里yaml.safe_load读进来转成 Python dict最后再按目标格式序列化完全没障碍。所以中心配置用了 YAML并不影响面向不同代理生成 JSON 或 TOML。4. agent-sync一条命令同步四个代理有了统一配置源剩下的事情就是写生成脚本。这里的核心原则是“不要手工覆盖代理已有的其他配置只操作我们负责的那部分”。4.1 Claude Code 的同步逻辑Claude Code 提供了claude mcp命令来管理 MCP 服务器所以最优做法不是直接写配置文件而是调用官方 CLI。以用户级范围为例claude mcp remove --scope user filesystem 2/dev/null claude mcp add --scope user filesystem -- npx --yes modelcontextprotocol/server-filesystem ~/Documents ~/Projects/work需要注意--这个分隔符。--之前的部分是给claude mcp自己的参数--之后的部分会被原样透传给真正的服务器命令。我第一次写的时候就漏了它结果--yes被 claude 命令截获解析报错折腾了十来分钟才反应过来。为什么要先 remove 再 add因为同步脚本会被反复执行如果某次你在中心配置里删掉了一个服务器脚本必须能把残留的旧注册清理掉。所以流程是先把它认识的所有旧注册全删掉再按当前配置重新注册。这样脚本天然幂等执行多少次结果都一样。验证同步是否成功运行claude mcp list就能看到。4.2 Codex 的同步逻辑Codex 的配置在~/.codex/config.tomlMCP 服务器放在[mcp_servers.*]段model gpt-5 provider openai [mcp_servers.filesystem] command npx args [--yes, modelcontextprotocol/server-filesystem, ~/Documents, ~/Projects/work] [mcp_servers.git] command npx args [--yes, modelcontextprotocol/server-git]这里容易犯的低级错误是 TOML 的语法细节。args必须是数组而且字符串要用引号包住漏掉引号会让 TOML 解析器认为它是一个裸词静默忽略掉。还有一个更隐蔽的问题如果你整文件覆盖写入会把用户自己设置的model、provider、api_key等配置全部冲掉。所以脚本里一定要先读旧配置再合并mcp_servers段最后写回。这也是我后来改用toml库而不是字符串拼接的原因。4.3 Cursor 的同步逻辑Cursor 的 MCP 配置是 JSON 格式放在~/.cursor/mcp.json用户级或项目目录的.cursor/mcp.json项目级{ mcpServers: { filesystem: { command: npx, args: [--yes, modelcontextprotocol/server-filesystem, ~/Documents, ~/Projects/work] }, git: { command: npx, args: [--yes, modelcontextprotocol/server-git] } } }Cursor 的字段名是驼峰mcpServers不是下划线mcp_servers。两个写法混用是新手最容易踩的坑工具列表永远加载不出来。同步脚本里生成 JSON 之后我建议再用jq或者读回验证一下字段名别等到打开 Cursor 才发现一片空白。4.4 Hermes Agent 的同步逻辑Hermes Agent 是我后来才加进来的终端代理它同样支持 MCP但它作为一个更新鲜的工具配置路径和格式在不同版本间改过几次。以我当时安装的版本为例配置在~/.hermes/config.json字段和 Cursor 一样是mcpServers{ mcpServers: { filesystem: { command: npx, args: [--yes, modelcontextprotocol/server-filesystem, ~/Documents, ~/Projects/work] } } }如果你用的版本路径不是这个运行hermes agent --help输出第一行一般就会告诉你配置目录在哪。这个例子也更加说明了为什么要做“统一源 生成器”新代理、小版本工具的配置格式本来就不稳定手工来回改会把人逼疯而写死在生成脚本里的模板格式一变只需改一段不用重新复制。4.5 同步脚本完整代码下面是我在用的完整脚本Python 写的依赖yaml和toml两个库。如果你机器上没有toml库脚本也会走一个简易兜底路径直接用字符串拼接生成 Codex 配置#!/usr/bin/env python3 agent-sync把统一 MCP/技能配置同步到本机所有 AI 代理。 import argparse import json import subprocess import sys from pathlib import Path try: import toml except ImportError: toml None try: import yaml except ImportError: yaml None SYNC_DIR Path.home() / .agent-sync CONFIG_FILE SYNC_DIR / config.yaml def log(msg): if not args.quiet: print([agent-sync], msg) def run(cmd): log( .join(cmd)) subprocess.run(cmd, checkTrue, stdoutsubprocess.DEVNULL) def load_config(): if not CONFIG_FILE.exists(): sys.exit(f缺少配置文件: {CONFIG_FILE}) with open(CONFIG_FILE, encodingutf-8) as f: return yaml.safe_load(f) def sync_claude_code(cfg): servers cfg[mcp_servers] for name in servers: run([claude, mcp, remove, --scope, user, name]) for name, s in servers.items(): run([claude, mcp, add, --scope, user, name, --, s[command], *s[args]]) skills_home Path.home() / .claude / skills skills_home.mkdir(parentsTrue, exist_okTrue) for skill in cfg.get(skills, []): src SYNC_DIR / skills / skill dst skills_home / skill if dst.exists() or dst.is_symlink(): dst.unlink() dst.symlink_to(src, target_is_directoryTrue) def sync_codex(cfg): codex_cfg Path.home() / .codex / config.toml codex_cfg.parent.mkdir(parentsTrue, exist_okTrue) data {} if codex_cfg.exists() and toml: with open(codex_cfg, encodingutf-8) as f: data toml.load(f) data[mcp_servers] {} for name, s in cfg[mcp_servers].items(): data[mcp_servers][name] {command: s[command], args: s[args]} if toml: with open(codex_cfg, w, encodingutf-8) as f: toml.dump(data, f) else: lines [[mcp_servers]] for name, s in cfg[mcp_servers].items(): lines.append( f{name} {{ command {s[command]}, args {json.dumps(s[args])} }} ) with open(codex_cfg, w, encodingutf-8) as f: f.write(\n.join(lines)) def write_json_config(path, servers, keymcpServers): path.parent.mkdir(parentsTrue, exist_okTrue) existing {} if path.exists(): with open(path, encodingutf-8) as f: existing json.load(f) existing[key] {} for name, s in servers.items(): existing[key][name] {command: s[command], args: s[args]} with open(path, w, encodingutf-8) as f: json.dump(existing, f, ensure_asciiFalse, indent2) def sync_cursor(cfg): write_json_config(Path.home() / .cursor / mcp.json, cfg[mcp_servers], keymcpServers) rules_dir Path.home() / .cursor / rules rules_dir.mkdir(parentsTrue, exist_okTrue) for skill in cfg.get(skills, []): dst rules_dir / f{skill}.mdc content f--- description: 在需要 {skill} 技能时读取 ~/.agent-sync/skills/{skill}/SKILL.md 并按其步骤执行。 globs: [*.py, *.js, *.ts, *.md] --- # {skill} 完整内容见中心仓库~/.agent-sync/skills/{skill}/SKILL.md dst.write_text(content, encodingutf-8) def sync_hermes(cfg): write_json_config(Path.home() / .hermes / config.json, cfg[mcp_servers], keymcpServers) def main(): cfg load_config() sync_claude_code(cfg) sync_codex(cfg) sync_cursor(cfg) sync_hermes(cfg) log(全部完成Claude Code / Codex / Cursor / Hermes Agent 已同步。) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--quiet, actionstore_true) args parser.parse_args() main()脚本的核心思想非常直接读取中心 YAML然后分别调用各个代理的写入逻辑。write_json_config被 Cursor 和 Hermes 共用区别只在目标路径和字段名。sync_codex做了合并而不是覆盖避免误伤config.toml里的其他配置。如果你在 Windows 上运行有两点要改一是claude、npx这些命令可能要写成npx.cmd的完整路径因为子进程调用有时候找不到.cmd后缀二是符号链接可能需要管理员权限这时候可以把symlink_to换成os.symlink的普通文件链接或者直接用Path.copytree复制整个技能目录。不过能跑 Linux 或 WSL 的话体验会顺滑很多。4.6 让它自动跑shell 启动时的“静默同步”脚本写好了但“0 配置”的最后一步还没完成每次新加一个服务器难道还得手动去跑一遍脚本吗那只能说把“配四个文件”变成“配一个文件加跑一条命令”还不是真正的零配置。我的做法是把同步脚本挂到 shell 启动时让它静默执行。以 zsh 为例在~/.zshrc末尾加几行if [ -f $HOME/.agent-sync/agent-sync.py ]; then python3 $HOME/.agent-sync/agent-sync.py --quiet fi这样每次打开一个新的终端窗口脚本就会把最新的中心配置同步到所有代理。你甚至不用记得同步这件事只要重新开一个终端或者新开代理窗口工具就已经更新好了。有人担心每次开终端都跑一遍脚本会不会很慢。实测下来这个脚本本身只是写几个小文件和执行几条claude mcp命令不会去启动任何 MCP 服务器进程所以耗时一般在几百毫秒以内。加了--quiet后连输出都没有完全感觉不到它的存在。不过如果你特别在意启动速度也可以做成懒加载不在.zshrc里跑而是用一个函数包装claude、codex这些命令调用前先同步一次。我试过这种方案但说实话收益不大反而让命令行为变得不可预测最后还是回到了启动时同步。5. 技能目录的四种吃法MCP 服务器只是问题的一半另一半是技能。技能的管理思路和服务器不太一样因为不同代理读取技能文件的位置和格式差异很大。5.1 中心技能仓库的结构中心技能仓库放在~/.agent-sync/skills/下每个技能一个子目录子目录里必须有SKILL.md作为主文件其他辅助文件按需放skills/code-review/ ├── SKILL.md └── examples/ └── before-after.mdSKILL.md的写法我没有定死但建议保持在几百行以内重点写清楚适用场景、操作步骤、检查清单。代理读技能的时候是把它当上下文塞给模型的太长会白白消耗 token。5.2 直接符号链接Claude Code 的方式Claude Code 的技能目录就是~/.claude/skills/格式正好就是“目录 SKILL.md”所以最省事的同步方式就是符号链接ln -sfn ~/.agent-sync/skills/code-review ~/.claude/skills/code-review链接之后你只需要维护中心目录这一份文件Claude Code 每次启动都会读取到最新内容。修改技能时也不用再跑什么同步步骤因为链接本身就是实时的。5.3 生成指路牌Cursor 和 Codex 的方式但直接符号链接不是万能的。Cursor 的规则文件要求.mdc后缀而且要有 YAML frontmatterCodex 的项目级提示走的是AGENTS.md。如果强行把SKILL.md链接成.mdc很多规则解析器会因为缺少 frontmatter 而忽略它。我的做法是生成一个“指路牌”文件而不是复制全文。以 Cursor 为例在~/.cursor/rules/下生成一个code-review.mdc--- description: 在需要代码审查技能时读取 ~/.agent-sync/skills/code-review/SKILL.md 并按其步骤执行。 globs: [*.py, *.js, *.ts] --- # code-review 完整内容请查看中心仓库~/.agent-sync/skills/code-review/SKILL.md指路牌本身非常小几乎不占上下文但它的效果是让代理知道“有这么个技能”并且明确告诉它去哪里拿完整版。因为完整版在中心目录所有代理看到的永远是最新内容不用像复制粘贴那样反复同步。Codex 的AGENTS.md同理在里面写一句“当任务涉及代码审查时必须读取~/.agent-sync/skills/code-review/SKILL.md并按其中步骤执行”即可。有些团队会把整篇技能复制到repo/AGENTS.md里那种模式对单个项目没问题但如果你想做全局技能指路牌显然是维护成本更低的方案。5.4 用 git 把技能串起来技能是会不断演化的。今天觉得 code-review 的检查清单该加一条“检查异常处理是否吞掉了关键错误”明天觉得应该删掉一条过时的规范。这时候如果没有版本管理很容易改乱。我给~/.agent-sync整个目录建了 git 仓库cd ~/.agent-sync git init git add . git commit -m init agent-sync这样每次技能调整都有历史记录想回滚就回滚。换新机器时只需要克隆这个仓库再安装好 Python 依赖跑一次脚本整个环境就回来了。如果团队里有多人需要同一套 MCP 工具和技能规范把仓库推到一个内部远程地址大家 pull 下来改自己的 profile 字段就行。6. 实操问题与避坑速查任何工具用久了总会遇到几个诡异的坑。把我在实际使用中反复遇到的几个问题整理出来方便你排查。6.1 先问三个排查问题MCP 工具不出现的时候我的排查顺序是固定的第一手动执行一次这个服务器的命令看它本身能不能跑起来。如果在命令行里都报错那问题根本不在配置同步而在服务器本身。第二在对应代理里查看它实际读到的配置。Claude Code 用claude mcp listCodex 直接打开config.toml检查Cursor 看~/.cursor/mcp.json。确认你改的内容真的写进了它读的文件。第三检查环境变量差异。有些代理是从图形界面启动的PATH 里没有 npx有些是从特定 shell 启动的~/.bashrc里的路径没加载。尤其是 macOS 上通过 Finder 启动的应用PATH 往往和终端里完全不同。这条路径走下来十次有八次能定位到问题。6.2 高频坑位表格下面是我实际踩过或者帮别人排查过的坑现象常见原因解决办法claude mcp add报参数错误忘了在命令前加--分隔符把服务器命令放在--之后Codex 工具一直不出现TOML 中字符串没加引号被解析器忽略确保command和args都是合法字符串数组Cursor 工具列表为空字段名写成了mcp_servers改成驼峰mcpServersnpx 首次启动特别慢需要的包还没下载到本地缓存提前执行一次npx --yes 包名预热缓存Windows 下代理找不到命令子进程没有.cmd后缀路径在配置里写npx.cmd绝对路径技能目录更新了但代理没反应代理读取的是启动时的快照完全退出代理进程再重新打开MCP 服务器太多导致上下文被占满工具定义数量太多用profile字段区分工作/个人场景按需同步运行脚本后 Codex 原有 api_key 丢失整文件覆盖了config.toml脚本改为合并mcp_servers段保留其他字段表格里的坑大多是配置格式的细节问题不是协议本身的问题。这也说明了为什么“统一源”有价值格式错误只需要在生成模板里修一次所有代理同时受益如果手工维护四个文件同样的错误你会在四个地方各踩一遍。6.3 关于“零配置”的正确期望最后必须诚实一点所谓“0 配置”不是指你完全不用写任何配置而是指“不再重复配置”。你仍然需要花十分钟写第一份中心 YAML需要维护技能仓库需要在换新代理时给脚本加一个新模板。但这十分钟换来的是以后每一次增删工具的真实零成本。我见过有人把这个方案理解成“安装完就能用”然后失望地发现还要装 Python 依赖、还要写 YAML。这其实搞错了重点。零配置想解决的是“维护”的痛点不是“初始化”的痛点。初始化的十分钟换来的是后续所有变更的零成本这是完全划算的。7. 我后续还打算折腾的方向这套方案目前已经稳定跑了几个月四个代理的 MCP 工具和技能始终一致我再也没因为配置不同步而白排查过问题。但说实话它还有不少可以演进的空间我也在慢慢补。一个方向是按项目切分 profile。现在中心配置是全局一份所有项目能用的工具完全相同。有些场景其实不需要全部工具比如写文档的项目根本用不上 sqlite 服务器。更好的设计是在config.yaml里定义几个 profile同步脚本根据当前项目目录自动选择启用哪一组工具。这样既能减少 token 消耗也能避免代理在无关的上下文里乱翻文件。另一个方向是给团队做一个共享仓库。把技能仓库放在一个内部地址成员各自维护自己的 profile公共的 MCP 工具列表由一个人统一更新。本质上是把“本机同步”升级成“团队同步”脚本结构不变只是多了一个git pull的环节。最后提一个我在实际操作中的体会这套方案最值钱的不是省下了多少时间而是它让你愿意频繁调整工具集了。以前改一次配置要动四个文件你会下意识抵触变更索性凑合着用旧方案现在改一份 YAML 就生效你可以随时把新工具加进来测试不合适就删掉。工具的粒度真正变小了使用的灵活性反而上来了。这种“低摩擦带来高调整频率”的效果比单纯省时间更值得关注。