MCP Python SDK Skill
Overview
Specialized skill for building Model Context Protocol (MCP) servers and clients using the official Python SDK. MCP enables applications to provide context for LLMs in a standardized way, separating context provision from LLM interaction. This skill covers both high-level FastMCP and low-level server implementations.
Core Capabilities
1. Server Creation (FastMCP)
- •Build MCP servers with minimal boilerplate
- •Expose tools, resources, and prompts
- •Automatic schema generation from Python types
- •Lifespan management for startup/shutdown
- •Built-in transport support (stdio, SSE, Streamable HTTP)
2. Resources
- •Expose data to LLMs through URI-based resources
- •Template-based resource URIs with parameters
- •Dynamic resource generation
- •Resource subscriptions and updates
- •Read-only data access patterns
3. Tools
- •Create executable functions for LLMs
- •Automatic parameter validation with Pydantic
- •Context access for logging and progress
- •Structured output support
- •Error handling and validation
4. Prompts
- •Reusable prompt templates
- •Parameterized prompts with arguments
- •Multi-message prompt conversations
- •Prompt argument completion
5. Client Development
- •Connect to MCP servers via multiple transports
- •Call tools and read resources
- •OAuth authentication support
- •Session management
- •Request handling
Installation & Setup
Install MCP SDK
bash
# Using uv (recommended) uv add "mcp[cli]" # Using pip pip install "mcp[cli]"
Set Up Project
bash
# Create new project with uv uv init mcp-server-demo cd mcp-server-demo uv add "mcp[cli]"
Environment Variables
bash
export OPENAI_API_KEY=sk-... # If using LLM features
FastMCP Quick Start
Basic Server
python
"""
Simple FastMCP server with tools, resources, and prompts.
Run with: uv run mcp dev server.py
"""
from mcp.server.fastmcp import FastMCP
# Create server instance
mcp = FastMCP("Demo Server")
# Add a tool
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
# Add a resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Get a personalized greeting."""
return f"Hello, {name}!"
# Add a prompt
@mcp.prompt()
def review_code(code: str) -> str:
"""Generate a code review prompt."""
return f"Please review this code:\n\n{code}"
Running the Server
bash
# Development mode with inspector uv run mcp dev server.py # Install in Claude Desktop uv run mcp install server.py # Direct execution python server.py # or uv run mcp run server.py
Resources
Basic Resource Definition
python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Resource Server")
# Simple static resource
@mcp.resource("config://settings")
def get_settings() -> str:
"""Return application settings."""
return '{"theme": "dark", "language": "en"}'
# Parameterized resource
@mcp.resource("file://documents/{filename}")
def read_file(filename: str) -> str:
"""Read a document by filename."""
# In production, read from actual filesystem
return f"Content of {filename}"
# Resource with multiple parameters
@mcp.resource("data://{category}/{id}")
def get_data(category: str, id: str) -> str:
"""Get data by category and ID."""
return f"Data for {category}/{id}"
Resource Lists and Templates
python
# Resources are automatically discovered from decorators
# Clients can list all available resources
# For dynamic resources, use resource templates
@mcp.resource("repo://{owner}/{repo}")
def get_repo_info(owner: str, repo: str) -> str:
"""Get information about a GitHub repository."""
return f"Repository: {owner}/{repo}"
# This creates a resource template that clients can discover
# and populate with specific owner/repo values
Resource Updates
python
from mcp.server.fastmcp import Context, FastMCP
mcp = FastMCP("Dynamic Resources")
@mcp.tool()
async def update_config(key: str, value: str, ctx: Context) -> str:
"""Update configuration and notify clients."""
# Update the config
config[key] = value
# Notify clients that resources changed
await ctx.session.send_resource_list_changed()
return f"Updated {key} to {value}"
Tools
Basic Tool Definition
python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Tool Server")
@mcp.tool()
def calculate_sum(numbers: list[int]) -> int:
"""Calculate the sum of a list of numbers."""
return sum(numbers)
@mcp.tool()
def get_weather(city: str, unit: str = "celsius") -> str:
"""Get weather for a city."""
# In production, call a weather API
return f"Weather in {city}: 22°{unit[0].upper()}"
Pydantic Validation
python
from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Validated Tools")
class OrderRequest(BaseModel):
product_id: str = Field(description="Product ID")
quantity: int = Field(gt=0, le=100, description="Quantity (1-100)")
priority: str = Field(pattern="^(low|medium|high)$")
@mcp.tool()
def place_order(order: OrderRequest) -> dict:
"""Place a product order with validation."""
return {
"order_id": "ORD-123",
"product": order.product_id,
"quantity": order.quantity,
"status": "confirmed"
}
Context Access in Tools
python
from mcp.server.fastmcp import Context, FastMCP
mcp = FastMCP("Context Tools")
@mcp.tool()
async def long_task(name: str, ctx: Context, steps: int = 5) -> str:
"""Execute a long-running task with progress updates."""
await ctx.info(f"Starting task: {name}")
for i in range(steps):
progress = (i + 1) / steps
await ctx.report_progress(
progress=progress,
total=1.0,
message=f"Step {i + 1}/{steps}"
)
await ctx.debug(f"Completed step {i + 1}")
return f"Task '{name}' completed successfully"
@mcp.tool()
async def read_data(ctx: Context) -> str:
"""Tool that reads from another resource."""
from pydantic import AnyUrl
# Read a resource using context
result = await ctx.read_resource(AnyUrl("config://settings"))
return f"Settings: {result}"
Structured Output
python
from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Structured Output")
class WeatherData(BaseModel):
"""Weather information structure."""
temperature: float = Field(description="Temperature in Celsius")
humidity: float = Field(description="Humidity percentage")
condition: str
wind_speed: float
@mcp.tool()
def get_weather_data(city: str) -> WeatherData:
"""Get structured weather data."""
return WeatherData(
temperature=22.5,
humidity=65.0,
condition="sunny",
wind_speed=5.2
)
# Also supports TypedDict, dataclasses, and dict[str, T]
from typing import TypedDict
class Location(TypedDict):
latitude: float
longitude: float
name: str
@mcp.tool()
def geocode(address: str) -> Location:
"""Get location coordinates."""
return {
"latitude": 51.5074,
"longitude": -0.1278,
"name": "London, UK"
}
Prompts
Basic Prompt Definition
python
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base
mcp = FastMCP("Prompt Server")
@mcp.prompt()
def code_review(code: str, language: str = "python") -> str:
"""Generate a code review prompt."""
return f"Review this {language} code:\n\n{code}"
@mcp.prompt(title="Debug Assistant")
def debug_help(error: str) -> list[base.Message]:
"""Generate a debugging conversation."""
return [
base.UserMessage("I'm encountering this error:"),
base.UserMessage(error),
base.AssistantMessage("I'll help debug that. What have you tried?")
]
# Prompts with multiple parameters
@mcp.prompt()
def greet_user(name: str, style: str = "friendly") -> str:
"""Generate a greeting in different styles."""
styles = {
"friendly": "write a warm, friendly greeting",
"formal": "write a formal, professional greeting",
"casual": "write a casual, relaxed greeting"
}
prompt = styles.get(style, styles["friendly"])
return f"{prompt} for someone named {name}."
Context Object
Context Properties and Methods
python
from mcp.server.fastmcp import Context, FastMCP
mcp = FastMCP("Context Demo")
@mcp.tool()
async def demo_context(ctx: Context) -> dict:
"""Demonstrate context capabilities."""
# Logging
await ctx.debug("Debug message")
await ctx.info("Info message")
await ctx.warning("Warning message")
await ctx.error("Error message")
# Progress reporting
await ctx.report_progress(
progress=0.5,
total=1.0,
message="Halfway done"
)
# Access server info
server_name = ctx.fastmcp.name
debug_mode = ctx.fastmcp.settings.debug
# Access request context
request_id = ctx.request_context.request_id
# Read resources
from pydantic import AnyUrl
resource = await ctx.read_resource(AnyUrl("config://settings"))
return {
"server": server_name,
"debug": debug_mode,
"request_id": request_id
}
Elicitation (User Input)
python
from pydantic import BaseModel, Field
from mcp.server.fastmcp import Context, FastMCP
mcp = FastMCP("Elicitation Demo")
class UserPreferences(BaseModel):
"""User preference schema."""
notify_email: bool = Field(description="Send email notifications")
theme: str = Field(default="dark", description="UI theme")
@mcp.tool()
async def configure_settings(ctx: Context) -> str:
"""Request user preferences."""
result = await ctx.elicit(
message="Please configure your preferences:",
schema=UserPreferences
)
if result.action == "accept" and result.data:
return f"Settings saved: {result.data.model_dump()}"
elif result.action == "decline":
return "User declined to provide preferences"
else:
return "Configuration cancelled"
Sampling (LLM Integration)
python
from mcp.server.fastmcp import Context, FastMCP
from mcp.types import SamplingMessage, TextContent
mcp = FastMCP("Sampling Demo")
@mcp.tool()
async def generate_text(topic: str, ctx: Context) -> str:
"""Generate text using LLM sampling."""
result = await ctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(
type="text",
text=f"Write a short poem about {topic}"
)
)
],
max_tokens=100
)
if result.content.type == "text":
return result.content.text
return str(result.content)
Lifespan Management
Server Startup/Shutdown
python
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass
from mcp.server.fastmcp import Context, FastMCP
# Mock database for example
class Database:
@classmethod
async def connect(cls) -> "Database":
print("Database connected")
return cls()
async def disconnect(self) -> None:
print("Database disconnected")
def query(self, sql: str) -> list:
return [{"result": "data"}]
@dataclass
class AppContext:
"""Application context with typed dependencies."""
db: Database
@asynccontextmanager
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
"""Manage application lifecycle."""
# Startup
db = await Database.connect()
try:
yield AppContext(db=db)
finally:
# Shutdown
await db.disconnect()
# Create server with lifespan
mcp = FastMCP("Database Server", lifespan=app_lifespan)
@mcp.tool()
def query_database(sql: str, ctx: Context[AppContext]) -> list:
"""Execute database query using shared connection."""
db = ctx.request_context.lifespan_context.db
return db.query(sql)
Transport Configuration
Stdio Transport (Default)
python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Stdio Server")
@mcp.tool()
def hello(name: str = "World") -> str:
return f"Hello, {name}!"
if __name__ == "__main__":
# Stdio is default transport
mcp.run()
# or explicitly: mcp.run(transport="stdio")
Streamable HTTP Transport
python
from mcp.server.fastmcp import FastMCP
# Stateful server (maintains sessions)
mcp = FastMCP("HTTP Server")
# Stateless server (no session persistence)
# mcp = FastMCP("HTTP Server", stateless_http=True)
# JSON responses instead of SSE
# mcp = FastMCP("HTTP Server", stateless_http=True, json_response=True)
@mcp.tool()
def greet(name: str) -> str:
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run(transport="streamable-http")
SSE Transport (Legacy)
python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("SSE Server")
if __name__ == "__main__":
mcp.run(transport="sse")
Mounting to Existing ASGI Server
python
from starlette.applications import Starlette
from starlette.routing import Mount
from mcp.server.fastmcp import FastMCP
# Create MCP servers
api_mcp = FastMCP("API Server", stateless_http=True)
chat_mcp = FastMCP("Chat Server", stateless_http=True)
@api_mcp.tool()
def api_status() -> str:
return "API is running"
@chat_mcp.tool()
def send_message(message: str) -> str:
return f"Message sent: {message}"
# Configure paths
api_mcp.settings.streamable_http_path = "/"
chat_mcp.settings.streamable_http_path = "/"
# Create Starlette app
app = Starlette(
routes=[
Mount("/api", app=api_mcp.streamable_http_app()),
Mount("/chat", app=chat_mcp.streamable_http_app()),
]
)
# Run with: uvicorn server:app
MCP Client Development
Basic Client (Stdio)
python
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from pydantic import AnyUrl
async def main():
# Configure server connection
server_params = StdioServerParameters(
command="uv",
args=["run", "server.py"]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# Initialize
await session.initialize()
# List resources
resources = await session.list_resources()
print(f"Resources: {[r.uri for r in resources.resources]}")
# List tools
tools = await session.list_tools()
print(f"Tools: {[t.name for t in tools.tools]}")
# Call a tool
result = await session.call_tool(
"add",
arguments={"a": 5, "b": 3}
)
print(f"Result: {result.content[0].text}")
# Read a resource
content = await session.read_resource(
AnyUrl("greeting://Alice")
)
print(f"Content: {content.contents[0].text}")
if __name__ == "__main__":
asyncio.run(main())
Streamable HTTP Client
python
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
async with streamablehttp_client("http://localhost:8000/mcp") as (
read, write, _
):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print(f"Tools: {[t.name for t in tools.tools]}")
if __name__ == "__main__":
asyncio.run(main())
OAuth Authentication
python
from mcp.client.auth import OAuthClientProvider, TokenStorage
from mcp.client.streamable_http import streamablehttp_client
from pydantic import AnyUrl
class InMemoryTokenStorage(TokenStorage):
"""Simple in-memory token storage."""
def __init__(self):
self.tokens = None
self.client_info = None
async def get_tokens(self):
return self.tokens
async def set_tokens(self, tokens):
self.tokens = tokens
async def get_client_info(self):
return self.client_info
async def set_client_info(self, client_info):
self.client_info = client_info
async def handle_redirect(auth_url: str):
print(f"Visit: {auth_url}")
async def handle_callback():
callback_url = input("Paste callback URL: ")
# Parse code and state from URL
return code, state
oauth = OAuthClientProvider(
server_url="http://localhost:8001",
client_metadata={
"client_name": "My MCP Client",
"redirect_uris": [AnyUrl("http://localhost:3000/callback")],
"grant_types": ["authorization_code", "refresh_token"],
"scope": "user"
},
storage=InMemoryTokenStorage(),
redirect_handler=handle_redirect,
callback_handler=handle_callback
)
async with streamablehttp_client(
"http://localhost:8001/mcp",
auth=oauth
) as (read, write, _):
# Use authenticated session
pass
Low-Level Server
Basic Low-Level Server
python
import asyncio
import mcp.server.stdio
import mcp.types as types
from mcp.server.lowlevel import Server, NotificationOptions
from mcp.server.models import InitializationOptions
server = Server("low-level-server")
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="add",
description="Add two numbers",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"}
},
"required": ["a", "b"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == "add":
result = arguments["a"] + arguments["b"]
return [types.TextContent(type="text", text=str(result))]
raise ValueError(f"Unknown tool: {name}")
async def main():
async with mcp.server.stdio.stdio_server() as (read, write):
await server.run(
read,
write,
InitializationOptions(
server_name="low-level-server",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={}
)
)
)
if __name__ == "__main__":
asyncio.run(main())
Structured Output (Low-Level)
python
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="get_weather",
description="Get weather data",
inputSchema={
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
},
outputSchema={
"type": "object",
"properties": {
"temperature": {"type": "number"},
"condition": {"type": "string"},
"humidity": {"type": "number"}
},
"required": ["temperature", "condition"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> dict:
"""Return structured data directly."""
if name == "get_weather":
return {
"temperature": 22.5,
"condition": "sunny",
"humidity": 65
}
raise ValueError(f"Unknown tool: {name}")
Authentication
OAuth Resource Server
python
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings
from mcp.server.fastmcp import FastMCP
from pydantic import AnyHttpUrl
class MyTokenVerifier(TokenVerifier):
"""Custom token verification logic."""
async def verify_token(self, token: str) -> AccessToken | None:
# Verify token with your authorization server
# Return AccessToken if valid, None if invalid
pass
mcp = FastMCP(
"Protected Server",
token_verifier=MyTokenVerifier(),
auth=AuthSettings(
issuer_url=AnyHttpUrl("https://auth.example.com"),
resource_server_url=AnyHttpUrl("http://localhost:8001"),
required_scopes=["user"]
)
)
@mcp.tool()
def protected_action() -> str:
"""This tool requires authentication."""
return "Action completed"
Best Practices
1. Resource Design
- •Keep resources read-only
- •Use clear URI schemes (e.g.,
file://,config://) - •Implement resource templates for dynamic content
- •Notify clients of resource changes
- •Cache expensive resource computations
2. Tool Implementation
- •Use descriptive names and docstrings
- •Validate inputs with Pydantic
- •Return structured output when possible
- •Report progress for long operations
- •Handle errors gracefully
3. Context Usage
- •Use context for logging instead of print()
- •Report progress for operations > 1 second
- •Access shared resources via lifespan context
- •Read resources through ctx.read_resource()
- •Use elicitation for user input
4. Server Configuration
- •Use lifespan for resource initialization
- •Choose appropriate transport (stdio for desktop, HTTP for web)
- •Enable stateless mode for scalability
- •Configure CORS for browser clients
- •Implement proper error handling
5. Client Development
- •Always call initialize() before other operations
- •Handle connection failures gracefully
- •Parse structured output when available
- •Use OAuth for protected servers
- •Implement token refresh logic
Common Patterns
Multi-Agent Coordination
python
# Create specialized agents as separate MCP servers
# Use resources to share state between servers
# Use tools for cross-server communication
@mcp.resource("shared://state")
def get_shared_state() -> str:
return json.dumps(shared_state)
@mcp.tool()
async def update_shared_state(key: str, value: str, ctx: Context) -> str:
shared_state[key] = value
await ctx.session.send_resource_updated("shared://state")
return "Updated"
Progressive Enhancement
python
# Start with basic tools
@mcp.tool()
def basic_search(query: str) -> list[str]:
return simple_search(query)
# Add advanced features progressively
@mcp.tool()
def advanced_search(
query: str,
filters: dict | None = None,
limit: int = 10
) -> list[dict]:
return complex_search(query, filters, limit)
Resource Pagination
python
from mcp.types import PaginatedRequestParams
@server.list_resources()
async def list_resources(
request: types.ListResourcesRequest
) -> types.ListResourcesResult:
cursor = request.params.cursor if request.params else None
start = int(cursor) if cursor else 0
end = start + 10
resources = [...] # Your resources
page = resources[start:end]
next_cursor = str(end) if end < len(resources) else None
return types.ListResourcesResult(
resources=page,
nextCursor=next_cursor
)
When to use this skill:
- •Building MCP servers to expose data/functionality to LLMs
- •Creating tools for LLM interaction
- •Developing MCP clients to consume server capabilities
- •Implementing authentication for protected resources
- •Building multi-server architectures
- •Creating reusable prompt templates
- •Integrating with Claude Desktop or other MCP clients
- •Building browser-based MCP applications