claude-code - 💡(How to fix) Fix bug(mcp): --mcp-config dynamic entries silently dropped when server name appears in disabledMcpServers

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…

--mcp-config is documented as a way to manually configure MCP servers for a single Claude Code session, overriding ~/.claude.json's mcpServers registrations. But the current implementation silently drops --mcp-config entries when the same server name appears in the canonical project's disabledMcpServers array — making the override unable to override.

Root Cause

  1. ~/.claude.json has projects.<some-canonical-path>.disabledMcpServers containing "foo".
  2. Start a Claude Code session in a directory whose canonical git root resolves to <some-canonical-path>.
  3. Invoke Claude with --mcp-config '{"mcpServers":{"foo":{"type":"http","url":"http://127.0.0.1:NNNN/"}}}' and --allowedTools mcp__foo__some_tool.
  4. Expected: the foo MCP server is registered for THIS session because --mcp-config is a manual override. mcp__foo__some_tool is in the tool catalog.
  5. Actual: the foo server is silently emitted as type: 'disabled'. No HTTP handshake. No tools registered. The system prompt's references to mcp__foo__* resolve to nothing. ToolSearch returns no matches.

Fix Action

Fix / Workaround

  1. --mcp-config parses correctly into dynamicMcpConfig at entrypoints/cli/main.tsx:1415-1521. The parsed config is tagged with scope: 'dynamic' (lines 1500-1503) to distinguish it from ~/.claude.json mcpServers entries.
  2. Merged into regularMcpConfigs at main.tsx:2386-2400 and passed to connectMcpBatch at :2729.
  3. Bug: getMcpToolsCommandsAndResources (services/mcp/client.ts:2226) walks each entry. At :2245 and :2288, it calls isMcpServerDisabled(name) BEFORE attempting the connection. If true: emits onConnectionAttempt({client: {type:'disabled', ...}}) and returns. Never attempts the HTTP handshake.
  4. isMcpServerDisabled (services/mcp/config.ts:1528) reads getCurrentProjectConfig().disabledMcpServers.
  5. getCurrentProjectConfig (utils/config.ts:1602) uses findCanonicalGitRoot (utils/git.ts:195), which resolves any worktree path back to its main checkout.
  6. The disable filter at client.ts:2245,:2288 does NOT check the scope field on the config. So a dynamic (--mcp-config) entry gets dropped if a static (~/.claude.json) entry with the same name is disabled at the project level.

Workarounds for now

Code Example

