给 Claude Code 和 Cursor 配 MCP 这事,我之前是真上头。一个 server 要维护两处配置:Claude Code 认项目根目录的.mcp.json,Cursor 认.cursor/mcp.json;格式看着差不多,字段细节又不完全一致。新增一个服务得改两个文件,删掉一个又容易漏清理,来回折腾半天,最后打开编辑器发现工具加载不出来,连着好几轮对话全耗在排查上。说句实在的,手写 JSON 浪费的时间是小头,烧掉的 Token 才是真肉疼。
后来我把这套流程收敛成了一个命令:项目里维护一份mcp.config.yaml,跑一次同步脚本,自动生成并更新 Claude Code 和 Cursor 两边的配置,同时把不用的 server 剪掉。实测下来配置不用碰 JSON,Token 消耗也肉眼可见地降了。这篇文章就把方案、脚本实现、省 Token 的原理和踩过的坑完整记录下来。适合在 Claude Code 和 Cursor 之间来回切换、手里已经攒了三五个 MCP server 的人;如果你是刚接触 MCP 的新手,也能把两个工具的配置机制一次看明白。
1. 先说说手工配置为什么这么让人上火
1.1 两套工具、两套配置:看起来一样其实不一样
先对齐一个事实:Claude Code 和 Cursor 对 MCP server 的配置入口并不相同。
Claude Code 的项目级配置放在项目根目录的.mcp.json;Cursor 的项目级配置放在.cursor/mcp.json。两者都是标准 JSON 文件,不认识的人第一次看到会以为只是目录名换了,但实际上它们归属不同的配置体系。Claude Code 还有用户级和项目级的作用域区分,用户级配置写在你本机用户目录的全局 JSON 里,项目级配置才写在.mcp.json;Cursor 也有自己的全局设置面板,项目级则读取.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 一个 key |
command | 启动命令 | 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", encoding="utf-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(parents=True, exist_ok=True) with open(out_path, "w", encoding="utf-8") as f: json.dump(payload, f, indent=2, ensure_ascii=False) 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的 server,cursor同理。enabled: false的 server 直接跳过,不会出现在任何产物里。
第三步,写文件。JSON 用indent=2格式化,保证 git diff 干净;ensure_ascii=False是为了避免中文注释或路径被转义成一堆\uXXXX,读起来更舒服。
环境变量这里处理得很小心:配置里写${GITHUB_TOKEN},同步时从当前进程环境读取并填充。这个方案比直接在配置里写明文要安全一点,毕竟配置文件可能要提交到 git;也比“完全不支持环境变量”的方案好用,很多 server 的鉴权信息都能通过这种方式接进来。
3.2 把脚本包装成“一条命令”
脚本本身还只是脚本,要变成“一条命令自动同步”,还得注册一个入口。我的做法是在package.json里加一个 script:
{ "scripts": { "mcp:sync": "python3 mcp-sync.py" } }这样项目里执行npm run mcp:sync就是一次完整同步。
如果你用的项目不是 Node.js 生态,也有几个常见方案:
- 写个 Makefile,
make mcp-sync; - 在 shell 配置里加 alias:
alias mcp-sync="python3 ~/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 助手的信息,包括工具名称、描述、参数 schema(JSON 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.json |
| Claude Code 里 server 显示未批准 | .mcp.json未被信任 | /mcp手动确认 |
| 启动失败,提示找不到命令 | PATH 不一致 / env 解析为空 | 改用绝对路径 command;先验证环境变量再同步 |
| 工具列表里出现两个相同名字的 server | 项目级和用户级配置重复 | 在/mcp或 Cursor 设置里检查作用域,移除多余配置 |
| 同步后 git diff 显示整个文件被重写 | 缩进或字段顺序变化 | 脚本本身按固定顺序输出,确认没有混入手动修改 |
最后再分享一个小技巧
实际用下来,最让我省心的一点是:这套方案把“配置工作”从每次对话里剥离了出去。以前每换一个项目就要折腾一遍.mcp.json,现在只需要把mcp.config.yaml拷过去,跑一次同步,然后正常开聊。我个人的建议是,刚开始不要贪多,先配两个真正高频的 MCP server 跑通流程,用一阵子之后再把新需求挨个加进去;手里的 server 数量越多,这套自动同步和按需裁剪的价值就越大。配置越干净,AI 助手反应越快,钱包也越稳。