Building MCP Servers with Python
Learn how to create powerful MCP servers using the Python SDK with tools, resources, and best practices.
Python is one of the most popular languages for building MCP servers thanks to its rich ecosystem and the excellent mcp package. This guide walks you through creating production-ready MCP servers in Python.
Getting Started
Installation
pip install mcp
Or with uv (recommended for modern Python projects):
uv add mcp
Minimal Server
The simplest MCP server in Python is just a few lines:
from mcp.server import Server
server = Server("hello-server")
if __name__ == "__main__":
server.run()
This starts a server that connects over stdio but doesn’t expose any tools or resources yet.
Adding Tools
Tools are async functions decorated with @server.tool():
from mcp.server import Server
server = Server("math-server")
@server.tool()
async def add(a: int, b: int) -> int:
"""Add two numbers together"""
return a + b
@server.tool()
async def multiply(a: int, b: int) -> int:
"""Multiply two numbers"""
return a * b
if __name__ == "__main__":
server.run()
The SDK automatically generates the JSON Schema for your tool parameters from Python type hints. The docstring becomes the tool description.
Tools with Complex Inputs
from typing import Literal
from pydantic import BaseModel
class UserQuery(BaseModel):
name: str
age: int
role: Literal["admin", "user", "viewer"]
@server.tool()
async def create_user(query: UserQuery) -> dict:
"""Create a new user in the system"""
# Your business logic here
return {"id": 123, **query.model_dump()}
Adding Resources
Resources expose data as accessible URIs:
from mcp.server import Server
server = Server("docs-server")
@server.resource("docs://{path}")
async def get_doc(path: str) -> str:
"""Serve documentation files"""
docs = {
"readme": "# Welcome\n\nThis is the documentation server.",
"api": "# API Reference\n\n## Tools\n- get_doc: Read documentation",
}
return docs.get(path, "Document not found")
@server.list_resources()
async def list_docs() -> list[dict]:
"""List all available documentation"""
return [
{"uri": "docs://readme", "name": "README", "mimeType": "text/markdown"},
{"uri": "docs://api", "name": "API Reference", "mimeType": "text/markdown"},
]
Adding Prompts
MCP supports prompt templates that AI clients can use:
@server.prompt()
async def code_review(language: str) -> list[dict]:
"""Generate a code review prompt template"""
return [
{
"role": "user",
"content": {
"type": "text",
"text": f"Please review this {language} code following best practices..."
}
}
]
Working with Environment Variables
import os
from mcp.server import Server
server = Server("api-server")
API_KEY = os.environ.get("API_KEY")
if not API_KEY:
raise RuntimeError("API_KEY environment variable is required")
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///local.db")
@server.tool()
async def fetch_data(endpoint: str) -> dict:
"""Fetch data from an external API"""
import httpx
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://api.example.com/{endpoint}",
headers={"Authorization": f"Bearer {API_KEY}"}
)
resp.raise_for_status()
return resp.json()
Error Handling
Always return meaningful error messages:
@server.tool()
async def divide(a: float, b: float) -> float:
"""Divide a by b"""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
For more complex error handling, you can return structured error responses:
from mcp.types import TextContent, ToolError
@server.tool()
async def safe_operation() -> dict:
try:
# Risky operation
result = risky_call()
return result
except PermissionError:
raise ToolError("You don't have permission to perform this operation")
except TimeoutError:
raise ToolError("The operation timed out. Try again later.")
Testing Your Server
Using MCP Inspector
npx @modelcontextprotocol/inspector python server.py
Unit Testing
import pytest
from your_server import server
@pytest.mark.asyncio
async def test_add_tool():
result = await server.call_tool("add", {"a": 2, "b": 3})
assert result[0].text == "5"
@pytest.mark.asyncio
async def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
await server.call_tool("divide", {"a": 1, "b": 0})
Complete Production Example
Here’s a fully-featured MCP server:
import os
import httpx
from mcp.server import Server
from mcp.types import ToolError
server = Server("production-server")
API_BASE = os.environ.get("API_BASE", "https://api.example.com/v1")
@server.tool()
async def search(query: str, limit: int = 10) -> list[dict]:
"""Search the knowledge base"""
if not query.strip():
raise ToolError("Search query cannot be empty")
if limit < 1 or limit > 100:
raise ToolError("Limit must be between 1 and 100")
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{API_BASE}/search",
params={"q": query, "limit": limit}
)
resp.raise_for_status()
return resp.json()
@server.resource("status://health")
async def health_check() -> str:
"""Health check endpoint"""
return "OK"
if __name__ == "__main__":
server.run()
Deployment
Standalone
python server.py
Docker
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "server.py"]
Claude Desktop Config
{
"mcpServers": {
"python-server": {
"command": "python",
"args": ["path/to/server.py"],
"env": {
"API_KEY": "sk-...",
"API_BASE": "https://api.example.com/v1"
}
}
}
}
Next Steps
- Use the MCPConfig Builder to generate your Python server configuration
- Explore the server templates for pre-built configurations
- Check the official Python SDK documentation for advanced features