// services/mcp/client.ts:2245
- if (isMcpServerDisabled(name)) {
+ if (config.scope !== 'dynamic' && isMcpServerDisabled(name)) {

// services/mcp/client.ts:2288
- if (isMcpServerDisabled(name)) {
+ if (config.scope !== 'dynamic' && isMcpServerDisabled(name)) {
RAW_BUFFERClick to expand / collapse

Summary

--mcp-config is documented as a way to manually configure MCP servers for a single Claude Code session, overriding ~/.claude.json's mcpServers registrations. But the current implementation silently drops --mcp-config entries when the same server name appears in the canonical project's disabledMcpServers array — making the override unable to override.

Repro

  1. ~/.claude.json has projects.<some-canonical-path>.disabledMcpServers containing "foo".
  2. Start a Claude Code session in a directory whose canonical git root resolves to <some-canonical-path>.
  3. Invoke Claude with --mcp-config '{"mcpServers":{"foo":{"type":"http","url":"http://127.0.0.1:NNNN/"}}}' and --allowedTools mcp__foo__some_tool.
  4. Expected: the foo MCP server is registered for THIS session because --mcp-config is a manual override. mcp__foo__some_tool is in the tool catalog.
  5. Actual: the foo server is silently emitted as type: 'disabled'. No HTTP handshake. No tools registered. The system prompt's references to mcp__foo__* resolve to nothing. ToolSearch returns no matches.

Root cause (sourcedive)

Investigated against the leaked @anthropic-ai/claude-code source mirror (~v1.0.x; current runtime is 2.1.144 — same architecture in this area):

  1. --mcp-config parses correctly into dynamicMcpConfig at entrypoints/cli/main.tsx:1415-1521. The parsed config is tagged with scope: 'dynamic' (lines 1500-1503) to distinguish it from ~/.claude.json mcpServers entries.
  2. Merged into regularMcpConfigs at main.tsx:2386-2400 and passed to connectMcpBatch at :2729.
  3. Bug: getMcpToolsCommandsAndResources (services/mcp/client.ts:2226) walks each entry. At :2245 and :2288, it calls isMcpServerDisabled(name) BEFORE attempting the connection. If true: emits onConnectionAttempt({client: {type:'disabled', ...}}) and returns. Never attempts the HTTP handshake.
  4. isMcpServerDisabled (services/mcp/config.ts:1528) reads getCurrentProjectConfig().disabledMcpServers.
  5. getCurrentProjectConfig (utils/config.ts:1602) uses findCanonicalGitRoot (utils/git.ts:195), which resolves any worktree path back to its main checkout.
  6. The disable filter at client.ts:2245,:2288 does NOT check the scope field on the config. So a dynamic (--mcp-config) entry gets dropped if a static (~/.claude.json) entry with the same name is disabled at the project level.

pendingMcpServers (consumed by tools/ToolSearchTool/ToolSearchTool.ts:334) filters appState.mcp.clients by type === 'pending'. A 'disabled' entry is invisible there; its tools never appear in deferred_tools_delta.

Why this is the bug, not a feature

The conceptual model of --mcp-config is a session-scoped manual override. Users invoking it explicitly want to enable a server for THIS session regardless of broader configuration. The disabledMcpServers array's intent is to be a project-wide default — that an explicit session-scoped flag wouldn't honor it is the documented user expectation, but the current code path treats both as equal and filters dynamic on the static blacklist.

Suggested fix

Two-line guard at the disable check sites:

// services/mcp/client.ts:2245
- if (isMcpServerDisabled(name)) {
+ if (config.scope !== 'dynamic' && isMcpServerDisabled(name)) {

// services/mcp/client.ts:2288
- if (isMcpServerDisabled(name)) {
+ if (config.scope !== 'dynamic' && isMcpServerDisabled(name)) {

Same guard belongs in useManageMCPConnections.ts:891-893 (the filter step before getMcpToolsCommandsAndResources runs).

The scope: 'dynamic' tag is already set at main.tsx:1500-1503 and survives the merge, so the discriminator is available — no new state needed.

Workarounds for now

Two viable ones:

  1. Scrub the server name from disabledMcpServers in ~/.claude.json (across all project entries that contain it). This restores the dynamic override but defeats the intent of disabledMcpServers (which is to keep the server disabled in static config).
  2. Use a server name that's never appeared in any project's disabledMcpServers — i.e., rename the --mcp-config server to foo-session or foo-dynamic. Requires coordinated change on three surfaces (the --mcp-config flag, the --allowedTools flag, and any system-prompt-injected mcp__foo__* references).

Real-world trigger

Discovered during a HAPImatic (https://github.com/MattStarfield/hapimatic-private — fork of tiann/hapi) worker session 2026-05-20. HAPImatic injects an HTTP MCP server via --mcp-config '{"mcpServers":{"hapi":{"type":"http","url":"http://127.0.0.1:<port>/"}}}' on every Claude session spawn, providing change_title and image_fetch tools. A user-installed enforce-default-disabled hook had added "hapi" to every project's disabledMcpServers. The --mcp-config override never engaged. Both tools silently absent in every session, but the system prompt continued to promise them.

Environment

  • Claude Code version: 2.1.144
  • Platform: Linux (Raspberry Pi 5, ARM64)
  • Node: v24.11.1

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