
给 Claude Code 和 Cursor 配 MCP 这事我之前是真上头。一个 server 要维护两处配置Claude Code 认项目根目录的.mcp.jsonCursor 认.cursor/mcp.json格式看着差不多字段细节又不完全一致。新增一个服务得改两个文件删掉一个又容易漏清理来回折腾半天最后打开编辑器发现工具加载不出来连着好几轮对话全耗在排查上。说句实在的手写 JSON 浪费的时间是小头烧掉的 Token 才是真肉疼。后来我把这套流程收敛成了一个命令项目里维护一份mcp.config.yaml跑一次同步脚本自动生成并更新 Claude Code 和 Cursor 两边的配置同时把不用的 server 剪掉。实测下来配置不用碰 JSONToken 消耗也肉眼可见地降了。这篇文章就把方案、脚本实现、省 Token 的原理和踩过的坑完整记录下来。适合在 Claude Code 和 Cursor 之间来回切换、手里已经攒了三五个 MCP server 的人如果你是刚接触 MCP 的新手也能把两个工具的配置机制一次看明白。1. 先说说手工配置为什么这么让人上火1.1 两套工具、两套配置看起来一样其实不一样先对齐一个事实Claude Code 和 Cursor 对 MCP server 的配置入口并不相同。Claude Code 的项目级配置放在项目根目录的.mcp.jsonCursor 的项目级配置放在.cursor/mcp.json。两者都是标准 JSON 文件不认识的人第一次看到会以为只是目录名换了但实际上它们归属不同的配置体系。Claude Code 还有用户级和项目级的作用域区分用户级配置写在你本机用户目录的全局 JSON 里项目级配置才写在.mcp.jsonCursor 也有自己的全局设置面板项目级则读取.cursor/mcp.json。再看内容结构两边都遵循 MCP 协议的mcpServers容器// .mcp.json — Claude Code 项目级配置 { mcpServers: { filesystem: { type: stdio, command: npx, args: [-y, modelcontextprotocol/server-filesystem, /tmp] } } }// .cursor/mcp.json — Cursor 项目级配置 { mcpServers: { filesystem: { type: stdio, command: npx, args: [-y, modelcontextprotocol/server-filesystem, /tmp] } } }看起来几乎一样问题恰恰出在这个“几乎”上。你把.mcp.json里的内容原样复制到.cursor/mcp.json大多数场景确实能跑但一旦涉及环境变量、命令路径、全局作用域两边就会开始出现各种不一致。同一个 server在 Claude Code 里用npx能起来在 Cursor 里却经常报“找不到命令”因为 Cursor 作为图形应用启动时不会加载你 shell 里的 PATH。这种差异只有踩过坑的人才会注意但每次踩坑都得搭进去好几轮对话去排查。1.2 手写 JSON 真正的坑不在格式而在“同步”格式问题其实是最好解决的真正的坑是“同步”。举个最常见的例子你加了一个新的 MCP server在 Claude Code 里调通了很开心然后打开 Cursor 继续用发现工具没出现。为什么因为你只改了.mcp.json忘了改.cursor/mcp.json。反过来也成立你在 Cursor 的设置面板里加了个全局 server回到 Claude Code 终端里还得手动再配一遍。第二个坑是删除。某个 server 不用了你从 Claude Code 的配置里删掉但 Cursor 那边还留着一份。结果就是每次打开 Cursor它都会尝试去启动这个已经不需要的进程工具列表里多一个永远加载失败的入口。AI 助手并不知道这个工具其实已经废弃它还可能在思考时反复尝试调用白白浪费 Token。第三个坑是环境变量。不少 MCP server 需要 API key、token 这类环境变量。Claude Code 在终端里跑能继承你.zshrc或.bashrc里的变量Cursor 启动时不一定会带这些。于是同一个 server终端里好好的到了 Cursor 里就鉴权失败。配置里写不写环境变量、写在哪个作用域都变得异常敏感。这些问题的本质都一样配置存在两份没有任何机制保证它们始终一致。只要靠手早晚会漏。1.3 配置乱才会烧 Token这锅必须算清楚很多人没意识到MCP 配置混乱会直接反映在 Token 消耗上。原理不复杂AI 助手每次请求时需要把可用的工具列表包括每个工具的名字、描述、参数结构跟随上下文一起发送。你全局配置里挂着的每一个 MCP server即便这次对话完全没用上它的工具定义也会占用上下文空间。更隐蔽的是“错误重试”的消耗。一个 MCP server 因为 JSON 写错了、路径不对、环境变量缺失而连不上AI 助手并不会立刻放弃。它可能在工具调用失败后换一种方式再次尝试或者把这个失败记录重新组织语言向你解释。这个过程消耗的上下文往往是正常调用工具的好几倍。表面上看是“配置有点问题”实际上每一次调试失败都在持续烧 Token。所以配置管理不只是维护便利性问题它直接影响你的使用成本和效率。这也是为什么我后来下定决心把两套配置收敛成一份。2. 解法一份配置源管住两个工具2.1 设计原则Source of Truth 只有一个我的核心思路很简单建立一个“单一事实来源”Source of Truth所有 MCP server 的配置只在这个文件里维护然后通过脚本生成两个工具各自需要的配置产物。改配置只改一处跑一次同步命令两边同时更新。具体就是用项目根目录的mcp.config.yaml作为唯一配置源用一个同步脚本读取 YAML分别生成.mcp.json和.cursor/mcp.json生成结果纳入 git 版本管理团队共享时可以直接对比差异重点脚本不只是“复制”而是按目标工具的规则做过滤和转换。这个设计有几个好处。首先是可审计git diff 能清楚看到哪个 server 改了、哪个删了其次是可复用换新项目时把 YAML 拷过去跑一遍同步就完成初始化最后是天然防漏脚本会同时更新两个文件不会再出现“只改了一个”的情况。2.2 配置文件长什么样mcp.config.yaml的完整结构是这样的version: 1 servers: filesystem: command: npx args: - -y - modelcontextprotocol/server-filesystem - . targets: [claude, cursor] enabled: true git: command: npx args: - -y - some/git-mcp-server env: GITHUB_TOKEN: ${GITHUB_TOKEN} targets: [cursor] enabled: true playwright: command: npx args: - -y - some/playwright-mcp targets: [claude] enabled: false字段说明字段含义可选值 / 示例servers所有要管理的 MCP server 集合每个 server 一个 keycommand启动命令npx、python、可执行文件绝对路径args启动参数列表数组形式避免手动拼接字符串转义问题env需要注入的环境变量支持${VAR}占位符同步时从真实环境读取targets这个 server 要同步给哪些工具claude、cursor可同时指定enabled是否启用true/false关闭时不写入任何配置你可能注意到我特意把参数写成了数组而不是一个长字符串。这是从 JSON 配置时代留下来的经验数组可以原样落到配置里不用处理引号和转义各种奇奇怪怪的包名也不会出错。2.3 为什么选 YAML 而不是继续用 JSON既然最终产物是 JSON为什么不直接用一个“配置源 JSON”原因有两点。第一YAML 支持注释。同一个 server 为什么要配、某个环境变量从哪里拿、哪个 server 在哪个项目里验证过这些信息可以直接写进配置团队里其他人一看就明白。JSON 要实现类似效果只能靠额外文档文档和配置分开后很快就会过时。第二diff 可读性。YAML 按缩进组织git 里看出入很直观JSON 稍微复杂一点尤其是嵌套层级比较深的时候diff 输出经常是一整块被替换定位不到具体变化。配置源文件会被频繁更新这点差异体验差距很大。当然如果你团队实在不习惯 YAML也可以退一步用 JSONC带注释的 JSON或者直接维护一个简单的 JS 模块。核心不是文件格式而是“单一配置源 生成多个目标”这套思路格式只是载体。3. 一条命令全搞定同步脚本实现细节3.1 核心逻辑一个 Python 脚本就够了我选 Python 写同步脚本理由是跨平台、依赖少、逻辑直观。完整实现如下#!/usr/bin/env python3 mcp-sync: 从单一配置源同步 MCP server 配置到 Claude Code 与 Cursor。 import json import os import sys from pathlib import Path import yaml CONFIG_PATH Path(mcp.config.yaml) OUTPUTS { claude: Path(.mcp.json), cursor: Path(.cursor/mcp.json), } def load_config(path: Path) - dict: with open(path, r, encodingutf-8) as f: return yaml.safe_load(f) def resolve_env(env: dict) - dict: resolved {} for key, value in env.items(): if isinstance(value, str) and value.startswith(${) and value.endswith(}): var_name value[2:-1] resolved[key] os.environ.get(var_name, ) else: resolved[key] value return resolved def build_server_entry(server: dict) - dict: entry { type: stdio, command: server[command], args: server.get(args, []), } if server.get(env): entry[env] resolve_env(server[env]) return entry def build_target_config(config: dict, target: str) - dict: servers {} for name, server in config[servers].items(): if not server.get(enabled, True): continue if target not in server.get(targets, [claude, cursor]): continue servers[name] build_server_entry(server) return {mcpServers: servers} def write_outputs(config: dict): for target, out_path in OUTPUTS.items(): payload build_target_config(config, target) out_path.parent.mkdir(parentsTrue, exist_okTrue) with open(out_path, w, encodingutf-8) as f: json.dump(payload, f, indent2, ensure_asciiFalse) f.write(\n) print(f[mcp-sync] wrote {out_path} ({len(payload[mcpServers])} servers)) if __name__ __main__: if not CONFIG_PATH.exists(): sys.exit(f缺少配置文件: {CONFIG_PATH}) cfg load_config(CONFIG_PATH) write_outputs(cfg)逻辑拆开也就三步。第一步读 YAML。yaml.safe_load把配置变成字典后续操作全部走 Python 数据结构不用再碰字符串拼接。第二步按目标工具过滤。以targets为筛选条件claude工具只会拿到声明了包含claude的 servercursor同理。enabled: false的 server 直接跳过不会出现在任何产物里。第三步写文件。JSON 用indent2格式化保证 git diff 干净ensure_asciiFalse是为了避免中文注释或路径被转义成一堆\uXXXX读起来更舒服。环境变量这里处理得很小心配置里写${GITHUB_TOKEN}同步时从当前进程环境读取并填充。这个方案比直接在配置里写明文要安全一点毕竟配置文件可能要提交到 git也比“完全不支持环境变量”的方案好用很多 server 的鉴权信息都能通过这种方式接进来。3.2 把脚本包装成“一条命令”脚本本身还只是脚本要变成“一条命令自动同步”还得注册一个入口。我的做法是在package.json里加一个 script{ scripts: { mcp:sync: python3 mcp-sync.py } }这样项目里执行npm run mcp:sync就是一次完整同步。如果你用的项目不是 Node.js 生态也有几个常见方案写个 Makefilemake mcp-sync在 shell 配置里加 aliasalias mcp-syncpython3 ~/tools/mcp-sync.py用 Git 的 pre-commit 钩子在提交前自动跑一遍同步保证仓库里的配置永远是最新生成的。我自己一般两种方式结合个人项目用 alias团队项目用 npm script。前者的好处是不受项目类型限制后者胜在随项目走、队友拉下来就知道怎么跑。3.3 同步脚本顺带做的三件事如果你仔细看上面的代码会发现这个脚本不只是在“复制配置”它做了三件手工维护很难坚持的事情第一过滤。enabled: false的 server 和不在targets范围内的工具一律不写入产物。这意味着你可以把暂时不用的 server 保留在配置源里但不会让它在任何一边生效。这一点对省 Token 尤其重要后面专门说。第二环境变量展开。统一在脚本层完成${VAR}替换产物里已经是真实值。这也避免了不同工具对环境变量的处理方式不一致导致的问题。第三幂等覆盖。每次同步都是全量重写目标文件不是“添加缺失项”。这样配置源里删除一个 server同步后目标文件里也会消失不会出现“删不掉”的残留。配合 git 使用删除操作的变更记录一目了然。如果你希望同步脚本更“聪明”一些还可以加一个清理逻辑先读取目标文件里已有的 server 列表和配置源对比把配置源里已经不存在但目标文件里还残留的 server 自动移除。不过我的建议是保留“全量重写”这个简单行为因为全量重写本身就天然解决了残留问题再加清理逻辑反而复杂。4. 省 Token 不是玄学这笔账我算过4.1 一个 MCP 工具定义到底占多少 Token先说结论一个 MCP 工具的定义在上下文里占用几百到上千 token具体取决于描述长短和参数结构复杂度。每个工具暴露给 AI 助手的信息包括工具名称、描述、参数 schemaJSON Schema 格式有时候还带枚举值示例。常见的 server 工具一个定义大概在 200 到 500 token 之间如果描述写得特别啰嗦、参数特别多上千也不奇怪。而 AI 助手在每次请求时是需要携带整个工具列表的。假设你挂了 5 个 MCP server平均每个 server 暴露 4 个工具那么单是工具定义就要占5 servers × 4 tools × 300 tokens ≈ 6,000 tokens这 6,000 token 不管你这个对话里到底有没有用上任何一个工具它都会被算进请求头里。对话超过 20 轮这部分固定开销就是 120,000 token。这只是配置层面的浪费还没有算工具调用失败后反复重试的额外消耗。4.2 三条省 Token 的配置纪律基于这个认知我总结出三条配置纪律全部体现在mcp.config.yaml的写法里。第一条只开当前项目需要的 server其他一律enabled: false。比如你在一个纯前端项目里写代码就不需要给 AI 挂数据库相关的 server。配置源里可以留着但enabled要关掉这样同步产物里不会有它上下文里就少一份工具定义。第二条用targets隔离工具。有些 server 只在 Cursor 里用有些只适合在 Claude Code 终端里跑。把它们分别声明到对应targets互不干扰。比如带 GUI 操作的浏览器工具在终端场景下意义不大就没必要推到 Claude Code 那里去占上下文。第三条精简描述和参数别把顺手能做的事做成“超大工具”。MCP 工具的描述文字会原样进入上下文。描述里写一整段背景说明不如写成“获取订单列表可按状态筛选”这种一句话版本。参数同理能少一个字段就少一个字段。工具定义的精简程度会直接反映到 Token 消耗上这也是很多人容易忽略的点。4.3 我这边一个项目的实测数据用一个我参与的模拟项目举例。接入同步脚本之前全局开启了 8 个 MCP server工具总数大概在 21 个左右每轮请求光是工具定义就要多花掉 6,000 到 7,000 token。一个 20 轮的典型会话大约有 12 万到 14 万 token 是花在“带着一堆没用的工具定义”上面。整理配置之后按项目实际需求只保留了 3 个 server工具数降到 7 个。每轮请求的工具定义开销压到 2,000 token 出头20 轮下来节省了大约 8 万 token。这个数字没有计入“配置正常后失败重试几乎消失”的收益实际体感比账面上更明显。有一点必须说明省 Token 的本质不是“MCP 本身费钱”而是“无用的工具定义和反复失败在烧钱”。如果你只有一个 server、三个工具那手动配置也好、脚本同步也好差别确实不大。但一旦工具多了、项目多了配置管理的收益会指数级放大。5. 常见问题与排查技巧实录5.1 改了配置不生效怎么办这是最常被问的问题。跑完mcp-sync文件都写了但 Cursor 里看不到新工具Claude Code 里也调不出来。先说 Cursor.cursor/mcp.json的改动Cursor 会监控文件变化但某些版本需要重新加载窗口或者重启才会重新读取。遇到不生效先试Reload Window不行就重启应用。另外注意检查一下有没有把配置文件放在正确的项目根目录Cursor 的项目级配置只有在对应项目根目录下才生效。再说 Claude Code项目级.mcp.json第一次生效还需要一个信任步骤。在 Claude Code 会话里输入/mcp查看当前加载的 server 列表如果看到的是“not approved”之类的状态需要手动确认一次。确认过之后后续同步新 server 也会沿用这个信任关系。如果加了新 server 后仍看不到先重启会话再检查/mcp的输出。5.2 命令里的环境变量不见了前面提到的 PATH 问题在脚本化之后依然可能遇到。因为mcp-sync.py在终端里跑它读取环境变量用的是当前 shell 的环境。如果你在.zshrc里配了NODE_OPTIONS或者自定义了 PATH终端里启动脚本没问题但 Cursor 是图形应用未必继承这些。两个解决办法。第一个是在配置里把 command 写成绝对路径比如/usr/local/bin/npx、/opt/homebrew/bin/python避免依赖 PATH。第二个是被环境变量卡住时先手动在终端跑一次 server确认环境变量值是否正确再重新执行npm run mcp:sync。5.3 YAML 一改就跑不动了YAML 对缩进敏感这个特性平时是优点但在手改配置时也容易踩坑。最常见的报错就是yaml.scanner.ScannerError原因多半是 tab 和空格混用了或者某个字段少了一个空格。我的建议是手改完先跑一遍脚本脚本能跑通说明语法没问题。如果项目里有人不熟悉 YAML可以改成一个更宽松的 JSONC 源文件代价是丢掉注释能力。自己权衡即可。另外始终推荐在编辑器里装一个 YAML 格式化插件提交前格式化一下能省掉大多数缩进问题。5.4 团队协作时配置漂移多个人维护同一个项目配置源文件放进了 git但有人改完没跑同步直接提交了旧的.mcp.json和.cursor/mcp.json就会产生配置漂移也就是生成物和源码不一致。我的处理方式是把两个生成文件也从 git 里管起来并且在提交前跑一次同步让生成物保持最新。更严格的团队可以在 CI 里加一道检查拉取代码后重新跑mcp-sync然后git diff --exit-code如果生成物和提交内容不一致直接拦下这次提交。这个做法成本很低但能有效防住漏同步的问题。5.5 问题速查表现象可能原因处理方式Cursor 里看不到新 server未重载窗口 / 配置文件路径不对重载窗口或重启确认文件在项目根目录的.cursor/mcp.jsonClaude Code 里 server 显示未批准.mcp.json未被信任/mcp手动确认启动失败提示找不到命令PATH 不一致 / env 解析为空改用绝对路径 command先验证环境变量再同步工具列表里出现两个相同名字的 server项目级和用户级配置重复在/mcp或 Cursor 设置里检查作用域移除多余配置同步后 git diff 显示整个文件被重写缩进或字段顺序变化脚本本身按固定顺序输出确认没有混入手动修改最后再分享一个小技巧实际用下来最让我省心的一点是这套方案把“配置工作”从每次对话里剥离了出去。以前每换一个项目就要折腾一遍.mcp.json现在只需要把mcp.config.yaml拷过去跑一次同步然后正常开聊。我个人的建议是刚开始不要贪多先配两个真正高频的 MCP server 跑通流程用一阵子之后再把新需求挨个加进去手里的 server 数量越多这套自动同步和按需裁剪的价值就越大。配置越干净AI 助手反应越快钱包也越稳。