1. MCP Architecture
The Model Context Protocol (MCP) is an open standard that connects AI agents to external systems. Instead of hard-coding tool implementations, MCP provides a standardized interface for tool discovery, invocation, and result handling. Claude Code uses MCP natively — every external integration (file I/O, web search, databases) is an MCP server.
flowchart LR
H["Host (Claude Code)"] --> C1["MCP Client"]
C1 -->|"stdio"| S1["MCP Server: Filesystem"]
C1 -->|"stdio"| S2["MCP Server: Database"]
C1 -->|"HTTP"| S3["MCP Server: API Gateway"]
S1 -->|"tools/list"| C1
S2 -->|"tools/call"| C1
S3 -->|"resources/read"| C1
1.1 MCP Resource Types
MCP servers expose three types of capabilities:
| Type | Purpose | Example | How Agent Uses |
|---|---|---|---|
| Tools | Actions the model can invoke | search_docs, create_file | Model calls via tool_use blocks |
| Resources | Read-only data the model can access | File contents, database schemas | Attached as context in prompts |
| Prompts | Reusable prompt templates | Code review template, analysis framework | User selects; expands into messages |
from mcp.server import Server
from mcp.types import Tool, Resource, TextContent
# MCP server exposing all three resource types
server = Server("my-tools")
# Tools — model invokes these autonomously
@server.tool()
async def search_knowledge_base(query: str, max_results: int = 5) -> str:
"""Search internal documentation by keyword query.
Returns matching documents with titles and excerpts."""
results = await db.search(query, limit=max_results)
return "\n".join(f"- {r.title}: {r.excerpt}" for r in results)
# Resources — read-only context the model can access
@server.resource("schema://database/tables")
async def get_db_schema() -> str:
"""Returns the current database schema for context."""
return await db.get_schema_ddl()
# Prompts — reusable templates
@server.prompt("code-review")
async def code_review_prompt(language: str, focus: str = "bugs") -> list:
"""Template for structured code review."""
return [
{"role": "user", "content": f"Review this {language} code for {focus}. "
"Structure your review as: 1) Critical issues 2) Improvements 3) Positive patterns"}
]
1.2 Transport Layers
MCP supports two transport mechanisms for communication between clients and servers:
import json
# Transport 1: stdio (local process)
# - MCP server runs as a child process
# - Communication via stdin/stdout (JSON-RPC 2.0)
# - Best for: local development, single-user tools
# - Config: {"command": "python", "args": ["server.py"]}
# Transport 2: Streamable HTTP (remote)
# - MCP server runs as an HTTP service
# - Communication via HTTP POST (JSON-RPC 2.0)
# - Best for: shared servers, production, multi-user
# - Config: {"url": "https://mcp.example.com/v1"}
# JSON-RPC message format (same for both transports)
request = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_knowledge_base",
"arguments": {"query": "refund policy", "max_results": 3}
}
}
response = {
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{"type": "text", "text": "- Refund Policy v3: Full refunds within 30 days..."}
]
}
}
2. Built-In Tools
Claude Code ships with built-in tools that are always available without additional MCP server configuration. These cover the fundamental operations an agent needs for file manipulation, command execution, and web access.
2.1 Read & Write Tools
# Built-in Read tool — reads file contents
# Name: "Read"
# The agent uses this to inspect files before editing
read_input = {
"file_path": "/path/to/file.py",
"offset": 0, # Start line (0-indexed)
"limit": 100 # Number of lines to read
}
# Returns: file content as text
# Built-in Write tool — creates or overwrites files
# Name: "Write"
# The agent uses this after planning edits
write_input = {
"file_path": "/path/to/file.py",
"content": "import os\n\ndef main():\n print('Hello')\n"
}
# Returns: confirmation of write
# Built-in Edit tool — surgical line-level edits
# Name: "Edit"
# Replaces specific lines without rewriting entire file
edit_input = {
"file_path": "/path/to/file.py",
"old_string": "def main():\n print('Hello')",
"new_string": "def main():\n print('Hello, World!')"
}
# Returns: confirmation with diff preview
2.2 Bash Tool
# Built-in Bash tool — executes shell commands
# Name: "Bash"
# The agent uses this for: running tests, installing packages,
# git operations, compilation, and system inspection
bash_input = {
"command": "cd /project && python -m pytest tests/ -v --tb=short",
"timeout": 30000 # Timeout in ms (safety cap)
}
# Returns: stdout + stderr combined output
# Security considerations (CCA Task 2.4):
# - Commands run in a sandboxed environment
# - Network access may be restricted
# - File system access scoped to workspace
# - Destructive commands (rm -rf) require approval in interactive mode
2.3 WebFetch Tool
# Built-in WebFetch tool — retrieves web content
# Name: "WebFetch"
# The agent uses this for: reading documentation, checking APIs,
# verifying public information
fetch_input = {
"url": "https://docs.anthropic.com/en/docs/agents",
"prompt": "Extract the main concepts about agentic loops"
}
# Returns: extracted/summarized content from the URL
# Key behaviors:
# - Respects robots.txt
# - Returns text content (HTML stripped)
# - Can extract specific information via prompt parameter
# - Timeout limits prevent hanging on slow sites
allowedTools in settings. The exam tests understanding of when tools require approval vs. auto-execute.
Enterprise Knowledge Base Agent
A consulting firm connected their Claude agent to MCP servers for Notion (documents), Slack (messages), and PostgreSQL (client data). The agent can answer questions like “What did we discuss with Client X last week?” by searching across all three systems through unified MCP tool calls. The unified protocol eliminated 3 months of custom integration work and enabled adding new data sources in hours instead of weeks.
3. Building MCP Servers
3.1 Python MCP Server
Building a custom MCP server lets you expose any backend system to Claude. Here is a complete example using the official mcp Python SDK:
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import TextContent
import asyncio
import json
# Create server instance
server = Server("customer-support")
@server.tool()
async def get_customer(identifier: str) -> str:
"""Look up a customer by email or ID.
Returns: customer profile with name, plan, status, and verification state.
Use this FIRST before any order or refund operations."""
# In production, this calls your actual customer database
customer = await db.customers.find_one({"email": identifier})
if not customer:
return json.dumps({"error": "Customer not found", "type": "not_found"})
return json.dumps({
"customer_id": customer["id"],
"name": customer["name"],
"plan": customer["plan"],
"status": customer["status"],
"verified": True
})
@server.tool()
async def process_refund(order_id: str, customer_id: str, amount: float, reason: str) -> str:
"""Process a refund for a verified customer's order.
Maximum $500 without escalation. Returns transaction confirmation."""
if amount > 500:
return json.dumps({
"error": "Amount exceeds limit",
"type": "limit_exceeded",
"suggestion": "Escalate to human for refunds over $500"
})
result = await payments.refund(order_id, amount, reason)
return json.dumps({
"success": True,
"transaction_id": result.id,
"amount_refunded": amount
})
# Run with stdio transport
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream)
if __name__ == "__main__":
asyncio.run(main())
3.2 MCP Server Configuration
MCP servers are configured in Claude Code’s settings file (.claude/settings.json or project-level .mcp.json):
{
"mcpServers": {
"customer-support": {
"command": "python",
"args": ["./mcp-servers/customer-support/server.py"],
"env": {
"DATABASE_URL": "postgresql://localhost:5432/support"
}
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
}
},
"remote-api": {
"url": "https://mcp.internal.company.com/v1",
"headers": {
"Authorization": "Bearer ${API_TOKEN}"
}
}
}
}
# Installing community MCP servers
# GitHub server (official)
npx -y @modelcontextprotocol/server-github
# Filesystem server (official)
npx -y @modelcontextprotocol/server-filesystem /path/to/allowed/dir
# PostgreSQL server
npx -y @modelcontextprotocol/server-postgres postgresql://localhost/mydb
# Custom Python server
pip install mcp
python my_server.py
4. Dynamic Tool Discovery
MCP enables dynamic tool discovery — the agent learns what tools are available at runtime by querying connected MCP servers. This is fundamentally different from static tool lists hardcoded in the application.
import json
# MCP tool discovery protocol
# 1. Client sends tools/list request to each connected server
discovery_request = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}
# 2. Server responds with available tools + schemas
discovery_response = {
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "get_customer",
"description": "Look up a customer by email or ID...",
"inputSchema": {
"type": "object",
"properties": {
"identifier": {"type": "string", "description": "Email or customer ID"}
},
"required": ["identifier"]
}
},
{
"name": "process_refund",
"description": "Process a refund for a verified customer...",
"inputSchema": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"customer_id": {"type": "string"},
"amount": {"type": "number"},
"reason": {"type": "string"}
},
"required": ["order_id", "customer_id", "amount", "reason"]
}
}
]
}
}
# 3. Agent's tool list is assembled dynamically from all connected servers
# This means: adding a new MCP server instantly gives the agent new capabilities
# without changing any agent code
MCP Questions on the CCA
The exam tests: (1) which transport to use for local vs. remote servers, (2) the difference between tools, resources, and prompts, (3) how tool discovery works at runtime, (4) permission models for built-in tools. It does NOT test MCP server implementation details — focus on the architectural concepts.
npx @modelcontextprotocol/server-filesystem). Connect your Claude agent to it. Then have the agent: (1) list files in a directory, (2) read a specific file, (3) create a summary of all .py files found. Verify the agent correctly uses the MCP tools.
5. Remote MCP Servers & Tunnels (CCA 7.2)
Local MCP servers (stdio transport) are great for development, but production agents need to access tools hosted on remote infrastructure — APIs running in the cloud, shared services across teams, or enterprise tools behind firewalls. Remote MCP uses HTTP transport and tunnels to bridge these gaps.
Analogy: Local MCP is like having tools on your desk — fast and private. Remote MCP is like calling a specialist in another office — you need a phone line (HTTP) or a secure tunnel (for firewalled services).
5.1 Transport Types: stdio vs HTTP
import json
# Transport 1: stdio (LOCAL)
# - MCP server runs as a child process on the same machine
# - Communication: stdin/stdout with JSON-RPC 2.0
# - Best for: development, single-user, tools that need local filesystem access
# - Config example:
stdio_config = {
"my-local-tool": {
"command": "python",
"args": ["mcp_server.py"],
"env": {"DATABASE_URL": "sqlite:///local.db"}
}
}
# Transport 2: Streamable HTTP (REMOTE)
# - MCP server runs as an HTTP service (anywhere: cloud, kubernetes, another machine)
# - Communication: HTTP POST with JSON-RPC 2.0
# - Best for: shared tools, production, multi-user, enterprise
# - Config example:
http_config = {
"shared-crm-tool": {
"url": "https://mcp.internal.company.com/crm",
"headers": {
"Authorization": "Bearer ${MCP_AUTH_TOKEN}"
}
}
}
# When to use which:
# stdio: development, filesystem tools, single-user
# HTTP: production, shared services, team tools, enterprise APIs
print("stdio: local process, fast, private, needs local access")
print("HTTP: remote service, shareable, scalable, needs network")
5.2 MCP Tunnels (Firewall Bridging)
import json
# MCP Tunnels solve: "My MCP server is behind a firewall/VPN,
# but my agent (or Anthropic's hosted agent) needs to reach it."
# A tunnel creates a secure connection FROM your internal network
# TO Anthropic's infrastructure — no inbound firewall rules needed.
# Tunnel setup (conceptual — actual commands vary by deployment):
# 1. QUICKSTART (Docker)
# Run the tunnel agent on any machine inside your network:
docker_command = """
docker run -d \\
--name mcp-tunnel \\
-e TUNNEL_TOKEN=tun_abc123... \\
-e MCP_SERVER_URL=http://internal-crm:8080 \\
anthropic/mcp-tunnel:latest
"""
# 2. HELM (Kubernetes)
helm_values = """
# values.yaml for MCP tunnel Helm chart
tunnel:
token: tun_abc123...
targets:
- name: internal-crm
url: http://crm-service.default.svc:8080
- name: internal-jira
url: http://jira.internal:8080
replicas: 2 # HA: run multiple tunnel instances
"""
# 3. DOCKER COMPOSE (multi-service)
compose_config = """
services:
mcp-tunnel:
image: anthropic/mcp-tunnel:latest
environment:
TUNNEL_TOKEN: ${TUNNEL_TOKEN}
extra_hosts:
- "internal-api:host-gateway"
restart: unless-stopped
"""
# How tunnels work:
# 1. Tunnel agent connects OUTBOUND to Anthropic (no firewall changes)
# 2. Anthropic's agent infrastructure routes tool calls through the tunnel
# 3. Tunnel agent forwards requests to your internal MCP server
# 4. Response travels back through the same tunnel
# Security:
# - All traffic is TLS encrypted
# - Tunnel tokens are scoped to specific MCP servers
# - No inbound ports needed (outbound-only connection)
# - Tunnel certificates can be pinned for zero-trust environments
print("Tunnels: outbound-only connections, no firewall changes needed")
print("Deploy via: Docker, Helm, Docker Compose, or standalone binary")
5.3 MCP Configuration: Project vs User Scope (CCA 7.3)
import json
# MCP server configuration has TWO scopes:
# 1. Project scope (.mcp.json) — committed to repo, shared with team
# 2. User scope (~/.claude.json) — personal, not committed
# PROJECT SCOPE: .mcp.json (in project root)
# Shared with all team members via version control
project_mcp = {
"servers": {
"project-db": {
"command": "python",
"args": ["tools/db_server.py"],
"env": {
"DATABASE_URL": "${DATABASE_URL}" # Env var expansion!
}
},
"shared-api": {
"url": "https://mcp.team-tools.internal/api",
"headers": {
"Authorization": "Bearer ${TEAM_API_TOKEN}"
}
}
}
}
# USER SCOPE: ~/.claude.json (personal machine only)
# For tools only YOU need (personal assistants, private APIs)
user_mcp = {
"servers": {
"my-notes": {
"command": "node",
"args": ["/home/user/tools/notes-server.js"]
},
"personal-calendar": {
"url": "https://my-calendar-mcp.vercel.app",
"headers": {
"Authorization": "Bearer ${MY_CALENDAR_TOKEN}"
}
}
}
}
# KEY FEATURES:
# 1. Environment variable expansion: ${VAR_NAME} is replaced at runtime
# 2. Multi-server simultaneous access: agent sees tools from ALL configured servers
# 3. Server isolation: each server's tools are namespaced (no name collisions)
# 4. Project + User merge: both scopes active simultaneously
# PRECEDENCE:
# If same server name in both scopes: project scope wins
# Tools from all servers are merged into one tool list for the agent
# Community vs Custom servers:
# Community: pre-built servers for common services (GitHub, Slack, Jira, etc.)
# Custom: your own servers for internal tools (build with mcp SDK)
print("Project scope (.mcp.json): shared with team, version controlled")
print("User scope (~/.claude.json): personal tools, not committed")
print("Both active simultaneously — agent sees merged tool list")
${VAR_NAME} syntax enables environment variable expansion in configs. (4) Multi-server access: all servers merge into one tool list. (5) HTTP transport for production/shared; stdio for local/development.
6. Agent SDK: MCP Connection & Tool Search
In Sections 3–5, you built and configured MCP servers externally. The Agent SDK provides two ways to connect to them programmatically: (1) passing server configs to query() via mcp_servers, and (2) using tool search for deferred loading when you have many tools.
6.1 Connecting MCP Servers via query()
Pass external MCP servers directly to the Agent SDK. The SDK starts the server process, discovers its tools, and registers them alongside built-in tools:
# Connecting External MCP Servers to the Agent SDK
# Requires: pip install claude-agent-sdk
# Pre-requisite: MCP servers available as commands (npm packages or local scripts)
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def main():
"""Connect multiple MCP servers to an Agent SDK query."""
async for message in query(
prompt="Find all open GitHub issues labeled 'bug' and check if any have related Jira tickets.",
options=ClaudeAgentOptions(
# Connect external MCP servers by command
mcp_servers={
# stdio transport: SDK starts this process and connects via stdin/stdout
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {"GITHUB_TOKEN": "${GITHUB_TOKEN}"}, # From env
},
# Another server — tools from all servers are merged
"jira": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-jira"],
"env": {"JIRA_TOKEN": "${JIRA_TOKEN}"},
},
},
# Pre-approve specific tools from these servers
# Format: mcp____
allowed_tools=[
"mcp__github__list_issues",
"mcp__github__get_issue",
"mcp__jira__search_issues",
],
),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())
6.2 Tool Search (Deferred Loading)
When you have 50+ tools across multiple servers, loading them all upfront wastes context window space. Tool search solves this: only tool names are loaded initially; full JSON schemas are fetched on demand when Claude determines it needs a specific tool. Tool search is enabled by default in the Agent SDK — no explicit opt-in required.
allowed_tools. There is no "ToolSearch" tool name to include. It’s an automatic background behavior controlled by the ENABLE_TOOL_SEARCH environment variable. When active, Claude automatically searches your tool catalog before calling MCP tools it hasn’t loaded yet.
# Tool Search — Automatic Deferred Loading (ON by default)
# Requires: pip install claude-agent-sdk
# Tool search works transparently: Claude discovers tools when it needs them.
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def main():
"""Tool search is ON by default — no explicit config needed.
Claude sees tool NAMES from connected servers, and fetches full
schemas on-demand when a task requires a tool it hasn't loaded yet.
"""
options = ClaudeAgentOptions(
mcp_servers={
"crm": {
"command": "python",
"args": ["./mcp_servers/crm_server.py"],
},
"analytics": {
"command": "python",
"args": ["./mcp_servers/analytics_server.py"],
},
},
# Pre-approve all tools from both servers via wildcards
# (required — without this, Claude sees tools but can't call them)
allowed_tools=[
"mcp__crm__*", # All tools from CRM server
"mcp__analytics__*", # All tools from analytics server
],
# Tool search is ON by default. To customize:
env={
# "ENABLE_TOOL_SEARCH": "true" # Always on (default behavior)
# "ENABLE_TOOL_SEARCH": "auto:5" # Activate when tools exceed 5% of context
# "ENABLE_TOOL_SEARCH": "false" # Off: load all schemas upfront (faster for <10 tools)
},
)
async for message in query(
prompt="Find the customer's recent orders and check their loyalty points.",
options=options,
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())
ENABLE_TOOL_SEARCH=false — loading everything upfront avoids the extra round-trip. The auto mode activates when tool definitions exceed 10% of the context window. Tool search requires Claude Sonnet 4+ or Opus 4+ (no Haiku support).
6.3 Wildcard Permissions for MCP Tools
Instead of listing every tool individually, use wildcards to approve all tools from a trusted server:
# Wildcard Permissions — Approve All Tools from a Server
# Requires: pip install claude-agent-sdk
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def main():
async for message in query(
prompt="Generate a weekly report from our analytics data.",
options=ClaudeAgentOptions(
mcp_servers={
"analytics": {
"command": "python",
"args": ["./mcp_servers/analytics_server.py"],
},
},
# Wildcard: approve ALL tools from the analytics server
# Use when: you trust the server completely and want all its tools available
allowed_tools=[
"mcp__analytics__*", # All tools from analytics server
"Read", # Plus built-in Read
],
# Alternative: approve all MCP tools from all servers
# allowed_tools=["mcp__*"], # Very permissive — use with caution
),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())
mcp__* bypass per-tool approval. Only use with fully trusted servers. For third-party MCP servers, always list individual tools explicitly.
Next in the SDK Track
In Part 8: CLAUDE.md, Rules & Skills, we shift to CCA Domain 3 — configuring agent behavior via CLAUDE.md files, project-level rules, skill definitions, and allowedTools restrictions. Covers CCA Tasks 3.1 and 3.2.
7. Skills + MCP: The Knowledge Layer
MCP gives Claude the ability to access your service. Skills give Claude the ability to use it correctly. Together they are complete — MCP without skills leaves users wondering what to do next; skills without MCP are instructions without any real-world data.
flowchart LR
subgraph MCP["MCP — Connectivity Layer"]
direction TB
A1["Real-time data access"]
A2["Tool invocation"]
A3["What Claude can do"]
end
subgraph Skills["Skills — Knowledge Layer"]
direction TB
B1["Workflow best practices"]
B2["Domain expertise"]
B3["How Claude should do it"]
end
MCP <-->|"Together"| Skills
Skills --> C["Reliable, consistent agent"]
MCP --> C
7.1 Why Skills Complete MCP
Without skills paired with your MCP server, users face a cold-start problem:
| Without Skills | With Skills |
|---|---|
| Users connect MCP but don’t know what to do next | Pre-built workflows activate automatically when needed |
| Support tickets: “How do I do X with your integration?” | Consistent, reliable tool usage out of the box |
| Each conversation starts from scratch | Best practices embedded in every interaction |
| Inconsistent results because users prompt differently each time | Lower learning curve for your integration |
The kitchen analogy: MCP provides the professional kitchen — access to tools, ingredients, and equipment. Skills provide the recipes — step-by-step instructions on how to create something valuable. Together they enable users to accomplish complex tasks without figuring out every step themselves.
7.2 MCP Enhancement Patterns
Three skill categories work well with MCP servers:
Used for: Workflow guidance that enhances the tool access an MCP server provides.
Real example: sentry-code-review skill (from Sentry) — “Automatically analyzes and fixes detected bugs in GitHub Pull Requests using Sentry’s error monitoring data via their MCP server.”
Key techniques for MCP enhancement skills:
- Coordinate multiple MCP calls in sequence (fetch issue → analyze → create fix → comment on PR)
- Embed domain expertise users would otherwise need to specify each time
- Provide context so Claude uses the right MCP tools in the right order
- Include error handling for common MCP issues (connection failures, auth expiry, rate limits)
# Example: MCP Enhancement SKILL.md
# This skill wraps your MCP server with workflow knowledge
---
name: linear-sprint-planner
description: >
Plans Linear project sprints using team velocity and task prioritization.
Use when user mentions "sprint", "sprint planning", "Linear tasks",
or asks to "create sprint" or "plan this sprint".
metadata:
mcp-server: linear
author: Your Company
version: 1.0.0
---
## Sprint Planning Workflow
When the user asks to plan a sprint:
### Step 1: Gather Context
1. Use `linear_get_team` to fetch team details and velocity
2. Use `linear_list_issues` to get unplanned backlog items
3. Use `linear_get_cycles` to check current sprint capacity
### Step 2: Analyze & Prioritize
- Sort by: priority (urgent first), then effort (small tasks for end of sprint)
- Check for blockers: mark items with unresolved dependencies as blocked
- Match capacity: sum estimated points to not exceed team velocity
### Step 3: Create Sprint
1. Use `linear_create_cycle` with: title, start date, end date
2. Use `linear_add_issues_to_cycle` for each prioritized item
3. Use `linear_update_issue` to assign owners
### Output
Report: sprint name, total points, # tasks, owner breakdown
Highlight any items that didn't fit (backlog for next sprint)