Claude Code MCP Assembly: Seven Config Sources, Three Capability Surfaces

3 min readAI Agents

How MCP server configuration resolves across seven priority layers, how getMcpServerSignature() prevents duplicate connections, and how MCP tools, commands, and resources expand into the session capability surface.

ai-agentsclaude-codemcpintegrationsruntime

When people talk about MCP, they often describe it as if it were just a way to expose remote tools.

That is not what Claude Code is doing. MCP is a capability import layer — config merging matters as much as transport, and external servers get adapted into Claude Code's native runtime objects.

File structure

Files covered in this post4 files
src/
├── services/
│   ├── mcp/
│   │   ├── config.ts
│   │   ├── client.ts
│   │   ├── ChromeMcpClient.ts
│   │   └── types.ts

src/services/mcp/config.ts

Config loading, scope merging, policy filtering, and deduplication

Critical

Loads MCP definitions from enterprise, plugin, user, project, local, dynamic, and claude.ai sources, then applies precedence, approval, deduplication, and policy rules before connection begins.

Module
MCP Capability Assembly

Key Exports

  • getClaudeCodeMcpConfigs
  • getAllMcpConfigs
  • getMcpConfigByName
  • getMcpServerSignature

Why It Matters

  • MCP starts as a config-governance problem before it becomes a connection problem.
  • Enterprise policy, plugin-only locks, and project approval all shape the final server set.
  • Deduplication is content-based, because two differently named configs may still point at the same server.

src/services/mcp/client.ts

Connection, discovery, wrapping, and retry handling

Critical

Connects to MCP servers across multiple transports, discovers tools/prompts/resources, wraps them into Claude Code runtime objects, and handles auth, retries, truncation, and session recovery.

Module
MCP Runtime

Key Exports

  • connectToServer
  • fetchToolsForClient
  • fetchResourcesForClient
  • getMcpToolsCommandsAndResources

Why It Matters

  • Claude Code treats MCP as a first-class extension plane, not a side table bolted onto tools.ts.
  • A single MCP server can expand three user-facing surfaces: tools, commands, and resources.
  • Needs-auth and session-expired are modeled as stable runtime states, not just exception strings.

The core idea

Claude Code does not import MCP in one step.

It uses a staged assembly pipeline:

  1. load server definitions from multiple scopes
  2. apply precedence, approval, and policy rules
  3. deduplicate configs that would hit the same underlying server
  4. connect with transport-specific logic
  5. discover tools, prompts, skills, and resources
  6. wrap them into Claude Code's own runtime contracts

That means the real unit here is not "MCP connection."

It is capability assembly.

MCP assembly pipeline

The stable path from raw MCP config to product-visible capability.

  1. 1

    Load scoped config

    getClaudeCodeMcpConfigs / getAllMcpConfigs

    Claude Code reads MCP definitions from enterprise, plugin, user, project, local, dynamic, and claude.ai sources.

  2. 2

    Filter and deduplicate

    getMcpServerSignature / dedupPluginMcpServers / dedupClaudeAiMcpServers

    Project approval, plugin-only policy, enterprise exclusivity, and signature-based dedup all run before network connection begins.

  3. 3

    Connect with transport-aware logic

    connectToServer

    Different server types use different transports, auth wrappers, and cache behavior, but normalize into one MCP connection shape.

  4. 4

    Discover capability classes

    fetchToolsForClient / fetchCommandsForClient / fetchResourcesForClient

    Connected servers can contribute tools, prompts, MCP-derived skills, and resources.

  5. 5

    Adapt into Claude Code runtime objects

    getMcpToolsCommandsAndResources

    MCP tools become Tool objects, prompts become Command objects, and resources are exposed through shared resource tools.

Why config.ts matters more than it first looks

If you only look at client.ts, you might think MCP is mostly about transport.

But config.ts is where the real product policy lives.

It decides:

  • which scopes are allowed
  • which project servers are approved
  • whether enterprise config is exclusive
  • whether MCP is locked to plugin-only
  • whether plugin servers should be suppressed as duplicates
  • whether claude.ai connectors should be suppressed as duplicates

Only after that does the runtime even attempt to connect.

That is good architecture. It keeps policy and transport from bleeding into each other.

Fast config path versus slow connector path

One of the cleaner choices here is that Claude Code distinguishes:

  • a fast local config path
  • a slower connector-aware path

getClaudeCodeMcpConfigs() stays mostly local and cheap.

getAllMcpConfigs() is the broader path that also waits for claude.ai connector discovery and then merges the result.

That split is useful because startup does not always want the full network-backed picture.

Sometimes it only needs the local, policy-vetted baseline first.

