MCP配置同步:Claude Code与Cursor单一源自动化方案
1. 手动维护 MCP 配置这件事到底卡在哪如果你同时用 Claude Code 和 Cursor又恰好给它们配过 MCPModel Context Protocol服务大概率经历过这样的循环在 Claude Code 的配置文件里写一遍 JSON切到 Cursor 又得在它自己的配置目录里再写一遍两边字段格式还不太一样。改一个参数两个文件都得动漏掉一处就出现这个工具在 A 里能用、在 B 里报错的诡异现象。MCP 本身是 Anthropic 推出的开放协议作用是让 AI 编程助手能调用外部工具和数据源——读数据库、查文档、跑命令、访问 API 都靠它。Claude Code 和 Cursor 都支持 MCP但各自的配置入口、字段命名、路径约定并不统一。Claude Code 走的是~/.claude.json或项目级.mcp.jsonCursor 走的是~/.cursor/mcp.json或项目级.cursor/mcp.json。字段上两边虽然都叫mcpServers但环境变量传递、命令参数、超时设置这些细节经常对不上。手动同步的痛点集中在三块重复劳动、格式漂移、Token 浪费。前两个好理解第三个容易被忽略——每次会话启动AI 助手会把 MCP 工具的描述塞进上下文配置写得越啰嗦、工具越多占用的 Token 就越多。一个臃肿的 MCP 配置可能在你还没开始提问时就吃掉几千 Token 的上下文预算。这篇要聊的就是怎么用一个命令把这件事自动化从单一配置源出发自动生成 Claude Code 和 Cursor 各自需要的配置文件顺带把 Token 占用压下来。适合已经在用这两个工具、被 MCP 配置折腾过、或者正准备上手 MCP 但不想一开始就踩坑的人。下面会从原理、方案设计、实操步骤到踩坑经验完整走一遍。2. 为什么两边配置不能直接复制粘贴2.1 Claude Code 与 Cursor 的 MCP 配置差异很多人第一反应是写一份 JSON两边复制过去不就行了。实测下来直接复制能跑通的比例大概只有一半剩下的会在启动时报错或者工具静默失效。差异主要来自几个层面。配置文件的加载优先级不同。Claude Code 会同时读用户级和项目级配置项目级覆盖用户级Cursor 也有类似机制但项目级配置的路径和命名规则不一样。你把用户级配置复制到项目级可能因为路径解析基准不同导致相对路径失效。字段兼容性有细微差别。两边都认command、args、env这几个核心字段但 Cursor 对env里的变量展开更严格Claude Code 相对宽松。有些 MCP 服务依赖cwd工作目录字段Claude Code 支持Cursor 在某些版本里不认得靠args里传路径绕过。工具描述的处理方式不同。这是影响 Token 的关键。Claude Code 会把 MCP 工具的完整 JSON Schema 塞进上下文Cursor 则做了部分裁剪。同一个 MCP 服务在 Claude Code 里可能占 800 Token在 Cursor 里只占 500。如果你手动写配置时给工具加了冗长的description两边都会被放大。下面这张表是我实测后整理的差异对照覆盖最常见的几个字段配置项Claude CodeCursor处理建议用户级路径~/.claude.json~/.cursor/mcp.json分别生成不共用项目级路径.mcp.json.cursor/mcp.json路径不同需映射环境变量展开宽松支持默认值严格未定义即报错统一在源配置里补全cwd字段支持部分版本不支持用args传绝对路径工具描述长度全量注入部分裁剪源配置里精简 description超时设置timeout毫秒timeout秒生成时做单位转换2.2 Token 到底被什么吃掉了MCP 配置影响 Token 的路径有两条。一条是配置本身的体积——配置文件越大AI 助手读取和解析时占用的上下文越多。另一条更隐蔽工具描述注入。每个 MCP 工具都会带一段 JSON Schema 描述它的参数这段描述在每次会话开始时被塞进系统提示或工具列表里。我做过一个粗略测量一个配置了 5 个 MCP 服务、每个服务平均 3 个工具的项目Claude Code 启动时工具描述部分大约占用 3200 Token。如果把每个工具的description从 50 字精简到 15 字能压到 1800 Token 左右。别小看这 1400 Token在长对话里它会被反复计入累积成本相当可观。注意精简 description 不是让你删掉必要信息。工具名、参数名、参数类型这些是 AI 正确调用的基础不能省。能省的是那些这个工具用于……的冗余铺垫以及重复出现在多个工具里的通用说明。2.3 单一配置源的核心思路既然两边格式不同、又都要维护最合理的方案是维护一份源配置用脚本生成两边需要的格式。源配置只写一次包含所有 MCP 服务的完整定义生成脚本负责字段映射、路径转换、单位换算、description 精简。这样改一处两边同步更新不会漂移。这个思路不新鲜本质就是配置管理的单一真实来源Single Source of Truth原则。难点在于映射规则要覆盖两边的差异以及生成过程要幂等——重复执行不会产生重复条目或破坏已有配置。3. 设计一个能落地的同步方案3.1 源配置的格式设计源配置我用 YAML 而不是 JSON原因是 YAML 支持注释、多行字符串、锚点复用写起来比 JSON 舒服得多。格式大致长这样# mcp-source.yaml servers: filesystem: command: npx args: - -y - modelcontextprotocol/server-filesystem - /Users/me/projects env: {} description: 本地文件读写 tools: - name: read_file desc: 读取文件内容 - name: write_file desc: 写入文件 database: command: npx args: - -y - modelcontextprotocol/server-postgres env: DATABASE_URL: postgresql://localhost:5432/mydb description: Postgres 查询 tools: - name: query desc: 执行 SQL 查询关键设计点tools字段是可选的用来覆盖或精简工具描述。如果某个 MCP 服务自带的工具描述已经很精简可以不写tools生成脚本会保留原始描述。如果写了生成脚本会用你提供的desc替换掉服务自带的冗长描述。env里我建议直接写值不要用${VAR}这种占位符。原因是两边对环境变量展开的支持不一致写死值虽然不够灵活但能避免在 A 里能展开、在 B 里报未定义的问题。如果确实需要动态值可以在生成脚本里做替换。3.2 字段映射与单位换算规则生成脚本的核心是一张映射表。Claude Code 和 Cursor 的字段对应关系如下command→ 两边同名直接复制args→ 两边同名直接复制env→ 两边同名但 Cursor 要求所有值都是字符串数字要转成字符串timeout→ Claude Code 用毫秒Cursor 用秒生成时除以 1000cwd→ Claude Code 保留Cursor 丢弃并合并到args里description→ 两边都保留但生成时按tools里的desc覆盖单位换算这块容易出错。我一开始没注意源配置里写timeout: 30000生成到 Cursor 变成 30000 秒等于 8 个多小时实际效果是超时永远不触发。后来改成源配置统一用毫秒生成 Cursor 配置时除以 1000才正常。3.3 生成脚本的幂等性保证脚本要能反复执行而不出问题。做法是每次生成前先读取目标文件解析成对象然后用源配置里的服务覆盖同名服务保留目标文件里源配置没有的服务。这样既同步了源配置的改动又不会删掉你手动加的其他服务。写入时用先写临时文件再原子替换的方式避免写到一半失败导致配置文件损坏。具体就是写到xxx.json.tmpfsync之后rename覆盖原文件。这个细节在 macOS 和 Linux 上都适用Windows 上rename的行为略有不同需要先删除目标文件。4. 一步步把同步命令跑起来4.1 环境准备与依赖安装脚本我用 Node.js 写原因是 Claude Code 和 Cursor 本身都依赖 Node 生态环境里大概率已经有 Node。版本要求 18 以上因为用到了fs/promises的一些新 API。node --version # 确认 18依赖只需要两个js-yaml解析 YAMLproper-lockfile做文件锁防止并发执行时互相覆盖。npm init -y npm install js-yaml proper-lockfile如果你不想装依赖也可以用 Python 写PyYAML是标准选择。但考虑到 MCP 服务本身多是 Node 包统一用 Node 更省心。4.2 源配置文件的编写要点把源配置放在项目根目录的.mcp/目录下命名source.yaml。这样它和生成出来的配置文件在同一层级路径引用简单。编写时有几个要点路径用绝对路径。相对路径在两边解析基准不同容易出问题。如果必须用相对路径在生成脚本里统一转成绝对路径。env 值全部写成字符串。即使值是数字或布尔也加引号。Cursor 对类型敏感。description 控制在 20 字以内。这是给 AI 看的不是给人看的越短越好。tools 列表只列需要精简的工具。不需要精简的不用写脚本会保留原样。一个完整的源配置示例servers: filesystem: command: npx args: - -y - modelcontextprotocol/server-filesystem - /Users/me/projects env: {} description: 本地文件读写 timeout: 30000 tools: - name: read_file desc: 读文件 - name: write_file desc: 写文件 - name: list_directory desc: 列目录 fetch: command: npx args: - -y - modelcontextprotocol/server-fetch env: {} description: 网页抓取 timeout: 600004.3 生成脚本的核心逻辑脚本分四步读源配置、读目标配置、合并、写回。核心代码大概这样const fs require(fs/promises); const path require(path); const yaml require(js-yaml); async function loadSource(srcPath) { const raw await fs.readFile(srcPath, utf8); return yaml.load(raw); } function buildClaudeConfig(source) { const servers {}; for (const [name, cfg] of Object.entries(source.servers)) { servers[name] { command: cfg.command, args: cfg.args, env: cfg.env || {}, ...(cfg.timeout ? { timeout: cfg.timeout } : {}), ...(cfg.cwd ? { cwd: cfg.cwd } : {}), }; } return { mcpServers: servers }; } function buildCursorConfig(source) { const servers {}; for (const [name, cfg] of Object.entries(source.servers)) { const args [...cfg.args]; if (cfg.cwd) args.push(cfg.cwd); servers[name] { command: cfg.command, args, env: Object.fromEntries( Object.entries(cfg.env || {}).map(([k, v]) [k, String(v)]) ), ...(cfg.timeout ? { timeout: Math.round(cfg.timeout / 1000) } : {}), }; } return { mcpServers: servers }; } async function mergeAndWrite(targetPath, newConfig) { let existing { mcpServers: {} }; try { const raw await fs.readFile(targetPath, utf8); existing JSON.parse(raw); } catch (e) { // 文件不存在或解析失败用空配置 } const merged { ...existing, mcpServers: { ...existing.mcpServers, ...newConfig.mcpServers, }, }; const tmp targetPath .tmp; await fs.writeFile(tmp, JSON.stringify(merged, null, 2), utf8); await fs.rename(tmp, targetPath); } async function main() { const source await loadSource(.mcp/source.yaml); const home process.env.HOME; await mergeAndWrite( path.join(home, .claude.json), buildClaudeConfig(source) ); await mergeAndWrite( path.join(home, .cursor, mcp.json), buildCursorConfig(source) ); console.log(MCP 配置已同步); } main().catch((e) { console.error(e); process.exit(1); });这段代码的关键点mergeAndWrite先读已有配置再合并保证幂等buildCursorConfig里做了String(v)转换和 timeout 单位换算写入用临时文件加 rename保证原子性。4.4 把命令挂到日常流程里脚本写好后用package.json的 scripts 挂一个命令{ scripts: { mcp:sync: node .mcp/sync.js } }之后改完源配置跑npm run mcp:sync就行。如果想更省事可以用nodemon监听源配置文件变化自动执行npx nodemon --watch .mcp/source.yaml --exec npm run mcp:sync这样你编辑源配置保存的瞬间两边配置就同步好了。实测下来这个组合很稳改配置的体验从改两处、重启两个工具变成改一处、自动同步。5. 实测中踩到的坑和排查过程5.1 Cursor 不认 cwd 字段的排查第一次跑完脚本Claude Code 里文件系统 MCP 正常工作Cursor 里却报无法访问指定目录。我一开始以为是路径写错了检查了源配置里的路径确认是绝对路径且存在。排查过程是这样的先看 Cursor 的 MCP 日志发现它启动服务时传的args里没有那个路径。回去看生成的 Cursor 配置发现cwd字段确实没写进去——因为我的buildCursorConfig里把cwd合并到了args但合并逻辑写错了args.push(cfg.cwd)这行在cwd为空时也会 push 一个undefined导致参数错位。修复方式是在 push 前判断if (cfg.cwd) args.push(cfg.cwd);这个坑的教训是字段映射不能想当然生成后要实际启动服务验证。光看 JSON 结构对没用得让 MCP 服务真正跑起来。5.2 Token 占用不降反升的意外精简了工具描述后我满心期待 Token 占用下降结果测出来反而涨了 200 多。这个反直觉的结果让我排查了好一阵。原因出在工具名冲突上。我精简 description 时把两个不同服务的工具都改成了读文件AI 在调用时无法区分于是它在上下文里保留了更多候选信息来做判断反而增加了 Token。另外有些 MCP 服务的工具描述里包含了参数约束比如路径必须是绝对路径我精简时把这些约束也删了AI 调用时反复试错多轮对话累积的 Token 更多。修正做法description 精简要保留区分性信息和关键约束。比如读文件改成读本地文件绝对路径写文件改成写本地文件覆盖。这样既短又能区分。5.3 配置文件被覆盖导致的手动配置丢失有一次我手动在 Cursor 配置里加了一个临时 MCP 服务跑了一次同步脚本后它消失了。检查代码发现我的合并逻辑是源配置覆盖目标配置但源配置里没有那个临时服务合并时existing.mcpServers被newConfig.mcpServers覆盖了——我写成了{ ...newConfig.mcpServers }而不是{ ...existing.mcpServers, ...newConfig.mcpServers }。这个 bug 的隐蔽性在于第一次跑没问题目标文件为空第二次跑才暴露。修复后我加了个测试用例专门验证目标文件有源配置没有的服务时合并后该服务保留。提示任何做配置合并的脚本都要写一个保留未知字段的测试。这是配置管理脚本最容易出错的地方而且往往在用了几天后才被发现。5.4 并发执行时的文件损坏有次我同时开了两个终端一个跑同步脚本一个在编辑配置结果配置文件变成了半截 JSON两个工具都启动失败。原因是两个进程同时写同一个文件没有锁。修复方式是引入proper-lockfile在读写前加锁const lockfile require(proper-lockfile); async function safeWrite(targetPath, content) { const release await lockfile.lock(targetPath, { retries: 3 }); try { await fs.writeFile(targetPath, content, utf8); } finally { await release(); } }加锁后并发执行会串行化不会再出现半截文件。这个坑在单人开发时不容易遇到但如果你用 CI 或者多终端操作就很容易踩。6. 让这套方案更耐用的几个调整6.1 按项目隔离配置用户级配置适合放通用的 MCP 服务比如文件系统、fetch项目级配置适合放项目专属的服务比如连项目数据库的 MCP。我的做法是源配置支持scope字段servers: filesystem: scope: user # ... project-db: scope: project # ...生成脚本根据scope决定写到用户级还是项目级路径。这样切项目时项目专属的 MCP 不会污染其他项目通用服务又不用每个项目重复配。6.2 用环境变量管理敏感值数据库连接串这类敏感值不该写进源配置提交到仓库。我的处理是源配置里用占位符生成脚本从环境变量读取实际值env: DATABASE_URL: ${DB_URL}生成时做替换function resolveEnv(env) { const out {}; for (const [k, v] of Object.entries(env)) { const match String(v).match(/^\$\{(\w)\}$/); out[k] match ? (process.env[match[1]] || ) : v; } return out; }这样源配置可以安全提交实际值放在.env文件里记得加进.gitignore。6.3 加一个校验步骤生成后加一步校验检查生成的 JSON 是否合法、必需字段是否齐全、路径是否存在。校验失败就报错退出不写文件。这样能避免生成出坏配置导致工具启动失败。function validate(config) { for (const [name, cfg] of Object.entries(config.mcpServers)) { if (!cfg.command) throw new Error(${name} 缺少 command); if (!Array.isArray(cfg.args)) throw new Error(${name} 的 args 不是数组); } }校验逻辑不复杂但能挡住大部分低级错误。我现在的流程是生成 → 校验 → 写入三步都过了才算成功。6.4 定期清理不再使用的服务MCP 服务装多了Token 占用会线性增长。我养成的习惯是每个月过一遍源配置把最近没用过的服务注释掉跑一次同步。注释掉的服务不会出现在生成的配置里Token 占用自然下降。如果哪天又需要取消注释再同步即可。这个习惯带来的收益比想象中大。我上个月清理了 3 个不常用的 MCP 服务Claude Code 启动时的工具描述 Token 从 2800 降到了 1900 左右长对话的响应速度也有可感知的提升。7. 关于这套方案我自己的使用体会这套同步方案我用了大概两个月最大的感受是配置管理这件事自动化一次省心很久。以前每次加新 MCP 服务都要在两个工具里各配一遍还要担心格式对不对现在只改源配置跑个命令就完事。有几个细节值得再强调一下。源配置的tools字段不要一开始就写满先让服务自带的描述跑一段时间发现哪个工具的描述确实冗长再精简。精简时保留区分性信息和关键约束别为了短而短。生成脚本的合并逻辑一定要测保留未知字段的场景这是最容易出 bug 的地方。另外如果你用的是团队协作环境源配置可以提交到仓库生成脚本也提交但生成的配置文件~/.claude.json、~/.cursor/mcp.json不要提交它们应该由每个人本地生成。这样团队里每个人的 MCP 配置都从同一份源配置派生不会出现我这边能用你那边不能用的情况。最后分享一个小技巧如果你不确定某个 MCP 服务该不该保留先把它注释掉跑一周。一周内没想起来用它基本就可以删了。MCP 服务的价值在于高频使用低频服务留着只是占 Token。