Spec file
Cursor mcp.json: a self-check for local and remote server fields and secrets
Tell stdio from remote server fields in Cursor mcp.json, and use a self-written script to check interpolation and plaintext secrets.
Cursor configures MCP servers in mcp.json, with .cursor/mcp.json in a project and ~/.cursor/mcp.json globally. Local command servers and remote URL servers use different fields, and secrets should be referenced through ${env:NAME} rather than written into the file. This guide organizes the documented fields and interpolation rules, and adds a self-written Node.js script that finds a misplaced envFile and plaintext secrets before the configuration is committed. The script is not official validation, Cursor was not started and no MCP server was contacted.
- Verified
- · Not tested by us
- Applies to
- Cursor Applies to Cursor MCP configuration. The page gives no unified version floor; fields follow the official page as of 2026-11-05.[1]
- Tools
File locations
| Path | Scope | How it is read |
|---|---|---|
./.cursor/mcp.json | Project-level MCP configuration that applies to that project only. | It travels with the repository when placed in the project, so reference secrets with ${env:NAME}.[1] |
~/.cursor/mcp.json | Global MCP configuration available in every project. | Lives in the home directory and is not committed with a repository.[1] |
When to use
- You are writing or reviewing mcp.json and want to check field placement and secret handling before enabling it.[1]
When not to use
- You need team allowlists, managed distribution or network controls, which are administrator settings and not part of mcp.json.[1]
Field groups and interpolation
The docs list three transports: stdio, SSE and Streamable HTTP. With stdio, Cursor manages a local process; SSE and Streamable HTTP are servers reached by URL and use OAuth.[1]
| Kind | Fields | Notes |
|---|---|---|
| stdio | type, command, args, env, envFile | command must be on the system path or a full path; envFile applies to stdio only |
| Remote | url, headers, auth | Remote servers do not support envFile; use environment variable interpolation instead |
| Static OAuth | auth.CLIENT_ID (required), CLIENT_SECRET, scopes | For providers that need a fixed client or lack dynamic registration; when scopes is omitted Cursor discovers them through /.well-known/oauth-authorization-server |
On interpolation, the docs say Cursor resolves variables in command, args, env, url and headers, with the syntax ${env:NAME}, ${userHome}, ${workspaceFolder}, ${workspaceFolderBasename}, ${pathSeparator} and ${/}. Values in auth support the same interpolation, and the docs advise keeping the Client ID and Client Secret in environment variables rather than hardcoding them.[1]
A self-written check script
This Node.js script reads an mcp.json file and checks each server: that exactly one of command and url is present, whether envFile appears on a remote server, whether env and headers entries whose names look like secrets (containing key, token, secret or auth) are plaintext rather than ${env:...} references, whether auth.CLIENT_SECRET is plaintext, and it notes servers without a type field. The rules are my own reading of the official field list, not official validation.[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"]);
}
In the sample, docs-local is a stdio server, tickets-remote is a remote server that references credentials through environment variables, and bad-remote deliberately puts an envFile, a plaintext header and a plaintext CLIENT_SECRET on a remote server. The sample is original and its addresses and values are placeholders.[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'
]
Documentation gaps and what is not covered
- The type field: the official stdio field table marks type as required, but the mcp.json examples on the same page omit it. The script only notes a missing type instead of failing it; whether it is required is for your local Cursor to settle.
- Interpolation fields: the docs list the fields where variables are resolved without including auth, while another passage says values in auth support the same interpolation. Both statements coexist, and the script treats ${env:...} in auth as acceptable.
- The script does not handle the SSE versus Streamable HTTP distinction, the static OAuth callback URLs (the docs give fixed web and desktop callbacks) or extension API registration, and it does not check whether a value really is a secret.
- Administrator MCP allowlists, network modes and team distribution live outside mcp.json, and the docs say they can restrict which servers run.
The security advice in the docs is to install servers only from trusted developers and repositories, review what data and APIs a server will reach, use keys with minimal permissions and audit the server code for critical integrations; Cursor asks for approval before using MCP tools by default. When the script reports a plaintext secret, rotate it before switching to a ${env:...} reference, since it may already have entered version history. Only the official field descriptions were checked here: Cursor was not started, no MCP server was contacted, and neither server safety nor cost is certified.[1]
Sources
All links are official pages; verification dates are listed per source, and links point to the version checked.