6 min read
MCP — Model Context Protocol
What is MCP?
MCP (Model Context Protocol) is an open standard created by Anthropic that defines how AI models connect to external tools, data sources, and services. Think of it as USB-C for AI — a universal connector so any AI model can use any tool without custom integration code.
Without MCP:
Claude uses "tool_use" ──┐
OpenAI uses "function_calling" ──┤ Each needs custom glue code
Gemini uses "function_declarations" ──┘
With MCP:
Any model ──────────► MCP Protocol ──────────► Any tool/data source
(universal standard)Core Architecture
┌─────────────────────────────────────────────────────┐
│ MCP HOST │
│ (Claude Desktop, Cursor, your app) │
│ │
│ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ MCP Client │◄────►│ LLM (Claude/GPT/etc) │ │
│ └──────┬──────┘ └─────────────────────────┘ │
└──────────┼──────────────────────────────────────────┘
│ (stdio / SSE / HTTP)
▼
┌─────────────────────────────────────────────────────┐
│ MCP SERVER │
│ (your tool implementation) │
│ │
│ ┌──────────┐ ┌───────────┐ ┌─────────────────┐ │
│ │ Tools │ │ Resources │ │ Prompts │ │
│ │(actions) │ │ (data) │ │ (templates) │ │
│ └──────────┘ └───────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────┘Three MCP Primitives
1. TOOLS — functions the AI can call (like function calling)
Example: get_weather(city), create_issue(title, body), query_db(sql)
2. RESOURCES — data the AI can read (like context injection)
Example: file://project/README.md, db://customers/table, git://repo/diff
3. PROMPTS — reusable templates the AI can request
Example: "code_review" prompt, "explain_error" promptTransport Mechanisms
1. stdio (local):
Host spawns server as subprocess, communicates via stdin/stdout
Best for: local tools, dev tools, CLI integrations
2. HTTP + SSE (remote):
Server runs as HTTP endpoint, events streamed via SSE
Best for: remote services, web integrations, shared tools
3. Streamable HTTP (new, v2024-11):
Single HTTP endpoint for both request and response streaming
Best for: cloud deploymentsBuilding an MCP Server (Python)
python# pip install mcp
from mcp.server.fastmcp import FastMCP
from typing import Annotated
import httpx
import sqlite3
# Create the MCP server
mcp = FastMCP("My Dev Tools")
# ── TOOL: Simple function the AI can call ────────────────────────────────
@mcp.tool()
def get_weather(city: str) -> str:
"""Get current weather for a city."""
resp = httpx.get(f"https://wttr.in/{city}?format=3")
return resp.text
# ── TOOL: With complex input schema ──────────────────────────────────────
@mcp.tool()
def run_sql(
query: Annotated[str, "SQL SELECT query to execute"],
database: Annotated[str, "Database name"] = "main"
) -> list[dict]:
"""Execute a read-only SQL query against the database."""
if not query.strip().upper().startswith("SELECT"):
raise ValueError("Only SELECT queries allowed") # Safety!
conn = sqlite3.connect(f"{database}.db")
conn.row_factory = sqlite3.Row
cursor = conn.execute(query)
return [dict(row) for row in cursor.fetchall()]
# ── RESOURCE: Data the AI can read ───────────────────────────────────────
@mcp.resource("docs://{topic}")
def get_docs(topic: str) -> str:
"""Get internal documentation for a topic."""
docs = {
"api": "# API Docs\nBase URL: https://api.example.com/v1...",
"auth": "# Auth Docs\nUse Bearer token in Authorization header...",
}
return docs.get(topic, f"No docs found for: {topic}")
# ── PROMPT: Reusable template ──────────────────────────────────────────
@mcp.prompt()
def code_review(language: str, code: str) -> str:
"""Standard code review prompt."""
return f"""You are a senior {language} developer.
Review this code for: bugs, security issues, performance, style.
Be specific and actionable.
```{language}
{code}
```"""
# Run the server
if __name__ == "__main__":
mcp.run() # stdio by default
# mcp.run(transport="sse", host="0.0.0.0", port=8000) # HTTP+SSEBuilding an MCP Client (connecting to servers)
pythonfrom mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import anthropic
async def run_with_mcp():
# Connect to your MCP server
server_params = StdioServerParameters(
command="python",
args=["my_server.py"],
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# List available tools
tools = await session.list_tools()
print([t.name for t in tools.tools])
# → ['get_weather', 'run_sql']
# Convert MCP tools to Anthropic tool format
anthropic_tools = [
{
"name": t.name,
"description": t.description,
"input_schema": t.inputSchema,
}
for t in tools.tools
]
# Use Claude with MCP tools
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=anthropic_tools,
messages=[{"role": "user", "content": "What's the weather in Tokyo?"}]
)
# Handle tool call
if response.stop_reason == "tool_use":
tool_use = next(b for b in response.content if b.type == "tool_use")
result = await session.call_tool(tool_use.name, tool_use.input)
print(result.content)MCP with LangChain
pythonfrom langchain_mcp_adapters.tools import load_mcp_tools
from langchain_anthropic import ChatAnthropic
from langgraph.prebuilt import create_react_agent
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def build_agent_with_mcp():
server_params = StdioServerParameters(
command="python",
args=["my_server.py"],
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# Load MCP tools directly into LangChain format
tools = await load_mcp_tools(session)
model = ChatAnthropic(model="claude-opus-4-6")
agent = create_react_agent(model, tools)
result = await agent.ainvoke({
"messages": [{"role": "user", "content": "What's the weather in Paris?"}]
})
print(result["messages"][-1].content)MCP Server Examples (Real-World)
python# 1. GitHub MCP Server (already exists officially)
# npx @modelcontextprotocol/server-github
# Tools: create_issue, list_prs, get_file, create_branch, etc.
# 2. PostgreSQL MCP Server
@mcp.tool()
async def query_database(sql: str) -> list[dict]:
"""Query the production database (read-only)."""
async with asyncpg.connect(DATABASE_URL) as conn:
rows = await conn.fetch(sql)
return [dict(row) for row in rows]
@mcp.resource("schema://{table}")
async def get_table_schema(table: str) -> str:
"""Get the schema for a database table."""
# Returns CREATE TABLE statement
...
# 3. Internal API MCP Server
@mcp.tool()
async def get_customer(customer_id: str) -> dict:
"""Fetch customer details from CRM."""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{CRM_BASE_URL}/customers/{customer_id}",
headers={"Authorization": f"Bearer {API_KEY}"}
)
return resp.json()
# 4. File System MCP Server
@mcp.resource("file://{path}")
def read_file(path: str) -> str:
"""Read a file from the project directory."""
safe_path = Path(BASE_DIR) / path
if not safe_path.resolve().is_relative_to(BASE_DIR):
raise ValueError("Path traversal not allowed") # Security!
return safe_path.read_text()MCP Configuration (.mcp.json / claude_desktop_config.json)
json{
"mcpServers": {
"my-tools": {
"command": "python",
"args": ["/path/to/my_server.py"],
"env": {
"DATABASE_URL": "postgresql://...",
"API_KEY": "sk-..."
}
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..."
}
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
}
}
}MCP vs Traditional Function Calling
| MCP | Function Calling | |
|---|---|---|
| Standard | Open protocol (any model) | Provider-specific |
| Reuse | One server, many clients | Rebuild per integration |
| Discoverability | Auto via list_tools |
Manually defined each time |
| Resources | Built-in concept | No equivalent |
| Prompts | Built-in concept | No equivalent |
| Transport | stdio, SSE, HTTP | API calls only |
| Ecosystem | Growing fast (100s of servers) | Fragmented |
| Best for | Production, shared tools | Simple one-off use |
Official MCP Servers to Know
@modelcontextprotocol/server-github — GitHub issues, PRs, code
@modelcontextprotocol/server-filesystem — File system operations
@modelcontextprotocol/server-postgres — PostgreSQL queries
@modelcontextprotocol/server-slack — Slack messages
@modelcontextprotocol/server-puppeteer — Web scraping/browser
@modelcontextprotocol/server-google-drive — Drive file access
mcp-server-sqlite — SQLite databases
mcp-server-docker — Docker container managementSecurity Considerations
python# 1. Input validation — ALWAYS validate tool inputs
@mcp.tool()
def delete_record(table: str, id: str) -> str:
"""Delete a record."""
# Whitelist allowed tables
ALLOWED_TABLES = {"orders", "sessions", "temp_data"}
if table not in ALLOWED_TABLES:
raise ValueError(f"Table '{table}' not allowed")
# Use parameterized queries
conn.execute("DELETE FROM ? WHERE id = ?", (table, id))
# 2. Scope limitation — tools should do one thing
# BAD: general "execute_code" tool
# GOOD: specific "run_unit_tests", "format_code", "lint_check"
# 3. No secrets in tool output
@mcp.tool()
def get_user(user_id: str) -> dict:
user = db.get_user(user_id)
return {
"id": user.id,
"name": user.name,
"email": user.email,
# "password_hash": user.password_hash, # NEVER include
# "api_key": user.api_key, # NEVER include
}
# 4. Rate limiting
from functools import lru_cache
import time
_last_call: dict[str, float] = {}
def rate_limit(tool_name: str, min_interval: float = 1.0):
now = time.time()
if tool_name in _last_call:
elapsed = now - _last_call[tool_name]
if elapsed < min_interval:
raise RuntimeError(f"Rate limit: wait {min_interval - elapsed:.1f}s")
_last_call[tool_name] = now[prev·next]