Inside Claude Code: Plugging Into Everything with MCP
How Claude Code connects to external tools through the Model Context Protocol — and why having four extension systems is three too many. Part 6 of 10.
The Platform Instinct
A tool that cannot learn new tricks ages fast. The best tools are not just tools — they are platforms. They let you plug in whatever you need, whenever you need it, without waiting for an upstream team to ship a new integration.
Claude Code understood this from the start. Rather than hard-coding every integration (a Jira tool, a Postgres tool, a Confluence tool…), it adopted the Model Context Protocol (MCP) as its primary extension mechanism. MCP is an open standard that lets any external server expose tools, resources, and prompts through a uniform interface. Claude Code connects to MCP servers, discovers what they offer, and merges those capabilities into its unified tool registry — the same registry that powers its built-in tools.
But what’s interesting is that the story does not end there. Claude Code also has three additional extension systems: Plugins, Skills, and Hooks. Each was designed to solve a specific problem. Each works differently. And the overlap between them creates genuine architectural tension — a case study in what happens when extensibility evolves faster than consolidation.
The Problem: A World of External Systems
AI coding assistants do not live in a vacuum. Real engineering workflows touch dozens of systems: issue trackers, CI pipelines, databases, documentation sites, cloud consoles, internal APIs, observability dashboards. A coding tool that cannot reach these systems is stuck answering questions about code without understanding the context that code operates in.
The naive approach is to build integrations directly: a Jira tool, a GitHub tool, a Postgres tool, each maintained by the core team. This scales linearly with the number of integrations, requires the core team to understand every external API, and means users wait for features to be built upstream.
The platform approach is different. You define a protocol — a contract that says “here is how you expose a tool, here is how you expose a resource, here is how authentication works” — and let anyone implement it. The core tool only needs to speak the protocol. Individual integrations become external servers that anyone can build, test, deploy, and share independently.
Claude Code chose the platform approach. The protocol it adopted is MCP.
How Claude Code Solves It: The MCP Architecture
MCP in Claude Code is implemented across two primary files: src/services/mcp/types.ts defines the configuration schemas and type system, while src/services/mcp/client.ts handles connection management, transport negotiation, tool discovery, and result processing. In the current snapshot, this subsystem is several thousand lines and one of the most sophisticated parts of the codebase.
The Connection Flow
The journey from configuration to a working tool starts with config loading and ends with tools merged into the unified registry:
I found that the configuration system supports seven distinct scopes, defined in the ConfigScopeSchema:
export const ConfigScopeSchema = lazySchema(() =>
z.enum([
'local', // .claude/settings.local.json in project
'user', // ~/.claude/settings.json
'project', // .claude/settings.json in project root
'dynamic', // runtime-injected by IDE extensions
'enterprise', // organization-wide managed config
'claudeai', // Claude.ai proxy servers
'managed', // managed config file path
]),
)
Seven scopes is a lot. The hierarchy exists because Claude Code runs in wildly different contexts — a solo developer on a laptop, a team member in a monorepo, a contractor behind an enterprise proxy, a user of the Claude.ai web interface. Each context needs different defaults, different permissions, and different servers. The scopes resolve in priority order, with more specific scopes (local) overriding more general ones (enterprise).
Transport Types
MCP is transport-agnostic by design. Claude Code supports multiple transport types, each optimized for a different deployment pattern:
The transport selection happens in the connectToServer function, which is memoized so that repeated connections to the same server reuse the existing client:
export const connectToServer = memoize(
async (
name: string,
serverRef: ScopedMcpServerConfig,
serverStats?: { ... },
): Promise<MCPServerConnection> => {
let transport
if (serverRef.type === 'sse') {
const authProvider = new ClaudeAuthProvider(name, serverRef)
const combinedHeaders = await getMcpServerHeaders(name, serverRef)
transport = new SSEClientTransport(new URL(serverRef.url), options)
} else if (serverRef.type === 'http') {
// ... StreamableHTTPClientTransport ...
} else if (serverRef.type === 'ws') {
// ... WebSocketTransport with TLS and proxy support ...
} else if (serverRef.type === 'sdk') {
// ... SdkControlClientTransport for in-process ...
} else {
// Default: stdio — spawn child process
transport = new StdioClientTransport({ command, args, env })
}
},
)
The stdio transport is the most common for local development — it spawns a child process and communicates over stdin/stdout. The sse and http transports handle remote servers with full OAuth support. The sdk transport is used for in-process servers that ship bundled with Claude Code itself, avoiding serialization overhead entirely.
Authentication is per-server and supports two models. Standard OAuth follows the OAuth 2.1 flow with optional PKCE, configured per-server via the oauth field. Cross-App Access (XAA) is an enterprise feature where the IdP configuration is shared globally and individual servers just set xaa: true to opt in.
The Four Extension Systems
Here is where the architecture gets interesting — and, arguably, messy. MCP is not the only way to extend Claude Code. There are four distinct extension systems, each with its own registration mechanism, lifecycle, and capabilities.
Let me walk through each one.
MCP is the heavyweight. It handles external tool integration through a well-defined protocol with discovery, authentication, and resource management. When you connect a database server or a custom API, you use MCP. The tools it discovers are wrapped as MCPTool instances and injected into the same registry as built-in tools — the model cannot tell the difference.
Plugins are the newest system and, right now, the emptiest. The src/plugins/bundled/index.ts file tells the whole story:
export function initBuiltinPlugins(): void {
// No built-in plugins registered yet — this is the scaffolding for
// migrating bundled skills that should be user-toggleable.
}
I found this fascinating. The plugin system was designed to give users a /plugin UI where they could enable or disable individual features. But no plugins have been migrated to this system yet. It is pure scaffolding — infrastructure waiting for content.
Skills are the workhorse extension system for workflow-level capabilities. A skill is a slash command (like /simplify, /debug, /remember) that injects a specialized prompt into the conversation. Skills are registered at startup, with 13+ bundled:
export function initBundledSkills(): void {
registerUpdateConfigSkill()
registerKeybindingsSkill()
registerVerifySkill()
registerDebugSkill()
registerLoremIpsumSkill()
registerSkillifySkill()
registerRememberSkill()
registerSimplifySkill()
registerBatchSkill()
registerStuckSkill()
// ... plus feature-flagged skills for dream, hunter, loop, etc.
}
Each skill is a BundledSkillDefinition with a name, description, trigger conditions, allowed tools, optional hooks, and a getPromptForCommand function that generates the prompt content. But there is overlap with MCP: the MCP_SKILLS feature flag enables fetching skills from MCP servers, blurring the line between the two systems.
Hooks are the lifecycle callback system. They let you run shell commands at specific points in Claude Code’s execution cycle. The current SDK event list defines 27 hook events:
export const HOOK_EVENTS = [
'PreToolUse', 'PostToolUse', 'PostToolUseFailure',
'Notification', 'UserPromptSubmit',
'SessionStart', 'SessionEnd',
'Stop', 'StopFailure',
'SubagentStart', 'SubagentStop',
'PreCompact', 'PostCompact',
'PermissionRequest', 'PermissionDenied',
'Setup', 'TeammateIdle',
'TaskCreated', 'TaskCompleted',
'Elicitation', 'ElicitationResult',
'ConfigChange', 'WorktreeCreate', 'WorktreeRemove',
'InstructionsLoaded', 'CwdChanged', 'FileChanged',
] as const
Hooks support conditional execution via the if field, which uses permission rule syntax (e.g., "Bash(git *)") to filter which tool calls trigger the hook. They can be defined in settings files or within skill definitions — another overlap point.
What’s Smart About This Design
The MCP integration itself is genuinely well-engineered. Several design decisions stand out.
✅ Smart Pattern: Transport-agnostic design means any deployment model works. A solo developer runs
stdioservers as local processes. A team shares remotehttpservers. An enterprise routes through SSE proxies with OAuth. The sameconnectToServerfunction handles all of them through a clean branching pattern.
Seven-scope configuration handles every deployment scenario from individual developer to enterprise fleet. The scope hierarchy means an enterprise can mandate certain MCP servers while still letting individual developers add their own local tools.
Memoized connections via connectToServer = memoize(...) prevent redundant handshakes. Once a server is connected, the client is cached and reused. Session expiration is handled through McpSessionExpiredError, which clears the cache and triggers reconnection.
Per-server authentication with both standard OAuth and enterprise XAA means security is not an afterthought. The ClaudeAuthProvider class implements the MCP SDK’s auth interface, handling token refresh, step-up authentication detection, and 401 retry logic.
Unified tool registry is the crown jewel. MCP tools are wrapped as MCPTool instances that implement the same Tool interface as built-in tools. The model sees a flat list of capabilities — it does not know or care whether a tool is built-in, provided by a local MCP server, or proxied from a remote service.
What Could Be Better
The four-system extension architecture is the primary source of confusion. Here is the honest assessment.
⚠️ Watch Out: Plugins are empty scaffolding. The plugin system has infrastructure (registration, UI commands, MCP server contribution) but zero actual plugins. The comment in the source makes it clear that plugins were meant to subsume parts of the skill system, but the migration never happened.
Skills partially overlap with MCP. The MCP_SKILLS feature flag lets MCP servers provide skills, which means a skill can be either a compiled-in bundled definition or something fetched from an MCP server at runtime. This dual provenance makes the mental model harder to reason about. Are skills a prompt pattern? A delivery mechanism? A UI concept (slash commands)?
Hooks are defined in three places. Hook configurations can appear in settings files, in skill definitions (hooks field on BundledSkillDefinition), or in plugin configurations. Three definition sites for the same concept means three places to check when debugging why a hook fires or does not fire.
No single extension story. If someone asks “how do I extend Claude Code?”, the answer depends on what kind of extension they mean: new tools (MCP), toggleable features (plugins, but empty), workflow templates (skills), or lifecycle automation (hooks). Four answers to one question is three too many.
A Consolidated Extension Model
A cleaner architecture would center on MCP as the single protocol, with skills as MCP-wrapped workflows and hooks as a standard MCP event system:
In this model, skills become MCP servers that use the sdk transport — they run in-process, expose their prompt templates as MCP resources, and their slash commands as MCP tools. Hooks become MCP notifications that external servers can subscribe to. The plugin concept dissolves entirely: “enabling a plugin” is just “enabling an MCP server” in the settings UI. One protocol. One mental model. One place to look when something goes wrong.
This is not a theoretical exercise. The sdk transport already exists for in-process servers. The MCP_SKILLS feature flag already enables skill fetching from MCP servers. The pieces are there — they just have not been fully assembled.
Key Takeaway
Pick one extension protocol and go deep. Multiple half-baked systems create confusion — and none of them fully mature. Claude Code’s MCP integration is excellent: transport-agnostic, well-authenticated, cleanly merged into the tool registry. But the surrounding extension systems (plugins with no plugins, skills that are sometimes MCP and sometimes not, hooks defined in three places) dilute the story. The path forward is consolidation around MCP, using its existing transport types to subsume the other systems rather than running them in parallel.
The best platforms are not the ones with the most extension points. They are the ones with one extension point that works for everything.
This is Part 6 of the “Inside Claude Code” series.
← Part 5: Who’s Allowed to Do What? | Part 7: Remembering What Matters →