1. 从一次 MCP 调用失败说起:settings.json 到底在管什么
如果你在 Cursor 里配过 MCP,大概率遇到过这种场景:配置文件写完了,重启 Cursor,工具列表里那个 server 是灰的,或者干脆没出现。点开日志一看,报错信息只有一行spawn npx ENOENT或者MCP error -32000: Connection closed,完全不知道从哪下手。
这个问题的根源在于,很多人把mcpServers当成了 MCP 协议的一部分。实际上它跟 MCP 协议本身没有半点关系。mcpServers是 Cursor 作为 MCP Host 的进程启动描述,它只解决一件事:怎么把 MCP Server 这个进程拉起来,然后通过 stdio 跟它说话。至于这个 Server 内部暴露了哪些 tools、schema 长什么样、用 Java 还是 Python 写的,mcpServers一概不关心。
把这两层分清楚之后,配置语法就变得非常好理解了。command是可执行文件,args是参数数组,env是注入的环境变量,三个字段各司其职。报错定位也有了方向:进程起不来就看command和args,进程起来了但握手失败就看 stdout 有没有被日志污染,工具列表为空就看tools/list返回了什么。
这篇会从settings.json的骨架开始,把每个字段的含义、Cursor 对 MCP Server 的运行时假设、以及一次完整调用链的每个环节拆开讲。最后给一段可以直接复制的配置,配合逐项验证动作,让你在本地跑通一次从自然语言到 tool result 的完整链路。如果你在配置过程中需要确认模型侧的连通性,可以用 TaoToken 模型对话 先验证一下 API 是否正常,避免把模型问题和 MCP 配置问题混在一起排查。
2. TaoToken 前置:把模型侧和 MCP 侧解耦
在拆配置语法之前,先花几分钟把模型侧的接入确认掉。原因很简单:MCP 调用链的最后一环是 LLM 决定要不要调 tool,如果模型 API 本身不通,你会在 Cursor 里看到一堆莫名其妙的超时,然后误以为是 MCP Server 的问题。
TaoToken 在这里的角色是提供兼容 OpenAI 协议的模型接入层。你需要在 Cursor 的模型设置里填上 API 地址和 Key,让 Cursor 能把上下文和 tools 描述发给模型。具体操作是:登录 TaoToken 控制台,在 API Keys 页面 创建一个 Key,然后在 Cursor 的 Settings → Models 里把 OpenAI API Base 改成https://taotoken.net/api,填入 Key。
这里有个容易踩的坑:Cursor 的模型设置和 MCP 设置是两个独立的入口。模型设置管的是 LLM 请求走哪个 endpoint,MCP 设置管的是本地进程怎么启动。两者互不影响,但调用链上又是串联的。所以排查问题时,先用 TaoToken 模型对话 确认模型能正常返回,再去折腾mcpServers。
如果你打算长期在 Cursor 里跑 Agent 类的编码任务,MCP tool 调用会非常频繁,每次调用都会消耗 token。这种情况下可以看一下 Coding Plan,它的计费方式对高频 tool call 场景更友好。接入细节可以参考 接入文档,里面有 Cursor 的具体配置截图。
3. settings.json 骨架:mcpServers 的通用结构与字段语义
Cursor 的 MCP 配置入口在 Settings → Tools & MCP → New MCP Server,点进去之后你会看到一个 JSON 编辑器。这个 JSON 的顶层结构是固定的:
{ "mcpServers": { "<server-id>": { "command": "<executable>", "args": ["<arg1>", "<arg2>"], "env": { "<key>": "<value>" } } } }这个结构在 Node、Python、Java、Go 之间是完全通用的。下面逐项拆开。
3.1 server-id:逻辑标识,不参与协议
<server-id>是 MCP Server 的逻辑 ID,只用于 Cursor 内部管理、UI 展示和日志提示。它可以是任意字符串,推荐用 kebab-case。关键点是:这个 ID 不会传给 Server,也不参与 MCP 协议。你叫它memory还是my-memory-server,对 Server 进程没有任何影响。
3.2 command:进程入口,只能是一个可执行文件
command的语义是启动 MCP Server 的可执行文件。这里有一条硬性规则:只能是一个可执行文件,不允许带参数。参数必须拆到args里。
{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-memory"] }上面这个配置里,npx是 command,-y和包名是 args。如果你写成"command": "npx -y @modelcontextprotocol/server-memory",Cursor 会尝试找一个名字叫npx -y @modelcontextprotocol/server-memory的可执行文件,然后报ENOENT。
常见的 command 值包括npx、node、python3、java、/usr/bin/go,以及 Windows 上的绝对路径如C:\\Users\\wtyy\\AppData\\Local\\Programs\\WtyyHelper\\wtyyhelper-mcp.exe。
3.3 args:参数数组,顺序严格保留
args是传给 command 的参数数组,等价于 shell 里的command arg1 arg2 arg3。规则是:必须是数组,每个元素是一个独立参数,顺序严格保留,Cursor 不做拼接也不做转义。
{ "command": "java", "args": ["-jar", "/path/to/server.jar"] }{ "command": "python3", "args": ["server.py"] }{ "command": "npx", "args": ["-y", "@playwright/mcp@latest", "--browser", "chrome"] }注意--browser和chrome是两个独立的数组元素,不能合并成一个字符串。
3.4 env:环境变量注入,会覆盖系统值
env是可选字段,用于在启动 MCP Server 时注入环境变量。Key 和 Value 都必须是字符串,会和系统环境合并,如果冲突则覆盖系统值。
{ "env": { "MEMORY_FILE_PATH": "C:\\Users\\wtyy\\.mcp-storage\\memory.json", "LOG_LEVEL": "debug" } }在配置文件里硬编码敏感信息是有风险的。MCP 配置支持通过${}语法引用环境变量:
{ "mcpServers": { "secure-api": { "type": "http", "url": "https://api.example.com/mcp", "headers": { "Authorization": "Bearer ${API_TOKEN}", "X-API-Key": "${API_KEY:-default-key}" } } } }${VAR_NAME}直接引用,变量不存在会报错;${VAR_NAME:-default}在变量不存在时使用默认值。这个语法在 stdio 类型的 server 里同样适用于env字段。
3.5 一份完整的配置示例
把上面几个字段组合起来,一份包含 stdio 和 SSE 两种类型的配置长这样:
{ "mcpServers": { "memory": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-memory"], "env": { "MEMORY_FILE_PATH": "C:\\Users\\wtyy\\.mcp-storage\\memory.json" } }, "sequential-thinking": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-sequential-thinking"] }, "playwright": { "command": "npx", "args": ["-y", "@playwright/mcp@latest", "--browser", "chrome"] }, "gitlab": { "command": "npx", "args": ["-y", "@zereight/mcp-gitlab"], "env": { "GITLAB_PERSONAL_ACCESS_TOKEN": "glpat-******", "GITLAB_API_URL": "https://git.example.com", "GITLAB_READ_ONLY_MODE": "false" } }, "local-helper": { "command": "C:\\Users\\wtyy\\AppData\\Local\\Programs\\WtyyHelper\\wtyyhelper-mcp.exe", "args": ["--mcp"], "env": {} }, "amap-sse": { "url": "https://mcp.amap.com/sse?key=YOUR_AMAP_KEY" } } }注意最后那个amap-sse用的是url字段而不是command,这是 SSE 类型的远程 MCP Server,不需要本地启动进程。
4. 调用链拆解:从自然语言到 tool result 的完整路径
配置写对了只是第一步,真正跑通需要理解 Cursor 和 MCP Server 之间的通信过程。这条链路可以拆成七个环节。
4.1 Cursor 启动 MCP Server 进程
Cursor 读取mcpServers配置后,会为每个 server-id 启动一个独立的进程。启动命令就是command+args拼接,环境变量是系统环境加上env字段的覆盖。进程启动后,Cursor 通过 stdin 写入、stdout 读取来通信。
这里有一条硬性规则:stdio 是唯一通道。Cursor 不支持 socket、http、grpc 作为本地 server 的通信方式,Server 也不需要监听任何端口。一个 Server 对应一个进程,Cursor 退出时 Server 进程结束。
4.2 初始化握手:initialize
进程启动后,Cursor 发送的第一个请求是initialize:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "clientInfo": { "name": "cursor", "version": "x.y.z" } } }MCP Server 需要返回自己的能力声明:
{ "jsonrpc": "2.0", "id": 1, "result": { "capabilities": { "tools": {} } } }如果返回里没有capabilities.tools,Cursor 会认为这个 Server 不支持 tools,后续不会向它发起 tool 调用。
4.3 拉取工具列表:tools/list
握手成功后,Cursor 发送tools/list请求:
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }Server 返回自己暴露的所有工具:
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "get_user_by_id", "description": "根据用户 id 查询用户信息(name、email、age)", "inputSchema": { "type": "object", "properties": { "id": { "type": "integer", "description": "用户唯一 ID" } }, "required": ["id"] } } ] } }description和inputSchema是 LLM 判断要不要调用这个工具的核心依据。description 写得越清楚,模型命中率越高。
4.4 LLM 决定是否调用 tool
Cursor 把当前会话的上下文、所有可用工具的 name/description/inputSchema 一起发给 LLM。LLM 根据用户的自然语言输入,决定是否要调用某个 tool,以及传什么参数。
这里有个实际经验:如果配置了很多 MCP Server,而 tools 名称很相似,模型可能会误调用。可以在 prompt 里明确指定,比如「调用 get_git_mr_diffs 这个 mcp,分析这个 MR 的改动」,命中率会明显提升。
4.5 Cursor 发起 tools/call
LLM 返回 tool call 意图后,Cursor 解析出要调用的 tool 名称和参数,向对应的 MCP Server 发送:
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_user_by_id", "arguments": { "id": 123 } } }4.6 MCP Server 执行并返回结果
Server 收到请求后执行业务逻辑,返回结果:
{ "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "json", "json": { "id": 123, "name": "Alice", "email": "alice@example.com", "age": 28 } } ] } }4.7 Cursor 把结果喂回 LLM
Cursor 拿到 tool result 后,把它作为上下文的一部分再发给 LLM,LLM 生成最终回复。至此一次完整的调用链结束。
整个链路里,mcpServers只负责第 1 步的进程启动,后面 6 步都是 MCP 协议在管。这就是为什么配置语法和协议要分开理解。
5. 常见报错定位:从进程启动到握手失败的排查路径
配置写完之后跑不通,报错信息往往很模糊。下面按调用链的顺序,把常见报错和定位方法列出来。
5.1 spawn ENOENT:command 找不到
这是最常见的报错,意思是 Cursor 尝试启动command指定的可执行文件,但系统 PATH 里找不到。
排查步骤:先在终端里手动执行一遍command+args的拼接命令,看能不能跑起来。如果终端里能跑但 Cursor 里报 ENOENT,通常是 PATH 环境变量的问题。Cursor 启动子进程时继承的是系统环境变量,如果你用的是 nvm 管理的 node,npx 的路径可能不在系统 PATH 里。
解决办法是用绝对路径。在终端里执行which npx(macOS/Linux)或where npx(Windows),把结果填到command里。
5.2 Connection closed:进程启动后立即退出
这个报错说明进程起来了,但很快就退出了,Cursor 还没来得及完成握手。
常见原因有三个:一是args里的包名写错了,npx 下载失败后退出;二是env里缺少必要的环境变量,Server 启动时校验失败;三是 Server 把日志写到了 stdout,污染了 JSON-RPC 通道。
排查方法是把command和args拿到终端里手动执行,观察输出。如果终端里能看到正常的启动日志,但 Cursor 里报 Connection closed,那大概率是 stdout 污染问题。
5.3 工具列表为空:capabilities.tools 没返回
进程正常启动、握手也完成了,但 Cursor 的工具列表里看不到这个 Server 的 tools。
这说明initialize的返回里没有capabilities.tools,或者tools/list返回了空数组。检查 Server 代码里initialize的响应,确认capabilities字段里有tools。如果是用现成的 Server 包,检查版本是否匹配。
5.4 stdout 污染:日志写错流了
这是最隐蔽的问题。MCP 协议规定:stdout 只能输出 MCP JSON-RPC 消息,stderr 可以输出任意日志。如果 Server 把调试日志打到了 stdout,Cursor 会解析失败,判定 Server 异常。
排查方法是手动运行 Server,观察 stdout 的输出。如果看到非 JSON 的内容,就是污染了。解决办法是把日志重定向到 stderr,比如在 Python 里用print(..., file=sys.stderr),在 Node 里用console.error。
5.5 JSON-RPC 格式问题:一行一个 JSON
MCP 的 JSON-RPC 消息要求一行一个 JSON,UTF-8 编码,必须 flush。Cursor 不支持 chunked 或 streaming。如果 Server 返回的 JSON 跨了多行,或者没有 flush,Cursor 会一直等不到完整消息。
5.6 环境变量引用失败:${VAR_NAME} 报错
如果配置里用了${API_TOKEN}但系统环境里没有这个变量,Cursor 会报错。检查方式是确认变量已经在系统环境里设置,或者改用${API_TOKEN:-default}提供默认值。
6. 语义一致 CTA:把配置跑通之后
配置跑通之后,你会看到 Cursor 的工具列表里出现你配置的 MCP Server,展开能看到具体的 tools。这时候可以在对话里输入「调用 get_user_by_id 查询 id 为 123 的用户」,观察 Cursor 是否命中这个 tool,以及返回结果是否符合预期。
如果调用链在模型侧出现问题,比如 LLM 一直不调用 tool,或者返回超时,可以回到 TaoToken 模型对话 单独验证模型是否正常。如果需要在 Cursor 里长期跑 Agent 任务,Coding Plan 的计费方式更适合高频 tool call 场景。接入过程中遇到配置问题,接入文档 里有 Cursor 的完整配置示例,API Keys 页面 可以管理你的 Key。
最后留一个实用技巧:配置多个 MCP Server 时,先用最小的配置跑通一个,确认调用链完整之后再逐个添加。每加一个就重启 Cursor 验证一次,这样出问题时能快速定位是哪个 Server 的配置有问题。