Every AI model today faces the same problem: it’s trapped inside a box. It can think, but it can’t do anything in the real world — no file access, no database queries, no API calls — unless you wire up each integration by hand.
Model Context Protocol (MCP) fixes this. It’s an open standard that gives AI models a universal way to connect to tools and data. Think of it as USB-C for AI: one protocol, any tool, any model.
In this tutorial, you’ll build a working MCP server in Python from scratch.
What is MCP?
MCP is an open protocol created by Anthropic in November 2024, now governed by the Linux Foundation’s Agentic AI Foundation with backing from OpenAI, Google, Microsoft, AWS, and Cloudflare.
The numbers speak for themselves:
- 97 million monthly SDK downloads
- 21,600+ GitHub stars on the Python SDK
- Supported by Claude, ChatGPT, VS Code, Cursor, JetBrains, and more
- $6.3M raised by Manufact (Feb 2026) just for MCP tooling
If you want to understand how to architect AI systems that actually work in production, Designing Machine Learning Systems by Chip Huyen covers the full lifecycle—including how to integrate tools and services like MCP into real-world ML pipelines.
Architecture: Hosts, Clients, Servers
┌─────────────────────────────────────────┐
│ MCP Host (Claude Desktop, VS Code) │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Client 1 │ │ Client 2 │ ... │
│ └────┬─────┘ └────┬─────┘ │
└───────┼──────────────┼──────────────────┘
│ │
┌────▼────┐ ┌────▼────┐
│ Server A│ │ Server B│
│(Database)│ │(GitHub) │
└─────────┘ └─────────┘
- Host: The AI application (Claude Desktop, VS Code, Cursor)
- Client: Maintains a 1:1 connection to a server
- Server: Exposes tools, resources, and prompts to the AI
Three Primitives
| Primitive | Who Controls It | Example |
|---|---|---|
| Tools | The AI model decides when to call | query_database(), create_issue() |
| Resources | The application loads as context | db://schema, file://config.yaml |
| Prompts | The user selects explicitly | “Code review template”, “SQL analysis” |
Transport
MCP supports two transport modes:
- stdio: Server runs as a local subprocess. Zero network overhead. Best for local tools.
- Streamable HTTP: Server runs remotely over HTTP + SSE. Supports OAuth 2.1 auth.
Setting Up
# Install with uv (recommended)
uv init mcp-demo && cd mcp-demo
uv add "mcp[cli]"
# Or with pip
pip install "mcp[cli]"
Requires Python 3.10+.
Building Your First MCP Server
Create server.py:
from mcp.server.fastmcp import FastMCP
# Initialize server
mcp = FastMCP("demo-server")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two numbers."""
return a * b
@mcp.resource("config://app")
def get_config() -> str:
"""Return application configuration."""
return "debug=true\nlog_level=INFO\nmax_retries=3"
@mcp.prompt()
def explain_code(code: str, language: str = "python") -> str:
"""Generate a prompt to explain code."""
return f"Explain this {language} code step by step:\n\n```{language}\n{code}\n```"
if __name__ == "__main__":
mcp.run(transport="stdio")
That’s it. The @mcp.tool() decorator auto-generates the tool schema from your type hints and docstring. No JSON schema boilerplate needed.
Test it:
# Launch the MCP Inspector for interactive testing
npx -y @modelcontextprotocol/inspector uv run server.py
For building robust production servers like your MCP implementation, Python Cookbook by David Beazley is an excellent reference for handling concurrency, networking, and real-world server patterns.
Real-World Example: SQLite Database Server
Here’s a practical MCP server that lets AI models query a SQLite database safely:
from mcp.server.fastmcp import FastMCP
import sqlite3
mcp = FastMCP("sqlite-explorer")
DB_PATH = "example.db"
@mcp.resource("db://schema")
def get_schema() -> str:
"""Return the complete database schema."""
conn = sqlite3.connect(DB_PATH)
cursor = conn.execute(
"SELECT sql FROM sqlite_master WHERE type='table'"
)
schemas = [row[0] for row in cursor.fetchall() if row[0]]
conn.close()
return "\n\n".join(schemas)
@mcp.resource("db://tables")
def list_tables() -> str:
"""List all tables in the database."""
conn = sqlite3.connect(DB_PATH)
cursor = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
)
tables = [row[0] for row in cursor.fetchall()]
conn.close()
return "\n".join(tables)
@mcp.tool()
def query(sql: str) -> str:
"""Execute a read-only SQL query.
Args:
sql: A SELECT query to run against the database.
Only SELECT statements are allowed for safety.
"""
sql_stripped = sql.strip().upper()
if not sql_stripped.startswith("SELECT"):
return "Error: Only SELECT queries allowed."
dangerous = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE"]
if any(kw in sql_stripped for kw in dangerous):
return "Error: Write operations not permitted."
conn = sqlite3.connect(DB_PATH)
try:
cursor = conn.execute(sql)
columns = [desc[0] for desc in cursor.description]
rows = cursor.fetchall()
if not rows:
return "No results found."
result = [dict(zip(columns, row)) for row in rows]
return str(result[:50]) # Limit output size
except Exception as e:
return f"SQL Error: {e}"
finally:
conn.close()
@mcp.tool()
def table_stats(table_name: str) -> str:
"""Get row count and column info for a table.
Args:
table_name: Name of the table to analyze.
"""
# Prevent SQL injection
if not table_name.isalnum():
return "Error: Invalid table name."
conn = sqlite3.connect(DB_PATH)
try:
count = conn.execute(
f"SELECT COUNT(*) FROM {table_name}"
).fetchone()[0]
columns = conn.execute(
f"PRAGMA table_info({table_name})"
).fetchall()
col_info = [
f" {c[1]} ({c[2]})" + (" PRIMARY KEY" if c[5] else "")
for c in columns
]
return f"Table: {table_name}\nRows: {count}\nColumns:\n" + "\n".join(col_info)
except Exception as e:
return f"Error: {e}"
finally:
conn.close()
@mcp.prompt()
def analyze_table(table_name: str) -> str:
"""Create an analysis prompt for a database table."""
return f"Analyze the '{table_name}' table:\n1. Check the schema using db://schema\n2. Run a query to see sample data\n3. Get table_stats for row counts\n4. Report any data quality issues"
Using With Claude Desktop
Add this to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"sqlite-explorer": {
"command": "uv",
"args": ["--directory", "/path/to/your/project", "run", "server.py"]
}
}
}
Now Claude can explore your database conversationally:
“What tables are in the database?”
“Show me the top 10 users by signup date”
“Are there any NULL values in the orders table?”
Advanced: Lifecycle Management
For servers that need database connections or API clients, use the lifespan pattern:
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from dataclasses import dataclass
import aiohttp
from mcp.server.fastmcp import FastMCP, Context
@dataclass
class AppContext:
session: aiohttp.ClientSession
@asynccontextmanager
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
"""Manage HTTP session lifecycle."""
session = aiohttp.ClientSession()
try:
yield AppContext(session=session)
finally:
await session.close()
mcp = FastMCP("api-server", lifespan=app_lifespan)
@mcp.tool()
async def fetch_url(url: str, ctx: Context) -> str:
"""Fetch content from a URL.
Args:
url: The URL to fetch (must be HTTPS).
"""
if not url.startswith("https://"):
return "Error: Only HTTPS URLs allowed."
session = ctx.request_context.lifespan_context.session
async with session.get(url) as resp:
text = await resp.text()
return text[:5000] # Truncate large responses
MCP vs Function Calling vs LangChain
| Function Calling | LangChain Tools | MCP | |
|---|---|---|---|
| Layer | API feature | App framework | Wire protocol |
| Portability | One provider | One framework | Any host |
| Discovery | Static (prompt-time) | Static | Dynamic (tools/list) |
| State | Stateless | Framework-managed | Session-based |
| Reusability | Re-implement per provider | Re-implement per chain | Write once, use everywhere |
Function calling is a feature. LangChain is a framework. MCP is a protocol. They complement each other — you can use MCP servers inside LangChain via langchain-mcp-adapters.
Security Best Practices
MCP servers run with real system access. Take security seriously:
1. Input Validation
@mcp.tool()
def read_file(path: str) -> str:
"""Read a file from the allowed directory only."""
import os
# Prevent path traversal
safe_path = os.path.realpath(path)
if not safe_path.startswith("/allowed/directory/"):
return "Error: Access denied."
with open(safe_path) as f:
return f.read()
2. Principle of Least Privilege
- Read-only database access for query tools
- Restrict file access to specific directories
- Use scoped API tokens, not admin credentials
3. Never Trust Tool Annotations
- MCP server descriptions are self-reported
- Only install servers from trusted sources
- The ClawHub malicious skills incident (Feb 2026) showed compromised MCP registries are a real threat
4. Human-in-the-Loop
- Always confirm destructive operations (file writes, API calls)
- Log all tool invocations for auditing
Deploying a Remote MCP Server
For team access, deploy over HTTP with authentication:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("remote-server")
# ... define tools, resources, prompts ...
if __name__ == "__main__":
mcp.run(
transport="streamable-http",
host="0.0.0.0",
port=8000,
)
Connect from Claude Code:
claude mcp add --transport http my-server http://your-server:8000/mcp
For production, add OAuth 2.1 authentication and HTTPS via a reverse proxy (nginx, Caddy).
The MCP Ecosystem in 2026
The ecosystem has exploded since Anthropic open-sourced MCP:
Timeline:
- Nov 2024: Anthropic creates MCP
- Mar 2025: OpenAI officially adopts it
- Jun 2025: Structured tool outputs, elicitation, security specs
- Dec 2025: Donated to Linux Foundation’s Agentic AI Foundation
- Feb 2026: Manufact raises $6.3M for MCP tooling; 97M monthly SDK downloads
Popular MCP Servers:
- Database: PostgreSQL, SQLite, MongoDB, Qdrant
- Dev Tools: GitHub, Git, filesystem, Docker
- Cloud: AWS, Cloudflare, Vercel
- Monitoring: Sentry, Datadog
- Communication: Slack, Gmail
- Search: Brave Search, Google Search
Browse thousands of community servers at mcp.so or the MCP Registry on GitHub.
Quick Reference: FastMCP Cheat Sheet
from mcp.server.fastmcp import FastMCP, Context
mcp = FastMCP("name")
# Tool (AI-controlled)
@mcp.tool()
def my_tool(arg: str) -> str:
"""Docstring becomes the tool description."""
return "result"
# Async tool with progress
@mcp.tool()
async def slow_tool(data: str, ctx: Context) -> str:
"""Long-running operation with progress."""
for i in range(10):
await ctx.report_progress(i, 10)
return "done"
# Resource (app-controlled context)
@mcp.resource("scheme://path")
def static_resource() -> str:
return "data"
@mcp.resource("scheme://path/{param}")
def dynamic_resource(param: str) -> str:
return f"data for {param}"
# Prompt (user-controlled template)
@mcp.prompt()
def my_prompt(topic: str) -> str:
return f"Analyze {topic} in detail."
# Run
mcp.run(transport="stdio") # Local
mcp.run(transport="streamable-http") # Remote
FAQ
What Python version does MCP require?
MCP’s Python SDK requires Python 3.10 or higher. The recommended setup is using uv for dependency management, which handles Python version management automatically. Install the SDK with uv add "mcp[cli]" or pip install "mcp[cli]".
Can I use MCP with ChatGPT, not just Claude?
Yes. OpenAI officially adopted MCP in March 2025, and ChatGPT Desktop supports MCP servers. The whole point of MCP being an open protocol is vendor neutrality — any MCP server you build works with Claude, ChatGPT, VS Code, Cursor, JetBrains IDEs, and any other MCP-compatible host without code changes.
Is MCP replacing function calling and LangChain?
No. MCP operates at the protocol layer (like HTTP), while function calling is an API feature and LangChain is an application framework. They are complementary. You can use MCP servers inside LangChain via the langchain-mcp-adapters package. MCP standardizes the tool connectivity; function calling and LangChain handle the orchestration logic on top.
Did you find this helpful?
Your support keeps this blog running and ad-free content coming.
☕ Buy me a coffeeMost Popular Posts
- Custom Metaclass in Python: 43% Faster Validation (12,954 views)
- yfinance Alternatives 2026: 7 Free APIs Compared (1,048 views)
- Python match-case: 7 Patterns That Beat if-elif Chains (982 views)
- YOLOv8 INT8 Quantization: 4x Faster on Jetson Orin (897 views)
- PaddleOCR vs EasyOCR vs Tesseract: Why PaddleOCR Is Slower (705 views)