MCP Deep Dive: Model Context Protocol Explained for Developers
Understand the Model Context Protocol (MCP), how it extends Claude Code with external tools, and when to use MCP vs skills for your workflows.
MCP Deep Dive: Model Context Protocol Explained for Developers
The Model Context Protocol (MCP) represents a fundamental shift in how AI assistants interact with external systems. While Claude Code's built-in skills provide powerful text-based customization, MCP opens the door to real tool integration—databases, APIs, file systems, and any service you can imagine.
This guide takes you from MCP fundamentals to building your own MCP servers, with practical examples and clear guidance on when to use MCP versus traditional skills.
What Is the Model Context Protocol?
MCP is an open standard that defines how AI models communicate with external tools and data sources. Think of it as a universal adapter between AI assistants and the tools developers use every day.
Before MCP, connecting an AI to a database required custom integration code. With MCP, you implement a standard interface once, and any MCP-compatible AI can use it.
The Architecture
MCP uses a client-server model:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Claude Code │────▶│ MCP Client │────▶│ MCP Server │
│ (AI Model) │◀────│ (Built-in) │◀────│ (Your Tool) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ External Data │
│ (DB, API, etc) │
└─────────────────┘
Claude Code is the AI that wants to use tools. MCP Client (built into Claude Code) speaks the MCP protocol. MCP Server (your code) translates MCP requests into actual tool operations.
Core Concepts
Tools: Actions the AI can take (query a database, send a message, create a file).
Resources: Data sources the AI can read (files, database records, API responses).
Prompts: Reusable prompt templates that can be parameterized.
Why MCP Matters
Before MCP
Every AI tool integration was custom:
- Cursor had its codebase integration
- GitHub Copilot had its own file reading
- Claude Code had separate tool implementations
Developers building tools had to integrate with each AI separately.
With MCP
One implementation, universal compatibility:
- Build an MCP server once
- Works with Claude Code, Claude Desktop, and any MCP-compatible AI
- Community servers provide instant functionality
The Ecosystem Effect
MCP creates a marketplace of tools:
- Database connectors (PostgreSQL, MySQL, MongoDB)
- API integrations (GitHub, Slack, Linear)
- File system tools (advanced file operations)
- Development tools (Docker, Kubernetes, AWS)
Instead of building integrations, you compose existing servers.
MCP vs Skills: When to Use Which
This is the critical question. Both extend Claude Code, but they serve different purposes.
Use Skills When:
-
You're providing context or instructions
- Coding standards
- Project architecture knowledge
- Workflow documentation
-
The task is text-based
- Code review guidelines
- Documentation templates
- Commit message formats
-
No external system interaction is needed
- Everything happens within Claude Code's existing capabilities
-
You want simplicity
- Skills are Markdown files
- No code to write or servers to run
Use MCP When:
-
You need to interact with external systems
- Databases
- APIs
- File systems beyond basic read/write
-
You need real-time data
- Current system state
- Live API responses
- Database queries
-
The operation requires specific tooling
- Docker commands
- Cloud provider APIs
- Custom business logic
-
You want structured input/output
- MCP tools have defined schemas
- Type-safe parameters and responses
The Decision Matrix
| Need | Use Skills | Use MCP |
|---|---|---|
| Coding standards | Yes | No |
| Database queries | No | Yes |
| API integrations | No | Yes |
| Workflow instructions | Yes | No |
| File templates | Yes | No |
| System monitoring | No | Yes |
| Code generation rules | Yes | No |
| Cloud deployments | No | Yes |
Combining Both
The most powerful setups use both:
# Skill: Database Operations Guide
When working with the database:
1. Use the `postgres` MCP server for queries
2. Always run SELECT before UPDATE/DELETE
3. Use transactions for multi-step operations
## Available MCP Tools
- `postgres.query`: Run SQL queries
- `postgres.list_tables`: See available tables
- `postgres.describe_table`: Get table schema
The skill provides context; MCP provides the actual tools.
Setting Up MCP in Claude Code
Configuration File
MCP servers are configured in your Claude Code settings. Create or edit ~/.claude/settings.json:
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"DATABASE_URL": "postgresql://user:pass@localhost:5432/mydb"
}
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "ghp_xxxxxxxxxxxx"
}
}
}
}
Project-Specific Configuration
For project-specific servers, create .claude/mcp.json:
{
"servers": {
"project-api": {
"command": "node",
"args": ["./scripts/mcp-server.js"],
"env": {
"API_KEY": "${PROJECT_API_KEY}"
}
}
}
}
Verifying Installation
After configuring, restart Claude Code and check available tools:
> What MCP tools do you have available?
I have access to the following MCP tools:
- postgres.query: Execute SQL queries
- postgres.list_tables: List database tables
- github.create_issue: Create GitHub issues
- github.list_repos: List repositories
Building Your First MCP Server
Let's build a practical MCP server: a task management tool that integrates with a local JSON file.
Step 1: Project Setup
mkdir mcp-tasks
cd mcp-tasks
npm init -y
npm install @modelcontextprotocol/sdk zod
Step 2: Server Implementation
Create server.js:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fs from "fs/promises";
import path from "path";
const TASKS_FILE = process.env.TASKS_FILE || "./tasks.json";
// Initialize tasks file if it doesn't exist
async function initTasksFile() {
try {
await fs.access(TASKS_FILE);
} catch {
await fs.writeFile(TASKS_FILE, JSON.stringify({ tasks: [] }, null, 2));
}
}
async function readTasks() {
const data = await fs.readFile(TASKS_FILE, "utf-8");
return JSON.parse(data);
}
async function writeTasks(data) {
await fs.writeFile(TASKS_FILE, JSON.stringify(data, null, 2));
}
// Create MCP server
const server = new Server(
{
name: "tasks",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Define available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_tasks",
description: "List all tasks, optionally filtered by status",
inputSchema: {
type: "object",
properties: {
status: {
type: "string",
enum: ["pending", "in_progress", "completed"],
description: "Filter by task status",
},
},
},
},
{
name: "add_task",
description: "Add a new task",
inputSchema: {
type: "object",
properties: {
title: {
type: "string",
description: "Task title",
},
description: {
type: "string",
description: "Task description",
},
priority: {
type: "string",
enum: ["low", "medium", "high"],
description: "Task priority",
},
},
required: ["title"],
},
},
{
name: "update_task",
description: "Update a task's status",
inputSchema: {
type: "object",
properties: {
id: {
type: "number",
description: "Task ID",
},
status: {
type: "string",
enum: ["pending", "in_progress", "completed"],
description: "New status",
},
},
required: ["id", "status"],
},
},
{
name: "delete_task",
description: "Delete a task",
inputSchema: {
type: "object",
properties: {
id: {
type: "number",
description: "Task ID",
},
},
required: ["id"],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
await initTasksFile();
const data = await readTasks();
switch (name) {
case "list_tasks": {
let tasks = data.tasks;
if (args?.status) {
tasks = tasks.filter((t) => t.status === args.status);
}
return {
content: [
{
type: "text",
text: JSON.stringify(tasks, null, 2),
},
],
};
}
case "add_task": {
const newTask = {
id: data.tasks.length + 1,
title: args.title,
description: args.description || "",
priority: args.priority || "medium",
status: "pending",
createdAt: new Date().toISOString(),
};
data.tasks.push(newTask);
await writeTasks(data);
return {
content: [
{
type: "text",
text: `Created task #${newTask.id}: ${newTask.title}`,
},
],
};
}
case "update_task": {
const task = data.tasks.find((t) => t.id === args.id);
if (!task) {
return {
content: [{ type: "text", text: `Task #${args.id} not found` }],
};
}
task.status = args.status;
task.updatedAt = new Date().toISOString();
await writeTasks(data);
return {
content: [
{
type: "text",
text: `Updated task #${args.id} to ${args.status}`,
},
],
};
}
case "delete_task": {
const index = data.tasks.findIndex((t) => t.id === args.id);
if (index === -1) {
return {
content: [{ type: "text", text: `Task #${args.id} not found` }],
};
}
data.tasks.splice(index, 1);
await writeTasks(data);
return {
content: [{ type: "text", text: `Deleted task #${args.id}` }],
};
}
default:
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
};
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
Step 3: Package Configuration
Update package.json:
{
"name": "mcp-tasks",
"version": "1.0.0",
"type": "module",
"main": "server.js",
"bin": {
"mcp-tasks": "./server.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.22.0"
}
}
Step 4: Configure Claude Code
Add to your MCP configuration:
{
"mcpServers": {
"tasks": {
"command": "node",
"args": ["/path/to/mcp-tasks/server.js"],
"env": {
"TASKS_FILE": "/path/to/tasks.json"
}
}
}
}
Step 5: Use It
> Add a task to review the API documentation with high priority
I'll add that task for you.
Created task #1: Review API documentation
> List all pending tasks
Here are your pending tasks:
[
{
"id": 1,
"title": "Review API documentation",
"description": "",
"priority": "high",
"status": "pending",
"createdAt": "2025-01-16T10:30:00.000Z"
}
]
> Update task 1 to in_progress
Updated task #1 to in_progress
Advanced MCP Patterns
Resource Providers
Beyond tools, MCP servers can expose resources—data that Claude can read:
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "tasks://all",
name: "All Tasks",
description: "Complete list of all tasks",
mimeType: "application/json",
},
{
uri: "tasks://summary",
name: "Task Summary",
description: "Summary statistics of tasks",
mimeType: "text/plain",
},
],
};
});
Prompt Templates
MCP servers can provide reusable prompts:
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "daily_standup",
description: "Generate a daily standup update",
arguments: [
{
name: "date",
description: "The date for the standup",
required: false,
},
],
},
],
};
});
Error Handling
Robust error handling is essential:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
// Tool implementation
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
Popular MCP Servers
Database Servers
PostgreSQL Server
{
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"DATABASE_URL": "postgresql://..."
}
}
}
SQLite Server
{
"sqlite": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sqlite"],
"env": {
"SQLITE_PATH": "./database.db"
}
}
}
Development Tools
GitHub Server
{
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "ghp_..."
}
}
}
Filesystem Server
{
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"]
}
}
Cloud Providers
AWS Server
{
"aws": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-aws"],
"env": {
"AWS_PROFILE": "default"
}
}
}
Security Considerations
Environment Variables
Never hardcode secrets. Use environment variables:
{
"servers": {
"api": {
"command": "node",
"args": ["server.js"],
"env": {
"API_KEY": "${API_KEY}"
}
}
}
}
Permission Scoping
Limit what MCP servers can access:
// Only allow read operations
if (request.params.name.includes("delete")) {
return {
content: [{ type: "text", text: "Delete operations are disabled" }],
isError: true,
};
}
Input Validation
Always validate inputs:
import { z } from "zod";
const AddTaskSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(1000).optional(),
priority: z.enum(["low", "medium", "high"]).optional(),
});
// In handler:
const validated = AddTaskSchema.parse(args);
Debugging MCP Servers
Logging
Add logging to your server:
const debug = process.env.DEBUG === "true";
function log(...args) {
if (debug) {
console.error("[MCP]", ...args);
}
}
log("Server starting...");
Testing Standalone
Test your server without Claude Code:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node server.js
Common Issues
Server not starting:
- Check the command path is correct
- Verify Node.js version compatibility
- Check for missing dependencies
Tools not appearing:
- Restart Claude Code after config changes
- Check JSON syntax in configuration
- Verify server starts without errors
Tool calls failing:
- Add logging to see what's received
- Check input schema matches what Claude sends
- Verify external service connectivity
Conclusion
MCP transforms Claude Code from a text-based assistant into a true development platform. By connecting to databases, APIs, and custom tools, you can build workflows that were previously impossible.
The key insight: skills provide knowledge and context; MCP provides capabilities and actions. Use both together for the most powerful setups.
Start with existing MCP servers to understand the patterns, then build your own for custom integrations. The protocol is simple, the SDK is well-documented, and the ecosystem is growing rapidly.
Want to create simpler text-based customizations? Check out our Complete Guide to Claude Code Skills for skill development patterns.