Why deduplication is based on server identity, not key names

The most practical detail in this subsystem is getMcpServerSignature().

Claude Code does not assume duplicate servers will share the same map key.

Instead it derives a signature from:

  • stdio command plus args
  • or normalized URL

That catches the real-world duplicate cases that matter:

  • plugin server and manual server launching the same process
  • rewritten proxy URL and raw vendor URL that actually point at the same backend

This is one of those small implementation details that reveals product maturity.

client.ts imports MCP into the native runtime vocabulary

The other major lesson is that Claude Code does not surface raw MCP SDK objects upward.

It wraps them into its own concepts.

For example, fetchToolsForClient() turns MCP tools into Claude Code Tool objects with:

  • normalized tool names
  • read-only and destructive hints
  • permission integration
  • classifier input support
  • tool-call progress handling
  • retry and auth recovery hooks

That is what makes an external capability feel native to the runtime.

MCP expands three surfaces, not one

Claude Code is also broader than the usual "MCP means more tools" framing.

A connected server can produce:

  1. tools for the model
  2. prompts that behave like command-like entrypoints
  3. resources that can be listed and read through shared MCP resource tools

That is a better way to think about the system:

  • MCP is not just remote execution
  • it is a structured capability source

The real lesson

Claude Code does not treat MCP as a thin connector plugin.

It treats it as a capability assembly subsystem that turns external definitions into governed, native runtime surface.

That is why services/mcp/** belongs near the core of the architecture, not off to the side as "integration code."

MCP assembly pipeline across runtimes

The staged assembly: load configs → filter and dedup → connect → discover → adapt. JS shows the actual pattern; Python and Go show the same shape.

typescriptconfig.ts + client.ts (simplified)

The actual MCP assembly pattern from learning-claude-code

// --- config.ts: policy layer (cheap, local-first) ---

// Fast path: returns locally-available configs only
async function getClaudeCodeMcpConfigs(cwd: string): Promise<McpServerConfig[]> {
const [enterprise, plugin, user, project, local, dynamic] = await Promise.all([
  getEnterpriseMcpConfigs(),
  getPluginMcpConfigs(),
  getUserMcpConfigs(),
  getProjectMcpConfigs(cwd),
  getLocalMcpConfigs(cwd),
  getDynamicMcpConfigs(),
])

return dedupAndFilter([enterprise, plugin, user, project, local, dynamic])
}

// Identity-based dedup — two configs are the same server if they produce the same signature
function getMcpServerSignature(config: McpServerConfig): string {
if (config.type === 'stdio') {
  return `stdio:${config.command} ${config.args?.join(' ')}`
}
return `url:${normalizeUrl(config.url)}`
}

// --- client.ts: connection + discovery + adaptation ---

async function getMcpToolsCommandsAndResources(
configs: McpServerConfig[],
context: ToolUseContext,
): Promise<{ tools: Tool[]; commands: Command[]; resources: ServerResource[] }> {
const clients = await Promise.all(configs.map(cfg => connectToServer(cfg, context)))
const connected = clients.filter(c => c.type === 'connected')

const [tools, commands, resources] = await Promise.all([
  Promise.all(connected.map(c => fetchToolsForClient(c, context))),
  Promise.all(connected.map(c => fetchCommandsForClient(c, context))),
  Promise.all(connected.map(c => fetchResourcesForClient(c, context))),
])

return {
  tools: tools.flat().map(wrapMcpTool),         // MCP tool → Claude Code Tool
  commands: commands.flat().map(wrapMcpCommand), // MCP prompt → Command
  resources: resources.flat(),
}
}

Why does Claude Code use content-based server identity (getMcpServerSignature) instead of config key names for MCP deduplication?

medium

Two MCP configs can have different names but still refer to the same underlying server process or endpoint.

  • ABecause config names are not guaranteed to be unique across sources
    Incorrect.This is true but not the primary motivation — it describes a symptom, not the real-world case the signature solves.
  • BTo catch real-world duplicates: a plugin config and a manual config that launch the same process, or a proxy URL and a raw vendor URL that reach the same backend
    Correct!Correct. getMcpServerSignature derives identity from stdio command+args or normalized URL. This catches the cases that matter in practice: two differently-named configs that hit the same server. Name-based dedup would miss them.
  • CBecause transport type (stdio vs HTTP) determines server identity
    Incorrect.Transport type is part of the signature, but it alone doesn't determine identity — the command/URL is the key part.
  • DTo support hot-reload when a config file changes the server name without changing the endpoint
    Incorrect.Signature-based dedup is about preventing duplicate connections at assembly time, not about config hot-reload.