Initial commit
This commit is contained in:
49
templates/.env.example
Normal file
49
templates/.env.example
Normal file
@@ -0,0 +1,49 @@
|
||||
# FastMCP Server Configuration
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# Server Configuration
|
||||
SERVER_NAME="My FastMCP Server"
|
||||
ENVIRONMENT="development" # development, staging, production
|
||||
|
||||
# API Configuration (if integrating with external API)
|
||||
API_BASE_URL="https://api.example.com"
|
||||
API_KEY="your-api-key-here"
|
||||
API_SECRET="your-api-secret-here"
|
||||
API_TIMEOUT="30"
|
||||
|
||||
# Database Configuration (if using database)
|
||||
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
|
||||
|
||||
# Cache Configuration
|
||||
CACHE_TTL="300" # Cache time-to-live in seconds (5 minutes)
|
||||
ENABLE_CACHE="true"
|
||||
|
||||
# Retry Configuration
|
||||
MAX_RETRIES="3"
|
||||
|
||||
# OpenAPI Configuration (if using OpenAPI integration)
|
||||
OPENAPI_SPEC_URL="https://api.example.com/openapi.json"
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL="INFO" # DEBUG, INFO, WARNING, ERROR
|
||||
|
||||
# Features (optional)
|
||||
ENABLE_PROGRESS_TRACKING="true"
|
||||
ENABLE_ELICITATION="true"
|
||||
ENABLE_SAMPLING="true"
|
||||
|
||||
# Rate Limiting (optional)
|
||||
RATE_LIMIT_REQUESTS="100" # Max requests per time window
|
||||
RATE_LIMIT_WINDOW="60" # Time window in seconds
|
||||
|
||||
# Security (optional)
|
||||
ALLOWED_ORIGINS="*"
|
||||
ENABLE_CORS="true"
|
||||
|
||||
# FastMCP Cloud (for deployment)
|
||||
# These will be set automatically by FastMCP Cloud
|
||||
# FASTMCP_ENV="production"
|
||||
# FASTMCP_REGION="us-west"
|
||||
|
||||
# Custom Configuration
|
||||
# Add any custom environment variables your server needs
|
||||
413
templates/api-client-pattern.py
Normal file
413
templates/api-client-pattern.py
Normal file
@@ -0,0 +1,413 @@
|
||||
"""
|
||||
FastMCP API Client Pattern
|
||||
===========================
|
||||
Manual API integration with connection pooling, caching, and retry logic.
|
||||
"""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
import httpx
|
||||
import os
|
||||
import time
|
||||
import asyncio
|
||||
from typing import Optional, Any, Dict
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
mcp = FastMCP("API Client Pattern")
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
class Config:
|
||||
"""API configuration from environment."""
|
||||
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.example.com")
|
||||
API_KEY = os.getenv("API_KEY", "")
|
||||
API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
|
||||
CACHE_TTL = int(os.getenv("CACHE_TTL", "300")) # 5 minutes
|
||||
MAX_RETRIES = int(os.getenv("MAX_RETRIES", "3"))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# API Client with Connection Pooling
|
||||
# ============================================================================
|
||||
|
||||
class APIClient:
|
||||
"""Singleton API client with connection pooling."""
|
||||
_instance: Optional[httpx.AsyncClient] = None
|
||||
|
||||
@classmethod
|
||||
async def get_client(cls) -> httpx.AsyncClient:
|
||||
"""Get or create the shared HTTP client."""
|
||||
if cls._instance is None:
|
||||
cls._instance = httpx.AsyncClient(
|
||||
base_url=Config.API_BASE_URL,
|
||||
headers={
|
||||
"Authorization": f"Bearer {Config.API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "FastMCP-Client/1.0"
|
||||
},
|
||||
timeout=httpx.Timeout(Config.API_TIMEOUT),
|
||||
limits=httpx.Limits(
|
||||
max_keepalive_connections=5,
|
||||
max_connections=10
|
||||
)
|
||||
)
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
async def cleanup(cls):
|
||||
"""Cleanup the HTTP client."""
|
||||
if cls._instance:
|
||||
await cls._instance.aclose()
|
||||
cls._instance = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Cache Implementation
|
||||
# ============================================================================
|
||||
|
||||
class SimpleCache:
|
||||
"""Time-based cache for API responses."""
|
||||
|
||||
def __init__(self, ttl: int = 300):
|
||||
self.ttl = ttl
|
||||
self.cache: Dict[str, Any] = {}
|
||||
self.timestamps: Dict[str, float] = {}
|
||||
|
||||
def get(self, key: str) -> Optional[Any]:
|
||||
"""Get cached value if not expired."""
|
||||
if key in self.cache:
|
||||
if time.time() - self.timestamps[key] < self.ttl:
|
||||
return self.cache[key]
|
||||
else:
|
||||
# Expired, remove it
|
||||
del self.cache[key]
|
||||
del self.timestamps[key]
|
||||
return None
|
||||
|
||||
def set(self, key: str, value: Any):
|
||||
"""Set cache value with timestamp."""
|
||||
self.cache[key] = value
|
||||
self.timestamps[key] = time.time()
|
||||
|
||||
def invalidate(self, pattern: Optional[str] = None):
|
||||
"""Invalidate cache entries."""
|
||||
if pattern:
|
||||
keys_to_delete = [k for k in self.cache if pattern in k]
|
||||
for key in keys_to_delete:
|
||||
del self.cache[key]
|
||||
del self.timestamps[key]
|
||||
else:
|
||||
self.cache.clear()
|
||||
self.timestamps.clear()
|
||||
|
||||
|
||||
# Global cache instance
|
||||
cache = SimpleCache(ttl=Config.CACHE_TTL)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Retry Logic with Exponential Backoff
|
||||
# ============================================================================
|
||||
|
||||
async def retry_with_backoff(
|
||||
func,
|
||||
max_retries: int = 3,
|
||||
initial_delay: float = 1.0,
|
||||
exponential_base: float = 2.0
|
||||
):
|
||||
"""Retry function with exponential backoff."""
|
||||
delay = initial_delay
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return await func()
|
||||
except (httpx.TimeoutException, httpx.NetworkError) as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries - 1:
|
||||
print(f"Attempt {attempt + 1} failed, retrying in {delay}s...")
|
||||
await asyncio.sleep(delay)
|
||||
delay *= exponential_base
|
||||
except httpx.HTTPStatusError as e:
|
||||
# Don't retry client errors (4xx)
|
||||
if 400 <= e.response.status_code < 500:
|
||||
raise
|
||||
last_exception = e
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(delay)
|
||||
delay *= exponential_base
|
||||
|
||||
raise last_exception
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# API Tools
|
||||
# ============================================================================
|
||||
|
||||
@mcp.tool()
|
||||
async def api_get(
|
||||
endpoint: str,
|
||||
use_cache: bool = True
|
||||
) -> dict:
|
||||
"""
|
||||
Make a GET request to the API.
|
||||
|
||||
Args:
|
||||
endpoint: API endpoint path (e.g., "/users/123")
|
||||
use_cache: Whether to use cached response if available
|
||||
|
||||
Returns:
|
||||
API response data or error
|
||||
"""
|
||||
cache_key = f"GET:{endpoint}"
|
||||
|
||||
# Check cache
|
||||
if use_cache:
|
||||
cached = cache.get(cache_key)
|
||||
if cached:
|
||||
return {
|
||||
"success": True,
|
||||
"data": cached,
|
||||
"from_cache": True,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Make request with retry
|
||||
async def make_request():
|
||||
client = await APIClient.get_client()
|
||||
response = await client.get(endpoint)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
try:
|
||||
data = await retry_with_backoff(make_request, max_retries=Config.MAX_RETRIES)
|
||||
|
||||
# Cache successful response
|
||||
if use_cache:
|
||||
cache.set(cache_key, data)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": data,
|
||||
"from_cache": False,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"HTTP {e.response.status_code}",
|
||||
"message": e.response.text,
|
||||
"endpoint": endpoint
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"endpoint": endpoint
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def api_post(
|
||||
endpoint: str,
|
||||
data: dict,
|
||||
invalidate_cache: bool = True
|
||||
) -> dict:
|
||||
"""
|
||||
Make a POST request to the API.
|
||||
|
||||
Args:
|
||||
endpoint: API endpoint path
|
||||
data: Request body data
|
||||
invalidate_cache: Whether to invalidate related cache entries
|
||||
|
||||
Returns:
|
||||
API response or error
|
||||
"""
|
||||
async def make_request():
|
||||
client = await APIClient.get_client()
|
||||
response = await client.post(endpoint, json=data)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
try:
|
||||
result = await retry_with_backoff(make_request, max_retries=Config.MAX_RETRIES)
|
||||
|
||||
# Invalidate cache for related endpoints
|
||||
if invalidate_cache:
|
||||
cache.invalidate(endpoint.split('/')[1] if '/' in endpoint else endpoint)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": result,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"HTTP {e.response.status_code}",
|
||||
"message": e.response.text
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def api_put(
|
||||
endpoint: str,
|
||||
data: dict,
|
||||
invalidate_cache: bool = True
|
||||
) -> dict:
|
||||
"""Make a PUT request to the API."""
|
||||
async def make_request():
|
||||
client = await APIClient.get_client()
|
||||
response = await client.put(endpoint, json=data)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
try:
|
||||
result = await retry_with_backoff(make_request)
|
||||
|
||||
if invalidate_cache:
|
||||
cache.invalidate(endpoint)
|
||||
|
||||
return {"success": True, "data": result}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def api_delete(
|
||||
endpoint: str,
|
||||
invalidate_cache: bool = True
|
||||
) -> dict:
|
||||
"""Make a DELETE request to the API."""
|
||||
async def make_request():
|
||||
client = await APIClient.get_client()
|
||||
response = await client.delete(endpoint)
|
||||
response.raise_for_status()
|
||||
return response.status_code
|
||||
|
||||
try:
|
||||
status = await retry_with_backoff(make_request)
|
||||
|
||||
if invalidate_cache:
|
||||
cache.invalidate(endpoint)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status_code": status,
|
||||
"deleted": True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def batch_api_requests(
|
||||
endpoints: list[str],
|
||||
use_cache: bool = True
|
||||
) -> dict:
|
||||
"""
|
||||
Make multiple GET requests in parallel.
|
||||
|
||||
Args:
|
||||
endpoints: List of endpoint paths
|
||||
use_cache: Whether to use cache
|
||||
|
||||
Returns:
|
||||
Batch results with successes and failures
|
||||
"""
|
||||
async def fetch_one(endpoint: str):
|
||||
return await api_get(endpoint, use_cache=use_cache)
|
||||
|
||||
results = await asyncio.gather(*[fetch_one(ep) for ep in endpoints])
|
||||
|
||||
successful = [r for r in results if r.get("success")]
|
||||
failed = [r for r in results if not r.get("success")]
|
||||
|
||||
return {
|
||||
"total": len(endpoints),
|
||||
"successful": len(successful),
|
||||
"failed": len(failed),
|
||||
"results": results
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def clear_cache(pattern: Optional[str] = None) -> dict:
|
||||
"""
|
||||
Clear API response cache.
|
||||
|
||||
Args:
|
||||
pattern: Optional pattern to match cache keys (clears all if not provided)
|
||||
|
||||
Returns:
|
||||
Cache clear status
|
||||
"""
|
||||
try:
|
||||
cache.invalidate(pattern)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Cache cleared{f' for pattern: {pattern}' if pattern else ''}"
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Resources
|
||||
# ============================================================================
|
||||
|
||||
@mcp.resource("info://api-status")
|
||||
async def api_status() -> dict:
|
||||
"""Check API connectivity and status."""
|
||||
try:
|
||||
client = await APIClient.get_client()
|
||||
response = await client.get("/health", timeout=5)
|
||||
return {
|
||||
"api_reachable": True,
|
||||
"status_code": response.status_code,
|
||||
"healthy": response.status_code == 200,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"api_reachable": False,
|
||||
"error": str(e),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
@mcp.resource("info://cache-stats")
|
||||
def cache_statistics() -> dict:
|
||||
"""Get cache statistics."""
|
||||
return {
|
||||
"total_entries": len(cache.cache),
|
||||
"ttl_seconds": cache.ttl,
|
||||
"entries": list(cache.cache.keys())
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
mcp.run()
|
||||
finally:
|
||||
# Cleanup on exit
|
||||
import asyncio
|
||||
asyncio.run(APIClient.cleanup())
|
||||
116
templates/basic-server.py
Normal file
116
templates/basic-server.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Basic FastMCP Server Template
|
||||
==============================
|
||||
A minimal working FastMCP server with essential patterns.
|
||||
"""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
import os
|
||||
|
||||
# Load environment variables (optional)
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# ============================================================================
|
||||
# CRITICAL: Server must be at module level for FastMCP Cloud
|
||||
# ============================================================================
|
||||
|
||||
mcp = FastMCP(
|
||||
name="My Basic Server",
|
||||
instructions="""
|
||||
This is a basic MCP server demonstrating core patterns.
|
||||
|
||||
Available tools:
|
||||
- greet: Say hello to someone
|
||||
- calculate: Perform basic math operations
|
||||
|
||||
Available resources:
|
||||
- info://status: Server status information
|
||||
"""
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Tools
|
||||
# ============================================================================
|
||||
|
||||
@mcp.tool()
|
||||
def greet(name: str, greeting: str = "Hello") -> str:
|
||||
"""
|
||||
Greet someone by name.
|
||||
|
||||
Args:
|
||||
name: The name of the person to greet
|
||||
greeting: The greeting to use (default: "Hello")
|
||||
|
||||
Returns:
|
||||
A greeting message
|
||||
"""
|
||||
return f"{greeting}, {name}!"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def calculate(operation: str, a: float, b: float) -> dict:
|
||||
"""
|
||||
Perform a mathematical operation.
|
||||
|
||||
Args:
|
||||
operation: The operation to perform (add, subtract, multiply, divide)
|
||||
a: First number
|
||||
b: Second number
|
||||
|
||||
Returns:
|
||||
Dictionary with the result or error message
|
||||
"""
|
||||
operations = {
|
||||
"add": lambda x, y: x + y,
|
||||
"subtract": lambda x, y: x - y,
|
||||
"multiply": lambda x, y: x * y,
|
||||
"divide": lambda x, y: x / y if y != 0 else None
|
||||
}
|
||||
|
||||
if operation not in operations:
|
||||
return {
|
||||
"error": f"Unknown operation: {operation}",
|
||||
"valid_operations": list(operations.keys())
|
||||
}
|
||||
|
||||
result = operations[operation](a, b)
|
||||
|
||||
if result is None:
|
||||
return {"error": "Division by zero"}
|
||||
|
||||
return {
|
||||
"operation": operation,
|
||||
"a": a,
|
||||
"b": b,
|
||||
"result": result
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Resources
|
||||
# ============================================================================
|
||||
|
||||
@mcp.resource("info://status")
|
||||
def server_status() -> dict:
|
||||
"""Get current server status."""
|
||||
from datetime import datetime
|
||||
|
||||
return {
|
||||
"server": "My Basic Server",
|
||||
"status": "operational",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main Execution
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run with stdio transport (default)
|
||||
mcp.run()
|
||||
|
||||
# Alternative: HTTP transport for testing
|
||||
# mcp.run(transport="http", port=8000)
|
||||
309
templates/client-example.py
Normal file
309
templates/client-example.py
Normal file
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
FastMCP Client Example
|
||||
======================
|
||||
Testing MCP servers with the FastMCP Client.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from fastmcp import Client
|
||||
from typing import Optional
|
||||
|
||||
|
||||
async def test_basic_server():
|
||||
"""Test a basic MCP server."""
|
||||
print("\n=== Testing Basic Server ===\n")
|
||||
|
||||
async with Client("basic-server.py") as client:
|
||||
# List available tools
|
||||
tools = await client.list_tools()
|
||||
print(f"Available tools: {len(tools)}")
|
||||
for tool in tools:
|
||||
print(f" - {tool.name}: {tool.description}")
|
||||
|
||||
# Call a tool
|
||||
print("\nCalling 'greet' tool...")
|
||||
result = await client.call_tool("greet", {"name": "World"})
|
||||
print(f"Result: {result.data}")
|
||||
|
||||
# Call another tool
|
||||
print("\nCalling 'calculate' tool...")
|
||||
result = await client.call_tool("calculate", {
|
||||
"operation": "add",
|
||||
"a": 10,
|
||||
"b": 5
|
||||
})
|
||||
print(f"Result: {result.data}")
|
||||
|
||||
# List resources
|
||||
print("\nAvailable resources:")
|
||||
resources = await client.list_resources()
|
||||
for resource in resources:
|
||||
print(f" - {resource.uri}: {resource.description}")
|
||||
|
||||
# Read a resource
|
||||
print("\nReading 'info://status' resource...")
|
||||
status = await client.read_resource("info://status")
|
||||
print(f"Status: {status}")
|
||||
|
||||
|
||||
async def test_tools_examples():
|
||||
"""Test the tools examples server."""
|
||||
print("\n=== Testing Tools Examples ===\n")
|
||||
|
||||
async with Client("tools-examples.py") as client:
|
||||
# Test sync tool
|
||||
print("Testing sync tool...")
|
||||
result = await client.call_tool("simple_sync_tool", {"text": "hello"})
|
||||
print(f"Sync result: {result.data}")
|
||||
|
||||
# Test async tool
|
||||
print("\nTesting async tool...")
|
||||
result = await client.call_tool("simple_async_tool", {"text": "WORLD"})
|
||||
print(f"Async result: {result.data}")
|
||||
|
||||
# Test validated search
|
||||
print("\nTesting validated search...")
|
||||
result = await client.call_tool("validated_search", {
|
||||
"params": {
|
||||
"query": "python",
|
||||
"limit": 5
|
||||
}
|
||||
})
|
||||
print(f"Search result: {result.data}")
|
||||
|
||||
# Test batch processing
|
||||
print("\nTesting batch process...")
|
||||
result = await client.call_tool("batch_process", {
|
||||
"items": ["item1", "item2", "item3"]
|
||||
})
|
||||
print(f"Batch result: {result.data}")
|
||||
|
||||
|
||||
async def test_resources_examples():
|
||||
"""Test the resources examples server."""
|
||||
print("\n=== Testing Resources Examples ===\n")
|
||||
|
||||
async with Client("resources-examples.py") as client:
|
||||
# List all resources
|
||||
resources = await client.list_resources()
|
||||
print(f"Total resources: {len(resources)}")
|
||||
|
||||
# Test static resource
|
||||
print("\nReading static resource...")
|
||||
config = await client.read_resource("data://config")
|
||||
print(f"Config: {config}")
|
||||
|
||||
# Test dynamic resource
|
||||
print("\nReading dynamic resource...")
|
||||
status = await client.read_resource("info://status")
|
||||
print(f"Status: {status}")
|
||||
|
||||
# Test resource template
|
||||
print("\nReading resource template...")
|
||||
profile = await client.read_resource("user://123/profile")
|
||||
print(f"User profile: {profile}")
|
||||
|
||||
|
||||
async def test_with_error_handling():
|
||||
"""Test server with comprehensive error handling."""
|
||||
print("\n=== Testing with Error Handling ===\n")
|
||||
|
||||
async with Client("error-handling.py") as client:
|
||||
# Test successful operation
|
||||
print("Testing successful operation...")
|
||||
try:
|
||||
result = await client.call_tool("divide_numbers", {
|
||||
"a": 10,
|
||||
"b": 2
|
||||
})
|
||||
print(f"Success: {result.data}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
# Test error case
|
||||
print("\nTesting error case (division by zero)...")
|
||||
try:
|
||||
result = await client.call_tool("divide_numbers", {
|
||||
"a": 10,
|
||||
"b": 0
|
||||
})
|
||||
print(f"Result: {result.data}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
# Test validation error
|
||||
print("\nTesting validation error...")
|
||||
try:
|
||||
result = await client.call_tool("validated_operation", {
|
||||
"data": ""
|
||||
})
|
||||
print(f"Result: {result.data}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
async def test_http_server():
|
||||
"""Test server running on HTTP transport."""
|
||||
print("\n=== Testing HTTP Server ===\n")
|
||||
|
||||
# Note: Server must be running on http://localhost:8000
|
||||
# Start with: python server.py --transport http --port 8000
|
||||
|
||||
try:
|
||||
async with Client("http://localhost:8000/mcp") as client:
|
||||
print("Connected to HTTP server")
|
||||
|
||||
tools = await client.list_tools()
|
||||
print(f"Available tools: {len(tools)}")
|
||||
|
||||
if tools:
|
||||
result = await client.call_tool(tools[0].name, {})
|
||||
print(f"Tool result: {result.data}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Could not connect to HTTP server: {e}")
|
||||
print("Make sure server is running with: python server.py --transport http --port 8000")
|
||||
|
||||
|
||||
async def test_with_handlers():
|
||||
"""Test server with client handlers (elicitation, progress, sampling)."""
|
||||
print("\n=== Testing with Handlers ===\n")
|
||||
|
||||
# Define handlers
|
||||
async def elicitation_handler(message: str, response_type: type, context: dict):
|
||||
"""Handle elicitation requests."""
|
||||
print(f"\n[ELICIT] {message}")
|
||||
return input("Your response: ")
|
||||
|
||||
async def progress_handler(progress: float, total: Optional[float], message: Optional[str]):
|
||||
"""Handle progress updates."""
|
||||
if total:
|
||||
pct = (progress / total) * 100
|
||||
print(f"\r[PROGRESS] {pct:.1f}% - {message}", end="", flush=True)
|
||||
else:
|
||||
print(f"\n[PROGRESS] {message}")
|
||||
|
||||
async def sampling_handler(messages, params, context):
|
||||
"""Handle sampling requests (LLM completions)."""
|
||||
print(f"\n[SAMPLE] LLM request with {len(messages)} messages")
|
||||
# In production, call actual LLM
|
||||
return {
|
||||
"content": "Mock LLM response",
|
||||
"model": params.get("model", "mock"),
|
||||
"usage": {"tokens": 100}
|
||||
}
|
||||
|
||||
# Create client with handlers
|
||||
async with Client(
|
||||
"server.py",
|
||||
elicitation_handler=elicitation_handler,
|
||||
progress_handler=progress_handler,
|
||||
sampling_handler=sampling_handler
|
||||
) as client:
|
||||
print("Client created with handlers")
|
||||
|
||||
# Test tools that use handlers
|
||||
# Note: Requires server to have tools using context.request_elicitation, etc.
|
||||
tools = await client.list_tools()
|
||||
print(f"Available tools: {len(tools)}")
|
||||
|
||||
|
||||
async def comprehensive_test():
|
||||
"""Run comprehensive test suite."""
|
||||
print("=" * 60)
|
||||
print("FastMCP Client Test Suite")
|
||||
print("=" * 60)
|
||||
|
||||
tests = [
|
||||
("Basic Server", test_basic_server),
|
||||
("Tools Examples", test_tools_examples),
|
||||
("Resources Examples", test_resources_examples),
|
||||
("Error Handling", test_with_error_handling),
|
||||
# ("HTTP Server", test_http_server), # Uncomment if HTTP server is running
|
||||
# ("With Handlers", test_with_handlers), # Uncomment if server supports handlers
|
||||
]
|
||||
|
||||
for test_name, test_func in tests:
|
||||
try:
|
||||
await test_func()
|
||||
except Exception as e:
|
||||
print(f"\n❌ {test_name} failed: {e}")
|
||||
else:
|
||||
print(f"\n✅ {test_name} passed")
|
||||
|
||||
print("\n" + "-" * 60)
|
||||
|
||||
print("\nTest suite completed!")
|
||||
|
||||
|
||||
async def interactive_client():
|
||||
"""Interactive client for manual testing."""
|
||||
print("\n=== Interactive FastMCP Client ===\n")
|
||||
|
||||
server_path = input("Enter server path or URL: ").strip()
|
||||
|
||||
if not server_path:
|
||||
server_path = "basic-server.py"
|
||||
print(f"Using default: {server_path}")
|
||||
|
||||
async with Client(server_path) as client:
|
||||
print(f"\n✅ Connected to: {server_path}\n")
|
||||
|
||||
while True:
|
||||
print("\nOptions:")
|
||||
print("1. List tools")
|
||||
print("2. List resources")
|
||||
print("3. Call tool")
|
||||
print("4. Read resource")
|
||||
print("5. Exit")
|
||||
|
||||
choice = input("\nChoice: ").strip()
|
||||
|
||||
if choice == "1":
|
||||
tools = await client.list_tools()
|
||||
print(f"\n📋 Available tools ({len(tools)}):")
|
||||
for i, tool in enumerate(tools, 1):
|
||||
print(f" {i}. {tool.name}")
|
||||
print(f" {tool.description}")
|
||||
|
||||
elif choice == "2":
|
||||
resources = await client.list_resources()
|
||||
print(f"\n📋 Available resources ({len(resources)}):")
|
||||
for i, resource in enumerate(resources, 1):
|
||||
print(f" {i}. {resource.uri}")
|
||||
print(f" {resource.description}")
|
||||
|
||||
elif choice == "3":
|
||||
tool_name = input("Tool name: ").strip()
|
||||
print("Arguments (as JSON): ")
|
||||
import json
|
||||
try:
|
||||
args = json.loads(input().strip())
|
||||
result = await client.call_tool(tool_name, args)
|
||||
print(f"\n✅ Result: {result.data}")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
|
||||
elif choice == "4":
|
||||
uri = input("Resource URI: ").strip()
|
||||
try:
|
||||
data = await client.read_resource(uri)
|
||||
print(f"\n✅ Data: {data}")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
|
||||
elif choice == "5":
|
||||
print("\nGoodbye!")
|
||||
break
|
||||
|
||||
else:
|
||||
print("Invalid choice")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--interactive":
|
||||
asyncio.run(interactive_client())
|
||||
else:
|
||||
asyncio.run(comprehensive_test())
|
||||
422
templates/error-handling.py
Normal file
422
templates/error-handling.py
Normal file
@@ -0,0 +1,422 @@
|
||||
"""
|
||||
FastMCP Error Handling Template
|
||||
================================
|
||||
Comprehensive error handling patterns with structured responses and retry logic.
|
||||
"""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
import asyncio
|
||||
import httpx
|
||||
from enum import Enum
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
mcp = FastMCP("Error Handling Examples")
|
||||
|
||||
# ============================================================================
|
||||
# Error Code Enum
|
||||
# ============================================================================
|
||||
|
||||
class ErrorCode(Enum):
|
||||
"""Standard error codes."""
|
||||
VALIDATION_ERROR = "VALIDATION_ERROR"
|
||||
NOT_FOUND = "NOT_FOUND"
|
||||
UNAUTHORIZED = "UNAUTHORIZED"
|
||||
RATE_LIMITED = "RATE_LIMITED"
|
||||
API_ERROR = "API_ERROR"
|
||||
TIMEOUT = "TIMEOUT"
|
||||
NETWORK_ERROR = "NETWORK_ERROR"
|
||||
UNKNOWN_ERROR = "UNKNOWN_ERROR"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Response Formatters
|
||||
# ============================================================================
|
||||
|
||||
def create_success(data: Any, message: str = "Success") -> Dict[str, Any]:
|
||||
"""Create structured success response."""
|
||||
return {
|
||||
"success": True,
|
||||
"message": message,
|
||||
"data": data,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
def create_error(
|
||||
code: ErrorCode,
|
||||
message: str,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create structured error response."""
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": code.value,
|
||||
"message": message,
|
||||
"details": details or {},
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Retry Logic
|
||||
# ============================================================================
|
||||
|
||||
async def retry_with_backoff(
|
||||
func,
|
||||
max_retries: int = 3,
|
||||
initial_delay: float = 1.0,
|
||||
exponential_base: float = 2.0,
|
||||
catch_exceptions: tuple = (Exception,)
|
||||
):
|
||||
"""
|
||||
Retry function with exponential backoff.
|
||||
|
||||
Args:
|
||||
func: Async function to retry
|
||||
max_retries: Maximum number of retry attempts
|
||||
initial_delay: Initial delay in seconds
|
||||
exponential_base: Base for exponential backoff
|
||||
catch_exceptions: Tuple of exceptions to catch and retry
|
||||
|
||||
Returns:
|
||||
Function result if successful
|
||||
|
||||
Raises:
|
||||
Last exception if all retries fail
|
||||
"""
|
||||
delay = initial_delay
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return await func()
|
||||
except catch_exceptions as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(delay)
|
||||
delay *= exponential_base
|
||||
|
||||
raise last_exception
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tools with Error Handling
|
||||
# ============================================================================
|
||||
|
||||
@mcp.tool()
|
||||
def divide_numbers(a: float, b: float) -> dict:
|
||||
"""
|
||||
Divide two numbers with error handling.
|
||||
|
||||
Args:
|
||||
a: Numerator
|
||||
b: Denominator
|
||||
|
||||
Returns:
|
||||
Result or error
|
||||
"""
|
||||
try:
|
||||
if b == 0:
|
||||
return create_error(
|
||||
ErrorCode.VALIDATION_ERROR,
|
||||
"Division by zero is not allowed",
|
||||
{"a": a, "b": b}
|
||||
)
|
||||
|
||||
result = a / b
|
||||
return create_success(
|
||||
{"result": result, "a": a, "b": b},
|
||||
"Division successful"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return create_error(
|
||||
ErrorCode.UNKNOWN_ERROR,
|
||||
f"Unexpected error: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def validated_operation(data: str, min_length: int = 1) -> dict:
|
||||
"""
|
||||
Operation with input validation.
|
||||
|
||||
Args:
|
||||
data: Input data to validate
|
||||
min_length: Minimum required length
|
||||
|
||||
Returns:
|
||||
Processed result or validation error
|
||||
"""
|
||||
# Validate input
|
||||
if not data:
|
||||
return create_error(
|
||||
ErrorCode.VALIDATION_ERROR,
|
||||
"Data is required",
|
||||
{"field": "data", "constraint": "not_empty"}
|
||||
)
|
||||
|
||||
if len(data) < min_length:
|
||||
return create_error(
|
||||
ErrorCode.VALIDATION_ERROR,
|
||||
f"Data must be at least {min_length} characters",
|
||||
{"field": "data", "min_length": min_length, "actual_length": len(data)}
|
||||
)
|
||||
|
||||
# Process data
|
||||
try:
|
||||
processed = data.upper()
|
||||
return create_success(
|
||||
{"original": data, "processed": processed},
|
||||
"Data processed successfully"
|
||||
)
|
||||
except Exception as e:
|
||||
return create_error(
|
||||
ErrorCode.UNKNOWN_ERROR,
|
||||
str(e)
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def resilient_api_call(url: str) -> dict:
|
||||
"""
|
||||
API call with retry logic and comprehensive error handling.
|
||||
|
||||
Args:
|
||||
url: URL to fetch
|
||||
|
||||
Returns:
|
||||
API response or detailed error
|
||||
"""
|
||||
async def make_request():
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
response = await client.get(url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
try:
|
||||
data = await retry_with_backoff(
|
||||
make_request,
|
||||
max_retries=3,
|
||||
catch_exceptions=(httpx.TimeoutException, httpx.NetworkError)
|
||||
)
|
||||
|
||||
return create_success(
|
||||
data,
|
||||
"API call successful"
|
||||
)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return create_error(
|
||||
ErrorCode.TIMEOUT,
|
||||
"Request timed out",
|
||||
{"url": url, "timeout_seconds": 10}
|
||||
)
|
||||
|
||||
except httpx.NetworkError as e:
|
||||
return create_error(
|
||||
ErrorCode.NETWORK_ERROR,
|
||||
"Network error occurred",
|
||||
{"url": url, "error": str(e)}
|
||||
)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 404:
|
||||
return create_error(
|
||||
ErrorCode.NOT_FOUND,
|
||||
"Resource not found",
|
||||
{"url": url, "status_code": 404}
|
||||
)
|
||||
elif e.response.status_code == 401:
|
||||
return create_error(
|
||||
ErrorCode.UNAUTHORIZED,
|
||||
"Unauthorized access",
|
||||
{"url": url, "status_code": 401}
|
||||
)
|
||||
elif e.response.status_code == 429:
|
||||
return create_error(
|
||||
ErrorCode.RATE_LIMITED,
|
||||
"Rate limit exceeded",
|
||||
{"url": url, "status_code": 429}
|
||||
)
|
||||
else:
|
||||
return create_error(
|
||||
ErrorCode.API_ERROR,
|
||||
f"HTTP {e.response.status_code}",
|
||||
{"url": url, "status_code": e.response.status_code}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return create_error(
|
||||
ErrorCode.UNKNOWN_ERROR,
|
||||
f"Unexpected error: {str(e)}",
|
||||
{"url": url}
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def batch_with_error_recovery(items: list[str]) -> dict:
|
||||
"""
|
||||
Batch process items with individual error recovery.
|
||||
|
||||
Args:
|
||||
items: List of items to process
|
||||
|
||||
Returns:
|
||||
Results with successes and failures tracked separately
|
||||
"""
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
for i, item in enumerate(items):
|
||||
try:
|
||||
# Simulate processing
|
||||
if not item:
|
||||
raise ValueError("Empty item")
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
results.append({
|
||||
"index": i,
|
||||
"item": item,
|
||||
"processed": item.upper(),
|
||||
"success": True
|
||||
})
|
||||
|
||||
except ValueError as e:
|
||||
errors.append({
|
||||
"index": i,
|
||||
"item": item,
|
||||
"error": str(e),
|
||||
"code": ErrorCode.VALIDATION_ERROR.value
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
errors.append({
|
||||
"index": i,
|
||||
"item": item,
|
||||
"error": str(e),
|
||||
"code": ErrorCode.UNKNOWN_ERROR.value
|
||||
})
|
||||
|
||||
return {
|
||||
"success": len(errors) == 0,
|
||||
"total": len(items),
|
||||
"successful": len(results),
|
||||
"failed": len(errors),
|
||||
"results": results,
|
||||
"errors": errors,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def safe_database_operation(query: str) -> dict:
|
||||
"""
|
||||
Simulated database operation with error handling.
|
||||
|
||||
Args:
|
||||
query: SQL query (simulated)
|
||||
|
||||
Returns:
|
||||
Query result or error
|
||||
"""
|
||||
try:
|
||||
# Validate query
|
||||
if "DROP" in query.upper():
|
||||
return create_error(
|
||||
ErrorCode.UNAUTHORIZED,
|
||||
"DROP operations not allowed",
|
||||
{"query": query}
|
||||
)
|
||||
|
||||
if not query.strip():
|
||||
return create_error(
|
||||
ErrorCode.VALIDATION_ERROR,
|
||||
"Query cannot be empty"
|
||||
)
|
||||
|
||||
# Simulate query execution
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Simulate success
|
||||
mock_data = [
|
||||
{"id": 1, "name": "Alice"},
|
||||
{"id": 2, "name": "Bob"}
|
||||
]
|
||||
|
||||
return create_success(
|
||||
{"rows": mock_data, "count": len(mock_data)},
|
||||
"Query executed successfully"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return create_error(
|
||||
ErrorCode.API_ERROR,
|
||||
f"Database error: {str(e)}",
|
||||
{"query": query}
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Resources with Error Handling
|
||||
# ============================================================================
|
||||
|
||||
@mcp.resource("health://detailed")
|
||||
async def detailed_health_check() -> dict:
|
||||
"""Comprehensive health check with error tracking."""
|
||||
checks = {}
|
||||
errors = []
|
||||
|
||||
# Check API connectivity
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
response = await client.get("https://api.example.com/health")
|
||||
checks["api"] = {
|
||||
"status": "healthy",
|
||||
"status_code": response.status_code
|
||||
}
|
||||
except Exception as e:
|
||||
checks["api"] = {
|
||||
"status": "unhealthy",
|
||||
"error": str(e)
|
||||
}
|
||||
errors.append(f"API check failed: {e}")
|
||||
|
||||
# Check system resources
|
||||
try:
|
||||
import psutil
|
||||
checks["system"] = {
|
||||
"status": "healthy",
|
||||
"cpu_percent": psutil.cpu_percent(),
|
||||
"memory_percent": psutil.virtual_memory().percent
|
||||
}
|
||||
except Exception as e:
|
||||
checks["system"] = {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
# Overall status
|
||||
all_healthy = all(
|
||||
check.get("status") == "healthy"
|
||||
for check in checks.values()
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "healthy" if all_healthy else "degraded",
|
||||
"checks": checks,
|
||||
"errors": errors if errors else None,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
259
templates/openapi-integration.py
Normal file
259
templates/openapi-integration.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
FastMCP OpenAPI Integration Template
|
||||
=====================================
|
||||
Auto-generate MCP server from OpenAPI/Swagger specification.
|
||||
"""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.openapi import RouteMap, MCPType
|
||||
import httpx
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.example.com")
|
||||
API_KEY = os.getenv("API_KEY", "")
|
||||
OPENAPI_SPEC_URL = os.getenv("OPENAPI_SPEC_URL", f"{API_BASE_URL}/openapi.json")
|
||||
|
||||
# ============================================================================
|
||||
# Load OpenAPI Specification
|
||||
# ============================================================================
|
||||
|
||||
def load_openapi_spec():
|
||||
"""Load OpenAPI specification from URL or file."""
|
||||
try:
|
||||
# Try loading from URL
|
||||
response = httpx.get(OPENAPI_SPEC_URL, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f"Failed to load OpenAPI spec from URL: {e}")
|
||||
|
||||
# Fallback: try loading from local file
|
||||
try:
|
||||
import json
|
||||
with open("openapi.json") as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
print("Error: OpenAPI spec not found. Please provide OPENAPI_SPEC_URL or openapi.json file")
|
||||
return None
|
||||
|
||||
|
||||
spec = load_openapi_spec()
|
||||
|
||||
# ============================================================================
|
||||
# Create Authenticated HTTP Client
|
||||
# ============================================================================
|
||||
|
||||
client = httpx.AsyncClient(
|
||||
base_url=API_BASE_URL,
|
||||
headers={
|
||||
"Authorization": f"Bearer {API_KEY}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
timeout=httpx.Timeout(30.0),
|
||||
limits=httpx.Limits(
|
||||
max_keepalive_connections=5,
|
||||
max_connections=10
|
||||
)
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Define Route Mapping Strategy
|
||||
# ============================================================================
|
||||
|
||||
route_maps = [
|
||||
# GET endpoints with path parameters → Resource Templates
|
||||
# Example: /users/{user_id} → resource template
|
||||
RouteMap(
|
||||
methods=["GET"],
|
||||
pattern=r".*\{.*\}.*", # Has path parameters
|
||||
mcp_type=MCPType.RESOURCE_TEMPLATE
|
||||
),
|
||||
|
||||
# GET endpoints without parameters → Static Resources
|
||||
# Example: /users → static resource
|
||||
RouteMap(
|
||||
methods=["GET"],
|
||||
pattern=r"^(?!.*\{.*\}).*$", # No path parameters
|
||||
mcp_type=MCPType.RESOURCE
|
||||
),
|
||||
|
||||
# POST/PUT/PATCH → Tools (create/update operations)
|
||||
RouteMap(
|
||||
methods=["POST", "PUT", "PATCH"],
|
||||
mcp_type=MCPType.TOOL
|
||||
),
|
||||
|
||||
# DELETE → Tools (delete operations)
|
||||
RouteMap(
|
||||
methods=["DELETE"],
|
||||
mcp_type=MCPType.TOOL
|
||||
),
|
||||
|
||||
# Exclude internal endpoints
|
||||
RouteMap(
|
||||
pattern=r"/internal/.*",
|
||||
mcp_type=MCPType.EXCLUDE
|
||||
),
|
||||
|
||||
# Exclude health checks
|
||||
RouteMap(
|
||||
pattern=r"/(health|healthz|readiness|liveness)",
|
||||
mcp_type=MCPType.EXCLUDE
|
||||
)
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# Generate MCP Server from OpenAPI
|
||||
# ============================================================================
|
||||
|
||||
if spec:
|
||||
mcp = FastMCP.from_openapi(
|
||||
openapi_spec=spec,
|
||||
client=client,
|
||||
name="API Integration Server",
|
||||
route_maps=route_maps
|
||||
)
|
||||
|
||||
print(f"✅ Generated MCP server from OpenAPI spec")
|
||||
print(f" Base URL: {API_BASE_URL}")
|
||||
|
||||
else:
|
||||
# Fallback: create empty server if spec not available
|
||||
mcp = FastMCP("API Integration Server")
|
||||
print("⚠️ Running without OpenAPI spec - please configure OPENAPI_SPEC_URL")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Add Custom Tools (on top of auto-generated ones)
|
||||
# ============================================================================
|
||||
|
||||
@mcp.tool()
|
||||
async def process_api_response(data: dict, operation: str = "format") -> dict:
|
||||
"""
|
||||
Process API response data.
|
||||
|
||||
Custom tool to transform or analyze data from API endpoints.
|
||||
"""
|
||||
if operation == "format":
|
||||
# Format the data nicely
|
||||
return {
|
||||
"formatted": True,
|
||||
"data": data,
|
||||
"count": len(data) if isinstance(data, (list, dict)) else 1
|
||||
}
|
||||
|
||||
elif operation == "summarize":
|
||||
# Summarize the data
|
||||
if isinstance(data, list):
|
||||
return {
|
||||
"type": "list",
|
||||
"count": len(data),
|
||||
"sample": data[:3] if len(data) > 3 else data
|
||||
}
|
||||
elif isinstance(data, dict):
|
||||
return {
|
||||
"type": "dict",
|
||||
"keys": list(data.keys()),
|
||||
"size": len(data)
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"type": type(data).__name__,
|
||||
"value": str(data)
|
||||
}
|
||||
|
||||
return {"error": f"Unknown operation: {operation}"}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def batch_api_request(endpoints: list[str]) -> dict:
|
||||
"""
|
||||
Make multiple API requests in parallel.
|
||||
|
||||
Useful for gathering data from multiple endpoints efficiently.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
async def fetch_endpoint(endpoint: str):
|
||||
try:
|
||||
response = await client.get(endpoint)
|
||||
response.raise_for_status()
|
||||
return {
|
||||
"endpoint": endpoint,
|
||||
"success": True,
|
||||
"data": response.json()
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"endpoint": endpoint,
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
# Execute all requests in parallel
|
||||
results = await asyncio.gather(*[fetch_endpoint(ep) for ep in endpoints])
|
||||
|
||||
successful = [r for r in results if r["success"]]
|
||||
failed = [r for r in results if not r["success"]]
|
||||
|
||||
return {
|
||||
"total": len(endpoints),
|
||||
"successful": len(successful),
|
||||
"failed": len(failed),
|
||||
"results": results
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Add Custom Resources
|
||||
# ============================================================================
|
||||
|
||||
@mcp.resource("info://api-config")
|
||||
def api_configuration() -> dict:
|
||||
"""Get API configuration details."""
|
||||
return {
|
||||
"base_url": API_BASE_URL,
|
||||
"spec_url": OPENAPI_SPEC_URL,
|
||||
"authenticated": bool(API_KEY),
|
||||
"spec_loaded": spec is not None
|
||||
}
|
||||
|
||||
|
||||
@mcp.resource("info://available-endpoints")
|
||||
def list_available_endpoints() -> dict:
|
||||
"""List all available API endpoints."""
|
||||
if not spec:
|
||||
return {"error": "OpenAPI spec not loaded"}
|
||||
|
||||
endpoints = []
|
||||
|
||||
for path, path_item in spec.get("paths", {}).items():
|
||||
for method in path_item.keys():
|
||||
if method.upper() in ["GET", "POST", "PUT", "PATCH", "DELETE"]:
|
||||
operation = path_item[method]
|
||||
endpoints.append({
|
||||
"path": path,
|
||||
"method": method.upper(),
|
||||
"summary": operation.get("summary", ""),
|
||||
"description": operation.get("description", "")
|
||||
})
|
||||
|
||||
return {
|
||||
"total": len(endpoints),
|
||||
"endpoints": endpoints
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main Execution
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
348
templates/prompts-examples.py
Normal file
348
templates/prompts-examples.py
Normal file
@@ -0,0 +1,348 @@
|
||||
"""
|
||||
FastMCP Prompts Examples
|
||||
=========================
|
||||
Examples of pre-configured prompts for LLMs.
|
||||
"""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from datetime import datetime
|
||||
|
||||
mcp = FastMCP("Prompts Examples")
|
||||
|
||||
# ============================================================================
|
||||
# Basic Prompts
|
||||
# ============================================================================
|
||||
|
||||
@mcp.prompt("help")
|
||||
def help_prompt() -> str:
|
||||
"""Generate help text for the server."""
|
||||
return """
|
||||
Welcome to the FastMCP Prompts Examples Server!
|
||||
|
||||
This server demonstrates various prompt patterns for LLM interactions.
|
||||
|
||||
Available Tools:
|
||||
- search: Search for items in the database
|
||||
- analyze: Analyze data and generate insights
|
||||
- summarize: Create summaries of text content
|
||||
|
||||
Available Resources:
|
||||
- info://status: Current server status
|
||||
- data://config: Server configuration
|
||||
- data://users: List of all users
|
||||
|
||||
How to Use:
|
||||
1. Use the search tool to find items
|
||||
2. Use the analyze tool to generate insights from data
|
||||
3. Use the summarize tool to create concise summaries
|
||||
|
||||
For specific tasks, use the pre-configured prompts:
|
||||
- /analyze: Analyze a topic in depth
|
||||
- /report: Generate a comprehensive report
|
||||
- /review: Review and provide feedback
|
||||
"""
|
||||
|
||||
|
||||
@mcp.prompt("analyze")
|
||||
def analyze_prompt(topic: str) -> str:
|
||||
"""Generate a prompt for analyzing a topic."""
|
||||
return f"""
|
||||
Please analyze the following topic: {topic}
|
||||
|
||||
Consider the following aspects:
|
||||
1. Current State: What is the current situation?
|
||||
2. Challenges: What are the main challenges or issues?
|
||||
3. Opportunities: What opportunities exist for improvement?
|
||||
4. Data Points: What data supports your analysis?
|
||||
5. Recommendations: What specific actions do you recommend?
|
||||
|
||||
Use the available tools to:
|
||||
- Search for relevant data using the search tool
|
||||
- Gather statistics and metrics
|
||||
- Review related information
|
||||
|
||||
Provide a structured analysis with:
|
||||
- Executive Summary
|
||||
- Detailed Findings
|
||||
- Data-Driven Insights
|
||||
- Actionable Recommendations
|
||||
"""
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Prompts with Parameters
|
||||
# ============================================================================
|
||||
|
||||
@mcp.prompt("report")
|
||||
def report_prompt(
|
||||
subject: str,
|
||||
timeframe: str = "last month",
|
||||
detail_level: str = "summary"
|
||||
) -> str:
|
||||
"""Generate a report prompt with parameters."""
|
||||
return f"""
|
||||
Generate a comprehensive report on: {subject}
|
||||
|
||||
Timeframe: {timeframe}
|
||||
Detail Level: {detail_level}
|
||||
|
||||
Report Structure:
|
||||
1. Executive Summary
|
||||
- Key findings
|
||||
- Critical metrics
|
||||
- Main recommendations
|
||||
|
||||
2. Data Analysis
|
||||
- Quantitative metrics
|
||||
- Trend analysis
|
||||
- Comparative analysis
|
||||
|
||||
3. Insights
|
||||
- Patterns discovered
|
||||
- Anomalies identified
|
||||
- Correlations found
|
||||
|
||||
4. Recommendations
|
||||
- Short-term actions
|
||||
- Long-term strategies
|
||||
- Resource requirements
|
||||
|
||||
Please use the available tools to gather:
|
||||
- Statistical data
|
||||
- User information
|
||||
- System metrics
|
||||
- Historical trends
|
||||
|
||||
Format: {detail_level.upper()}
|
||||
- "summary": High-level overview with key points
|
||||
- "detailed": In-depth analysis with supporting data
|
||||
- "comprehensive": Full analysis with all available data points
|
||||
"""
|
||||
|
||||
|
||||
@mcp.prompt("review")
|
||||
def review_prompt(
|
||||
item_type: str,
|
||||
item_id: str,
|
||||
focus_areas: str = "all"
|
||||
) -> str:
|
||||
"""Generate a review prompt."""
|
||||
return f"""
|
||||
Review the {item_type} (ID: {item_id})
|
||||
|
||||
Focus Areas: {focus_areas}
|
||||
|
||||
Review Criteria:
|
||||
1. Quality Assessment
|
||||
- Overall quality rating
|
||||
- Strengths identified
|
||||
- Areas for improvement
|
||||
|
||||
2. Completeness
|
||||
- Required elements present
|
||||
- Missing components
|
||||
- Suggestions for additions
|
||||
|
||||
3. Consistency
|
||||
- Internal consistency
|
||||
- Alignment with standards
|
||||
- Conformance to guidelines
|
||||
|
||||
4. Performance
|
||||
- Efficiency metrics
|
||||
- Resource utilization
|
||||
- Optimization opportunities
|
||||
|
||||
5. Recommendations
|
||||
- Priority improvements
|
||||
- Nice-to-have enhancements
|
||||
- Long-term considerations
|
||||
|
||||
Please gather relevant data using available tools and provide a structured review.
|
||||
"""
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Task-Specific Prompts
|
||||
# ============================================================================
|
||||
|
||||
@mcp.prompt("summarize")
|
||||
def summarize_prompt(content_type: str = "text") -> str:
|
||||
"""Generate a summarization prompt."""
|
||||
return f"""
|
||||
Create a comprehensive summary of the {content_type}.
|
||||
|
||||
Summary Guidelines:
|
||||
1. Key Points
|
||||
- Extract the most important information
|
||||
- Identify main themes or topics
|
||||
- Highlight critical details
|
||||
|
||||
2. Structure
|
||||
- Opening: Context and overview
|
||||
- Body: Main points organized logically
|
||||
- Closing: Conclusions and implications
|
||||
|
||||
3. Audience Consideration
|
||||
- Write for clarity and understanding
|
||||
- Define technical terms if needed
|
||||
- Provide context where necessary
|
||||
|
||||
4. Length
|
||||
- Brief: 2-3 sentences
|
||||
- Standard: 1 paragraph
|
||||
- Detailed: 2-3 paragraphs
|
||||
|
||||
Output Format:
|
||||
- Start with a one-sentence overview
|
||||
- Follow with detailed points
|
||||
- End with key takeaways
|
||||
|
||||
Use available tools to gather additional context if needed.
|
||||
"""
|
||||
|
||||
|
||||
@mcp.prompt("compare")
|
||||
def compare_prompt(item1: str, item2: str, criteria: str = "general") -> str:
|
||||
"""Generate a comparison prompt."""
|
||||
return f"""
|
||||
Compare and contrast: {item1} vs {item2}
|
||||
|
||||
Comparison Criteria: {criteria}
|
||||
|
||||
Analysis Framework:
|
||||
1. Similarities
|
||||
- Common features
|
||||
- Shared characteristics
|
||||
- Aligned goals or purposes
|
||||
|
||||
2. Differences
|
||||
- Unique features
|
||||
- Distinct characteristics
|
||||
- Divergent approaches
|
||||
|
||||
3. Strengths and Weaknesses
|
||||
- {item1} strengths
|
||||
- {item1} weaknesses
|
||||
- {item2} strengths
|
||||
- {item2} weaknesses
|
||||
|
||||
4. Use Cases
|
||||
- When to choose {item1}
|
||||
- When to choose {item2}
|
||||
- Situational recommendations
|
||||
|
||||
5. Conclusion
|
||||
- Overall assessment
|
||||
- Best fit scenarios
|
||||
- Decision factors
|
||||
|
||||
Please gather data using available tools and provide a balanced comparison.
|
||||
"""
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Workflow Prompts
|
||||
# ============================================================================
|
||||
|
||||
@mcp.prompt("troubleshoot")
|
||||
def troubleshoot_prompt(problem_description: str) -> str:
|
||||
"""Generate a troubleshooting prompt."""
|
||||
return f"""
|
||||
Troubleshoot the following issue:
|
||||
|
||||
Problem: {problem_description}
|
||||
|
||||
Troubleshooting Process:
|
||||
1. Problem Definition
|
||||
- Describe the issue clearly
|
||||
- Identify symptoms
|
||||
- Note when it started
|
||||
|
||||
2. Information Gathering
|
||||
- Use available tools to gather:
|
||||
* System status
|
||||
* Error logs
|
||||
* Configuration details
|
||||
* Recent changes
|
||||
|
||||
3. Analysis
|
||||
- Identify potential causes
|
||||
- Determine root cause
|
||||
- Assess impact
|
||||
|
||||
4. Solution Development
|
||||
- Propose solutions (short-term and long-term)
|
||||
- Evaluate each solution
|
||||
- Recommend best approach
|
||||
|
||||
5. Implementation Plan
|
||||
- Step-by-step resolution
|
||||
- Required resources
|
||||
- Expected timeline
|
||||
- Verification steps
|
||||
|
||||
6. Prevention
|
||||
- Preventive measures
|
||||
- Monitoring recommendations
|
||||
- Documentation needs
|
||||
|
||||
Please be systematic and thorough in your analysis.
|
||||
"""
|
||||
|
||||
|
||||
@mcp.prompt("plan")
|
||||
def plan_prompt(objective: str, constraints: str = "none") -> str:
|
||||
"""Generate a planning prompt."""
|
||||
return f"""
|
||||
Create a detailed plan for: {objective}
|
||||
|
||||
Constraints: {constraints}
|
||||
|
||||
Planning Framework:
|
||||
1. Objective Analysis
|
||||
- Clear definition of success
|
||||
- Key success criteria
|
||||
- Expected outcomes
|
||||
|
||||
2. Current State Assessment
|
||||
- Available resources
|
||||
- Existing capabilities
|
||||
- Known limitations
|
||||
|
||||
3. Strategy Development
|
||||
- Approach options
|
||||
- Recommended strategy
|
||||
- Rationale
|
||||
|
||||
4. Action Plan
|
||||
- Phase 1: Foundation
|
||||
- Phase 2: Implementation
|
||||
- Phase 3: Optimization
|
||||
|
||||
5. Resource Requirements
|
||||
- Personnel
|
||||
- Technology
|
||||
- Budget
|
||||
- Time
|
||||
|
||||
6. Risk Management
|
||||
- Identified risks
|
||||
- Mitigation strategies
|
||||
- Contingency plans
|
||||
|
||||
7. Success Metrics
|
||||
- KPIs to track
|
||||
- Measurement methods
|
||||
- Review milestones
|
||||
|
||||
Use available tools to gather supporting data and insights.
|
||||
"""
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
92
templates/pyproject.toml
Normal file
92
templates/pyproject.toml
Normal file
@@ -0,0 +1,92 @@
|
||||
[project]
|
||||
name = "my-fastmcp-server"
|
||||
version = "1.0.0"
|
||||
description = "My FastMCP Server"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "Your Name", email = "your.email@example.com"}
|
||||
]
|
||||
dependencies = [
|
||||
"fastmcp>=2.12.0",
|
||||
"httpx>=0.27.0",
|
||||
"python-dotenv>=1.0.0",
|
||||
"pydantic>=2.0.0",
|
||||
"psutil>=5.9.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.0.0",
|
||||
"pytest-asyncio>=0.23.0",
|
||||
"ruff>=0.3.0",
|
||||
"mypy>=1.8.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/yourusername/my-fastmcp-server"
|
||||
Repository = "https://github.com/yourusername/my-fastmcp-server"
|
||||
Issues = "https://github.com/yourusername/my-fastmcp-server/issues"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
src = ["py.typed"]
|
||||
|
||||
# Ruff Configuration
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long (handled by formatter)
|
||||
"B008", # do not perform function calls in argument defaults
|
||||
]
|
||||
|
||||
# Mypy Configuration
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = false
|
||||
disallow_any_generics = false
|
||||
check_untyped_defs = true
|
||||
|
||||
# Pytest Configuration
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
asyncio_mode = "auto"
|
||||
addopts = "-v --tb=short"
|
||||
|
||||
# Coverage Configuration
|
||||
[tool.coverage.run]
|
||||
source = ["src"]
|
||||
omit = ["*/tests/*", "*/test_*"]
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"def __repr__",
|
||||
"raise AssertionError",
|
||||
"raise NotImplementedError",
|
||||
"if __name__ == .__main__.:",
|
||||
"if TYPE_CHECKING:",
|
||||
]
|
||||
37
templates/requirements.txt
Normal file
37
templates/requirements.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
# FastMCP Core
|
||||
fastmcp>=2.13.0
|
||||
|
||||
# Storage Backends (for production persistence)
|
||||
py-key-value-aio>=0.1.0 # Memory, Disk, Redis, DynamoDB storage
|
||||
|
||||
# Encryption (for secure token storage)
|
||||
cryptography>=42.0.0
|
||||
|
||||
# HTTP Client (for API integrations)
|
||||
httpx>=0.27.0
|
||||
|
||||
# Environment Variables
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# Validation (optional, for complex validation)
|
||||
pydantic>=2.0.0
|
||||
|
||||
# System Monitoring (optional, for health checks)
|
||||
psutil>=5.9.0
|
||||
|
||||
# Async Support
|
||||
asyncio-extras>=1.3.2
|
||||
|
||||
# Optional Storage Backend Dependencies
|
||||
# Uncomment as needed for specific backends:
|
||||
# redis>=5.0.0 # For RedisStore
|
||||
# boto3>=1.34.0 # For DynamoDB
|
||||
# pymongo>=4.6.0 # For MongoDB
|
||||
# elasticsearch>=8.12.0 # For Elasticsearch
|
||||
|
||||
# Development Dependencies (optional)
|
||||
# Uncomment for development
|
||||
# pytest>=8.0.0
|
||||
# pytest-asyncio>=0.23.0
|
||||
# ruff>=0.3.0
|
||||
# mypy>=1.8.0
|
||||
216
templates/resources-examples.py
Normal file
216
templates/resources-examples.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
FastMCP Resources Examples
|
||||
===========================
|
||||
Examples of static resources, dynamic resources, and resource templates.
|
||||
"""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any
|
||||
import os
|
||||
|
||||
mcp = FastMCP("Resources Examples")
|
||||
|
||||
# ============================================================================
|
||||
# Static Resources
|
||||
# ============================================================================
|
||||
|
||||
@mcp.resource("data://config")
|
||||
def get_config() -> dict:
|
||||
"""Static configuration resource."""
|
||||
return {
|
||||
"version": "1.0.0",
|
||||
"environment": os.getenv("ENVIRONMENT", "development"),
|
||||
"features": ["search", "analytics", "notifications"],
|
||||
"limits": {
|
||||
"max_requests": 1000,
|
||||
"max_results": 100
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mcp.resource("info://server")
|
||||
def server_info() -> dict:
|
||||
"""Server metadata."""
|
||||
return {
|
||||
"name": "Resources Examples Server",
|
||||
"description": "Demonstrates various resource patterns",
|
||||
"version": "1.0.0",
|
||||
"capabilities": [
|
||||
"static_resources",
|
||||
"dynamic_resources",
|
||||
"resource_templates"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Dynamic Resources
|
||||
# ============================================================================
|
||||
|
||||
@mcp.resource("info://status")
|
||||
async def server_status() -> dict:
|
||||
"""Dynamic status resource (updated on each read)."""
|
||||
import psutil
|
||||
|
||||
return {
|
||||
"status": "operational",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"uptime_seconds": 0, # Would track actual uptime
|
||||
"system": {
|
||||
"cpu_percent": psutil.cpu_percent(interval=1),
|
||||
"memory_percent": psutil.virtual_memory().percent,
|
||||
"disk_percent": psutil.disk_usage('/').percent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mcp.resource("data://statistics")
|
||||
async def get_statistics() -> dict:
|
||||
"""Dynamic statistics resource."""
|
||||
return {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"total_requests": 1234, # Would track actual requests
|
||||
"active_connections": 5,
|
||||
"cache_hit_rate": 0.87,
|
||||
"average_response_time_ms": 45.3
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Resource Templates (with parameters)
|
||||
# ============================================================================
|
||||
|
||||
@mcp.resource("user://{user_id}/profile")
|
||||
async def get_user_profile(user_id: str) -> dict:
|
||||
"""Get user profile by ID."""
|
||||
# In production, fetch from database
|
||||
return {
|
||||
"id": user_id,
|
||||
"name": f"User {user_id}",
|
||||
"email": f"user{user_id}@example.com",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"role": "user"
|
||||
}
|
||||
|
||||
|
||||
@mcp.resource("user://{user_id}/posts")
|
||||
async def get_user_posts(user_id: str) -> List[dict]:
|
||||
"""Get posts for a specific user."""
|
||||
# In production, fetch from database
|
||||
return [
|
||||
{
|
||||
"id": 1,
|
||||
"user_id": user_id,
|
||||
"title": "First Post",
|
||||
"content": "Hello, world!",
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"user_id": user_id,
|
||||
"title": "Second Post",
|
||||
"content": "Another post",
|
||||
"created_at": "2024-01-02T00:00:00Z"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@mcp.resource("org://{org_id}/team/{team_id}/members")
|
||||
async def get_team_members(org_id: str, team_id: str) -> List[dict]:
|
||||
"""Get team members with org and team context."""
|
||||
# In production, fetch from database with filters
|
||||
return [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Alice",
|
||||
"role": "engineer",
|
||||
"org_id": org_id,
|
||||
"team_id": team_id
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Bob",
|
||||
"role": "designer",
|
||||
"org_id": org_id,
|
||||
"team_id": team_id
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@mcp.resource("api://{version}/config")
|
||||
async def get_versioned_config(version: str) -> dict:
|
||||
"""Get configuration for specific API version."""
|
||||
configs = {
|
||||
"v1": {
|
||||
"api_version": "v1",
|
||||
"endpoints": ["/users", "/posts"],
|
||||
"deprecated": True
|
||||
},
|
||||
"v2": {
|
||||
"api_version": "v2",
|
||||
"endpoints": ["/users", "/posts", "/comments", "/likes"],
|
||||
"deprecated": False
|
||||
}
|
||||
}
|
||||
|
||||
return configs.get(version, {"error": f"Unknown version: {version}"})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# File-based Resources
|
||||
# ============================================================================
|
||||
|
||||
@mcp.resource("file://docs/{filename}")
|
||||
async def get_documentation(filename: str) -> dict:
|
||||
"""Get documentation file content."""
|
||||
# In production, read actual files
|
||||
docs = {
|
||||
"getting-started.md": {
|
||||
"filename": "getting-started.md",
|
||||
"content": "# Getting Started\n\nWelcome to the docs!",
|
||||
"last_modified": "2024-01-01T00:00:00Z"
|
||||
},
|
||||
"api-reference.md": {
|
||||
"filename": "api-reference.md",
|
||||
"content": "# API Reference\n\nAPI documentation here.",
|
||||
"last_modified": "2024-01-02T00:00:00Z"
|
||||
}
|
||||
}
|
||||
|
||||
return docs.get(filename, {"error": f"File not found: {filename}"})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# List-style Resources
|
||||
# ============================================================================
|
||||
|
||||
@mcp.resource("data://users")
|
||||
async def list_users() -> List[dict]:
|
||||
"""List all users."""
|
||||
# In production, fetch from database
|
||||
return [
|
||||
{"id": "1", "name": "Alice", "email": "alice@example.com"},
|
||||
{"id": "2", "name": "Bob", "email": "bob@example.com"},
|
||||
{"id": "3", "name": "Charlie", "email": "charlie@example.com"}
|
||||
]
|
||||
|
||||
|
||||
@mcp.resource("data://categories")
|
||||
def list_categories() -> List[str]:
|
||||
"""List available categories."""
|
||||
return [
|
||||
"Technology",
|
||||
"Science",
|
||||
"Business",
|
||||
"Entertainment",
|
||||
"Sports"
|
||||
]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
425
templates/self-contained-server.py
Normal file
425
templates/self-contained-server.py
Normal file
@@ -0,0 +1,425 @@
|
||||
"""
|
||||
Self-Contained FastMCP Server
|
||||
==============================
|
||||
Production pattern with all utilities in one file.
|
||||
Avoids circular import issues common in cloud deployment.
|
||||
"""
|
||||
|
||||
from fastmcp import FastMCP, Context
|
||||
import os
|
||||
import time
|
||||
import asyncio
|
||||
import httpx
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# ============================================================================
|
||||
# Configuration (Self-contained)
|
||||
# ============================================================================
|
||||
|
||||
class Config:
|
||||
"""Application configuration from environment variables."""
|
||||
SERVER_NAME = os.getenv("SERVER_NAME", "Self-Contained Server")
|
||||
SERVER_VERSION = "1.0.0"
|
||||
API_BASE_URL = os.getenv("API_BASE_URL", "")
|
||||
API_KEY = os.getenv("API_KEY", "")
|
||||
CACHE_TTL = int(os.getenv("CACHE_TTL", "300"))
|
||||
MAX_RETRIES = int(os.getenv("MAX_RETRIES", "3"))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Utilities (All in one place)
|
||||
# ============================================================================
|
||||
|
||||
def format_success(data: Any, message: str = "Success") -> Dict[str, Any]:
|
||||
"""Format successful response."""
|
||||
return {
|
||||
"success": True,
|
||||
"message": message,
|
||||
"data": data,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
def format_error(error: str, code: str = "ERROR") -> Dict[str, Any]:
|
||||
"""Format error response."""
|
||||
return {
|
||||
"success": False,
|
||||
"error": error,
|
||||
"code": code,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# API Client (Lazy Initialization)
|
||||
# ============================================================================
|
||||
|
||||
class APIClient:
|
||||
"""Singleton HTTP client with lazy initialization."""
|
||||
_instance: Optional[httpx.AsyncClient] = None
|
||||
|
||||
@classmethod
|
||||
async def get_client(cls) -> Optional[httpx.AsyncClient]:
|
||||
"""Get or create HTTP client (only when needed)."""
|
||||
if not Config.API_BASE_URL or not Config.API_KEY:
|
||||
return None
|
||||
|
||||
if cls._instance is None:
|
||||
cls._instance = httpx.AsyncClient(
|
||||
base_url=Config.API_BASE_URL,
|
||||
headers={"Authorization": f"Bearer {Config.API_KEY}"},
|
||||
timeout=httpx.Timeout(30.0)
|
||||
)
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
async def cleanup(cls):
|
||||
"""Cleanup HTTP client."""
|
||||
if cls._instance:
|
||||
await cls._instance.aclose()
|
||||
cls._instance = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Cache (Simple Implementation)
|
||||
# ============================================================================
|
||||
|
||||
class SimpleCache:
|
||||
"""Time-based cache."""
|
||||
_cache: Dict[str, Any] = {}
|
||||
_timestamps: Dict[str, float] = {}
|
||||
|
||||
@classmethod
|
||||
def get(cls, key: str) -> Optional[Any]:
|
||||
"""Get cached value if not expired."""
|
||||
if key in cls._cache:
|
||||
if time.time() - cls._timestamps[key] < Config.CACHE_TTL:
|
||||
return cls._cache[key]
|
||||
else:
|
||||
del cls._cache[key]
|
||||
del cls._timestamps[key]
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def set(cls, key: str, value: Any):
|
||||
"""Set cache value."""
|
||||
cls._cache[key] = value
|
||||
cls._timestamps[key] = time.time()
|
||||
|
||||
@classmethod
|
||||
def clear(cls):
|
||||
"""Clear all cache."""
|
||||
cls._cache.clear()
|
||||
cls._timestamps.clear()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Retry Logic
|
||||
# ============================================================================
|
||||
|
||||
async def retry_with_backoff(func, max_retries: int = 3):
|
||||
"""Retry function with exponential backoff."""
|
||||
delay = 1.0
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return await func()
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(delay)
|
||||
delay *= 2
|
||||
|
||||
raise last_exception
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Server Creation (Module Level - Required for Cloud!)
|
||||
# ============================================================================
|
||||
|
||||
mcp = FastMCP(
|
||||
name=Config.SERVER_NAME,
|
||||
instructions=f"""
|
||||
{Config.SERVER_NAME} v{Config.SERVER_VERSION}
|
||||
|
||||
A self-contained MCP server with production patterns.
|
||||
|
||||
Available tools:
|
||||
- process_data: Process and transform data
|
||||
- fetch_from_api: Fetch data from external API (if configured)
|
||||
- calculate: Perform calculations
|
||||
|
||||
Available resources:
|
||||
- info://status: Server status
|
||||
- info://config: Configuration details
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tools
|
||||
# ============================================================================
|
||||
|
||||
@mcp.tool()
|
||||
async def process_data(
|
||||
data: List[Dict[str, Any]],
|
||||
operation: str = "summarize"
|
||||
) -> dict:
|
||||
"""
|
||||
Process a list of data items.
|
||||
|
||||
Args:
|
||||
data: List of data items
|
||||
operation: Operation to perform (summarize, filter, transform)
|
||||
|
||||
Returns:
|
||||
Processed result
|
||||
"""
|
||||
try:
|
||||
if operation == "summarize":
|
||||
return format_success({
|
||||
"count": len(data),
|
||||
"first": data[0] if data else None,
|
||||
"last": data[-1] if data else None
|
||||
})
|
||||
|
||||
elif operation == "filter":
|
||||
filtered = [d for d in data if d.get("active", False)]
|
||||
return format_success({
|
||||
"original_count": len(data),
|
||||
"filtered_count": len(filtered),
|
||||
"data": filtered
|
||||
})
|
||||
|
||||
elif operation == "transform":
|
||||
transformed = [
|
||||
{**d, "processed_at": datetime.now().isoformat()}
|
||||
for d in data
|
||||
]
|
||||
return format_success({"data": transformed})
|
||||
|
||||
else:
|
||||
return format_error(f"Unknown operation: {operation}", "INVALID_OPERATION")
|
||||
|
||||
except Exception as e:
|
||||
return format_error(str(e), "PROCESSING_ERROR")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def fetch_from_api(
|
||||
endpoint: str,
|
||||
use_cache: bool = True
|
||||
) -> dict:
|
||||
"""
|
||||
Fetch data from external API.
|
||||
|
||||
Args:
|
||||
endpoint: API endpoint path
|
||||
use_cache: Whether to use cached response
|
||||
|
||||
Returns:
|
||||
API response or error
|
||||
"""
|
||||
if not Config.API_BASE_URL:
|
||||
return format_error("API not configured", "API_NOT_CONFIGURED")
|
||||
|
||||
cache_key = f"api:{endpoint}"
|
||||
|
||||
# Check cache
|
||||
if use_cache:
|
||||
cached = SimpleCache.get(cache_key)
|
||||
if cached:
|
||||
return format_success(cached, "Retrieved from cache")
|
||||
|
||||
# Fetch from API
|
||||
try:
|
||||
client = await APIClient.get_client()
|
||||
if not client:
|
||||
return format_error("API client not available", "CLIENT_ERROR")
|
||||
|
||||
async def make_request():
|
||||
response = await client.get(endpoint)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
data = await retry_with_backoff(make_request, max_retries=Config.MAX_RETRIES)
|
||||
|
||||
# Cache the result
|
||||
if use_cache:
|
||||
SimpleCache.set(cache_key, data)
|
||||
|
||||
return format_success(data, "Fetched from API")
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error(
|
||||
f"HTTP {e.response.status_code}",
|
||||
"HTTP_ERROR"
|
||||
)
|
||||
except Exception as e:
|
||||
return format_error(str(e), "API_ERROR")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def calculate(operation: str, a: float, b: float) -> dict:
|
||||
"""
|
||||
Perform mathematical operations.
|
||||
|
||||
Args:
|
||||
operation: add, subtract, multiply, divide
|
||||
a: First number
|
||||
b: Second number
|
||||
|
||||
Returns:
|
||||
Calculation result
|
||||
"""
|
||||
operations = {
|
||||
"add": lambda x, y: x + y,
|
||||
"subtract": lambda x, y: x - y,
|
||||
"multiply": lambda x, y: x * y,
|
||||
"divide": lambda x, y: x / y if y != 0 else None
|
||||
}
|
||||
|
||||
if operation not in operations:
|
||||
return format_error(
|
||||
f"Unknown operation: {operation}",
|
||||
"INVALID_OPERATION"
|
||||
)
|
||||
|
||||
result = operations[operation](a, b)
|
||||
|
||||
if result is None:
|
||||
return format_error("Division by zero", "DIVISION_BY_ZERO")
|
||||
|
||||
return format_success({
|
||||
"operation": operation,
|
||||
"a": a,
|
||||
"b": b,
|
||||
"result": result
|
||||
})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def batch_process_with_progress(
|
||||
items: List[str],
|
||||
context: Context
|
||||
) -> dict:
|
||||
"""
|
||||
Process items with progress tracking.
|
||||
|
||||
Args:
|
||||
items: List of items to process
|
||||
context: FastMCP context for progress reporting
|
||||
|
||||
Returns:
|
||||
Processing results
|
||||
"""
|
||||
results = []
|
||||
total = len(items)
|
||||
|
||||
for i, item in enumerate(items):
|
||||
# Report progress
|
||||
await context.report_progress(
|
||||
progress=i + 1,
|
||||
total=total,
|
||||
message=f"Processing {i + 1}/{total}: {item}"
|
||||
)
|
||||
|
||||
# Simulate processing
|
||||
await asyncio.sleep(0.1)
|
||||
results.append({
|
||||
"item": item,
|
||||
"processed": item.upper(),
|
||||
"index": i
|
||||
})
|
||||
|
||||
return format_success({
|
||||
"total": total,
|
||||
"results": results
|
||||
}, "Batch processing complete")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def clear_cache() -> dict:
|
||||
"""Clear all cached data."""
|
||||
try:
|
||||
SimpleCache.clear()
|
||||
return format_success({"cleared": True}, "Cache cleared")
|
||||
except Exception as e:
|
||||
return format_error(str(e), "CACHE_ERROR")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Resources
|
||||
# ============================================================================
|
||||
|
||||
@mcp.resource("info://status")
|
||||
async def server_status() -> dict:
|
||||
"""Get current server status."""
|
||||
return {
|
||||
"server": Config.SERVER_NAME,
|
||||
"version": Config.SERVER_VERSION,
|
||||
"status": "operational",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"api_configured": bool(Config.API_BASE_URL and Config.API_KEY),
|
||||
"cache_entries": len(SimpleCache._cache)
|
||||
}
|
||||
|
||||
|
||||
@mcp.resource("info://config")
|
||||
def server_config() -> dict:
|
||||
"""Get server configuration (non-sensitive)."""
|
||||
return {
|
||||
"server_name": Config.SERVER_NAME,
|
||||
"version": Config.SERVER_VERSION,
|
||||
"cache_ttl": Config.CACHE_TTL,
|
||||
"max_retries": Config.MAX_RETRIES,
|
||||
"api_configured": bool(Config.API_BASE_URL)
|
||||
}
|
||||
|
||||
|
||||
@mcp.resource("health://check")
|
||||
async def health_check() -> dict:
|
||||
"""Comprehensive health check."""
|
||||
checks = {}
|
||||
|
||||
# Check API
|
||||
if Config.API_BASE_URL:
|
||||
try:
|
||||
client = await APIClient.get_client()
|
||||
if client:
|
||||
response = await client.get("/health", timeout=5)
|
||||
checks["api"] = response.status_code == 200
|
||||
except:
|
||||
checks["api"] = False
|
||||
else:
|
||||
checks["api"] = None # Not configured
|
||||
|
||||
# Check cache
|
||||
checks["cache"] = True # Always available
|
||||
|
||||
return {
|
||||
"status": "healthy" if all(v for v in checks.values() if v is not None) else "degraded",
|
||||
"checks": checks,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main Execution
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
print(f"Starting {Config.SERVER_NAME}...")
|
||||
mcp.run()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down...")
|
||||
finally:
|
||||
# Cleanup
|
||||
import asyncio
|
||||
asyncio.run(APIClient.cleanup())
|
||||
221
templates/tools-examples.py
Normal file
221
templates/tools-examples.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
FastMCP Tools Examples
|
||||
======================
|
||||
Comprehensive examples of tool patterns: sync, async, validation, error handling.
|
||||
"""
|
||||
|
||||
from fastmcp import FastMCP, Context
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
|
||||
mcp = FastMCP("Tools Examples")
|
||||
|
||||
# ============================================================================
|
||||
# Basic Tools
|
||||
# ============================================================================
|
||||
|
||||
@mcp.tool()
|
||||
def simple_sync_tool(text: str) -> str:
|
||||
"""Simple synchronous tool."""
|
||||
return text.upper()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def simple_async_tool(text: str) -> str:
|
||||
"""Simple asynchronous tool."""
|
||||
await asyncio.sleep(0.1) # Simulate async operation
|
||||
return text.lower()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tools with Validation
|
||||
# ============================================================================
|
||||
|
||||
class SearchParams(BaseModel):
|
||||
"""Validated search parameters."""
|
||||
query: str = Field(min_length=1, max_length=100, description="Search query")
|
||||
limit: int = Field(default=10, ge=1, le=100, description="Maximum results")
|
||||
offset: int = Field(default=0, ge=0, description="Offset for pagination")
|
||||
|
||||
@validator("query")
|
||||
def clean_query(cls, v):
|
||||
return v.strip()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def validated_search(params: SearchParams) -> dict:
|
||||
"""
|
||||
Search with validated parameters.
|
||||
|
||||
Pydantic automatically validates all parameters.
|
||||
"""
|
||||
return {
|
||||
"query": params.query,
|
||||
"limit": params.limit,
|
||||
"offset": params.offset,
|
||||
"results": [
|
||||
{"id": 1, "title": f"Result for: {params.query}"},
|
||||
{"id": 2, "title": f"Another result for: {params.query}"}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tools with Optional Parameters
|
||||
# ============================================================================
|
||||
|
||||
@mcp.tool()
|
||||
def process_text(
|
||||
text: str,
|
||||
uppercase: bool = False,
|
||||
prefix: Optional[str] = None,
|
||||
suffix: Optional[str] = None
|
||||
) -> str:
|
||||
"""Process text with optional transformations."""
|
||||
result = text
|
||||
|
||||
if uppercase:
|
||||
result = result.upper()
|
||||
|
||||
if prefix:
|
||||
result = f"{prefix}{result}"
|
||||
|
||||
if suffix:
|
||||
result = f"{result}{suffix}"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tools with Complex Return Types
|
||||
# ============================================================================
|
||||
|
||||
@mcp.tool()
|
||||
async def batch_process(items: List[str]) -> Dict[str, Any]:
|
||||
"""Process multiple items and return detailed results."""
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
for i, item in enumerate(items):
|
||||
try:
|
||||
# Simulate processing
|
||||
await asyncio.sleep(0.1)
|
||||
results.append({
|
||||
"index": i,
|
||||
"item": item,
|
||||
"processed": item.upper(),
|
||||
"success": True
|
||||
})
|
||||
except Exception as e:
|
||||
errors.append({
|
||||
"index": i,
|
||||
"item": item,
|
||||
"error": str(e),
|
||||
"success": False
|
||||
})
|
||||
|
||||
return {
|
||||
"total": len(items),
|
||||
"successful": len(results),
|
||||
"failed": len(errors),
|
||||
"results": results,
|
||||
"errors": errors,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tools with Error Handling
|
||||
# ============================================================================
|
||||
|
||||
@mcp.tool()
|
||||
async def safe_operation(data: dict) -> dict:
|
||||
"""Operation with comprehensive error handling."""
|
||||
try:
|
||||
# Validate input
|
||||
if not data:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Data is required",
|
||||
"code": "VALIDATION_ERROR"
|
||||
}
|
||||
|
||||
# Simulate operation
|
||||
await asyncio.sleep(0.1)
|
||||
processed_data = {k: v.upper() if isinstance(v, str) else v for k, v in data.items()}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": processed_data,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except KeyError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Missing key: {e}",
|
||||
"code": "KEY_ERROR"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"code": "UNKNOWN_ERROR"
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tools with Context (for Elicitation, Progress, Sampling)
|
||||
# ============================================================================
|
||||
|
||||
@mcp.tool()
|
||||
async def tool_with_progress(count: int, context: Context) -> dict:
|
||||
"""Tool that reports progress."""
|
||||
results = []
|
||||
|
||||
for i in range(count):
|
||||
# Report progress if available
|
||||
if context and hasattr(context, 'report_progress'):
|
||||
await context.report_progress(
|
||||
progress=i + 1,
|
||||
total=count,
|
||||
message=f"Processing item {i + 1}/{count}"
|
||||
)
|
||||
|
||||
# Simulate work
|
||||
await asyncio.sleep(0.1)
|
||||
results.append(f"Item {i + 1}")
|
||||
|
||||
return {
|
||||
"count": count,
|
||||
"results": results,
|
||||
"status": "completed"
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def tool_with_elicitation(context: Context) -> dict:
|
||||
"""Tool that requests user input."""
|
||||
if context and hasattr(context, 'request_elicitation'):
|
||||
# Request user input
|
||||
user_name = await context.request_elicitation(
|
||||
prompt="What is your name?",
|
||||
response_type=str
|
||||
)
|
||||
|
||||
return {
|
||||
"greeting": f"Hello, {user_name}!",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
else:
|
||||
return {"error": "Elicitation not available"}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
Reference in New Issue
Block a user