/ 21 min read

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

Architecture Flow

See the diagram above for a visual overview of this flow.

Try the Interactive SimulationFull View →

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:

MCP Connection Flow
Config Load
Resolve 7 Scopes
local
user
project
dynamic
enterprise
claudeai
managed
Merge Server Configs
Select Transport
Create Transport Instance
Auth Required?
YES
OAuth / XAA Flow
NO
Direct Connect
MCP Client Handshake
Tool Discovery: listTools
Resource Discovery: listResources
Wrap as MCPTool Instances
Merge into Unified Tool 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:

Transport Types
MCP Client
stdio
Spawns child process, stdin/stdout pipes. Best for: local tools
sse
Server-Sent Events, HTTP + streaming. Best for: remote servers
sse-ide
IDE-specific SSE, internal transport. Best for: VS Code, JetBrains
http
StreamableHTTP, standard HTTP. Best for: stateless APIs
ws
WebSocket, full duplex. Best for: real-time bidirectional
sdk
In-process SDK, no serialization. Best for: bundled servers

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.

Four Extension Systems
MCP (External Tools)
External server processesTransport-agnostic protocolTool + Resource discoveryOAuth authentication
Plugins (User-Toggleable Features)
Enable/disable in /plugin UIBuilt-in plugin scaffoldingCan contribute MCP serversCurrently: empty registry
Skills (Reusable Workflows)
Slash-command invocationBundled at compile timePrompt-based: inject instructions13+ bundled skills
Hooks (Lifecycle Callbacks)
27 lifecycle eventsShell command executionPreToolUse / PostToolUsePermission filtering via if conditions
MCP←→PluginsMCP servers can be contributed by plugins
MCP←→SkillsMCP_SKILLS flag: skills fetched from MCP servers
Skills←→HooksSkills can define hooks via HooksSettings
Plugins←→SkillsPlugins intended to become user-toggleable skills

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 stdio servers as local processes. A team shares remote http servers. An enterprise routes through SSE proxies with OAuth. The same connectToServer function 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:

Consolidated Extension Model
User Toggle Layer (formerly Plugins)
Enable/disable any MCP serverUI for managing extensionsNo separate plugin concept needed
Unified Extension Protocol (MCP)
Single registration mechanism
Single discovery protocol
Single auth model
MCP Tools
External server toolsIn-process SDK tools
MCP Workflows (formerly Skills)
Prompt templates as MCP resourcesSlash commands as MCP tool callsBundled via sdk transport
MCP Lifecycle Events (formerly Hooks)
PreToolUse / PostToolUse as MCP notificationsSession events as MCP resourcesConditional filtering via MCP sampling

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 →