MCP (Model Context Protocol) Complete Guide: The Standard Connecting AI to Your Tools
Every AI application needs to talk to external systems — databases, APIs, file systems, search engines. Before 2025, every integration was custom: write a LangChain tool, build a custom function call, hack together a plugin. It was like the pre-USB era where every device had its own cable.
MCP (Model Context Protocol) changes that. It's an open protocol that standardizes how AI models connect to external tools and data sources — one protocol, any LLM, any tool.
What Is MCP?
MCP is an open standard (originally developed by Anthropic) that defines how AI applications discover and call external tools. Think of it as **USB-C for AI integrations** — a universal connector that any MCP-compatible client can use to talk to any MCP-compatible server.
┌─────────────────┐ ┌────────────────┐ ┌─────────────────┐
│ MCP Client │◄───►│ MCP Protocol │◄───►│ MCP Server │
│ (Claude, VS │ │ (JSON-RPC │ │ (DB connector, │
│ Code, Cursor) │ │ over stdio │ │ file system, │
│ │ │ or SSE) │ │ search, API) │
└─────────────────┘ └────────────────┘ └─────────────────┘
**The key insight**: MCP decouples tool implementation from the AI client. A single MCP server written once works with Claude Desktop, VS Code extensions, Cursor, and any other MCP-compatible client. No more "write this tool for LangChain, rewrite it for OpenAI, rewrite it for Claude."
Architecture
MCP has four core concepts:
1. Host (Client Application)
The AI-powered application the user interacts with: Claude Desktop, VS Code, Cursor, a custom chat app.
2. Client (MCP Client)
A lightweight transport layer inside the host that manages connections to MCP servers. It speaks the MCP protocol over the wire.
3. Server (MCP Server)
A lightweight service that exposes resources and tools through MCP. Each server is focused on one domain: filesystem access, database queries, API integrations.
4. Protocol
The standard JSON-RPC message format that clients and servers use to communicate. Two transport modes:
Communication Flow
User: "Find all bugs assigned to me"
│
▼
Host (Claude Desktop)
│ MCP Client asks servers for available tools
▼
MCP Server (GitHub) ───► Returns: list_issues, get_repo, search_code
│
▼
LLM decides: call list_issues with assignee:me
│
▼
MCP Server executes GitHub API call
│
▼
Returns issue data to LLM
│
▼
Host shows: "Found 3 bugs assigned to you:..."
MCP vs Alternatives
| Feature | MCP | OpenAI Function Calling | LangChain Tools | Custom API |
|---------|-----|------------------------|-----------------|------------|
| **Standardized** | ✅ Open standard | ❌ OpenAI-only | ❌ LangChain-only | ❌ Custom |
| **Cross-client** | ✅ Any MCP client | ❌ OpenAI API only | ❌ LangChain only | ❌ One app only |
| **Tool discovery** | ✅ Auto-discover tools | ✅ Schema-based | ✅ Defined in code | ❌ Hard-coded |
| **Dynamic resources** | ✅ Yes (file system, DB) | ❌ No | ❌ No | ❌ No |
| **Security model** | ✅ Host controls permissions | ⚠️ Per-call | ⚠️ Per-call | ⚠️ Per-call |
| **Async/long-running** | ✅ Notifications, progress | ❌ Request-response only | ❌ Request-response only | ⚠️ Depends |
| **Ecosystem** | Growing fast | Mature but narrow | Good | None |
| **Setup complexity** | Low | Low | Medium | High |
Why MCP Wins
**Before MCP**: Every AI tool integration was bespoke
**With MCP**: Write once, use everywhere
How to Build an MCP Server
The MCP SDK (available for Python, TypeScript, Java, and Go) makes building a server trivial. Here's a complete example in Python:
Python MCP Server
# server.py
from mcp.server import Server
from mcp.types import Tool, TextContent
import httpx
import json
app = Server("weather-server")
@app.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="get_weather",
description="Get current weather for a city",
input_schema={
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"],
},
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "get_weather":
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://api.openweathermap.org/data/2.5/weather",
params={"q": arguments["city"], "appid": "YOUR_KEY"}
)
data = resp.json()
return [TextContent(
type="text",
text=json.dumps(data["main"], indent=2)
)]
raise ValueError(f"Unknown tool: {name}")
if __name__ == "__main__":
app.run(transport="stdio")
Running the Server
// claude_desktop_config.json
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["/path/to/server.py"]
}
}
}
Now your weather tool works in Claude Desktop, VS Code (with Cline or Continue extension), Cursor, and any other MCP host — with zero changes.
TypeScript MCP Server
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{ name: "github-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: "search_repos",
description: "Search GitHub repositories",
inputSchema: {
type: "object",
properties: {
query: { type: "string" }
},
required: ["query"]
}
}]
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const response = await fetch(
`https://api.github.com/search/repositories?q=${args.query}`
);
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data.items.slice(0, 5)) }]
};
});
const transport = new StdioServerTransport();
await server.connect(transport);
MCP Resources: Beyond Tools
MCP isn't just about calling tools — it also defines **resources** (data sources the server exposes). This is where MCP surpasses simple function calling.
@app.list_resources()
async def list_resources() -> list[Resource]:
return [
Resource(
uri="file:///logs/app.log",
name="Application Logs",
description="Live application log file",
mime_type="text/plain",
),
Resource(
uri="postgres://users",
name="Users Table",
description="User database table schema and sample rows",
mime_type="application/json",
),
]
@app.read_resource()
async def read_resource(uri: str) -> str:
if uri == "file:///logs/app.log":
return open("/var/log/app.log").read()[-10000:]
if uri.startswith("postgres://"):
# Query schema dynamically
return json.dumps(query_table_schema(uri))
raise ValueError(f"Unknown resource: {uri}")
Resources give the AI client dynamic access to your data — it can read log files, query database schemas, browse file systems — all through the same protocol.
MCP Prompts (Templates)
MCP servers can also expose **prompt templates** — reusable instructions that guide how the LLM should behave when using your tools:
@app.list_prompts()
async def list_prompts() -> list[Prompt]:
return [
Prompt(
name="analyze_logs",
description="Analyze application logs for errors",
arguments=[{
"name": "severity",
"description": "Minimum log severity level",
"required": False,
}],
)
]
@app.get_prompt()
async def get_prompt(name: str, arguments: dict) -> PromptMessage:
severity = arguments.get("severity", "ERROR")
return PromptMessage(
role="user",
content={
"type": "text",
"text": f"""Read the application logs and:
1. Find all {severity} level entries
2. Group them by component
3. Identify patterns
4. Suggest fixes
Use the log resource and tools to investigate."""
}
)
Production MCP Server Patterns
1. Authentication and Credentials
MCP servers should never hard-code credentials. Use environment variables or a config file:
import os
DB_URL = os.environ.get("MCP_DB_URL", "postgresql://localhost:5432")
API_KEY = os.environ.get("MCP_API_KEY")
app = Server("production-server")
2. Rate Limiting
Protect external APIs from aggressive tool calls:
import time
from collections import defaultdict
class RateLimiter:
def __init__(self, max_calls: int, window: float):
self.max_calls = max_calls
self.window = window
self.calls = defaultdict(list)
def check(self, key: str = "default") -> bool:
now = time.time()
self.calls[key] = [t for t in self.calls[key] if now - t < self.window]
if len(self.calls[key]) >= self.max_calls:
return False
self.calls[key].append(now)
return True
3. Tool Result Caching
Avoid repeating expensive operations:
from functools import lru_cache
from datetime import datetime, timedelta
cache = {}
def cached(ttl_seconds: int = 300):
def decorator(func):
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key in cache:
result, timestamp = cache[key]
if datetime.now() - timestamp < timedelta(seconds=ttl_seconds):
return result
result = func(*args, **kwargs)
cache[key] = (result, datetime.now())
return result
return wrapper
return decorator
4. Structured Logging
Every tool call should be traceable:
import structlog
logger = structlog.get_logger()
# In the call_tool handler
logger.info("tool_call", tool=name, arguments=arguments,
duration_ms=elapsed_ms, success=True)
MCP Ecosystem (2026)
Popular MCP Servers
| Server | Purpose | Official? |
|--------|---------|-----------|
| **Filesystem** | Read/write files, directory listing | ✅ Anthropic |
| **GitHub** | Issues, PRs, repos, search | ✅ Anthropic |
| **PostgreSQL** | Query databases, schema inspection | ✅ Anthropic |
| **Slack** | Messages, channels, search | ✅ Anthropic |
| **SQLite** | Local database operations | ✅ Community |
| **Docker** | Container management | ✅ Community |
| **Kubernetes** | Pod management, logs | ✅ Community |
| **Puppeteer** | Browser automation | ✅ Community |
| **Jira** | Issue tracking | ✅ Community |
| **Sentry** | Error monitoring | ✅ Community |
| **PagerDuty** | Incident management | ✅ Community |
| **Elasticsearch** | Search operations | ✅ Community |
MCP Client Hosts
| Host | Type | MCP Support |
|------|------|-------------|
| **Claude Desktop** | Desktop app | ✅ Full support |
| **VS Code** (Cline) | IDE extension | ✅ Full |
| **VS Code** (Continue) | IDE extension | ✅ Full |
| **Cursor** | IDE | ✅ Full |
| **Windsurf** | IDE | ✅ Full |
| **Zed** | Editor | ✅ Native |
| **Sourcegraph Cody** | IDE extension | ✅ Full |
| **JetBrains** | IDE | ✅ Via plugins |
| **OpenAI** | API | ❌ (proprietary tools only) |
| **Google Gemini** | API | ❌ (proprietary tools only) |
When to Build vs Use Existing
Build an MCP Server When:
Use an Existing Server When:
Security Considerations
MCP's stdio transport is surprisingly secure — the server runs as a local subprocess with your permissions. But there are risks:
- Read-only servers for exploration, separate write servers
- Parameter whitelists in tool handlers
- Confirmation prompts for destructive operations
2. **Resource leaks**: LLMs can read large files. Set size limits on resource responses:
```python
MAX_RESOURCE_SIZE = 100_000 # bytes
```
3. **Credentials in config**: The `claude_desktop_config.json` stores server launch commands. Never embed API keys — use environment variables.
4. **Tool sandboxing**: Consider Docker containers for untrusted MCP servers:
```json
{
"mcpServers": {
"community-server": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"--network", "none",
"community/mcp-server:latest"
]
}
}
}
```
Real-World Architecture: AI-Powered Dev Environment
Here's how a real production setup looks:
┌─────────────────────────────────────────────────┐
│ VS Code (Host) │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Claude │ │ Cline │ │ Continue │ │
│ │ Desktop │ │ (VS Code)│ │ (VS Code) │ │
│ └────┬─────┘ └────┬─────┘ └──────┬────────┘ │
│ │ │ │ │
└───────┼──────────────┼───────────────┼───────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ MCP Protocol (JSON-RPC) │
├─────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ GitHub │ │ Postgres │ │ Filesystem │ │
│ │ Server │ │ Server │ │ Server │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Docker │ │ Slack │ │ Jira │ │
│ │ Server │ │ Server │ │ Server │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
└─────────────────────────────────────────────────┘
Each server is independently runnable, testable, and replaceable. Add or remove capabilities by adding or removing MCP servers — no code changes needed.
Getting Started
2. **Try existing servers** → GitHub, Filesystem, SQLite (Anthropic's official servers)
3. **Build your first server** → Use the Python SDK, ~50 lines of code
4. **Add to VS Code** → Install Cline or Continue extension
5. **Go multi-host** → Same MCP server works across all clients
The MCP ecosystem doubles roughly every quarter. What takes 50 lines today will be a one-line npm install by next quarter. The best time to build on top of it was last year — the second best time is now.
Summary
| Concept | What It Means |
|---------|---------------|
| **MCP** | Open protocol for AI-to-tool communication |
| **Transport** | stdio (local) or SSE (remote) |
| **Tools** | Functions the LLM can call |
| **Resources** | Data sources the LLM can read |
| **Prompts** | Templates for how to use tools |
| **Host** | The app running the LLM |
| **Server** | Exposes tools/resources via MCP |
MCP is to AI integrations what HTTP is to web APIs — a universal standard that makes everything compose. Every new MCP-compatible tool and client increases the value of every other MCP component. The network effect is what makes it inevitable.