规范文件
Cursor mcp.json:本地与远程服务的字段和密钥写法自查
区分 Cursor mcp.json 里 stdio 与远程服务的字段,用自写脚本检查插值写法与密钥是否明文。
Cursor 用 mcp.json 配置 MCP 服务器,项目里放 .cursor/mcp.json,全局放 ~/.cursor/mcp.json。本地命令服务和远程地址服务用的字段不同,密钥应当通过 ${env:名称} 引用而不是写进文件。本文依据官方文档整理字段与插值规则,并附一个自写的 Node.js 检查脚本,用来在提交配置前发现 envFile 用错位置和明文密钥。脚本不是官方校验,没有启动 Cursor,也没有连接任何 MCP 服务。
什么时候用
- 要手写或审阅 mcp.json,并想在启用前检查字段位置与密钥写法。[1]
什么时候不用
- 需要团队级的允许名单、托管分发或网络管控,那属于管理员设置,不在 mcp.json 里。[1]
字段分组与插值规则
官方说明 Cursor 支持三种传输方式:stdio、SSE 和 Streamable HTTP。stdio 由 Cursor 管理本地进程;SSE 与 Streamable HTTP 是按地址访问的服务,认证方式为 OAuth。[1]
| 类型 | 字段 | 说明 |
|---|---|---|
| stdio | type、command、args、env、envFile | command 必须在系统路径中或写完整路径;envFile 只适用于 stdio |
| 远程 | url、headers、auth | 远程服务不支持 envFile,应改用环境变量插值 |
| 静态 OAuth | auth.CLIENT_ID(必填)、CLIENT_SECRET、scopes | 提供商要求固定客户端或不支持动态注册时使用;省略 scopes 时 Cursor 通过 /.well-known/oauth-authorization-server 发现 |
插值方面,官方写明 Cursor 会解析 command、args、env、url 和 headers 里的变量,语法包括 ${env:NAME}、${userHome}、${workspaceFolder}、${workspaceFolderBasename}、${pathSeparator} 和 ${/};auth 的值也支持同样的插值,官方建议用环境变量存放 Client ID 与 Client Secret,而不是写死。[1]
自写检查脚本
这个 Node.js 脚本读取一份 mcp.json,逐个服务器检查:command 与 url 是否恰好出现一个、envFile 是否出现在远程服务上、env 和 headers 里名字像密钥(含 key、token、secret、auth)的项是否是未使用 ${env:...} 的明文、auth.CLIENT_SECRET 是否明文,并对缺少 type 字段的服务给出提示。判断规则是我按官方字段表自己写的,不是官方校验。[1]
import fs from "node:fs";
const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const secretName = /key|token|secret|auth/i;
const literal = value => typeof value === "string" && value !== "" && !/\$\{env:[\w-]+\}/.test(value);
for (const [name, server] of Object.entries(config.mcpServers ?? {})) {
const notes = [];
const local = "command" in server, remote = "url" in server;
if (local === remote) notes.push("need exactly one of command / url");
if (remote && "envFile" in server) notes.push("envFile is stdio-only");
for (const [key, value] of Object.entries(server.env ?? {}))
if (secretName.test(key) && literal(value)) notes.push(`literal value for env.${key}`);
for (const [key, value] of Object.entries(server.headers ?? {}))
if (secretName.test(key) && literal(value)) notes.push(`literal value for headers.${key}`);
if (literal(server.auth?.CLIENT_SECRET)) notes.push("literal auth.CLIENT_SECRET");
if (!("type" in server)) notes.push("info: no type field");
console.log(name, "->", notes.length ? notes : ["no findings"]);
}
样例里 docs-local 是 stdio 服务,tickets-remote 是用环境变量引用凭据的远程服务,bad-remote 故意把 envFile、明文头和明文 CLIENT_SECRET 放在远程服务上。样例是原创的,其中的地址与值均为占位。[1]
{
"mcpServers": {
"docs-local": {
"type": "stdio",
"command": "node",
"args": ["${workspaceFolder}/tools/docs-server.mjs"],
"env": { "DOCS_API_KEY": "${env:DOCS_API_KEY}" },
"envFile": "${workspaceFolder}/.env.mcp"
},
"tickets-remote": {
"url": "https://mcp.example.com/tickets",
"headers": { "Authorization": "Bearer ${env:TICKETS_TOKEN}" },
"auth": { "CLIENT_ID": "${env:TICKETS_CLIENT_ID}", "CLIENT_SECRET": "${env:TICKETS_CLIENT_SECRET}", "scopes": ["read"] }
},
"bad-remote": {
"url": "https://mcp.example.com/bad",
"envFile": ".env",
"headers": { "Authorization": "Bearer demo-literal" },
"auth": { "CLIENT_ID": "demo", "CLIENT_SECRET": "demo-literal" }
}
}
}
docs-local -> [ 'no findings' ]
tickets-remote -> [ 'info: no type field' ]
bad-remote -> [
'envFile is stdio-only',
'literal value for headers.Authorization',
'literal auth.CLIENT_SECRET',
'info: no type field'
]
文档里不一致的地方与没有覆盖的部分
- type 字段:官方 stdio 字段表把 type 标为必填,但同页的 mcp.json 示例并没有写 type。脚本对缺少 type 的服务只给出提示而不判错,是否必填以你本机 Cursor 的实际表现为准。
- 插值字段:官方说明列出的可解析字段不含 auth,但另一段又说 auth 的值支持同样的插值。两处说法并存,脚本按「auth 也可以用 ${env:...}」处理。
- 脚本不处理 SSE 与 Streamable HTTP 的差别、静态 OAuth 的回调地址(官方给出固定的网页与桌面回调)、扩展 API 注册,也不检查值本身是否真的是密钥。
- 管理员的 MCP 允许名单、网络模式和团队分发在 mcp.json 之外,官方说明它们会限制哪些服务器可以运行。
官方对安全的提醒是:只安装来自可信开发者和仓库的服务器,核对服务器会访问的数据与 API,使用权限最小的密钥,必要时审阅服务器源码;Cursor 默认在使用 MCP 工具前请求批准。出现脚本提示的明文密钥,先轮换该密钥再改成 ${env:...} 引用,因为它可能已经进入过版本历史。本文只核验官方字段说明,没有启动 Cursor、没有连接 MCP 服务,也不认证任何服务器的安全性或费用。[1]
来源
以下均为官方页面,核验日期见各条;链接指向核验时的版本。