Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Guides creation of high-quality MCP servers in TypeScript or Python (FastMCP) to connect LLMs with external services.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/python_mcp_server.md
1# Python MCP Server Implementation Guide23## Overview45This document provides Python-specific best practices and examples for implementing MCP servers using the MCP Python SDK. It covers server setup, tool registration patterns, input validation with Pydantic, error handling, and complete working examples.67---89## Quick Reference1011### Key Imports12```python13from mcp.server.fastmcp import FastMCP14from pydantic import BaseModel, Field, field_validator, ConfigDict15from typing import Optional, List, Dict, Any16from enum import Enum17import httpx18```1920### Server Initialization21```python22mcp = FastMCP("service_mcp")23```2425### Tool Registration Pattern26```python27@mcp.tool(name="tool_name", annotations={...})28async def tool_function(params: InputModel) -> str:29# Implementation30pass31```3233---3435## MCP Python SDK and FastMCP3637The official MCP Python SDK provides FastMCP, a high-level framework for building MCP servers. It provides:38- Automatic description and inputSchema generation from function signatures and docstrings39- Pydantic model integration for input validation40- Decorator-based tool registration with `@mcp.tool`4142**For complete SDK documentation, use WebFetch to load:**43`https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md`4445## Server Naming Convention4647Python MCP servers must follow this naming pattern:48- **Format**: `{service}_mcp` (lowercase with underscores)49- **Examples**: `github_mcp`, `jira_mcp`, `stripe_mcp`5051The name should be:52- General (not tied to specific features)53- Descriptive of the service/API being integrated54- Easy to infer from the task description55- Without version numbers or dates5657## Tool Implementation5859### Tool Naming6061Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names.6263**Avoid Naming Conflicts**: Include the service context to prevent overlaps:64- Use "slack_send_message" instead of just "send_message"65- Use "github_create_issue" instead of just "create_issue"66- Use "asana_list_tasks" instead of just "list_tasks"6768### Tool Structure with FastMCP6970Tools are defined using the `@mcp.tool` decorator with Pydantic models for input validation:7172```python73from pydantic import BaseModel, Field, ConfigDict74from mcp.server.fastmcp import FastMCP7576# Initialize the MCP server77mcp = FastMCP("example_mcp")7879# Define Pydantic model for input validation80class ServiceToolInput(BaseModel):81'''Input model for service tool operation.'''82model_config = ConfigDict(83str_strip_whitespace=True, # Auto-strip whitespace from strings84validate_assignment=True, # Validate on assignment85extra='forbid' # Forbid extra fields86)8788param1: str = Field(..., description="First parameter description (e.g., 'user123', 'project-abc')", min_length=1, max_length=100)89param2: Optional[int] = Field(default=None, description="Optional integer parameter with constraints", ge=0, le=1000)90tags: Optional[List[str]] = Field(default_factory=list, description="List of tags to apply", max_items=10)9192@mcp.tool(93name="service_tool_name",94annotations={95"title": "Human-Readable Tool Title",96"readOnlyHint": True, # Tool does not modify environment97"destructiveHint": False, # Tool does not perform destructive operations98"idempotentHint": True, # Repeated calls have no additional effect99"openWorldHint": False # Tool does not interact with external entities100}101)102async def service_tool_name(params: ServiceToolInput) -> str:103'''Tool description automatically becomes the 'description' field.104105This tool performs a specific operation on the service. It validates all inputs106using the ServiceToolInput Pydantic model before processing.107108Args:109params (ServiceToolInput): Validated input parameters containing:110- param1 (str): First parameter description111- param2 (Optional[int]): Optional parameter with default112- tags (Optional[List[str]]): List of tags113114Returns:115str: JSON-formatted response containing operation results116'''117# Implementation here118pass119```120121## Pydantic v2 Key Features122123- Use `model_config` instead of nested `Config` class124- Use `field_validator` instead of deprecated `validator`125- Use `model_dump()` instead of deprecated `dict()`126- Validators require `@classmethod` decorator127- Type hints are required for validator methods128129```python130from pydantic import BaseModel, Field, field_validator, ConfigDict131132class CreateUserInput(BaseModel):133model_config = ConfigDict(134str_strip_whitespace=True,135validate_assignment=True136)137138name: str = Field(..., description="User's full name", min_length=1, max_length=100)139email: str = Field(..., description="User's email address", pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')140age: int = Field(..., description="User's age", ge=0, le=150)141142@field_validator('email')143@classmethod144def validate_email(cls, v: str) -> str:145if not v.strip():146raise ValueError("Email cannot be empty")147return v.lower()148```149150## Response Format Options151152Support multiple output formats for flexibility:153154```python155from enum import Enum156157class ResponseFormat(str, Enum):158'''Output format for tool responses.'''159MARKDOWN = "markdown"160JSON = "json"161162class UserSearchInput(BaseModel):163query: str = Field(..., description="Search query")164response_format: ResponseFormat = Field(165default=ResponseFormat.MARKDOWN,166description="Output format: 'markdown' for human-readable or 'json' for machine-readable"167)168```169170**Markdown format**:171- Use headers, lists, and formatting for clarity172- Convert timestamps to human-readable format (e.g., "2024-01-15 10:30:00 UTC" instead of epoch)173- Show display names with IDs in parentheses (e.g., "@john.doe (U123456)")174- Omit verbose metadata (e.g., show only one profile image URL, not all sizes)175- Group related information logically176177**JSON format**:178- Return complete, structured data suitable for programmatic processing179- Include all available fields and metadata180- Use consistent field names and types181182## Pagination Implementation183184For tools that list resources:185186```python187class ListInput(BaseModel):188limit: Optional[int] = Field(default=20, description="Maximum results to return", ge=1, le=100)189offset: Optional[int] = Field(default=0, description="Number of results to skip for pagination", ge=0)190191async def list_items(params: ListInput) -> str:192# Make API request with pagination193data = await api_request(limit=params.limit, offset=params.offset)194195# Return pagination info196response = {197"total": data["total"],198"count": len(data["items"]),199"offset": params.offset,200"items": data["items"],201"has_more": data["total"] > params.offset + len(data["items"]),202"next_offset": params.offset + len(data["items"]) if data["total"] > params.offset + len(data["items"]) else None203}204return json.dumps(response, indent=2)205```206207## Error Handling208209Provide clear, actionable error messages:210211```python212def _handle_api_error(e: Exception) -> str:213'''Consistent error formatting across all tools.'''214if isinstance(e, httpx.HTTPStatusError):215if e.response.status_code == 404:216return "Error: Resource not found. Please check the ID is correct."217elif e.response.status_code == 403:218return "Error: Permission denied. You don't have access to this resource."219elif e.response.status_code == 429:220return "Error: Rate limit exceeded. Please wait before making more requests."221return f"Error: API request failed with status {e.response.status_code}"222elif isinstance(e, httpx.TimeoutException):223return "Error: Request timed out. Please try again."224return f"Error: Unexpected error occurred: {type(e).__name__}"225```226227## Shared Utilities228229Extract common functionality into reusable functions:230231```python232# Shared API request function233async def _make_api_request(endpoint: str, method: str = "GET", **kwargs) -> dict:234'''Reusable function for all API calls.'''235async with httpx.AsyncClient() as client:236response = await client.request(237method,238f"{API_BASE_URL}/{endpoint}",239timeout=30.0,240**kwargs241)242response.raise_for_status()243return response.json()244```245246## Async/Await Best Practices247248Always use async/await for network requests and I/O operations:249250```python251# Good: Async network request252async def fetch_data(resource_id: str) -> dict:253async with httpx.AsyncClient() as client:254response = await client.get(f"{API_URL}/resource/{resource_id}")255response.raise_for_status()256return response.json()257258# Bad: Synchronous request259def fetch_data(resource_id: str) -> dict:260response = requests.get(f"{API_URL}/resource/{resource_id}") # Blocks261return response.json()262```263264## Type Hints265266Use type hints throughout:267268```python269from typing import Optional, List, Dict, Any270271async def get_user(user_id: str) -> Dict[str, Any]:272data = await fetch_user(user_id)273return {"id": data["id"], "name": data["name"]}274```275276## Tool Docstrings277278Every tool must have comprehensive docstrings with explicit type information:279280```python281async def search_users(params: UserSearchInput) -> str:282'''283Search for users in the Example system by name, email, or team.284285This tool searches across all user profiles in the Example platform,286supporting partial matches and various search filters. It does NOT287create or modify users, only searches existing ones.288289Args:290params (UserSearchInput): Validated input parameters containing:291- query (str): Search string to match against names/emails (e.g., "john", "@example.com", "team:marketing")292- limit (Optional[int]): Maximum results to return, between 1-100 (default: 20)293- offset (Optional[int]): Number of results to skip for pagination (default: 0)294295Returns:296str: JSON-formatted string containing search results with the following schema:297298Success response:299{300"total": int, # Total number of matches found301"count": int, # Number of results in this response302"offset": int, # Current pagination offset303"users": [304{305"id": str, # User ID (e.g., "U123456789")306"name": str, # Full name (e.g., "John Doe")307"email": str, # Email address (e.g., "[email protected]")308"team": str # Team name (e.g., "Marketing") - optional309}310]311}312313Error response:314"Error: <error message>" or "No users found matching '<query>'"315316Examples:317- Use when: "Find all marketing team members" -> params with query="team:marketing"318- Use when: "Search for John's account" -> params with query="john"319- Don't use when: You need to create a user (use example_create_user instead)320- Don't use when: You have a user ID and need full details (use example_get_user instead)321322Error Handling:323- Input validation errors are handled by Pydantic model324- Returns "Error: Rate limit exceeded" if too many requests (429 status)325- Returns "Error: Invalid API authentication" if API key is invalid (401 status)326- Returns formatted list of results or "No users found matching 'query'"327'''328```329330## Complete Example331332See below for a complete Python MCP server example:333334```python335#!/usr/bin/env python3336'''337MCP Server for Example Service.338339This server provides tools to interact with Example API, including user search,340project management, and data export capabilities.341'''342343from typing import Optional, List, Dict, Any344from enum import Enum345import httpx346from pydantic import BaseModel, Field, field_validator, ConfigDict347from mcp.server.fastmcp import FastMCP348349# Initialize the MCP server350mcp = FastMCP("example_mcp")351352# Constants353API_BASE_URL = "https://api.example.com/v1"354355# Enums356class ResponseFormat(str, Enum):357'''Output format for tool responses.'''358MARKDOWN = "markdown"359JSON = "json"360361# Pydantic Models for Input Validation362class UserSearchInput(BaseModel):363'''Input model for user search operations.'''364model_config = ConfigDict(365str_strip_whitespace=True,366validate_assignment=True367)368369query: str = Field(..., description="Search string to match against names/emails", min_length=2, max_length=200)370limit: Optional[int] = Field(default=20, description="Maximum results to return", ge=1, le=100)371offset: Optional[int] = Field(default=0, description="Number of results to skip for pagination", ge=0)372response_format: ResponseFormat = Field(default=ResponseFormat.MARKDOWN, description="Output format")373374@field_validator('query')375@classmethod376def validate_query(cls, v: str) -> str:377if not v.strip():378raise ValueError("Query cannot be empty or whitespace only")379return v.strip()380381# Shared utility functions382async def _make_api_request(endpoint: str, method: str = "GET", **kwargs) -> dict:383'''Reusable function for all API calls.'''384async with httpx.AsyncClient() as client:385response = await client.request(386method,387f"{API_BASE_URL}/{endpoint}",388timeout=30.0,389**kwargs390)391response.raise_for_status()392return response.json()393394def _handle_api_error(e: Exception) -> str:395'''Consistent error formatting across all tools.'''396if isinstance(e, httpx.HTTPStatusError):397if e.response.status_code == 404:398return "Error: Resource not found. Please check the ID is correct."399elif e.response.status_code == 403:400return "Error: Permission denied. You don't have access to this resource."401elif e.response.status_code == 429:402return "Error: Rate limit exceeded. Please wait before making more requests."403return f"Error: API request failed with status {e.response.status_code}"404elif isinstance(e, httpx.TimeoutException):405return "Error: Request timed out. Please try again."406return f"Error: Unexpected error occurred: {type(e).__name__}"407408# Tool definitions409@mcp.tool(410name="example_search_users",411annotations={412"title": "Search Example Users",413"readOnlyHint": True,414"destructiveHint": False,415"idempotentHint": True,416"openWorldHint": True417}418)419async def example_search_users(params: UserSearchInput) -> str:420'''Search for users in the Example system by name, email, or team.421422[Full docstring as shown above]423'''424try:425# Make API request using validated parameters426data = await _make_api_request(427"users/search",428params={429"q": params.query,430"limit": params.limit,431"offset": params.offset432}433)434435users = data.get("users", [])436total = data.get("total", 0)437438if not users:439return f"No users found matching '{params.query}'"440441# Format response based on requested format442if params.response_format == ResponseFormat.MARKDOWN:443lines = [f"# User Search Results: '{params.query}'", ""]444lines.append(f"Found {total} users (showing {len(users)})")445lines.append("")446447for user in users:448lines.append(f"## {user['name']} ({user['id']})")449lines.append(f"- **Email**: {user['email']}")450if user.get('team'):451lines.append(f"- **Team**: {user['team']}")452lines.append("")453454return "\n".join(lines)455456else:457# Machine-readable JSON format458import json459response = {460"total": total,461"count": len(users),462"offset": params.offset,463"users": users464}465return json.dumps(response, indent=2)466467except Exception as e:468return _handle_api_error(e)469470if __name__ == "__main__":471mcp.run()472```473474---475476## Advanced FastMCP Features477478### Context Parameter Injection479480FastMCP can automatically inject a `Context` parameter into tools for advanced capabilities like logging, progress reporting, resource reading, and user interaction:481482```python483from mcp.server.fastmcp import FastMCP, Context484485mcp = FastMCP("example_mcp")486487@mcp.tool()488async def advanced_search(query: str, ctx: Context) -> str:489'''Advanced tool with context access for logging and progress.'''490491# Report progress for long operations492await ctx.report_progress(0.25, "Starting search...")493494# Log information for debugging495await ctx.log_info("Processing query", {"query": query, "timestamp": datetime.now()})496497# Perform search498results = await search_api(query)499await ctx.report_progress(0.75, "Formatting results...")500501# Access server configuration502server_name = ctx.fastmcp.name503504return format_results(results)505506@mcp.tool()507async def interactive_tool(resource_id: str, ctx: Context) -> str:508'''Tool that can request additional input from users.'''509510# Request sensitive information when needed511api_key = await ctx.elicit(512prompt="Please provide your API key:",513input_type="password"514)515516# Use the provided key517return await api_call(resource_id, api_key)518```519520**Context capabilities:**521- `ctx.report_progress(progress, message)` - Report progress for long operations522- `ctx.log_info(message, data)` / `ctx.log_error()` / `ctx.log_debug()` - Logging523- `ctx.elicit(prompt, input_type)` - Request input from users524- `ctx.fastmcp.name` - Access server configuration525- `ctx.read_resource(uri)` - Read MCP resources526527### Resource Registration528529Expose data as resources for efficient, template-based access:530531```python532@mcp.resource("file://documents/{name}")533async def get_document(name: str) -> str:534'''Expose documents as MCP resources.535536Resources are useful for static or semi-static data that doesn't537require complex parameters. They use URI templates for flexible access.538'''539document_path = f"./docs/{name}"540with open(document_path, "r") as f:541return f.read()542543@mcp.resource("config://settings/{key}")544async def get_setting(key: str, ctx: Context) -> str:545'''Expose configuration as resources with context.'''546settings = await load_settings()547return json.dumps(settings.get(key, {}))548```549550**When to use Resources vs Tools:**551- **Resources**: For data access with simple parameters (URI templates)552- **Tools**: For complex operations with validation and business logic553554### Structured Output Types555556FastMCP supports multiple return types beyond strings:557558```python559from typing import TypedDict560from dataclasses import dataclass561from pydantic import BaseModel562563# TypedDict for structured returns564class UserData(TypedDict):565id: str566name: str567email: str568569@mcp.tool()570async def get_user_typed(user_id: str) -> UserData:571'''Returns structured data - FastMCP handles serialization.'''572return {"id": user_id, "name": "John Doe", "email": "[email protected]"}573574# Pydantic models for complex validation575class DetailedUser(BaseModel):576id: str577name: str578email: str579created_at: datetime580metadata: Dict[str, Any]581582@mcp.tool()583async def get_user_detailed(user_id: str) -> DetailedUser:584'''Returns Pydantic model - automatically generates schema.'''585user = await fetch_user(user_id)586return DetailedUser(**user)587```588589### Lifespan Management590591Initialize resources that persist across requests:592593```python594from contextlib import asynccontextmanager595596@asynccontextmanager597async def app_lifespan():598'''Manage resources that live for the server's lifetime.'''599# Initialize connections, load config, etc.600db = await connect_to_database()601config = load_configuration()602603# Make available to all tools604yield {"db": db, "config": config}605606# Cleanup on shutdown607await db.close()608609mcp = FastMCP("example_mcp", lifespan=app_lifespan)610611@mcp.tool()612async def query_data(query: str, ctx: Context) -> str:613'''Access lifespan resources through context.'''614db = ctx.request_context.lifespan_state["db"]615results = await db.query(query)616return format_results(results)617```618619### Transport Options620621FastMCP supports two main transport mechanisms:622623```python624# stdio transport (for local tools) - default625if __name__ == "__main__":626mcp.run()627628# Streamable HTTP transport (for remote servers)629if __name__ == "__main__":630mcp.run(transport="streamable_http", port=8000)631```632633**Transport selection:**634- **stdio**: Command-line tools, local integrations, subprocess execution635- **Streamable HTTP**: Web services, remote access, multiple clients636637---638639## Code Best Practices640641### Code Composability and Reusability642643Your implementation MUST prioritize composability and code reuse:6446451. **Extract Common Functionality**:646- Create reusable helper functions for operations used across multiple tools647- Build shared API clients for HTTP requests instead of duplicating code648- Centralize error handling logic in utility functions649- Extract business logic into dedicated functions that can be composed650- Extract shared markdown or JSON field selection & formatting functionality6516522. **Avoid Duplication**:653- NEVER copy-paste similar code between tools654- If you find yourself writing similar logic twice, extract it into a function655- Common operations like pagination, filtering, field selection, and formatting should be shared656- Authentication/authorization logic should be centralized657658### Python-Specific Best Practices6596601. **Use Type Hints**: Always include type annotations for function parameters and return values6612. **Pydantic Models**: Define clear Pydantic models for all input validation6623. **Avoid Manual Validation**: Let Pydantic handle input validation with constraints6634. **Proper Imports**: Group imports (standard library, third-party, local)6645. **Error Handling**: Use specific exception types (httpx.HTTPStatusError, not generic Exception)6656. **Async Context Managers**: Use `async with` for resources that need cleanup6667. **Constants**: Define module-level constants in UPPER_CASE667668## Quality Checklist669670Before finalizing your Python MCP server implementation, ensure:671672### Strategic Design673- [ ] Tools enable complete workflows, not just API endpoint wrappers674- [ ] Tool names reflect natural task subdivisions675- [ ] Response formats optimize for agent context efficiency676- [ ] Human-readable identifiers used where appropriate677- [ ] Error messages guide agents toward correct usage678679### Implementation Quality680- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented681- [ ] All tools have descriptive names and documentation682- [ ] Return types are consistent across similar operations683- [ ] Error handling is implemented for all external calls684- [ ] Server name follows format: `{service}_mcp`685- [ ] All network operations use async/await686- [ ] Common functionality is extracted into reusable functions687- [ ] Error messages are clear, actionable, and educational688- [ ] Outputs are properly validated and formatted689690### Tool Configuration691- [ ] All tools implement 'name' and 'annotations' in the decorator692- [ ] Annotations correctly set (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)693- [ ] All tools use Pydantic BaseModel for input validation with Field() definitions694- [ ] All Pydantic Fields have explicit types and descriptions with constraints695- [ ] All tools have comprehensive docstrings with explicit input/output types696- [ ] Docstrings include complete schema structure for dict/JSON returns697- [ ] Pydantic models handle input validation (no manual validation needed)698699### Advanced Features (where applicable)700- [ ] Context injection used for logging, progress, or elicitation701- [ ] Resources registered for appropriate data endpoints702- [ ] Lifespan management implemented for persistent connections703- [ ] Structured output types used (TypedDict, Pydantic models)704- [ ] Appropriate transport configured (stdio or streamable HTTP)705706### Code Quality707- [ ] File includes proper imports including Pydantic imports708- [ ] Pagination is properly implemented where applicable709- [ ] Filtering options are provided for potentially large result sets710- [ ] All async functions are properly defined with `async def`711- [ ] HTTP client usage follows async patterns with proper context managers712- [ ] Type hints are used throughout the code713- [ ] Constants are defined at module level in UPPER_CASE714715### Testing716- [ ] Server runs successfully: `python your_server.py --help`717- [ ] All imports resolve correctly718- [ ] Sample tool calls work as expected719- [ ] Error scenarios handled gracefully