AGENTS.md•34.8 kB
## Python Coding Agent Rules
These rules serve as a definitive guide when creating Python projects to favor minimal, human-verifiable outputs that are easy to reason about and simple to maintain.
### Environment
- Always create a dedicated virtual environment with `uv`.
- If a Python version is specified, use it: `uv venv --python 3.xx`. Otherwise, default to `uv venv --python 3.12`.
- Create a new uv package via `uv init`
- Install dependencies with `uv add ...`.
- Call ALL python commands with `uv` first so we use the environemnt (e.g. `uv run python ...`)
### Package Management
### Dependencies and Research
- Prefer the standard library. Add external dependencies only when strictly necessary or explicitly stated.
- When using any non-built-in module, consult the Context7 MCP server for documentation and examples to accelerate development.
### Simplicity-First Delivery
- Build only the minimum code required to achieve the goal.
- Do not add extensive error handling, tests, or logging unless explicitly requested.
- Keep the project structure shallow and obvious; avoid unnecessary frameworks or abstractions.
### Iteration and Verification
- Work in small, focused steps that are easy for a human to run and verify.
- After each step, be ready to incorporate corrections or design updates based on feedback or new insights.
### Temporary Experiments
- You may create small test modules or scripts to validate ideas or isolate features.
- Remove these once they have served their purpose to keep the repository clean.
### Code Style
- Prefer clarity over cleverness: descriptive names, type hints, and small functions.
- Avoid deep nesting and unnecessary indirection; keep control flow straightforward.
### Documentation and Commands
- Provide only the essential commands to run or reproduce results.
- Keep documentation brief and practical; include just enough context to onboard a mid-level developer quickly.
## Python MCP Development Rules
These rules serve as a definitive guide when creating MCP (Model Context Protocol) servers and clients with Python, emphasizing simplicity, reliability, and agent-friendly patterns. **Follow the MVP approach**: start simple, add complexity only when your specific use case requires it.
### Environment and Dependencies
- Always use Python 3.12 or newer for MCP projects.
- For MCP servers requiring client functionality, use fastMCP 2.10 or newer (github.com/jlowin/fastmcp).
- When data validation is required, use the most up-to-date version of `pydantic` 2.xx.
- Always consult Context7 MCP for searchable, up-to-date documentation and code examples before deciding on an approach.
### MCP Configuration Format
MCP servers are configured through JSON configuration files (typically `mcp.json`) that define how MCP Host applications connect to and launch servers. Understanding this format is essential for providing clear setup instructions with your Python MCP servers.
#### Configuration File Structure
The configuration uses a root `mcpServers` object where each key is a unique server identifier and each value contains the connection configuration:
```json
{
"mcpServers": {
"my-server-name": {
// Server configuration object
}
}
}
```
**Server Identifier Guidelines:**
- Use camelCase naming (e.g., "weatherTools", "databaseHelper")
- Avoid whitespace and special characters
- Make names descriptive of the server's purpose
- Names appear in MCP Host UIs for user reference
#### Local Server Configuration (stdio Transport)
Local servers run as subprocesses and communicate via standard input/output. This is the most common configuration for Python MCP servers.
**Required Fields:**
- `type`: Must be `"stdio"`
- `command`: Executable or command to launch the server
**Optional Fields:**
- `args`: Array of command-line arguments
- `env`: Environment variables as key-value pairs
- `cwd`: Working directory for command execution
- `timeout`: Request timeout in seconds
```json
{
"mcpServers": {
"pythonCalculator": {
"type": "stdio",
"command": "uv",
"args": ["run", "--directory", "/path/to/project", "python", "-m", "calculator_server"]
},
"localFileServer": {
"type": "stdio",
"command": "python",
"args": ["/path/to/file_server.py"],
"cwd": "/path/to/project",
"env": {
"LOG_LEVEL": "INFO",
"DATA_PATH": "/path/to/data"
}
},
"dockerizedServer": {
"type": "stdio",
"command": "docker",
"args": [
"run", "--rm", "-i",
"my-mcp-server:latest"
]
}
}
}
```
#### Remote Server Configuration (http/sse Transport)
Remote servers run independently and are accessed via HTTP. This is ideal for shared services, cloud deployments, or servers requiring network access.
**Required Fields:**
- `type`: Either `"http"` or `"sse"`
- `url`: Full URL to the server endpoint
**Optional Fields:**
- `headers`: HTTP headers (typically for authentication)
- `timeout`: Network request timeout
```json
{
"mcpServers": {
"cloudWeatherAPI": {
"type": "http",
"url": "https://my-mcp-server.herokuapp.com/mcp/",
"headers": {
"Authorization": "Bearer your-api-key-here",
"X-Client-Version": "1.0"
}
},
"internalService": {
"type": "http",
"url": "http://localhost:8000/mcp/",
"timeout": 30
}
}
}
```
#### Security Considerations
**For Local Servers:**
- The `command` and `args` fields enable arbitrary code execution
- Only use trusted server implementations
- Validate all paths and avoid dynamic argument construction
- Be cautious with `env` variables containing secrets
**For Remote Servers:**
- Never hardcode API keys or secrets in the configuration file
- Use environment variables or secure credential management
- Ensure HTTPS for production deployments
- Validate server certificates and origins
#### Configuration Documentation Guidelines
**Always analyze your server code first** to determine what configuration options it actually uses. Only include examples for features your server implements.
**Steps to create server-specific configuration examples:**
1. **Examine environment variables** your server reads (e.g., `os.getenv()` calls)
2. **Check HTTP configuration** if your server supports HTTP transport (`HOST`, `PORT` variables)
3. **Identify authentication** if your server validates headers or credentials
4. **Review file paths** and working directory requirements
**Example Analysis Process:**
```python
# Server code analysis:
user_managed = os.getenv("USER_MANAGED", "false") # → Include in env example
mcp_host = os.getenv("HOST", "127.0.0.1") # → Server supports HTTP
mcp_port = os.getenv("PORT", None) # → HTTP port configuration
# No auth header checks → Don't include authorization
# Results in configuration examples:
# ✓ Basic stdio config
# ✓ stdio config with USER_MANAGED env var
# ✓ HTTP config (since server supports it)
# ✗ Authorization headers (server doesn't use them)
```
**Configuration Template Structure:**
```python
"""
MCP Server Configuration Examples:
=== Local/stdio Configuration ===
{
"mcpServers": {
"serverName": {
"type": "stdio",
"command": "uv",
"args": ["run", "--directory", "/path/to/project", "python", "server_file.py"]
}
}
}
[Include environment section only if server uses environment variables]
=== Local/stdio Configuration with Environment ===
{
"mcpServers": {
"serverName": {
"type": "stdio",
"command": "uv",
"args": ["run", "--directory", "/path/to/project", "python", "server_file.py"],
"env": {
"ACTUAL_ENV_VAR": "value"
}
}
}
}
[Include HTTP section only if server supports HTTP transport]
=== Remote/HTTP Configuration ===
{
"mcpServers": {
"serverName": {
"type": "http",
"url": "https://your-domain.com/mcp/"
}
}
}
[Include headers section only if server validates authentication]
=== Remote/HTTP Configuration with Authentication ===
{
"mcpServers": {
"serverName": {
"type": "http",
"url": "https://your-domain.com/mcp/",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
Place this configuration in:
- VS Code: .vscode/mcp.json (project) or user settings
- Claude Desktop: claude_desktop_config.json
- Cursor: .cursor/mcp.json (project) or ~/.cursor/mcp.json (user)
- LM Studio: ~/.lmstudio/mcp.json
"""
```
#### Common Configuration Patterns
**Python Package Installation:**
```json
{
"mcpServers": {
"packagedServer": {
"type": "stdio",
"command": "uvx",
"args": ["my-mcp-package"]
}
}
}
```
**Development with uv:**
```json
{
"mcpServers": {
"devServer": {
"type": "stdio",
"command": "uv",
"args": ["run", "--directory", "/path/to/project", "python", "server.py"]
}
}
}
```
**Virtual Environment:**
```json
{
"mcpServers": {
"venvServer": {
"type": "stdio",
"command": "/path/to/venv/bin/python",
"args": ["/path/to/server.py"]
}
}
}
```
### Server Instructions
- **Always include clear, concise instructions** when creating FastMCP servers to help agents understand purpose and usage.
- Keep instructions brief - agents already understand tools through type annotations and docstrings.
- Focus on **what the server does** and **when to use it**, not how individual tools work.
- Use simple, direct language that explains the server's domain and typical workflows.
```python
# Clear purpose and usage guidance
mcp = FastMCP(
name="FileAnalysisServer",
instructions="""
Analyzes files and directories for insights.
Use for code analysis, file statistics, and content extraction.
Tools work with local file paths and common file formats.
"""
)
# Domain-specific context
mcp = FastMCP(
name="DatabaseServer",
instructions="""
Manages SQLite database operations.
Use for data queries, schema inspection, and basic CRUD operations.
Automatically handles connection pooling and transaction safety.
"""
)
# Workflow-oriented guidance
mcp = FastMCP(
name="DocumentProcessor",
instructions="""
Processes documents through analysis pipeline.
Start with upload_document, then use analyze_content or extract_metadata.
Supports PDF, DOCX, and plain text formats.
"""
)
```
#### Instructions Best Practices
- **Start with the server's core purpose** in one clear sentence.
- **Mention primary use cases** or scenarios where this server is helpful.
- **Note any important limitations** or requirements (file formats, authentication, etc.).
- **Indicate common workflows** when tools have dependencies or suggested order.
- **Avoid repeating tool documentation** - let type hints and docstrings handle specifics.
### Server Design Patterns
#### Entrypoint Structure
- Design FastMCP servers to support both stdio and streamable-http transport modes.
- Keep the server module clean by isolating transport logic in the entrypoint.
- Use this standard pattern for maximum flexibility:
```python
from fastmcp import FastMCP
import os
# Create server with clear instructions
mcp = FastMCP(
name="CalculatorServer",
instructions="""
Performs mathematical calculations and analysis.
Use for arithmetic, statistical operations, and number formatting.
All operations handle standard Python numeric types.
"""
)
@mcp.tool
def add(a: float, b: float) -> float:
"""Add two numbers together."""
return a + b
@mcp.tool
def calculate_average(numbers: list[float]) -> dict:
"""Calculate average and statistics for a list of numbers."""
if not numbers:
raise ValueError("Cannot calculate average of empty list")
avg = sum(numbers) / len(numbers)
return {"average": avg, "count": len(numbers), "sum": sum(numbers)}
if __name__ == "__main__":
mcp_host = os.getenv("HOST", "127.0.0.1")
mcp_port = os.getenv("PORT", None)
if mcp_port:
mcp.run(port=int(mcp_port), host=mcp_host, transport="streamable-http")
else:
mcp.run()
```
### Client Development
- When creating MCP clients, prioritize using the MCPConfig structure from fastMCP.
- This approach enables standard MCP configuration JSON format compatibility.
- Maintain consistency with established MCP client patterns.
### Agent Integration
- For MCP servers or clients that interact with agents, use the latest version of `pydantic-ai`.
- Design agent interactions to be stateless and predictable.
- Keep agent-facing interfaces simple and well-documented.
### Code Organization
- Structure MCP projects with clear separation between transport, business logic, and data models.
- Avoid deep nesting in tool definitions and resource handlers.
- Use descriptive names for MCP tools and resources that clearly indicate their purpose.
### Session Management and Caching
#### Session Identification (Keep It Simple)
- FastMCP automatically provides session tracking through the `Context` object.
- Access session identifiers only when you need to cache data or maintain state between tool calls.
- **Start without session management** unless your specific use case requires it.
```python
from fastmcp import FastMCP, Context
mcp = FastMCP(name="SessionAwareServer")
@mcp.tool
def get_user_preferences(user_id: str, ctx: Context) -> dict:
"""Get user preferences with session awareness."""
# Access session identifiers when needed
session_id = ctx.session_id # Persistent across tool calls in same session
client_id = ctx.client_id # Identifies the client (may be None)
request_id = ctx.request_id # Unique per tool call
# Use session_id as cache key for persistent data
cache_key = f"user_prefs:{session_id}:{user_id}"
return {"preferences": "cached_data", "session": session_id}
```
#### Request-Level State Management
- Use `ctx.set_state()` and `ctx.get_state()` for sharing data within a single request.
- State is automatically isolated between requests - no cleanup needed.
- This is perfect for middleware patterns and request-scoped caching.
```python
@mcp.tool
def initialize_user_session(user_id: str, ctx: Context) -> dict:
"""Initialize user data for subsequent tool calls."""
# Store data for other tools in this request
ctx.set_state("current_user", user_id)
ctx.set_state("user_permissions", ["read", "write"])
return {"user": user_id, "initialized": True}
@mcp.tool
def perform_user_action(action: str, ctx: Context) -> dict:
"""Perform action using previously stored user data."""
# Access data set by previous tool calls
user_id = ctx.get_state("current_user")
permissions = ctx.get_state("user_permissions")
if not user_id:
raise ValueError("User session not initialized")
return {"action": action, "user": user_id, "allowed": action in permissions}
```
#### Session-Based Caching Patterns
- Use `session_id` for data that should persist across multiple tool calls.
- Combine with external storage (Redis, SQLite) only when needed.
- Keep cache keys simple and predictable.
```python
import sqlite3
from pathlib import Path
# Simple session cache using SQLite (add only when needed)
def get_session_cache(session_id: str, key: str) -> str | None:
"""Simple session-based cache getter."""
db_path = Path("session_cache.db")
with sqlite3.connect(db_path) as conn:
cursor = conn.execute(
"SELECT value FROM cache WHERE session_id = ? AND key = ?",
(session_id, key)
)
result = cursor.fetchone()
return result[0] if result else None
def set_session_cache(session_id: str, key: str, value: str) -> None:
"""Simple session-based cache setter."""
db_path = Path("session_cache.db")
with sqlite3.connect(db_path) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS cache (
session_id TEXT, key TEXT, value TEXT,
PRIMARY KEY (session_id, key)
)
""")
conn.execute(
"INSERT OR REPLACE INTO cache VALUES (?, ?, ?)",
(session_id, key, value)
)
@mcp.tool
def remember_preference(setting: str, value: str, ctx: Context) -> dict:
"""Remember a user preference for this session."""
session_id = ctx.session_id
set_session_cache(session_id, f"pref:{setting}", value)
return {"saved": setting, "value": value, "session": session_id}
@mcp.tool
def get_preference(setting: str, ctx: Context) -> dict:
"""Retrieve a previously saved preference."""
session_id = ctx.session_id
value = get_session_cache(session_id, f"pref:{setting}")
return {"setting": setting, "value": value, "found": value is not None}
```
#### Command History and Reference Tracking
- Use request_id to track individual command executions.
- Store command results when you need to reference previous executions.
- Keep history simple - store only what you actually need to reference.
```python
@mcp.tool
def execute_command(command: str, ctx: Context) -> dict:
"""Execute a command and track it for reference."""
request_id = ctx.request_id
session_id = ctx.session_id
# Execute the command (simplified)
result = f"Output of: {command}"
# Store for potential reference by other tools
set_session_cache(session_id, f"cmd:{request_id}", command)
set_session_cache(session_id, f"result:{request_id}", result)
ctx.info(f"Command executed with ID: {request_id}")
return {
"command": command,
"result": result,
"request_id": request_id,
"reference": f"Use request ID {request_id} to reference this command"
}
@mcp.tool
def reference_previous_command(request_id: str, ctx: Context) -> dict:
"""Reference a previously executed command by its request ID."""
session_id = ctx.session_id
command = get_session_cache(session_id, f"cmd:{request_id}")
result = get_session_cache(session_id, f"result:{request_id}")
if not command:
return {"error": f"No command found for request ID: {request_id}"}
return {
"original_command": command,
"original_result": result,
"request_id": request_id
}
```
#### Session Management Best Practices
- **Start simple**: Use only request-level state (`ctx.set_state/get_state`) for most cases.
- **Add session persistence** only when you need data to survive across multiple tool calls.
- **Use external storage** (Redis/SQLite) only when you need persistence beyond the server lifetime.
- **Keep identifiers accessible**: Log session_id and request_id in tool outputs when users need to reference them.
- **Clean up when possible**: Implement session cleanup for long-running servers, but don't over-engineer it.
### Tool Development
#### Tool Registration Approaches
- **Start simple with `@mcp.tool` decorator** for straightforward tools with minimal logic.
- For complex tools, separate business logic from MCP registration and add tools programmatically.
- Use `mcp.tool(function)` method for registering existing functions without decorators.
- Choose the approach that keeps your code clear and maintainable.
```python
from fastmcp import FastMCP
mcp = FastMCP(name="CalculatorServer")
# Simple decorator approach for basic tools
@mcp.tool
def add(a: int, b: int) -> int:
"""Adds two integer numbers together."""
return a + b
# Method 1: Register existing function without decorator
def multiply(a: int, b: int) -> int:
"""Multiplies two numbers."""
return a * b
# Register the function as a tool
mcp.tool(multiply)
# Method 2: Register with custom name
def divide_numbers(a: float, b: float) -> float:
"""Divides two numbers."""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# Register with custom name and options
mcp.tool(divide_numbers, name="divide")
# Method 3: Separate complex business logic (no wrapper needed)
def process_complex_data(input_data: dict) -> dict:
"""Process complex data with business logic."""
# Heavy processing, multiple steps, etc.
# This function is already properly annotated for MCP
return {"processed": input_data, "status": "complete"}
# Register directly - no wrapper needed
mcp.tool(process_complex_data)
```
#### Async vs Sync Functions
- **Start with sync functions** for simple tools unless you specifically need async I/O.
- Use async functions when you have actual I/O operations (database, API calls, file access).
- Don't add async complexity unless it's required for your use case.
```python
from fastmcp import FastMCP
mcp = FastMCP()
# Start simple: Sync for basic operations
@mcp.tool
def calculate_factorial(n: int) -> int:
"""Calculate factorial of a number."""
return math.factorial(n)
# Use async when you actually need I/O
@mcp.tool
async def fetch_user_data(user_id: str) -> dict:
"""Fetch user data from database."""
return await database.get_user(user_id)
```
#### Type Annotations (Essential)
- **Always use basic type annotations** for parameters and return values.
- Start with simple types (`str`, `int`, `dict`) and add complexity only when needed.
- Type annotations enable automatic schema generation and validation.
```python
# Start simple
@mcp.tool
def process_text(text: str, max_length: int = 100) -> dict:
"""Process text data."""
return {"result": text[:max_length], "length": len(text)}
# Add complexity only when needed
from typing import Literal
@mcp.tool
def process_with_format(
text: str,
format: Literal["json", "xml"] = "json"
) -> dict:
"""Process text with specific format."""
# Implementation...
```
#### Parameter Validation (When Needed)
- Start without validation constraints unless your use case specifically requires them.
- Add `Field` validation only when you need to enforce specific business rules.
- Keep validation simple and focused on actual requirements.
```python
# Simple approach (preferred)
@mcp.tool
def analyze_metrics(count: int, user_id: str) -> dict:
"""Analyze metrics."""
if count < 0 or count > 100:
raise ValueError("Count must be between 0 and 100")
return {"count": count, "user": user_id}
# Add Field validation only when business rules require it
from typing import Annotated
from pydantic import Field
@mcp.tool
def validate_user_id(
user_id: Annotated[str, Field(pattern=r"^[A-Z]{2}\d{4}$")]
) -> dict:
"""Validate user ID format when strict format required."""
return {"valid": True, "user_id": user_id}
```
#### Return Values and Structured Output
- **Always annotate return types** to enable automatic output schema generation.
- Leverage FastMCP 2.10+ structured output for machine-readable results.
- Object-like returns (dict, Pydantic models) automatically become structured content.
- Primitive returns get wrapped under a "result" key in structured output.
```python
from dataclasses import dataclass
from fastmcp import FastMCP
mcp = FastMCP()
@dataclass
class UserProfile:
name: str
age: int
email: str
@mcp.tool
def get_user_profile(user_id: str) -> UserProfile:
"""Get a user's profile information."""
return UserProfile(name="Alice", age=30, email="alice@example.com")
@mcp.tool
def calculate_sum(a: int, b: int) -> int:
"""Add two numbers (result wrapped automatically)."""
return a + b # Returns {"result": 8} in structured output
```
#### Tool Annotations and Metadata
- Use annotations to provide semantic hints about tool behavior.
- Set `readOnlyHint=True` for tools that don't modify state.
- Mark potentially destructive operations with appropriate annotations.
- Add descriptive titles for better user interfaces.
```python
@mcp.tool(
name="calculate_sum",
description="Add two numbers together safely",
annotations={
"title": "Calculate Sum",
"readOnlyHint": True,
"idempotentHint": True,
"openWorldHint": False
},
tags={"math", "calculation"}
)
def add_numbers(a: float, b: float) -> float:
"""Add two numbers together."""
return a + b
```
#### Context Access
- Use `Context` parameter for accessing MCP features (logging, progress, resources).
- Implement progress reporting for long-running operations.
- Leverage context for resource access and LLM sampling capabilities.
```python
from fastmcp import FastMCP, Context
mcp = FastMCP(name="ContextDemo")
@mcp.tool
async def process_large_file(file_uri: str, ctx: Context) -> dict:
"""Process a large file with progress reporting."""
await ctx.info(f"Starting to process {file_uri}")
# Read resource through context
resource = await ctx.read_resource(file_uri)
data = resource[0].content if resource else ""
# Report progress
await ctx.report_progress(progress=50, total=100)
# Process data...
result = {"size": len(data), "processed": True}
await ctx.report_progress(progress=100, total=100)
return result
```
#### Advanced Tool Features
- Use `exclude_args` to hide runtime-injected parameters from tool schema.
- Implement dynamic tool enabling/disabling for feature flags and maintenance.
- Use `ToolResult` for complete control over content and structured output.
- Add custom metadata with `meta` parameter for versioning and application-specific data.
```python
from fastmcp.tools.tool import ToolResult
from fastmcp import FastMCP
mcp = FastMCP()
# Use these features only when you actually need them
@mcp.tool(exclude_args=["user_id"])
def get_user_profile(name: str, user_id: str = None) -> dict:
"""Get user profile (user_id injected by server)."""
return {"name": name, "user_id": user_id}
# Dynamic tool control for feature flags
@mcp.tool(enabled=False)
def maintenance_feature() -> str:
"""Feature under maintenance."""
return "Service unavailable"
maintenance_feature.enable() # Enable when ready
```
#### Separation of Concerns and Startup Registration
- **Design your functions to be MCP-ready from the start** with proper type annotations and docstrings.
- Register tools programmatically at startup when you have complex initialization.
- No wrapper functions needed if your business logic is already MCP-compatible.
```python
# business_logic.py - MCP-ready business logic
def process_document(content: str, format: str = "default") -> dict:
"""Process document content with specified format."""
# Heavy processing, multiple steps, validation, etc.
processed = content.upper() # Simplified example
return {
"original_length": len(content),
"processed_content": processed,
"format_used": format
}
def analyze_sentiment(text: str) -> dict:
"""Analyze text sentiment using ML/NLP processing."""
# Complex ML/NLP processing
return {"sentiment": "positive", "confidence": 0.85}
# mcp_server.py - Clean MCP server registration
from fastmcp import FastMCP
from .business_logic import process_document, analyze_sentiment
mcp = FastMCP(name="DocumentServer")
def register_tools():
"""Register all tools at startup time."""
# Register functions directly - no wrappers needed
mcp.tool(process_document)
mcp.tool(analyze_sentiment)
if __name__ == "__main__":
register_tools() # Set up tools at startup
mcp.run()
```
### Tool Response Format Policy
#### String vs Structured Response Guidelines
- **For text-only responses**: Return raw strings directly to avoid JSON wrapping overhead and string escaping
- **For structured data**: Use dictionaries, dataclasses, or Pydantic models to enable machine-readable output
- **Avoid primitive wrapping inefficiency**: Prefer returning structured objects over primitive types when the response contains meaningful data
```python
from datetime import datetime
from fastmcp import FastMCP
from fastmcp.tools.tool import ToolResult
from mcp.types import TextContent
mcp = FastMCP(name="ResponseServer")
# PREFERRED: Raw string for text-only responses
@mcp.tool
def generate_report(data: str) -> ToolResult:
"""Generate a human-readable report."""
report_text = f"Analysis Report:\n{data}\nGenerated on {datetime.now()}"
# Return raw text without JSON wrapping - more efficient
return ToolResult(content=[TextContent(type="text", text=report_text)])
# PREFERRED: Structured response for data that should be machine-readable
@mcp.tool
def analyze_metrics(data: str) -> dict:
"""Analyze data and return structured metrics."""
return {
"word_count": len(data.split()),
"char_count": len(data),
"timestamp": datetime.now().isoformat(),
"status": "completed"
}
# AVOID: Primitive returns that get auto-wrapped in {"result": value}
@mcp.tool
def bad_example(text: str) -> str:
"""This creates inefficient JSON wrapping."""
return f"Processed: {text}" # Becomes {"result": "Processed: text"}
# BETTER: Use ToolResult for text-only output
@mcp.tool
def better_example(text: str) -> ToolResult:
"""This returns raw text efficiently."""
return ToolResult(content=[TextContent(type="text", text=f"Processed: {text}")])
```
#### Response Format Decision Matrix
| Response Type | Use Case | Return Type | Rationale |
|---------------|----------|-------------|-----------|
| **Text Report** | Human-readable content, logs, formatted output | `ToolResult` with `TextContent` | Avoids JSON escaping, more efficient for text |
| **Status Message** | Simple success/error messages | `ToolResult` with `TextContent` | No need for structured parsing |
| **Data Analysis** | Metrics, counts, structured results | `dict` or dataclass | Enables machine processing |
| **Configuration** | Settings, parameters, key-value data | `dict` or Pydantic model | Clear structure, validation |
| **Mixed Content** | Text + structured data | `ToolResult` with both content and structured_content | Best of both worlds |
#### Implementation Guidelines
1. **Default to raw text**: If the primary use case is human consumption, use `ToolResult` with `TextContent`
2. **Structure when beneficial**: Use structured responses when clients need to programmatically process the data
3. **Avoid primitive auto-wrapping**: Never return bare strings, integers, or booleans unless you specifically want the `{"result": value}` wrapper
4. **Consider the consumer**: Think about whether agents/clients will parse the response or just display it
This policy minimizes unnecessary JSON overhead while preserving structured output capabilities where they add value.
### Error Handling
- **Start simple**: Let Python exceptions bubble up naturally for most cases.
- Use `ToolError` only when you need specific error messages for agents.
- Add error masking (`mask_error_details=True`) only in production when needed.
```python
from fastmcp import FastMCP
mcp = FastMCP(name="SimpleServer")
@mcp.tool
def divide(a: float, b: float) -> float:
"""Divide a by b."""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# Use ToolError only when you need agent-specific messaging
from fastmcp.exceptions import ToolError
@mcp.tool
def process_file(filename: str) -> dict:
"""Process a file."""
if not filename.endswith('.txt'):
raise ToolError("Only .txt files are supported")
# Process file...
return {"status": "processed"}
```
### Testing MCP Servers
#### In-Memory Testing (Preferred)
- **Always prefer in-memory testing** over subprocess/network-based testing for unit tests.
- Pass your FastMCP server instance directly to the FastMCP Client for zero-overhead connections.
- In-memory testing runs entirely within the same Python process, eliminating network complexity.
- This approach enables debugger breakpoints in both test code and server handlers.
```python
from fastmcp import FastMCP, Client
import pytest
@pytest.fixture
def weather_server():
server = FastMCP("WeatherServer")
@server.tool
def get_temperature(city: str) -> dict:
temps = {"NYC": 72, "LA": 85, "Chicago": 68}
return {"city": city, "temp": temps.get(city, 70)}
return server
@pytest.mark.asyncio
async def test_temperature_tool(weather_server):
async with Client(weather_server) as client:
result = await client.call_tool("get_temperature", {"city": "LA"})
assert result.data == {"city": "LA", "temp": 85}
```
#### Transport Inference for Testing
- The FastMCP Client automatically infers transport based on input:
- `FastMCP` instance → In-memory transport (ideal for testing)
- File path ending in `.py` → Python Stdio transport
- URL starting with `http://` or `https://` → HTTP transport
#### Mocking External Dependencies
- Use `unittest.mock.AsyncMock` for external services (databases, APIs) in tests.
- This ensures tests are fast, deterministic, and don't require external infrastructure.
```python
from unittest.mock import AsyncMock
async def test_database_tool():
server = FastMCP("DataServer")
# Mock the database
mock_db = AsyncMock()
mock_db.fetch_users.return_value = [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
@server.tool
async def list_users() -> list:
return await mock_db.fetch_users()
async with Client(server) as client:
result = await client.call_tool("list_users", {})
assert len(result.data) == 2
assert result.data[0]["name"] == "Alice"
mock_db.fetch_users.assert_called_once()
```
#### Testing Deployed Servers (When Necessary)
- Use HTTP transport testing only for deployment validation, authentication testing, or network behavior verification.
- Connect to running servers via their URL for integration tests.
```python
async def test_deployed_server():
# Connect to a running server
async with Client("http://localhost:8000/mcp/") as client:
await client.ping()
# Test with real network transport
tools = await client.list_tools()
assert len(tools) > 0
result = await client.call_tool("greet", {"name": "World"})
assert "Hello" in result.data
```
#### Key Testing Benefits
- **Performance**: In-memory tests execute instantly without network overhead.
- **Simplicity**: No server startup scripts, port management, or cleanup between tests.
- **Debugging**: Full debugger support across client and server code.
- **Reliability**: Eliminates network-related test flakiness and infrastructure dependencies.