MCP Python Server
Building MCP servers in Python using the official mcp SDK and the FastMCP high-level pattern. Covers project setup with uv, defining tools with type hints, async handlers, resources, prompts, stdio and SSE transports, context objects, and deployment strategies including uvx distribution.
You are an AI assistant helping developers build MCP servers in Python. The official `mcp` SDK provides the `FastMCP` class — a high-level, decorator-based API inspired by FastAPI. Your role is to guide developers through idiomatic Python MCP server development. ## Key Points - **Synchronous blocking calls** — never use `requests.get()` or `time.sleep()` in async handlers. Use `httpx` and `asyncio.sleep()`. - **Printing to stdout** — stdout is the transport channel for stdio servers. Use `ctx.info()` or `logging` to stderr. - **Missing docstrings** — FastMCP uses docstrings for tool descriptions. No docstring means no description, which means the model cannot use the tool effectively. - **Bare `except` clauses** — catch specific exceptions. Bare `except` hides bugs and makes debugging impossible. - **Global mutable state** — use the lifespan pattern for shared resources like database pools. Global state causes issues with testing and concurrent access. - Write comprehensive docstrings with `Args:` sections — they become the tool descriptions and parameter descriptions. - Use Python type hints extensively — `list[str]`, `dict[str, Any]`, `Optional[int]`, enums. They generate better schemas. - Use the lifespan pattern for database connections, HTTP clients, and other resources that need cleanup. - Test tools as regular async functions before connecting them to MCP. - Use `uv` for dependency management — it is fast and handles Python version management. - Read secrets from `os.environ`, never hardcode them. ## Quick Example ``` ### Async Patterns MCP servers in Python use asyncio. Handle concurrent operations properly: ``` ``` ### Error Handling Return errors as values, do not let exceptions propagate unhandled: ```
skilldb get mcp-server-skills/mcp-python-serverFull skill: 389 linesMCP Python Server
You are an AI assistant helping developers build MCP servers in Python. The official mcp SDK provides the FastMCP class — a high-level, decorator-based API inspired by FastAPI. Your role is to guide developers through idiomatic Python MCP server development.
Philosophy
Python MCP servers leverage type hints for automatic schema generation, async/await for concurrent operations, and decorators for clean tool definitions. FastMCP handles all the protocol plumbing — you write normal Python functions and the framework generates JSON Schema from type annotations, validates inputs, and formats responses. Use Python when your server wraps Python libraries, performs data processing, or when your team prefers Python.
Techniques
Project Setup with uv
# Create a new project
uv init my-mcp-server
cd my-mcp-server
# Add the MCP SDK
uv add "mcp[cli]"
Project structure:
my-mcp-server/
src/
my_mcp_server/
__init__.py
server.py
pyproject.toml
Configure pyproject.toml:
[project]
name = "my-mcp-server"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = ["mcp[cli]"]
[project.scripts]
my-mcp-server = "my_mcp_server.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Minimal Server
# src/my_mcp_server/server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool()
async def greet(name: str) -> str:
"""Generate a greeting for the given name.
Args:
name: The name to greet
"""
return f"Hello, {name}! Welcome to MCP."
@mcp.tool()
async def add(a: float, b: float) -> str:
"""Add two numbers together.
Args:
a: First number
b: Second number
"""
return str(a + b)
def main():
mcp.run()
if __name__ == "__main__":
main()
Run with: uv run my-mcp-server or python -m my_mcp_server.server
Type Hints Drive Schema Generation
FastMCP generates JSON Schema from Python type hints automatically:
from typing import Optional
from enum import Enum
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
@mcp.tool()
async def create_issue(
title: str,
body: str,
labels: list[str] | None = None,
priority: Priority = Priority.MEDIUM,
assignee: str | None = None,
) -> str:
"""Create a new issue in the project tracker.
Args:
title: Issue title (required)
body: Detailed description of the issue
labels: Optional list of label names to apply
priority: Issue priority level (low, medium, high)
assignee: Username to assign the issue to
"""
issue = await tracker.create_issue(
title=title,
body=body,
labels=labels or [],
priority=priority.value,
assignee=assignee,
)
return f"Created issue #{issue.number}: {issue.title}"
This generates a JSON Schema with proper types, enums, defaults, and required fields — all from the function signature.
Using the Context Object
FastMCP provides a Context object for logging, progress reporting, and accessing request metadata:
from mcp.server.fastmcp import FastMCP, Context
@mcp.tool()
async def process_files(directory: str, ctx: Context) -> str:
"""Process all files in a directory.
Args:
directory: Path to the directory to process
"""
files = list(Path(directory).glob("*"))
total = len(files)
results = []
for i, file in enumerate(files):
# Report progress
await ctx.report_progress(i, total)
# Log activity
await ctx.info(f"Processing {file.name}")
try:
result = await process_single_file(file)
results.append(result)
except Exception as e:
await ctx.warning(f"Failed to process {file.name}: {e}")
await ctx.report_progress(total, total)
return f"Processed {len(results)}/{total} files successfully"
The ctx parameter is automatically injected by FastMCP — do not include it in the tool's argument schema.
Resources
import json
@mcp.resource("config://app/settings")
async def get_settings() -> str:
"""Current application settings."""
with open("settings.json") as f:
return f.read()
@mcp.resource("db://tables/{table_name}/schema")
async def get_table_schema(table_name: str) -> str:
"""Schema for a database table.
Args:
table_name: Name of the table
"""
columns = await db.fetch(
"SELECT column_name, data_type, is_nullable "
"FROM information_schema.columns WHERE table_name = $1",
table_name,
)
return json.dumps([dict(c) for c in columns], indent=2)
@mcp.resource("metrics://system/health")
async def get_health() -> str:
"""Current system health metrics."""
metrics = {
"cpu_percent": psutil.cpu_percent(),
"memory_percent": psutil.virtual_memory().percent,
"disk_percent": psutil.disk_usage("/").percent,
}
return json.dumps(metrics, indent=2)
Prompts
@mcp.prompt()
async def review_code(code: str, language: str = "python") -> str:
"""Review code for bugs, style issues, and improvements.
Args:
code: The source code to review
language: Programming language of the code
"""
return (
f"Please review this {language} code for bugs, style issues, "
f"and potential improvements:\n\n```{language}\n{code}\n```\n\n"
"Focus on:\n"
"1. Correctness — are there any bugs?\n"
"2. Error handling — are edge cases covered?\n"
"3. Performance — any obvious inefficiencies?\n"
"4. Readability — is the code clear and well-structured?"
)
Async Patterns
MCP servers in Python use asyncio. Handle concurrent operations properly:
import asyncio
import httpx
@mcp.tool()
async def fetch_multiple(urls: list[str]) -> str:
"""Fetch content from multiple URLs concurrently.
Args:
urls: List of URLs to fetch
"""
async with httpx.AsyncClient(timeout=10) as client:
tasks = [client.get(url) for url in urls]
responses = await asyncio.gather(*tasks, return_exceptions=True)
results = []
for url, resp in zip(urls, responses):
if isinstance(resp, Exception):
results.append({"url": url, "error": str(resp)})
else:
results.append({
"url": url,
"status": resp.status_code,
"length": len(resp.text),
})
return json.dumps(results, indent=2)
Error Handling
Return errors as values, do not let exceptions propagate unhandled:
@mcp.tool()
async def query_database(query: str) -> str:
"""Execute a read-only SQL query.
Args:
query: SQL SELECT query to execute
"""
# Validate input
normalized = query.strip().upper()
if not normalized.startswith("SELECT"):
raise ValueError("Only SELECT queries are allowed")
forbidden = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "TRUNCATE"]
for kw in forbidden:
if kw in normalized:
raise ValueError(f"Query contains forbidden keyword: {kw}")
try:
rows = await db.fetch(query)
return json.dumps([dict(r) for r in rows], indent=2)
except asyncpg.PostgresError as e:
raise ValueError(f"Query failed: {e}")
When a tool function raises an exception, FastMCP catches it and returns it as an isError: true result with the exception message. Use ValueError for user-facing errors.
SSE Transport for Remote Servers
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("remote-server")
# Define tools...
if __name__ == "__main__":
mcp.run(transport="sse", host="0.0.0.0", port=3000)
Streamable HTTP Transport
mcp = FastMCP("remote-server")
# Define tools...
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=3000)
Distribution via uvx
Publish to PyPI and users can run without installing:
{
"mcpServers": {
"my-server": {
"command": "uvx",
"args": ["my-mcp-server"],
"env": {
"API_KEY": "..."
}
}
}
}
Or with pip:
{
"mcpServers": {
"my-server": {
"command": "python",
"args": ["-m", "my_mcp_server.server"]
}
}
}
Lifespan Management
Use the lifespan pattern to manage server startup and shutdown:
from contextlib import asynccontextmanager
from mcp.server.fastmcp import FastMCP
@asynccontextmanager
async def app_lifespan(server: FastMCP):
"""Initialize and clean up server resources."""
# Startup
db_pool = await asyncpg.create_pool("postgresql://localhost/mydb")
try:
yield {"db": db_pool}
finally:
# Shutdown
await db_pool.close()
mcp = FastMCP("db-server", lifespan=app_lifespan)
@mcp.tool()
async def query(sql: str, ctx: Context) -> str:
"""Run a SQL query."""
db = ctx.request_context.lifespan_context["db"]
rows = await db.fetch(sql)
return json.dumps([dict(r) for r in rows], indent=2)
Anti-Patterns
- Synchronous blocking calls — never use
requests.get()ortime.sleep()in async handlers. Usehttpxandasyncio.sleep(). - Printing to stdout — stdout is the transport channel for stdio servers. Use
ctx.info()orloggingto stderr. - Missing docstrings — FastMCP uses docstrings for tool descriptions. No docstring means no description, which means the model cannot use the tool effectively.
- Bare
exceptclauses — catch specific exceptions. Bareexcepthides bugs and makes debugging impossible. - Global mutable state — use the lifespan pattern for shared resources like database pools. Global state causes issues with testing and concurrent access.
Best Practices
- Write comprehensive docstrings with
Args:sections — they become the tool descriptions and parameter descriptions. - Use Python type hints extensively —
list[str],dict[str, Any],Optional[int], enums. They generate better schemas. - Use the lifespan pattern for database connections, HTTP clients, and other resources that need cleanup.
- Test tools as regular async functions before connecting them to MCP.
- Use
uvfor dependency management — it is fast and handles Python version management. - Read secrets from
os.environ, never hardcode them.
Install this skill directly: skilldb add mcp-server-skills
Related Skills
MCP Resources
Exposing data and content to AI clients through MCP resources. Covers resource URIs, listing and reading resources, resource templates with URI patterns, MIME types, subscriptions for real-time updates, and patterns for exposing files, database records, and API data as browsable resources.
MCP Security Audit
Use this skill when auditing the security of a Model Context Protocol server or client. Activate when users mention mcp server security audit, MCP vulnerabilities, tool poisoning, prompt injection via MCP, confused deputy, token passthrough, OAuth for MCP, rug-pull tool descriptions, or reviewing an MCP integration before connecting it to an agent with real capabilities.
MCP Testing and Debugging
Testing and debugging MCP servers effectively. Covers the MCP Inspector for interactive testing, unit testing individual tools, integration testing with in-memory transports, debugging transport issues, logging strategies, common failure modes, and systematic approaches to diagnosing protocol-level problems.
MCP Tools
Defining and implementing tools in MCP servers — the primary way AI assistants take actions through MCP. Covers tool definitions with JSON Schema inputs, writing tool handlers, returning structured results, error handling with isError, tool annotations for UI hints, and patterns for robust tool implementations.
MCP TypeScript Server
Building MCP servers in TypeScript using the official @modelcontextprotocol/sdk package. Covers project setup, the McpServer high-level API, defining tools with Zod schemas, stdio and SSE transports, streaming responses, error handling, and deployment as npm packages or standalone binaries.
MCP Auth and Security
Securing MCP servers with authentication, authorization, and defensive practices. Covers OAuth 2.1 integration for remote servers, API key management through environment variables, input validation and sanitization, rate limiting, sandboxing tool execution, path traversal prevention, and the principle of least privilege for tool design.