.mcp.json and claude mcp add: every config shape with examples
One JSON object per server, under a top-level mcpServers key, in
one of two transport shapes. Almost every configuration question that comes up — where does this
env var get expanded, which file actually wins — is really a question about scope precedence or
which of the two shapes you're looking at, so here's both in full plus the CLI that writes them
for you.
On this page
stdio servers
A local process, launched and owned by the CLI, talking JSON-RPC over its own stdin/stdout.
This is the shape for anything that runs on the same machine as Claude Code itself — a local
tool, a thin wrapper script, or the reserved-name adapter the Agent SDK's own
canUseTool wiring launches internally (see
the permission-prompt-tool guide for why the
literal name "stdio" is off-limits for your own server):
{
"mcpServers": {
"local-tool": {
"type": "stdio",
"command": "node",
"args": ["./mcp-server.mjs"],
"env": { "LOG_LEVEL": "debug" }
}
}
}
command and args are exactly what gets exec'd; env is
merged into the child process's environment on top of whatever it inherits from the parent shell,
not a replacement for it. There's no headers field on this shape — a stdio server has
no HTTP request to attach one to; authenticate however the process itself expects (its own env
var, a config file it reads on startup).
http (remote) servers
A server reachable over the network instead of spawned locally — the shape you want for anything hosted, shared across a team, or living behind a service you don't run on your own machine:
{
"mcpServers": {
"ourservice": {
"type": "http",
"url": "https://api.ourservice.dev/mcp",
"headers": { "Authorization": "Bearer ${OURSERVICE_API_KEY}" }
}
}
}
"streamable-http" is accepted as an alias for "http" in JSON config
files, if you're copying an example that spells it out more explicitly — both parse to the same
transport. There's no command/args on this shape; the CLI makes HTTP
requests to url directly rather than launching anything.
Env expansion in headers
${VAR} and ${VAR:-default} both expand inside header values (and
elsewhere in the config), so a committed .mcp.json never needs the literal key in
it:
{ "headers": { "Authorization": "Bearer ${OURSERVICE_API_KEY:-}" } }
Using an explicit empty default (${VAR:-}) rather than leaving the fallback off
entirely is worth doing deliberately: a missing variable then produces an obviously-broken empty
header instead of a config file that fails to parse at all, which is easier to notice and easier
to grep server-side auth-failure logs for. See
authenticating remote MCP servers for the deeper dive on
header auth specifically, including how it holds up against the MCP spec's 2026-07-28 revision.
Scopes and precedence
Where a server entry lives determines who else sees it and which copy wins if the same server name is defined more than once, highest precedence first:
local — private, ~/.claude.json, not committed, this machine only
project — .mcp.json, committed, shared with everyone who checks out the repo
user — cross-project, your own machine, applies everywhere you don't have a project override
project is almost always the right choice for a server every teammate needs
configured identically — commit the URL and the header name, never a literal secret
value, and let each person's own environment supply what ${VAR} expands to. Reach
for local when you need a personal override that shouldn't leak into the shared file
at all — pointing at a local dev instance of a server the rest of the team hits in production, say.
Plugin-provided servers
A Claude Code plugin can bundle its own .mcp.json inside the plugin directory,
using the identical mcpServers shape shown above — installing the plugin registers
whatever servers it declares without you writing any config yourself:
my-plugin/
.claude-plugin/plugin.json
.mcp.json <- same mcpServers shape, ships with the plugin
hooks/hooks.json
skills/
This is the mechanism behind a plugin that "just works" after /plugin install with
no manual server setup — the plugin's own .mcp.json is exactly as valid a source of
server config as a project's, just distributed differently. A server contributed this way still
participates in the same scope precedence as any other; if a plugin and your project both define
a server with the same name, the usual local > project > user ordering (plugin-provided
entries slot in at roughly the project level) decides which one Claude Code actually connects to
— worth checking deliberately with claude mcp list rather than assuming after
installing a plugin that adds a server you already had configured elsewhere.
claude mcp add and friends
The CLI writes the same JSON shapes above for you rather than requiring you to hand-edit a file:
claude mcp add --transport http ourservice https://api.ourservice.dev/mcp \
--header "Authorization: Bearer $OURSERVICE_API_KEY" --scope project
claude mcp add --transport stdio local-tool -- node ./mcp-server.mjs
claude mcp list
claude mcp get ourservice
claude mcp remove ourservice
--scope picks which of the three files above the entry gets written to;
omitting it defaults to local. claude mcp get is the fastest way to
confirm what actually ended up on disk after an add — worth running once after any
scripted setup rather than assuming the flags did what you intended.
Checking status
A server appearing in claude mcp list only means it's configured, not that it's
reachable. Per-server connection status — pending, connected,
failed, needs-auth, or disabled — shows up in the
system:init message at the start of a session, and the Agent SDK exposes the same
thing programmatically via mcpServerStatus() (TypeScript) or
get_mcp_status() (Python) if you're driving Claude Code from a script rather than a
terminal. Check it after any deploy that might have rotated a key on one side of an
http server but not the other — a stale header shows up as failed
immediately, not as a slow, confusing timeout.