Model Context Protocol (MCP): The Complete Developer Guide 2026
Everything you need to know about Model Context Protocol (MCP): what it is, how it works, building MCP servers from scratch, integrating with Claude Code and other LLM clients, and production best practices.
Model Context Protocol (MCP): The Complete Developer Guide 2026
Model Context Protocol (MCP) has become the USB standard of the AI world. Before MCP, every AI tool needed custom integrations β each IDE plugin, each agent framework, each LLM provider had its own way of connecting to databases, APIs, and file systems. MCP standardizes how LLMs connect to external context and tools.
This guide covers everything: what MCP is, how it works under the hood, building your first MCP server, integrating with Claude Code, and patterns for production use.
What Is MCP?
MCP is an open protocol (created by Anthropic, now community-maintained) that defines a standard communication layer between:
- MCP Hosts β LLM applications (Claude Code, Claude Desktop, custom agents)
- MCP Clients β built into hosts, manage connections to servers
- MCP Servers β expose tools, resources, and prompts to LLMs
βββββββββββββββββββββββ MCP Protocol βββββββββββββββββββββββ
β MCP Host β βββββββββββββββββββββ β MCP Server β
β (Claude Code, β βββββββββββββββββββββββΊ β (your integration) β
β Claude Desktop) β β β
βββββββββββββββββββββββ ββββββββββββ¬βββββββββββ
β
ββββββββββββΌβββββββββββ
β External Service β
β (DB, API, FS, etc) β
βββββββββββββββββββββββ
The Three Primitives
MCP servers expose three types of capabilities:
- Tools β functions the LLM can call (like function calling / tool use)
- Resources β data the LLM can read (files, database records, API responses)
- Prompts β reusable prompt templates with parameters
How MCP Works Under the Hood
MCP uses JSON-RPC 2.0 over one of several transports:
- stdio β subprocess stdin/stdout (most common for local servers)
- HTTP + SSE β for remote servers (Server-Sent Events for serverβclient messages)
- WebSocket β bidirectional streaming
Connection Lifecycle
Client Server
β β
βββββ initialize ββββββββββββββΊβ
βββββ initialized βββββββββββββ
β β
βββββ tools/list ββββββββββββββΊβ
βββββ [tool definitions] ββββββ
β β
βββββ tools/call ββββββββββββββΊβ (when LLM wants to use a tool)
βββββ tool result βββββββββββββ
β β
βββββ resources/list ββββββββββΊβ
βββββ [resource list] βββββββββ
β β
βββββ resources/read ββββββββββΊβ
βββββ resource content ββββββββ
Message Format
// Request (client β server)
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_database",
"arguments": {
"query": "latest sales data",
"limit": 10
}
}
}
// Response (server β client)
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "Found 10 records: ..."
}
]
}
}
Building Your First MCP Server
Option 1: Python SDK
pip install mcp
# server.py
import asyncio
from mcp.server import Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from mcp import types
# Create server instance
app = Server("my-mcp-server")
@app.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""Return available tools."""
return [
types.Tool(
name="get_weather",
description="Get current weather for a city",
inputSchema={
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name"
}
},
"required": ["city"]
}
),
types.Tool(
name="search_docs",
description="Search internal documentation",
inputSchema={
"type": "object",
"properties": {
"query": {"type": "string"},
"limit": {"type": "integer", "default": 5}
},
"required": ["query"]
}
)
]
@app.call_tool()
async def handle_call_tool(
name: str,
arguments: dict
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""Handle tool execution."""
if name == "get_weather":
city = arguments.get("city")
# Call actual weather API here
weather_data = await fetch_weather(city)
return [types.TextContent(
type="text",
text=f"Weather in {city}: {weather_data['temp']}Β°C, {weather_data['condition']}"
)]
elif name == "search_docs":
query = arguments.get("query")
limit = arguments.get("limit", 5)
results = await search_documentation(query, limit)
return [types.TextContent(
type="text",
text="\n".join([f"- {r['title']}: {r['snippet']}" for r in results])
)]
raise ValueError(f"Unknown tool: {name}")
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
InitializationOptions(
server_name="my-mcp-server",
server_version="0.1.0",
capabilities=app.get_capabilities(
notification_options=None,
experimental_capabilities={}
)
)
)
if __name__ == "__main__":
asyncio.run(main())
Option 2: TypeScript SDK
npm install @modelcontextprotocol/sdk
// server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{
name: "my-mcp-server",
version: "0.1.0",
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// List tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "query_database",
description: "Execute a read-only SQL query",
inputSchema: {
type: "object",
properties: {
sql: {
type: "string",
description: "SQL SELECT query to execute",
},
},
required: ["sql"],
},
} satisfies Tool,
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "query_database") {
const { sql } = args as { sql: string };
// Validate it's read-only
if (!sql.trim().toUpperCase().startsWith("SELECT")) {
throw new Error("Only SELECT queries are allowed");
}
const results = await db.query(sql);
return {
content: [
{
type: "text",
text: JSON.stringify(results, null, 2),
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
});
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
Adding Resources to Your MCP Server
Resources let LLMs read data without βcallingβ anything β like a file system or database view.
@app.list_resources()
async def handle_list_resources() -> list[types.Resource]:
return [
types.Resource(
uri="file:///project/docs",
name="Project Documentation",
description="All project documentation files",
mimeType="text/markdown"
),
types.Resource(
uri="db://users/recent",
name="Recent Users",
description="Last 100 registered users",
mimeType="application/json"
)
]
@app.read_resource()
async def handle_read_resource(uri: str) -> str:
if uri == "file:///project/docs":
docs = load_all_docs("/project/docs")
return "\n\n".join(docs)
elif uri == "db://users/recent":
users = await db.query("SELECT * FROM users ORDER BY created_at DESC LIMIT 100")
return json.dumps(users)
raise ValueError(f"Unknown resource: {uri}")
Integrating MCP with Claude Code
Claude Code (the Anthropic CLI) has native MCP support. Configure servers in ~/.claude/settings.json:
{
"mcpServers": {
"my-server": {
"command": "python",
"args": ["/path/to/server.py"],
"env": {
"DATABASE_URL": "postgresql://localhost/mydb",
"API_KEY": "your-api-key"
}
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..."
}
}
}
}
After configuration, Claude Code automatically connects to your servers at startup. You can then reference tools and resources in conversation.
Remote MCP Server (HTTP + SSE)
For hosted/shared servers:
{
"mcpServers": {
"remote-analytics": {
"url": "https://mcp.yourcompany.com/analytics",
"headers": {
"Authorization": "Bearer ${ANALYTICS_TOKEN}"
}
}
}
}
Building a Production MCP Server
Hereβs a complete example: a database MCP server with proper error handling, connection pooling, and security:
# production_db_server.py
import asyncio
import os
import logging
from contextlib import asynccontextmanager
from typing import Any
import asyncpg
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.server.models import InitializationOptions
from mcp import types
logger = logging.getLogger(__name__)
# Connection pool (initialized at startup)
pool: asyncpg.Pool | None = None
async def get_pool() -> asyncpg.Pool:
global pool
if pool is None:
pool = await asyncpg.create_pool(
os.environ["DATABASE_URL"],
min_size=2,
max_size=10,
command_timeout=30
)
return pool
app = Server("production-db-server")
ALLOWED_TABLES = {"products", "orders", "customers", "analytics"} # allowlist
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="query_table",
description="Query a database table with filters",
inputSchema={
"type": "object",
"properties": {
"table": {
"type": "string",
"enum": list(ALLOWED_TABLES),
"description": "Table to query"
},
"filters": {
"type": "object",
"description": "Key-value filters (WHERE clause)"
},
"limit": {
"type": "integer",
"default": 20,
"maximum": 100
}
},
"required": ["table"]
}
),
types.Tool(
name="get_schema",
description="Get the schema for a database table",
inputSchema={
"type": "object",
"properties": {
"table": {
"type": "string",
"enum": list(ALLOWED_TABLES)
}
},
"required": ["table"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
db = await get_pool()
if name == "query_table":
table = arguments["table"]
# Security: validate table is in allowlist
if table not in ALLOWED_TABLES:
raise ValueError(f"Table '{table}' not allowed")
filters = arguments.get("filters", {})
limit = min(arguments.get("limit", 20), 100)
# Build parameterized query (safe from SQL injection)
where_clauses = []
params = []
for i, (col, val) in enumerate(filters.items(), 1):
# Validate column name (alphanumeric + underscore only)
if not col.replace("_", "").isalnum():
raise ValueError(f"Invalid column name: {col}")
where_clauses.append(f"{col} = ${i}")
params.append(val)
where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
query = f"SELECT * FROM {table} {where_sql} LIMIT ${len(params)+1}"
params.append(limit)
async with db.acquire() as conn:
rows = await conn.fetch(query, *params)
result = [dict(row) for row in rows]
return [types.TextContent(
type="text",
text=f"Query returned {len(result)} rows:\n{json.dumps(result, indent=2, default=str)}"
)]
elif name == "get_schema":
table = arguments["table"]
if table not in ALLOWED_TABLES:
raise ValueError(f"Table '{table}' not allowed")
async with db.acquire() as conn:
columns = await conn.fetch(
"SELECT column_name, data_type, is_nullable "
"FROM information_schema.columns "
"WHERE table_name = $1 ORDER BY ordinal_position",
table
)
schema = [dict(col) for col in columns]
return [types.TextContent(
type="text",
text=f"Schema for {table}:\n{json.dumps(schema, indent=2)}"
)]
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
InitializationOptions(
server_name="production-db-server",
server_version="1.0.0",
capabilities=app.get_capabilities(
notification_options=None,
experimental_capabilities={}
)
)
)
if __name__ == "__main__":
asyncio.run(main())
Popular Pre-Built MCP Servers
You donβt always need to build from scratch. The MCP ecosystem has grown rapidly:
| Server | What it does | Install |
|---|---|---|
@modelcontextprotocol/server-filesystem | Read/write local files | npx -y @mcp/server-filesystem |
@modelcontextprotocol/server-github | GitHub repos, issues, PRs | npx -y @mcp/server-github |
@modelcontextprotocol/server-postgres | PostgreSQL queries | npx -y @mcp/server-postgres |
@modelcontextprotocol/server-brave-search | Brave web search | npx -y @mcp/server-brave-search |
@modelcontextprotocol/server-puppeteer | Browser automation | npx -y @mcp/server-puppeteer |
mcp-server-sqlite | SQLite database | uvx mcp-server-sqlite |
mcp-server-git | Git operations | uvx mcp-server-git |
@anthropic/mcp-server-bash | Execute bash commands | npx -y @anthropic/mcp-server-bash |
MCP Security Best Practices
MCP servers run with whatever permissions the host process has. Security matters:
- Input validation β validate all tool arguments before executing
- Allowlists β restrict which tables/files/operations are accessible
- Parameterized queries β never build SQL/commands via string concatenation
- Read-only by default β only expose write operations when explicitly needed
- Rate limiting β throttle expensive operations
- Audit logging β log all tool calls for security review
- Secrets in env vars β never hardcode credentials in server code
- Scope isolation β each server should have minimal required permissions
Testing Your MCP Server
Use the official MCP Inspector:
npx @modelcontextprotocol/inspector python server.py
This opens a web UI where you can:
- Browse available tools and resources
- Call tools with custom arguments
- Inspect request/response messages
- Debug connection issues
For automated testing:
# test_server.py
import pytest
import asyncio
from mcp.client.session import ClientSession
from mcp.client.stdio import stdio_client
@pytest.mark.asyncio
async def test_tools_available():
async with stdio_client(["python", "server.py"]) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
tool_names = [t.name for t in tools.tools]
assert "get_weather" in tool_names
assert "search_docs" in tool_names
@pytest.mark.asyncio
async def test_tool_call():
async with stdio_client(["python", "server.py"]) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("get_weather", {"city": "Tokyo"})
assert len(result.content) > 0
assert "Tokyo" in result.content[0].text
DevPlaybook Tools for MCP Development
When building MCP servers, these DevPlaybook tools are handy:
- JSON Schema Validator β validate your tool
inputSchemadefinitions - JSON Formatter β pretty-print MCP request/response payloads during debugging
- Base64 Encoder β encode binary data for MCP resource content
- Regex Tester β test patterns for parsing tool arguments
The MCP Ecosystem in 2026
MCP has seen explosive adoption:
- Claude Desktop and Claude Code ship with native MCP support
- Continue.dev, Cursor, and Zed have added MCP support
- AWS, Google Cloud, and Microsoft Azure all ship MCP servers for their services
- The official MCP registry lists 500+ community servers
- OpenAI announced MCP compatibility in early 2026
The protocol is also evolving:
- MCP 1.1 added OAuth 2.0 support for remote server authentication
- Batch operations β call multiple tools in a single round-trip
- Streaming resources β push updates to clients in real-time
- Agent-to-agent MCP β agents can expose their capabilities as MCP servers to other agents
Summary
MCP solves the NΓM integration problem in AI tooling. Instead of every LLM client needing custom integrations with every data source, MCP provides a standard protocol that any client can speak and any server can implement.
For developers, this means:
- Build once, use everywhere β one MCP server works with Claude Code, Claude Desktop, and any future MCP-compatible tool
- Standard security model β the protocol defines permission boundaries
- Rich tooling β inspector, SDKs for Python/TypeScript/Rust/Go, 500+ community servers
Whether youβre connecting an LLM to your database, internal wiki, SaaS APIs, or file system β MCP is the standard way to do it in 2026.
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links β we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.