
1. 手动维护 MCP 配置这件事到底卡在哪儿如果你同时用 Claude Code 和 Cursor又刚好在折腾 MCPModel Context Protocol大概率经历过这样的循环在 Claude Code 里加了一个 filesystem 服务写一遍 JSON切到 Cursor发现它不认这份配置得按 Cursor 的格式再写一遍过两天加了个新的 MCP server两边又得各改一次。改完还得重启、验证、排查为什么这个 server 没起来。MCP 本身是个好东西。它把模型和外部工具之间的调用协议标准化了让 Claude Code、Cursor 这类客户端能通过统一的接口去访问文件系统、数据库、浏览器、第三方 API。但问题在于每个客户端对 MCP 配置的存放位置、字段命名、启动方式都有自己的脾气。Claude Code 认~/.claude.json或者项目级的.mcp.jsonCursor 认~/.cursor/mcp.json字段上有的用commandargs有的还要求env、disabled、autoApprove这些附加项。你手动同步本质上是在做一件机器该做的事。更隐蔽的坑是 Token 消耗。MCP 的配置里如果塞了一堆用不上的 server或者每个 server 的description写得又臭又长这些内容会作为上下文的一部分被送进模型。你以为只是配置实际上每次对话都在为这些冗余描述付费。我见过一个配置里挂了 12 个 MCP server光工具描述就吃掉了几千 token 的上下文窗口真正干活的空间被压缩得厉害。所以这篇要解决的问题很具体用一个命令把一份 MCP 配置源自动同步到 Claude Code 和 Cursor同时把配置里不必要的描述精简掉降低 Token 占用。适合已经在用这两个工具、手里有多个 MCP server、并且被手动同步折磨过的人。如果你还没装 Claude Code 或者 Cursor后面也会顺带说清楚安装和基础配置的路径。2. 先搞清楚 Claude Code 和 Cursor 各自认哪份配置在动手写同步脚本之前必须先把两个客户端的配置读取逻辑摸清楚。这一步偷懒后面同步出来的文件大概率不生效你还得回头排查反而更费时间。2.1 Claude Code 的 MCP 配置层级Claude Code 的 MCP 配置分两个层级。用户级配置放在~/.claude.json里里面的mcpServers字段是全局生效的不管你从哪个目录启动 Claude Code 都能用。项目级配置放在项目根目录的.mcp.json只对当前项目生效适合那种只在特定仓库里才需要的 server比如某个项目专用的数据库连接。优先级上项目级会覆盖用户级里同名的 server。这个设计其实挺合理你全局配了一个通用的 filesystem server某个项目想换成指向自己目录的实例直接在项目里覆盖就行不用动全局配置。Claude Code 的 server 定义长这样{ mcpServers: { filesystem: { command: npx, args: [-y, modelcontextprotocol/server-filesystem, /path/to/dir], env: {} } } }字段很直白command是启动命令args是参数数组env是环境变量。注意command和args是分开的不是写成一整条 shell 字符串。这一点在同步的时候容易出错后面会讲。2.2 Cursor 的 MCP 配置位置与差异Cursor 的 MCP 配置默认在~/.cursor/mcp.json结构上跟 Claude Code 很像也是mcpServers下面挂一个个 server。但 Cursor 多了一些自己的字段比如disabled用来临时关掉某个 serverautoApprove用来控制哪些工具调用不需要手动确认。{ mcpServers: { filesystem: { command: npx, args: [-y, modelcontextprotocol/server-filesystem, /path/to/dir], env: {}, disabled: false, autoApprove: [] } } }如果你直接把 Claude Code 的配置复制给 Cursordisabled和autoApprove缺失Cursor 一般也能跑但你就失去了细粒度控制。反过来把 Cursor 的配置给 Claude Code多出来的字段 Claude Code 会忽略倒不至于报错但配置里留着一堆用不上的字段看着乱也增加了维护心智。2.3 两边字段的对照关系字段Claude CodeCursor说明command支持支持启动命令必填args支持支持参数数组必填env支持支持环境变量可选disabled不支持支持Cursor 独有控制启用状态autoApprove不支持支持Cursor 独有自动批准的工具列表description忽略忽略两边都不用于运行时但会进上下文最后一行是关键。description字段两边都不参与实际启动但它会作为工具描述的一部分被送进模型上下文。很多人从各种模板里抄配置description 写得特别详细结果每次对话都在为这些文字付 Token。同步脚本里应该主动把 description 精简或者去掉。3. 设计一份单一数据源的 MCP 配置同步的核心思路是你只维护一份源配置脚本负责把它转换成两个客户端各自需要的格式。这份源配置放在哪、长什么样直接决定了后面脚本的复杂度。3.1 源配置的存放位置选择我试过几种方案。放在项目根目录的.mcp-source.json好处是跟着项目走换机器 clone 下来就有坏处是如果你有多个项目共用同一批 server每个项目都得复制一份。放在用户目录的~/.mcp-source.json好处是全局唯一改一处所有项目受益坏处是项目特有的 server 没法放进去。最后我采用的是混合方案全局源配置放~/.mcp-source.json项目级源配置放项目根目录的.mcp-source.json。脚本运行时先读全局再用项目级的覆盖同名 server。这样通用的 filesystem、fetch 这类 server 放全局项目专用的数据库、内部 API 放项目级逻辑清晰。3.2 源配置的字段设计源配置不需要区分客户端用一套统一的字段脚本在输出时再按目标客户端做转换。我定义的源配置格式如下{ servers: { filesystem: { command: npx, args: [-y, modelcontextprotocol/server-filesystem, /Users/me/projects], env: {}, enabled: true, autoApprove: [read_file, list_directory], summary: 本地文件读写 } } }跟客户端配置的区别在于用enabled统一表示启用状态输出到 Claude Code 时直接忽略因为 Claude Code 没有这个字段不写就是启用输出到 Cursor 时转成disabled: !enabled。用summary代替冗长的description控制在十个字以内输出时作为精简描述。autoApprove只在 Cursor 输出时保留Claude Code 输出时丢弃。3.3 为什么要精简 description这里展开说一下 Token 的事。MCP server 的工具描述会作为 system prompt 的一部分进入上下文。一个 server 如果有 5 个工具每个工具的描述 50 个 token那就是 250 token。10 个 server 就是 2500 token。这还只是描述本身不包括工具名、参数 schema。我实测过一个配置把每个 server 的 description 从平均 80 字压到 10 字以内整体上下文占用下降了大概 15%。对于长对话来说这 15% 可能就是能不能多塞一轮对话的区别。所以源配置里我用summary强制自己写短描述脚本输出时也只带这一句不把原始的长描述透传过去。注意精简 description 不影响功能只影响模型对工具的理解程度。如果某个 server 的工具名本身就很清晰比如read_file、write_filedescription 甚至可以留空。只有那些工具名含义模糊的 server才需要一句简短说明。4. 写一个命令搞定双向同步的脚本脚本我用 Node.js 写原因是 Claude Code 和 Cursor 本身都依赖 Node 环境你机器上大概率已经有 node 和 npx不用额外装 Python 或者别的运行时。脚本逻辑不复杂核心就是读源配置、做字段映射、写目标文件。4.1 脚本的整体结构脚本分四步读取全局源配置、读取项目级源配置并合并、生成 Claude Code 格式、生成 Cursor 格式。每一步都做幂等处理重复运行结果一致不会因为跑了两遍就把配置搞乱。#!/usr/bin/env node const fs require(fs); const path require(path); const os require(os); const HOME os.homedir(); const GLOBAL_SOURCE path.join(HOME, .mcp-source.json); const PROJECT_SOURCE path.join(process.cwd(), .mcp-source.json); const CLAUDE_TARGET path.join(HOME, .claude.json); const CURSOR_TARGET path.join(HOME, .cursor, mcp.json); function readJson(file) { if (!fs.existsSync(file)) return null; try { return JSON.parse(fs.readFileSync(file, utf8)); } catch (e) { console.error(解析失败: ${file}); process.exit(1); } } function loadSources() { const global readJson(GLOBAL_SOURCE) || { servers: {} }; const project readJson(PROJECT_SOURCE) || { servers: {} }; return { ...global.servers, ...project.servers }; }合并逻辑用对象展开项目级覆盖全局同名 server。这里没有做深合并因为 server 定义是一个整体部分字段覆盖反而容易出问题直接整体替换更符合直觉。4.2 转换成 Claude Code 格式Claude Code 只需要command、args、env三个字段其他全部丢弃。enabled为 false 的 server 直接跳过因为 Claude Code 没有禁用字段不写就等于不启用。function toClaudeFormat(servers) { const result {}; for (const [name, cfg] of Object.entries(servers)) { if (cfg.enabled false) continue; result[name] { command: cfg.command, args: cfg.args || [], env: cfg.env || {} }; } return { mcpServers: result }; }写入的时候要注意~/.claude.json里除了mcpServers还有别的字段比如项目历史、用户偏好不能整个文件覆盖得读出来改mcpServers再写回去。function writeClaude(servers) { const existing readJson(CLAUDE_TARGET) || {}; existing.mcpServers toClaudeFormat(servers).mcpServers; fs.writeFileSync(CLAUDE_TARGET, JSON.stringify(existing, null, 2)); }4.3 转换成 Cursor 格式Cursor 需要额外处理disabled和autoApprove。enabled为 true 时disabled为 false反之亦然。autoApprove没有就默认空数组。function toCursorFormat(servers) { const result {}; for (const [name, cfg] of Object.entries(servers)) { result[name] { command: cfg.command, args: cfg.args || [], env: cfg.env || {}, disabled: cfg.enabled false, autoApprove: cfg.autoApprove || [] }; } return { mcpServers: result }; } function writeCursor(servers) { const dir path.dirname(CURSOR_TARGET); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const existing readJson(CURSOR_TARGET) || {}; existing.mcpServers toCursorFormat(servers).mcpServers; fs.writeFileSync(CURSOR_TARGET, JSON.stringify(existing, null, 2)); }注意 Cursor 的配置目录可能不存在第一次运行时得先创建。这个细节不处理的话脚本会直接报错退出。4.4 主流程与执行function main() { const servers loadSources(); const count Object.keys(servers).length; if (count 0) { console.log(源配置为空请先编辑 ~/.mcp-source.json); return; } writeClaude(servers); writeCursor(servers); console.log(已同步 ${count} 个 MCP server 到 Claude Code 和 Cursor); } main();把脚本存成~/.local/bin/mcp-sync.js加执行权限然后在 shell 配置里加个别名chmod x ~/.local/bin/mcp-sync.js echo alias mcp-syncnode ~/.local/bin/mcp-sync.js ~/.zshrc source ~/.zshrc之后每次改完源配置终端里敲一个mcp-sync就完事。两个客户端的配置文件同时更新不用来回切。5. 实测中遇到的几个坑和排查过程脚本跑通不代表万事大吉。我在实际用的时候踩了几个坑每个都花了不少时间排查这里完整记录一下排查链路你遇到类似问题时可以照着走。5.1 npx 启动慢导致的超时误判第一个坑是 server 启动超时。配置里用了npx -y modelcontextprotocol/server-filesystem第一次运行时 npx 要去下载包耗时可能十几秒。Claude Code 和 Cursor 对 server 启动都有超时限制超时后客户端会认为这个 server 挂了工具列表里看不到它。排查的时候我一开始以为是配置字段写错了反复检查 command 和 args都没问题。后来单独在终端里跑了一遍npx -y modelcontextprotocol/server-filesystem /path发现第一次跑了 20 多秒才起来第二次就快了。这才意识到是 npx 的下载缓存问题。解决办法有两个一是提前手动跑一遍把包缓存到本地二是把 npx 换成全局安装后的直接命令。我选的是后者npm install -g modelcontextprotocol/server-filesystem然后 command 直接写mcp-server-filesystem启动时间降到 1 秒以内。提示所有基于 npx 的 MCP server 都有这个首次启动慢的问题。如果你发现某个 server 时好时坏先怀疑是不是 npx 缓存没命中。5.2 路径里的空格把 args 拆错了第二个坑更隐蔽。我的项目路径里有个带空格的目录比如/Users/me/My Projects。在源配置里 args 写的是[-y, modelcontextprotocol/server-filesystem, /Users/me/My Projects]看起来没问题数组元素本身就是完整路径。但有一次我图省事把 command 和 args 合并成了一个字符串npx -y modelcontextprotocol/server-filesystem /Users/me/My Projects结果 server 启动后只能访问/Users/me/My后面的Projects被当成了另一个参数。原因是客户端在解析时按空格拆分了整条命令。这个坑的教训是command 和 args 必须分开写args 用数组每个元素是一个完整参数。路径里有空格时数组形式能正确保留。如果你从别处抄来的配置是合并成一条字符串的同步脚本里要主动拆开或者干脆在源配置里就强制用数组。5.3 两个客户端同时写配置的竞争第三个坑出现在我同时开着 Claude Code 和 Cursor 的时候。Claude Code 在运行时会定期把内存里的配置写回~/.claude.json如果这时候我的同步脚本也在写同一个文件就可能出现覆盖。有一次同步完发现刚加的 server 没了查了半天才发现是 Claude Code 退出时用旧的内存状态覆盖了文件。解决办法是同步前先退出两个客户端或者至少在同步后重启它们让配置重新加载。更稳妥的做法是脚本写入前先检查目标文件是否被占用但这个实现起来复杂我选择用最简单的约定改配置前先关客户端同步完再开。多花几秒钟省去排查覆盖问题的时间。5.4 Token 占用到底降了多少前面说精简 description 能降 Token具体降多少得实测。我用了一个笨办法在 Claude Code 里开一个新对话问一个无关紧要的问题然后看它报告的上下文使用量。精简前是 4200 token 左右精简后降到 3600 左右降幅约 14%。这个数字跟 server 数量和描述长度强相关。如果你只挂了两三个 server降幅可能只有几个百分点感知不明显。但如果你像我一样挂了十来个 server这个优化就很值。而且它是一次性的配置改好之后每次对话都受益。配置状态server 数量上下文占用降幅精简前11约 4200 token-精简后11约 3600 token14%精简后5约 1800 token-6. 让这套流程更顺手的几个延伸做法基础同步跑通之后我又加了几个小改进让日常使用更省心。这些不是必须的但加上之后体验会好很多。6.1 用 git 管理源配置源配置就一个 JSON 文件很适合放进 git 仓库。我建了一个私有的 dotfiles 仓库把~/.mcp-source.json软链接进去。换机器的时候 clone 下来跑一次同步脚本两个客户端的配置就都齐了。版本历史也能看到每个 server 是什么时候加的、参数怎么改的。软链接的命令是ln -s ~/dotfiles/mcp-source.json ~/.mcp-source.json。注意 Windows 上软链接需要管理员权限如果你在 Windows 上用直接复制文件也行就是得手动保持同步。6.2 加一个校验步骤同步脚本跑完之后我加了一步校验读回两个目标文件检查 server 数量是否跟源配置一致command 字段是否非空。不一致就报错退出避免写出一个半残的配置自己还不知道。function verify(servers) { const claude readJson(CLAUDE_TARGET); const cursor readJson(CURSOR_TARGET); const expected Object.keys(servers).filter(k servers[k].enabled ! false).length; const claudeCount Object.keys(claude.mcpServers || {}).length; const cursorCount Object.keys(cursor.mcpServers || {}).length; if (claudeCount ! expected || cursorCount ! expected) { console.error(校验失败: 期望 ${expected}, Claude ${claudeCount}, Cursor ${cursorCount}); process.exit(1); } console.log(校验通过); }这个校验帮我抓到过一次问题某个 server 的 command 写成了空字符串同步过去之后客户端启动失败但配置文件本身是合法的 JSON不校验根本发现不了。6.3 处理不同操作系统的路径差异如果你在多台机器上用Windows 和 macOS 的路径写法不一样。源配置里如果写死了/Users/me/...到 Windows 上就废了。我的做法是在源配置里用~表示用户目录脚本读取时替换成实际的 home 路径。function expandPath(p) { if (typeof p ! string) return p; return p.startsWith(~) ? path.join(HOME, p.slice(1)) : p; }对 args 数组里的每个元素都跑一遍 expandPathenv 里的值也跑一遍。这样源配置就能跨平台复用不用为每台机器单独改。6.4 定期清理不再使用的 serverMCP server 装多了上下文占用会上去启动时间也会变长。我养成了一个习惯每个月过一遍源配置把最近没用过的 server 的enabled设成 false。这样配置还在想用的时候改回 true 再同步一次就行不用重新查安装命令。判断哪个 server 没用过可以看客户端的日志。Claude Code 的日志在~/.claude/logs下面Cursor 的在~/.cursor/logs。搜一下 server 名字看看最近有没有调用记录。没有的话就可以考虑关掉了。7. 关于这套方案适用边界的几点体会这套同步方案解决的是多客户端配置一致性和Token 精简两个问题但它不是万能的。有几种情况你得另想办法。如果你的 MCP server 需要在不同项目里用不同的参数比如数据库连接串那项目级源配置是必须的全局配置只能放那些参数固定的 server。我现在的做法是全局只放 filesystem、fetch 这类无状态的 server所有带连接信息的都放项目级。另外这套方案假设你用的是 Claude Code 和 Cursor 这两个客户端。如果你还用别的支持 MCP 的工具得在脚本里加对应的输出函数。好在 MCP 的配置格式大同小异加一个转换函数的工作量不大照着 Cursor 那部分改改就行。最后说个我自己的使用节奏源配置我一般不动加新 server 的时候才打开编辑。平时就是偶尔跑一下mcp-sync确保两边一致。真正让我省心的是不用再记Claude Code 的配置在哪、Cursor 的配置在哪、这个字段那个客户端支不支持这些琐事。配置的事交给脚本脑子留给真正要解决的问题。