Claude Code MCP Assembly: Seven Config Sources, Three Capability Surfaces
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.
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
src/
├── services/
│ ├── mcp/
│ │ ├── config.ts
│ │ ├── client.ts
│ │ ├── ChromeMcpClient.ts
│ │ └── types.tssrc/services/mcp/config.ts
Config loading, scope merging, policy filtering, and deduplication
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.
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
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.
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:
- load server definitions from multiple scopes
- apply precedence, approval, and policy rules
- deduplicate configs that would hit the same underlying server
- connect with transport-specific logic
- discover tools, prompts, skills, and resources
- 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
Load scoped config
getClaudeCodeMcpConfigs / getAllMcpConfigsClaude Code reads MCP definitions from enterprise, plugin, user, project, local, dynamic, and claude.ai sources.
- 2
Filter and deduplicate
getMcpServerSignature / dedupPluginMcpServers / dedupClaudeAiMcpServersProject approval, plugin-only policy, enterprise exclusivity, and signature-based dedup all run before network connection begins.
- 3
Connect with transport-aware logic
connectToServerDifferent server types use different transports, auth wrappers, and cache behavior, but normalize into one MCP connection shape.
- 4
Discover capability classes
fetchToolsForClient / fetchCommandsForClient / fetchResourcesForClientConnected servers can contribute tools, prompts, MCP-derived skills, and resources.
- 5
Adapt into Claude Code runtime objects
getMcpToolsCommandsAndResourcesMCP 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:
- tools for the model
- prompts that behave like command-like entrypoints
- 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.
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?
mediumTwo 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.