MCP Servers¶
Extend Claude Code with external capabilities using the Model Context Protocol.
What You'll Learn¶
- What MCP is and why it matters
- Installing MCP servers
- Using MCP tools
- Building custom MCP servers
What is MCP?¶
The Model Context Protocol (MCP) is an open standard that lets AI assistants connect to external services. With MCP, Claude can:
- Access databases directly
- Query APIs
- Control applications
- Use specialized tools
Think of MCP servers as plugins that give Claude new abilities.
How MCP Works¶
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Claude Code │────▶│ MCP Server │────▶│ External │
│ │◀────│ │◀────│ Service │
└─────────────┘ └─────────────┘ └─────────────┘
- Claude decides it needs an external capability
- It calls the MCP server's tool
- The server interacts with the external service
- Results flow back to Claude
Installing MCP Servers¶
Configuration Location¶
MCP servers are configured in:
- User: ~/.claude/settings.json
- Project: .claude/settings.json
Basic Configuration¶
With Environment Variables¶
{
"mcpServers": {
"database": {
"command": "npx",
"args": ["-y", "@example/mcp-postgres"],
"env": {
"DATABASE_URL": "postgresql://localhost/mydb"
}
}
}
}
Popular MCP Servers¶
File System¶
Read and write files outside the working directory:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-fs", "/path/to/allow"]
}
}
}
PostgreSQL¶
Query and modify PostgreSQL databases:
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-postgres"],
"env": {
"POSTGRES_CONNECTION_STRING": "postgresql://user:pass@host/db"
}
}
}
}
Web Browser¶
Fetch and interact with web pages:
{
"mcpServers": {
"browser": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-browser"]
}
}
}
GitHub¶
Access GitHub repos, issues, and PRs:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-github"],
"env": {
"GITHUB_TOKEN": "ghp_xxxxx"
}
}
}
}
Using MCP Tools¶
Once configured, MCP tools are available to Claude automatically. Tools appear with the mcp__ prefix:
Example Interactions¶
Database Query:
Claude uses mcp__postgres__query to run SQL and return results.
GitHub Issues:
Claude uses mcp__github__list_issues to fetch and summarize.
Web Research:
Claude uses browser tools to fetch and read the page.
Building Custom MCP Servers¶
You can create MCP servers for any service. Here's a simple example:
Basic Structure¶
// my-mcp-server/index.js
import { McpServer } from "@anthropic/mcp-sdk";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
// Define a tool
server.tool({
name: "get_weather",
description: "Get current weather for a city",
parameters: {
type: "object",
properties: {
city: { type: "string", description: "City name" }
},
required: ["city"]
},
handler: async ({ city }) => {
// Your implementation
const weather = await fetchWeather(city);
return { temperature: weather.temp, condition: weather.condition };
}
});
server.run();
Package Configuration¶
{
"name": "my-mcp-server",
"version": "1.0.0",
"type": "module",
"bin": "./index.js",
"dependencies": {
"@anthropic/mcp-sdk": "^0.1.0"
}
}
Register Your Server¶
MCP Server Patterns¶
Read-Only Services¶
For safety, start with read-only tools:
server.tool({
name: "get_user",
description: "Get user info by ID (read-only)",
handler: async ({ userId }) => {
return await db.users.findById(userId);
}
});
With Confirmation¶
For write operations, require confirmation:
server.tool({
name: "delete_user",
description: "Delete a user (requires confirmation)",
parameters: {
userId: { type: "string" },
confirmed: { type: "boolean" }
},
handler: async ({ userId, confirmed }) => {
if (!confirmed) {
return { error: "Please confirm deletion by setting confirmed: true" };
}
return await db.users.delete(userId);
}
});
With Rate Limiting¶
Protect against excessive calls:
const rateLimit = {};
server.tool({
name: "send_email",
handler: async ({ to, subject, body }) => {
const now = Date.now();
if (rateLimit[to] && now - rateLimit[to] < 60000) {
return { error: "Rate limited: wait 1 minute between emails" };
}
rateLimit[to] = now;
return await sendEmail(to, subject, body);
}
});
Debugging MCP Servers¶
Enable Logging¶
Set environment variable:
Test Standalone¶
Run your server directly to test:
Check Server Status¶
In Claude Code:
Security Considerations¶
Principle of Least Privilege¶
Only expose necessary capabilities:
// Don't do this
server.tool({
name: "run_sql",
handler: async ({ query }) => db.raw(query) // Dangerous!
});
// Do this instead
server.tool({
name: "get_user_count",
handler: async () => db.users.count() // Specific, safe
});
Validate Inputs¶
Always validate tool inputs:
server.tool({
name: "get_file",
handler: async ({ path }) => {
// Validate path is within allowed directory
const resolved = resolve(path);
if (!resolved.startsWith(ALLOWED_DIR)) {
throw new Error("Access denied");
}
return await readFile(resolved);
}
});
Environment Variables¶
Keep secrets in environment variables, not code:
{
"mcpServers": {
"my-api": {
"command": "node",
"args": ["server.js"],
"env": {
"API_KEY": "${MY_API_KEY}"
}
}
}
}
Try It Yourself¶
Exercise: Add a GitHub MCP Server¶
- Get a GitHub token from https://github.com/settings/tokens
- Add to your settings:
- Restart Claude Code
- Try:
Exercise: Build a Simple MCP Server¶
Create an MCP server that provides the current time in different timezones:
- Create the server structure
- Add a
get_timetool that accepts a timezone - Register it in your settings
- Test it with Claude
Expert MCP Patterns¶
Multi-Server Architecture¶
Configure multiple MCP servers for comprehensive coverage:
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-postgres"],
"env": { "POSTGRES_CONNECTION_STRING": "${DATABASE_URL}" }
},
"redis": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-redis"],
"env": { "REDIS_URL": "${REDIS_URL}" }
},
"github": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-github"],
"env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
},
"slack": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-slack"],
"env": { "SLACK_TOKEN": "${SLACK_TOKEN}" }
},
"n8n": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-n8n"],
"env": {
"N8N_API_URL": "${N8N_URL}",
"N8N_API_KEY": "${N8N_API_KEY}"
}
}
}
}
Development vs Production Config¶
// .claude/settings.json (project - development)
{
"mcpServers": {
"database": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-postgres"],
"env": {
"POSTGRES_CONNECTION_STRING": "postgresql://localhost/myapp_dev"
}
}
}
}
// ~/.claude/settings.json (user - production read-only)
{
"mcpServers": {
"prod-database-readonly": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-server-postgres", "--read-only"],
"env": {
"POSTGRES_CONNECTION_STRING": "${PROD_DATABASE_URL}"
}
}
}
}
Building Production-Grade MCP Servers¶
// production-mcp-server/index.js
import { McpServer } from "@anthropic/mcp-sdk";
import { createLogger } from './logger.js';
import { validateInput, sanitizeOutput } from './security.js';
import { metrics } from './metrics.js';
const logger = createLogger('mcp-server');
const server = new McpServer({
name: "production-server",
version: "1.0.0",
});
// Middleware pattern for all tools
const withMiddleware = (handler) => async (params) => {
const startTime = Date.now();
const requestId = crypto.randomUUID();
logger.info({ requestId, params }, 'Tool invoked');
try {
// Validate inputs
const validated = validateInput(params);
// Execute handler
const result = await handler(validated);
// Sanitize outputs (remove sensitive data)
const sanitized = sanitizeOutput(result);
// Record metrics
metrics.recordSuccess(handler.name, Date.now() - startTime);
logger.info({ requestId, duration: Date.now() - startTime }, 'Tool completed');
return sanitized;
} catch (error) {
metrics.recordError(handler.name, error);
logger.error({ requestId, error }, 'Tool failed');
throw error;
}
};
// Production-ready tool with full error handling
server.tool({
name: "get_user",
description: "Get user by ID with full profile",
parameters: {
type: "object",
properties: {
userId: {
type: "string",
description: "User UUID",
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
},
includeProfile: {
type: "boolean",
description: "Include extended profile data",
default: false
}
},
required: ["userId"]
},
handler: withMiddleware(async ({ userId, includeProfile }) => {
const user = await db.users.findById(userId);
if (!user) {
return { error: "User not found", code: "USER_NOT_FOUND" };
}
// Remove sensitive fields
const { passwordHash, twoFactorSecret, ...safeUser } = user;
if (includeProfile) {
safeUser.profile = await db.profiles.findByUserId(userId);
}
return safeUser;
})
});
// Health check tool
server.tool({
name: "health_check",
description: "Check server health and connectivity",
handler: async () => {
const checks = {
database: await checkDatabase(),
redis: await checkRedis(),
timestamp: new Date().toISOString()
};
return checks;
}
});
server.run();
MCP Server with Caching¶
import NodeCache from 'node-cache';
const cache = new NodeCache({
stdTTL: 300, // 5 minute default TTL
checkperiod: 60 // Check for expired keys every 60s
});
server.tool({
name: "get_analytics",
description: "Get analytics data (cached for 5 minutes)",
parameters: {
type: "object",
properties: {
metric: { type: "string" },
startDate: { type: "string" },
endDate: { type: "string" }
}
},
handler: async ({ metric, startDate, endDate }) => {
const cacheKey = `analytics:${metric}:${startDate}:${endDate}`;
// Check cache first
const cached = cache.get(cacheKey);
if (cached) {
return { ...cached, fromCache: true };
}
// Fetch from source
const data = await analyticsService.query(metric, startDate, endDate);
// Cache result
cache.set(cacheKey, data);
return { ...data, fromCache: false };
}
});
MCP for Internal APIs¶
// internal-api-mcp/index.js
// Connect Claude to your company's internal APIs
server.tool({
name: "jira_create_issue",
description: "Create a Jira issue",
parameters: {
type: "object",
properties: {
project: { type: "string", description: "Project key (e.g., PROJ)" },
summary: { type: "string", description: "Issue title" },
description: { type: "string", description: "Issue description" },
issueType: { type: "string", enum: ["Bug", "Task", "Story"] },
priority: { type: "string", enum: ["Low", "Medium", "High", "Critical"] }
},
required: ["project", "summary", "issueType"]
},
handler: async (params) => {
const response = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers: {
'Authorization': `Basic ${JIRA_AUTH}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
fields: {
project: { key: params.project },
summary: params.summary,
description: params.description,
issuetype: { name: params.issueType },
priority: { name: params.priority || 'Medium' }
}
})
});
const issue = await response.json();
return {
key: issue.key,
url: `${JIRA_URL}/browse/${issue.key}`
};
}
});
server.tool({
name: "confluence_search",
description: "Search Confluence documentation",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
space: { type: "string", description: "Space key to search in" }
},
required: ["query"]
},
handler: async ({ query, space }) => {
const cql = space
? `text ~ "${query}" AND space = "${space}"`
: `text ~ "${query}"`;
const response = await fetch(
`${CONFLUENCE_URL}/rest/api/content/search?cql=${encodeURIComponent(cql)}`,
{ headers: { 'Authorization': `Basic ${CONFLUENCE_AUTH}` }}
);
const results = await response.json();
return results.results.map(r => ({
title: r.title,
url: `${CONFLUENCE_URL}${r._links.webui}`,
excerpt: r.excerpt
}));
}
});
Testing MCP Servers¶
// mcp-server.test.js
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { spawn } from 'child_process';
describe('MCP Server', () => {
let serverProcess;
beforeAll(async () => {
serverProcess = spawn('node', ['index.js'], {
env: { ...process.env, TEST_MODE: 'true' }
});
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for startup
});
afterAll(() => {
serverProcess.kill();
});
it('should return user data for valid ID', async () => {
const response = await callMcpTool('get_user', {
userId: 'test-user-123'
});
expect(response.email).toBeDefined();
expect(response.passwordHash).toBeUndefined(); // Should be sanitized
});
it('should handle invalid user ID', async () => {
const response = await callMcpTool('get_user', {
userId: 'invalid-id'
});
expect(response.error).toBe('User not found');
});
it('should respect rate limits', async () => {
// Make rapid requests
const promises = Array(10).fill().map(() =>
callMcpTool('send_notification', { message: 'test' })
);
const results = await Promise.all(promises);
const rateLimited = results.filter(r => r.error?.includes('rate'));
expect(rateLimited.length).toBeGreaterThan(0);
});
});
What's Next?¶
Learn about multi-agent workflows in 04-multi-agent.
For even deeper MCP integration with workflow automation, see the expert tutorial on n8n Integration.
Summary:
- MCP extends Claude CLI with external service access
- Configure servers in .claude/settings.json
- Popular servers: filesystem, database, GitHub, browser
- Build custom servers with the MCP SDK
- Production patterns: caching, logging, metrics, validation
- Security: validate inputs, limit privileges, protect secrets
- Test MCP servers like any other code
Learning Resources¶
Featured Video¶
Fireship: Model Context Protocol Explained (3M+ subscribers, 500K+ views)
Comprehensive MCP tutorial - extend Claude with databases, APIs, and external services.
Additional Resources¶
| Type | Resource | Description |
|---|---|---|
| 🎬 Video | MCP Workflows Deep Dive | All About AI - Advanced MCP patterns |
| 📚 Official Docs | MCP Specification | Official protocol documentation |
| 📖 Tutorial | Hugging Face MCP Course | Free MCP course (Anthropic partnership) |
| 🎓 Free Course | MCP Servers Repo | Official reference implementations |
| 💼 Commercial | Cursor MCP Course | MCP integration patterns |