Chapter 7: MCP — Connect Claude to Everything
Appendix links: A06 MCP Protocol Deep Dive covers the wire protocol, capability negotiation, transport layers, and building production-grade MCP servers.
What You'll Build
Three real MCP integrations that extend Claude beyond the filesystem:
- GitHub MCP — Analyze your own repos, triage issues, review PRs, all without leaving Claude
- Context7 MCP — Look up live documentation for any library, always current, never stale
- Playwright MCP — Automate browser testing, take screenshots, verify your deployed app works
These are the same MCP servers used in Everything Claude Code's .mcp.json config. Not theoretical — this is how production teams wire Claude into their toolchain.
How MCP Works
MCP (Model Context Protocol) is a standardized interface between Claude and external tools. Think of it as USB for AI — any tool that speaks MCP can plug into Claude and become a native tool.
Claude Code
|
|-- Built-in: Read, Write, Edit, Bash, Glob, Grep
|
+-- MCP Layer
|-- GitHub Server --> PRs, issues, branches, code search
|-- Context7 Server --> Live docs for any library
|-- Playwright Server --> Browser automation, screenshots
|-- Exa Server --> Web search with citations
|-- Memory Server --> Cross-session knowledge graph
|-- Your custom server --> Whatever you needWhy MCP Instead of Bash?
Without MCP, Claude can still call gh issue list via Bash. MCP is better because:
- Type safety: MCP tools have JSON Schema definitions. Claude knows exactly what parameters are valid.
- Process isolation: Each MCP server runs in its own process. A buggy tool can't crash Claude.
- Semantic clarity: Claude understands what
mcp__github__list_issuesdoes at a deeper level than parsingghCLI output. - Permission control: You can allow GitHub MCP tools while blocking raw
ghcommands.
Tool Naming
All MCP tools follow the pattern mcp__<server>__<tool>:
mcp__github__list_issues -- GitHub server, list_issues tool
mcp__context7__query-docs -- Context7 server, query-docs tool
mcp__playwright__navigate -- Playwright server, navigate toolThis naming makes them easy to target in hooks:
{
"hooks": {
"PreToolUse": [{
"matcher": "mcp__github__.*",
"hooks": [{ "type": "command", "command": "echo 'GitHub API call'" }]
}]
}
}Configuration
Two ways to add MCP servers:
CLI (recommended):
claude mcp add github -e GITHUB_PERSONAL_ACCESS_TOKEN -- npx -y @modelcontextprotocol/server-githubManual .mcp.json:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx"
}
}
}
}Scope
| Scope | File | Best For |
|---|---|---|
| user | ~/.claude/.mcp.json | Tools you use everywhere (GitHub, Memory) |
| project | .mcp.json in project root | Project-specific tools (commit to Git) |
Security rule: Never hardcode tokens. Use environment variables:
# In your .bashrc or .zshrc
export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxx
# Then reference it
claude mcp add github -e GITHUB_PERSONAL_ACCESS_TOKEN -- npx -y @modelcontextprotocol/server-githubECC's .mcp.json Reference
Here's what a production .mcp.json looks like, based on ECC's config:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "" }
},
"context7": {
"command": "npx",
"args": ["-y", "@anthropic-ai/context7-mcp"]
},
"playwright": {
"command": "npx",
"args": ["-y", "@anthropic-ai/claude-code-playwright"]
},
"memory": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory"]
},
"exa": {
"command": "npx",
"args": ["-y", "exa-mcp-server"],
"env": { "EXA_API_KEY": "" }
}
}
}You don't need all of them. Start with GitHub and Context7 — they cover 80% of real use cases.
Demo 18: GitHub MCP — Manage Your Repos from Claude
The Problem
You're working on a project and need to check open issues, find stale PRs, or review recent changes across multiple repos. Right now, that means switching to a browser, clicking around GitHub, losing context. With GitHub MCP, Claude does it all inline.
Set Up
1. Create a GitHub Token
Go to github.com/settings/tokens?type=beta and create a fine-grained token with:
- Repository access: Select your repos (or all)
- Permissions: Issues (R/W), Pull Requests (R/W), Contents (R/W)
Save it:
# Add to your shell profile
echo 'export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_your_token_here' >> ~/.bashrc
source ~/.bashrc2. Add the Server
claude mcp add github -e GITHUB_PERSONAL_ACCESS_TOKEN -- npx -y @modelcontextprotocol/server-githubVerify:
claude mcp list
# Expected: github: npx -y @modelcontextprotocol/server-github (stdio)3. Use It on Your Real Repos
mkdir -p ~/claude-demos/demo-18 && cd ~/claude-demos/demo-18
git init
claudeHere's where it gets useful. Use your actual repositories:
Triage open issues:
List all open issues in <your-username>/<your-repo>, sorted by most recent.
For each one, summarize the problem in one sentence.> List all open issues in myuser/web-app, sorted by most recent.
For each one, summarize the problem in one sentence.
● mcp__github__list_issues
owner: "myuser"
repo: "web-app"
state: "open"
sort: "created"
direction: "desc"
Allow? [y/n/a]: y
✔ 4 open issues found:
#23 (May 25) — Dashboard chart fails to render when date range
spans a DST transition
#21 (May 22) — Login page shows blank screen on Safari 17.4
#18 (May 15) — CSV export truncates rows after 10,000 entries
#12 (Apr 30) — Dark mode toggle does not persist across sessionsFind stale PRs:
Find all open PRs in <your-username>/<your-repo> that haven't been updated
in the last 14 days. List them with the author and last activity date.Review a specific PR:
Get the files changed in PR #42 of <your-username>/<your-repo>.
Summarize what the PR does and flag any concerns.Create an issue from a finding:
I found a SQL injection vulnerability in src/api/users.js:10. Create a
GitHub issue for it with appropriate labels, referencing the exact code location.What Just Happened?
Key takeaways:
- The MCP server handled authentication automatically using your environment variable
- Claude received structured JSON, not raw CLI output, so parsing was reliable
- You approved the tool call just like any built-in tool (Read, Bash, etc.)
- The MCP server runs as a child process -- it started when Claude launched and stops when you exit
What Happens Under the Hood
You: "List open issues in myuser/myrepo"
|
v
Claude selects: mcp__github__list_issues
|
v
Claude Code ---(stdio)---> GitHub MCP Server ---(HTTPS)---> GitHub API
|
v
GitHub returns JSON array of issues
|
v
Claude formats and presents the resultsThe MCP server runs as a child process of Claude Code. It starts automatically when you begin a session and stops when you exit.
Real-World Patterns
Morning standup context: Combine with a SessionStart hook to automatically show your assigned issues and PR review requests.
Issue-driven development: Ask Claude to read an issue, then implement the fix, then create a PR linking to the issue — all without leaving the terminal.
Cross-repo search: Search for code patterns across multiple repos using mcp__github__search_code.
Demo 19: Context7 MCP — Always-Current Documentation
The Problem
You're using a library and need to check the API for a specific function. Claude's training data might be months old. The docs might have changed. You could open a browser and search, or you could ask Claude to look it up in real-time.
Context7 is a documentation server that fetches live docs for thousands of libraries. It's listed in ECC's recommended MCP config.
Set Up
claude mcp add context7 -- npx -y @anthropic-ai/context7-mcpVerify:
claude mcp list
# Should show: context7: npx -y @anthropic-ai/context7-mcp (stdio)Use It
cd ~/claude-demos/demo-18
claudeLook up current API docs:
I'm using Drizzle ORM with PostgreSQL. Look up the current documentation for
defining a schema with relations. Show me the exact API for one-to-many relationships.> Look up the current Drizzle ORM docs for one-to-many relationships.
● mcp__context7__resolve-library-id
libraryName: "drizzle-orm"
→ Resolved: /drizzle-team/drizzle-orm (confidence: 0.97)
● mcp__context7__query-docs
libraryId: "/drizzle-team/drizzle-orm"
query: "one-to-many relations schema definition"
✔ Documentation retrieved (2,847 tokens):
## Relations in Drizzle ORM
To define a one-to-many relation, use the `relations()` helper:
```typescript
import { relations } from 'drizzle-orm';
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name'),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
authorId: integer('author_id').references(() => users.id),
title: text('title'),
});
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
...Claude calls mcp__context7__resolve-library-id to find the Drizzle docs, then mcp__context7__query-docs to fetch the relevant section.
Check for breaking changes:
Look up the Tailwind CSS v4 docs. What changed in the configuration format
compared to v3? I need to know if my tailwind.config.js needs updating.Get the latest API for a function you're using:
Look up the current Next.js docs for the App Router's generateMetadata function.
I need the TypeScript types and all available fields.What Just Happened?
Key takeaways:
- Two-step resolution: first find the library, then query its docs
- The docs returned are live and current, not from Claude's training data
- Context7 returns focused sections, not entire documentation sites, saving context tokens
Why This Beats Training Data
Claude's training data has a cutoff. Libraries ship new versions constantly. Context7 fetches the current documentation, so you get:
- Accurate API signatures (not deprecated ones)
- Current configuration formats
- Latest migration guides
- Recently added features
Pro Tip: Combine with /review
Add this to your /review skill instructions:
When reviewing code that uses third-party libraries, use Context7 to verify
that the API usage is current and follows recommended patterns.Now your code reviews catch outdated API usage automatically.
Demo 20: Playwright MCP — Browser Automation
The Problem
You've deployed your app. Is it actually working? Does the login flow complete? Are there console errors? You could open a browser and click through manually, or have Claude do it and report back with screenshots.
Set Up
# Install the MCP server
claude mcp add playwright -- npx -y @anthropic-ai/claude-code-playwright
# Install browser engines (first time only)
npx playwright install chromiumBuild a Test Page
mkdir -p ~/claude-demos/demo-20 && cd ~/claude-demos/demo-20
cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<title>Task Manager</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; }
h1 { margin-bottom: 20px; }
.add-form { display: flex; gap: 8px; margin-bottom: 20px; }
.add-form input { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; }
.add-form button { padding: 8px 16px; background: #2563eb; color: white; border: none; border-radius: 6px; cursor: pointer; }
.task { display: flex; align-items: center; gap: 8px; padding: 12px; border-bottom: 1px solid #eee; }
.task.done span { text-decoration: line-through; color: #999; }
.task input[type=checkbox] { width: 18px; height: 18px; }
.task span { flex: 1; }
.task button { background: none; border: none; color: #ef4444; cursor: pointer; font-size: 18px; }
#count { color: #666; margin-top: 12px; }
#error { color: #ef4444; margin-top: 8px; display: none; }
</style>
</head>
<body>
<h1>Task Manager</h1>
<div class="add-form">
<input type="text" id="taskInput" placeholder="Add a task..." />
<button onclick="addTask()">Add</button>
</div>
<div id="tasks"></div>
<div id="count"></div>
<div id="error"></div>
<script>
let tasks = [];
function addTask() {
const input = document.getElementById('taskInput');
const text = input.value.trim();
if (!text) {
document.getElementById('error').style.display = 'block';
document.getElementById('error').textContent = 'Task cannot be empty';
return;
}
document.getElementById('error').style.display = 'none';
tasks.push({ text, done: false });
input.value = '';
render();
}
function toggleTask(i) { tasks[i].done = !tasks[i].done; render(); }
function deleteTask(i) { tasks.splice(i, 1); render(); }
function render() {
const container = document.getElementById('tasks');
container.innerHTML = tasks.map((t, i) =>
`<div class="task ${t.done ? 'done' : ''}">
<input type="checkbox" ${t.done ? 'checked' : ''} onchange="toggleTask(${i})" />
<span>${t.text}</span>
<button onclick="deleteTask(${i})">x</button>
</div>`
).join('');
const remaining = tasks.filter(t => !t.done).length;
document.getElementById('count').textContent = `${remaining} task${remaining !== 1 ? 's' : ''} remaining`;
}
document.getElementById('taskInput').addEventListener('keypress', e => { if (e.key === 'Enter') addTask(); });
render();
</script>
</body>
</html>
EOFStart the server:
python -m http.server 8765 &Use It
claudeVisual smoke test:
Navigate to http://localhost:8765/ and take a screenshot. Tell me if the page
renders correctly and what UI elements you see.> Navigate to http://localhost:8765/ and take a screenshot.
● mcp__playwright__navigate
url: "http://localhost:8765/"
→ Page loaded (200 OK, 1.2s)
● mcp__playwright__screenshot
→ Screenshot captured (1280x720)
The page renders correctly. I can see:
- A heading: "Task Manager"
- An input field with placeholder "Add a task..."
- An "Add" button (blue, rounded)
- A counter showing "0 tasks remaining"
- No console errors detectedFunctional test:
On http://localhost:8765/, test the full task workflow:
1. Add a task called "Deploy to staging"
2. Add a task called "Write unit tests"
3. Check off "Deploy to staging"
4. Take a screenshot showing the current state
5. Tell me if the remaining count is correct> Test the full task workflow...
● mcp__playwright__fill
selector: "#taskInput"
value: "Deploy to staging"
● mcp__playwright__click
selector: ".add-form button"
→ Task added
● mcp__playwright__fill
selector: "#taskInput"
value: "Write unit tests"
● mcp__playwright__click
selector: ".add-form button"
→ Task added
● mcp__playwright__click
selector: ".task:first-child input[type=checkbox]"
→ Checkbox toggled
● mcp__playwright__screenshot
→ Screenshot captured (1280x720)
✔ Results:
- "Deploy to staging" shows with strikethrough (checked off)
- "Write unit tests" is unchecked
- Counter reads "1 task remaining" — correct
- All UI interactions worked as expectedError handling test:
Try to add an empty task (click Add without typing anything).
Take a screenshot — does it show an error message?Report:
Give me a QA report for http://localhost:8765/ covering:
- Does the page load correctly?
- Can you add tasks?
- Does checking/unchecking work?
- Does the counter update?
- Does error handling work for empty input?
Include screenshots for each test.What Just Happened?
Key takeaways:
- Claude controlled a real browser, not a simulation -- these are actual DOM interactions
- The screenshot gives Claude visual confirmation, not just DOM state
- Each Playwright action waited for the element to be ready before interacting
- The full workflow (navigate, fill, click, screenshot) mirrors what a QA engineer would do manually
Cleanup
kill %1 2>/dev/null # Stop the HTTP serverReal-World Applications
- Post-deploy verification: After
git push, have Claude check if your staging URL is up and working - Visual regression: Take screenshots before and after a CSS change, have Claude compare them
- E2E smoke tests: Run a full user flow (login, create item, verify) on your deployed app
- Competitive analysis: Navigate to a competitor's product and take notes on their UX patterns
ECC's /e2e-testing skill uses Playwright MCP with the Page Object Model pattern for structured, repeatable browser tests.
When Things Go Wrong
MCP Server Crash
The server process exits unexpectedly during a session:
> List open issues in myuser/web-app
● mcp__github__list_issues
⚠ Error: MCP server 'github' is not connected.
The server process exited with code 1.Diagnosis and fix:
# 1. Check if the server can start at all
npx -y @modelcontextprotocol/server-github 2>&1
# If you see "Error: GITHUB_PERSONAL_ACCESS_TOKEN is not set", the env var is missing
# 2. Verify the environment variable is exported
echo $GITHUB_PERSONAL_ACCESS_TOKEN
# Should print your token. If empty, re-export it:
export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_your_token_here
# 3. Restart Claude to reconnect all MCP servers
# Exit with Ctrl+C, then:
claudeMCP servers restart automatically when you start a new Claude session. If one crashes mid-session, exit and re-enter.
Token Expired or Revoked
GitHub returns 401 errors through the MCP server:
> Create an issue in myuser/web-app
● mcp__github__create_issue
⚠ Error: GitHub API returned 401 Unauthorized
{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"}Fix:
- Go to github.com/settings/tokens
- Check if your token is expired or revoked
- Generate a new token with the required permissions
- Update your shell profile and re-export:bash
export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_new_token_here - Restart Claude (MCP servers read env vars at startup)
Server Timeout
A slow network or overloaded API causes the MCP call to hang:
> Look up the Drizzle ORM docs for relations
● mcp__context7__resolve-library-id
libraryName: "drizzle-orm"
... (waiting)
⚠ Error: MCP call timed out after 30sCommon causes:
- Corporate proxy or VPN blocking outbound connections from
npxchild processes - The upstream API (GitHub, Context7) is experiencing downtime
- DNS resolution failure in restricted network environments
Fix:
# Test connectivity directly
curl -s https://api.github.com/rate_limit | head -5
# If behind a proxy, ensure npx inherits proxy settings
export HTTPS_PROXY=http://your-proxy:8080Context7 Missing Docs for a Library
Not all libraries are indexed. When Context7 cannot find documentation:
> Look up the docs for my-obscure-library
● mcp__context7__resolve-library-id
libraryName: "my-obscure-library"
→ No matching library found (confidence: 0.0)
I could not find documentation for "my-obscure-library" in Context7.
This library may not be indexed yet. I can try alternative approaches:
- Check the npm/PyPI registry for a README
- Read the source code directly if it is installed locally
- Search GitHub for the repositoryWorkaround: When Context7 does not have docs, Claude falls back to its training data or uses Bash to query package registries. You can also file a request to add the library to Context7's index.
Building Custom MCP Servers
When off-the-shelf servers don't cover your needs, build your own.
TypeScript (using the official SDK)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "internal-api", version: "1.0.0" });
server.tool(
"query_deployments",
"Get deployment status for a service",
{
service: z.string().describe("Service name, e.g. auth-api, web-app"),
environment: z.enum(["staging", "production"]).default("staging"),
},
async ({ service, environment }) => {
const res = await fetch(
`https://deploy.internal.co/api/status/${service}?env=${environment}`,
{ headers: { Authorization: `Bearer ${process.env.DEPLOY_TOKEN}` } }
);
const data = await res.json();
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);Register it:
claude mcp add internal-api -e DEPLOY_TOKEN -- npx tsx internal-mcp.tsPython (using FastMCP)
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("internal-api")
@mcp.tool()
def search_logs(service: str, query: str, hours: int = 1) -> str:
"""Search application logs by service and query string."""
# Your log search logic here
return f"Found 42 matches for '{query}' in {service} (last {hours}h)"
if __name__ == "__main__":
mcp.run(transport="stdio")claude mcp add internal-api -- python internal_mcp.pyDebugging MCP
When things don't work:
# 1. Check server list
claude mcp list
# 2. Test the server manually (see error output)
npx -y @modelcontextprotocol/server-github 2>&1
# 3. In a Claude session, check available tools
# Type: /tools
# 4. Force a tool call to test
# Ask: "Call mcp__github__list_issues for owner/repo"Common issues:
| Problem | Cause | Fix |
|---|---|---|
| Tools don't appear | Server didn't start | Check npx can run the package |
| "Permission denied" | Missing token | Verify environment variable is set |
| Timeout | Slow network/server | Check connectivity, increase timeout |
| "Server disconnected" | Process crashed | Check MCP logs in ~/.claude/logs/ |
Exercise: Multi-MCP Workflow
Set up GitHub + Context7 MCP servers, then run this workflow:
- Use GitHub MCP to list your repo's open issues
- Pick one that involves a library you use
- Use Context7 to look up the current docs for that library
- Have Claude propose a fix based on the current documentation
- Use GitHub MCP to create a branch and PR with the fix
This is the workflow ECC teams use daily — Claude as a full-loop development partner, from issue triage to PR creation.
Success Criteria
- [ ] Both MCP servers are configured and responding
- [ ] GitHub MCP can list issues and create PRs
- [ ] Context7 returns current documentation (not stale training data)
- [ ] Claude combines both sources to produce a real fix
- [ ] Tokens are stored in environment variables, not hardcoded
Knowledge Check
Summary
MCP turns Claude from a code editor into a connected development platform. The three servers in this chapter — GitHub, Context7, and Playwright — cover the most common integration needs.
Key points:
- Start with GitHub and Context7 — they provide the most value for the least setup
- Always use environment variables for tokens, never hardcode
- User scope for universal tools, project scope for team-shared configs
- Custom MCP servers take ~30 lines of TypeScript or Python
- MCP tools are hookable — use
mcp__github__.*matchers to audit API calls
Going deeper: See A06 MCP Protocol Deep Dive for the wire protocol details, capability negotiation, transport options (stdio vs SSE), and patterns for building production-grade MCP servers with error handling and retry logic.
Next: Chapter 8: Subagent Architecture — Build specialized agents for architecture analysis, security scanning, and test generation.