claude-code - 💡(How to fix) Fix MCP server config: project-level should merge with user-level (env, etc.), not replace the whole entry

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When the same MCP server name appears in both user-level (~/.claude.json) and project-level (<repo>/.mcp.json) config, Claude Code replaces the entire server entry rather than merging. The project entry has to spell out the full server definition (transport, command, args, env, etc.) just to override one field.

This forces hardcoded platform paths into checked-in config, which breaks any project deployed across multiple machines/OSes.

Root Cause

That would commit cleanly across machines because the project file never names a path. But CC's replace-not-merge semantics mean project-level has to spell out the full server definition with paths — so checking it in either breaks the Mac clone (if Linux paths win) or the Linux clone (if Mac paths win).

Fix Action

Fix / Workaround

Workarounds today

  1. Dispatch shim — project .mcp.json invokes ./tools/mcp-spawn.sh <name>, shim does uname branching. Works but every spawn pays a shell hop.
  2. Per-host files + gitignored symlink — commit .mcp.json.darwin + .mcp.json.linux, each clone symlinks one to .mcp.json at setup. Works but needs a per-clone setup step.
  3. Don't commit .mcp.json — defeats the purpose of project-level config.

a) Merge semantics — project-level partial entries merge into the user-level definition. Project can override single fields (env, args additions) without restating transport/command/args. Closest to how most config systems compose (Docker compose overlays, Kubernetes patches, npm package.json overrides, etc.).

RAW_BUFFERClick to expand / collapse

Summary

When the same MCP server name appears in both user-level (~/.claude.json) and project-level (<repo>/.mcp.json) config, Claude Code replaces the entire server entry rather than merging. The project entry has to spell out the full server definition (transport, command, args, env, etc.) just to override one field.

This forces hardcoded platform paths into checked-in config, which breaks any project deployed across multiple machines/OSes.

The pain, concretely

I'm running an MCP server (https://github.com/klercker/human-bridge) where the same project repo is checked out on macOS and on Linux LXCs. The natural shape would be:

  • User-level (per machine): MCP server's command + args (paths differ by OS — /opt/homebrew/bin/bun vs /usr/local/bin/bun, ~/Projects/... vs /opt/...).
  • Project-level (committed to git): just the env vars that scope this clone (HB_AGENT_ID, HB_CHANNEL, etc.).

That would commit cleanly across machines because the project file never names a path. But CC's replace-not-merge semantics mean project-level has to spell out the full server definition with paths — so checking it in either breaks the Mac clone (if Linux paths win) or the Linux clone (if Mac paths win).

Workarounds today

All ugly:

  1. Dispatch shim — project .mcp.json invokes ./tools/mcp-spawn.sh <name>, shim does uname branching. Works but every spawn pays a shell hop.
  2. Per-host files + gitignored symlink — commit .mcp.json.darwin + .mcp.json.linux, each clone symlinks one to .mcp.json at setup. Works but needs a per-clone setup step.
  3. Don't commit .mcp.json — defeats the purpose of project-level config.

What I'd want

Either:

a) Merge semantics — project-level partial entries merge into the user-level definition. Project can override single fields (env, args additions) without restating transport/command/args. Closest to how most config systems compose (Docker compose overlays, Kubernetes patches, npm package.json overrides, etc.).

b) Explicit extends — project entry says extends: \"<server-name>\", inherits everything from user-level, applies its own diff. More explicit, harder to surprise users with field collisions.

Either would let dual-deployed projects commit a single platform-independent .mcp.json with just the diff that's actually project-specific.

Why it matters

The dual-deploy pattern isn't exotic — anyone running personas/agents on multiple machines, or shipping plugins consumed across host OSes, hits this the moment they try to commit project-level MCP config. Today they reach for shims or symlink dances; ideally the harness handles composition for them.

Related

  • #17017 (project-level permissions replace global permissions instead of merging — same pattern, different config surface)
  • #51274 (agent-scoped MCP override — adjacent, but agent scope rather than project/user composition)

Repro

  1. User-level ~/.claude.json declares MCP server `foo` with full transport/command/args.
  2. Project-level <repo>/.mcp.json declares MCP server `foo` with only \"env\": { \"X\": \"y\" }.
  3. Open the project. Observe `foo` fails to spawn because command/args from user-level were not inherited.

Workaround confirms behavior: copy the user-level command/args into the project entry, server spawns successfully.

Environment: Claude Code (current as of May 2026), macOS Darwin 25.5.0.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING