.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.