631 lines
16 KiB
Markdown
631 lines
16 KiB
Markdown
# MCP Server Integration
|
|
|
|
Comprehensive guide for integrating Model Context Protocol (MCP) servers with Claude plugins.
|
|
|
|
## MCP Overview
|
|
|
|
MCP servers provide structured interfaces to external tools and services, enabling Claude to interact with databases, APIs, and other systems through a standardized protocol.
|
|
|
|
## Basic Configuration
|
|
|
|
### .mcp.json Structure
|
|
```json
|
|
{
|
|
"name": "restaurant-data-server",
|
|
"version": "1.0.0",
|
|
"description": "MCP server for restaurant database access",
|
|
"command": "python",
|
|
"args": ["servers/restaurant_mcp.py"],
|
|
"env": {
|
|
"DATABASE_URL": "${RESTAURANT_DB_URL}",
|
|
"API_KEY": "${RESTAURANT_API_KEY}"
|
|
},
|
|
"capabilities": {
|
|
"resources": true,
|
|
"tools": true,
|
|
"subscriptions": true
|
|
}
|
|
}
|
|
```
|
|
|
|
## Server Implementation
|
|
|
|
### Python MCP Server
|
|
```python
|
|
#!/usr/bin/env python3
|
|
# servers/restaurant_mcp.py
|
|
|
|
import asyncio
|
|
import json
|
|
from typing import Any, Dict, List
|
|
from mcp import MCPServer, Resource, Tool
|
|
|
|
class RestaurantMCPServer(MCPServer):
|
|
def __init__(self):
|
|
super().__init__("restaurant-data-server")
|
|
self.setup_tools()
|
|
self.setup_resources()
|
|
|
|
def setup_tools(self):
|
|
@self.tool("get_sales_data")
|
|
async def get_sales_data(date: str, location: str = None) -> Dict:
|
|
"""Retrieve sales data for specified date and location"""
|
|
# Implementation
|
|
return {
|
|
"date": date,
|
|
"location": location,
|
|
"total_sales": 15420.50,
|
|
"transactions": 342
|
|
}
|
|
|
|
@self.tool("update_inventory")
|
|
async def update_inventory(item_id: str, quantity: int) -> Dict:
|
|
"""Update inventory levels for an item"""
|
|
# Implementation
|
|
return {
|
|
"item_id": item_id,
|
|
"new_quantity": quantity,
|
|
"status": "updated"
|
|
}
|
|
|
|
def setup_resources(self):
|
|
@self.resource("menu_items")
|
|
async def get_menu_items() -> List[Resource]:
|
|
"""List all menu items"""
|
|
items = await fetch_menu_from_db()
|
|
return [
|
|
Resource(
|
|
id=f"menu_item_{item['id']}",
|
|
name=item['name'],
|
|
description=f"Menu item: {item['name']}",
|
|
metadata={"price": item['price'], "category": item['category']}
|
|
)
|
|
for item in items
|
|
]
|
|
|
|
if __name__ == "__main__":
|
|
server = RestaurantMCPServer()
|
|
asyncio.run(server.run())
|
|
```
|
|
|
|
### Node.js MCP Server
|
|
```javascript
|
|
#!/usr/bin/env node
|
|
// servers/restaurant_mcp.js
|
|
|
|
const { MCPServer, Tool, Resource } = require('@modelcontextprotocol/server');
|
|
|
|
class RestaurantMCPServer extends MCPServer {
|
|
constructor() {
|
|
super('restaurant-data-server');
|
|
this.setupTools();
|
|
this.setupResources();
|
|
}
|
|
|
|
setupTools() {
|
|
this.registerTool(new Tool({
|
|
name: 'get_sales_data',
|
|
description: 'Retrieve sales data',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
date: { type: 'string', format: 'date' },
|
|
location: { type: 'string' }
|
|
},
|
|
required: ['date']
|
|
},
|
|
handler: async ({ date, location }) => {
|
|
// Implementation
|
|
return {
|
|
date,
|
|
location,
|
|
total_sales: 15420.50,
|
|
transactions: 342
|
|
};
|
|
}
|
|
}));
|
|
}
|
|
|
|
setupResources() {
|
|
this.registerResourceProvider({
|
|
pattern: /^menu_items$/,
|
|
handler: async () => {
|
|
const items = await this.fetchMenuFromDB();
|
|
return items.map(item => ({
|
|
id: `menu_item_${item.id}`,
|
|
name: item.name,
|
|
content: JSON.stringify(item, null, 2),
|
|
mimeType: 'application/json'
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const server = new RestaurantMCPServer();
|
|
server.start();
|
|
```
|
|
|
|
## Tool Definitions
|
|
|
|
### Tool Schema
|
|
```json
|
|
{
|
|
"name": "analyze_customer_feedback",
|
|
"description": "Analyze customer feedback sentiment",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"feedback_id": {
|
|
"type": "string",
|
|
"description": "Unique feedback identifier"
|
|
},
|
|
"include_suggestions": {
|
|
"type": "boolean",
|
|
"default": true,
|
|
"description": "Include improvement suggestions"
|
|
}
|
|
},
|
|
"required": ["feedback_id"]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Complex Tool Example
|
|
```python
|
|
@server.tool("generate_report")
|
|
async def generate_report(
|
|
report_type: str,
|
|
start_date: str,
|
|
end_date: str,
|
|
format: str = "pdf",
|
|
filters: Dict[str, Any] = None
|
|
) -> Dict[str, Any]:
|
|
"""Generate comprehensive business report
|
|
|
|
Args:
|
|
report_type: Type of report (sales, inventory, customer)
|
|
start_date: Report start date (YYYY-MM-DD)
|
|
end_date: Report end date (YYYY-MM-DD)
|
|
format: Output format (pdf, excel, json)
|
|
filters: Additional filters to apply
|
|
|
|
Returns:
|
|
Report data and download URL
|
|
"""
|
|
# Validate inputs
|
|
if not validate_date_range(start_date, end_date):
|
|
raise ValueError("Invalid date range")
|
|
|
|
# Generate report
|
|
report_data = await compile_report_data(
|
|
report_type, start_date, end_date, filters
|
|
)
|
|
|
|
# Format output
|
|
if format == "pdf":
|
|
url = await generate_pdf_report(report_data)
|
|
elif format == "excel":
|
|
url = await generate_excel_report(report_data)
|
|
else:
|
|
url = await save_json_report(report_data)
|
|
|
|
return {
|
|
"report_type": report_type,
|
|
"period": f"{start_date} to {end_date}",
|
|
"download_url": url,
|
|
"summary": report_data.get("summary", {})
|
|
}
|
|
```
|
|
|
|
## Resource Management
|
|
|
|
### Static Resources
|
|
```python
|
|
@server.resource("config/database_schema")
|
|
async def get_database_schema() -> Resource:
|
|
"""Provide database schema documentation"""
|
|
schema = load_schema_file()
|
|
return Resource(
|
|
id="database_schema",
|
|
name="Restaurant Database Schema",
|
|
content=schema,
|
|
mimeType="text/markdown"
|
|
)
|
|
```
|
|
|
|
### Dynamic Resources
|
|
```python
|
|
@server.resource_pattern(r"^orders/(\d{4}-\d{2}-\d{2})$")
|
|
async def get_daily_orders(date: str) -> List[Resource]:
|
|
"""Get orders for a specific date"""
|
|
orders = await fetch_orders_by_date(date)
|
|
return [
|
|
Resource(
|
|
id=f"order_{order['id']}",
|
|
name=f"Order #{order['number']}",
|
|
content=json.dumps(order, indent=2),
|
|
mimeType="application/json",
|
|
metadata={
|
|
"customer": order['customer_name'],
|
|
"total": order['total_amount'],
|
|
"status": order['status']
|
|
}
|
|
)
|
|
for order in orders
|
|
]
|
|
```
|
|
|
|
### Subscription Resources
|
|
```python
|
|
@server.subscription("live_orders")
|
|
async def subscribe_to_orders(callback):
|
|
"""Subscribe to live order updates"""
|
|
async def order_handler(order):
|
|
await callback(Resource(
|
|
id=f"live_order_{order['id']}",
|
|
name=f"New Order #{order['number']}",
|
|
content=json.dumps(order),
|
|
mimeType="application/json"
|
|
))
|
|
|
|
# Register handler with order system
|
|
order_system.on_new_order(order_handler)
|
|
|
|
# Return unsubscribe function
|
|
return lambda: order_system.off_new_order(order_handler)
|
|
```
|
|
|
|
## Security Implementation
|
|
|
|
### Authentication
|
|
```python
|
|
class SecureRestaurantServer(MCPServer):
|
|
def __init__(self):
|
|
super().__init__("secure-restaurant-server")
|
|
self.auth_token = os.environ.get("MCP_AUTH_TOKEN")
|
|
|
|
async def authenticate(self, request):
|
|
"""Validate authentication token"""
|
|
token = request.headers.get("Authorization")
|
|
if not token or token != f"Bearer {self.auth_token}":
|
|
raise AuthenticationError("Invalid token")
|
|
|
|
async def handle_request(self, request):
|
|
await self.authenticate(request)
|
|
return await super().handle_request(request)
|
|
```
|
|
|
|
### Input Validation
|
|
```python
|
|
@server.tool("update_menu_item")
|
|
async def update_menu_item(item_id: str, updates: Dict) -> Dict:
|
|
"""Securely update menu item"""
|
|
# Validate item_id format
|
|
if not re.match(r"^[A-Z0-9]{8}$", item_id):
|
|
raise ValueError("Invalid item ID format")
|
|
|
|
# Validate allowed fields
|
|
allowed_fields = {"name", "price", "description", "category"}
|
|
invalid_fields = set(updates.keys()) - allowed_fields
|
|
if invalid_fields:
|
|
raise ValueError(f"Invalid fields: {invalid_fields}")
|
|
|
|
# Validate data types
|
|
if "price" in updates:
|
|
if not isinstance(updates["price"], (int, float)):
|
|
raise TypeError("Price must be numeric")
|
|
if updates["price"] < 0:
|
|
raise ValueError("Price cannot be negative")
|
|
|
|
# Apply updates
|
|
result = await db.update_menu_item(item_id, updates)
|
|
return {"status": "success", "updated": result}
|
|
```
|
|
|
|
### Rate Limiting
|
|
```python
|
|
from functools import wraps
|
|
import time
|
|
|
|
def rate_limit(max_calls=10, time_window=60):
|
|
calls = {}
|
|
|
|
def decorator(func):
|
|
@wraps(func)
|
|
async def wrapper(self, *args, **kwargs):
|
|
client_id = kwargs.get('client_id', 'default')
|
|
now = time.time()
|
|
|
|
# Clean old calls
|
|
calls[client_id] = [
|
|
t for t in calls.get(client_id, [])
|
|
if now - t < time_window
|
|
]
|
|
|
|
# Check rate limit
|
|
if len(calls[client_id]) >= max_calls:
|
|
raise RateLimitError(f"Rate limit exceeded: {max_calls}/{time_window}s")
|
|
|
|
# Record call
|
|
calls[client_id].append(now)
|
|
|
|
# Execute function
|
|
return await func(self, *args, **kwargs)
|
|
return wrapper
|
|
return decorator
|
|
|
|
# Usage
|
|
@server.tool("expensive_operation")
|
|
@rate_limit(max_calls=5, time_window=300)
|
|
async def expensive_operation(data: str) -> Dict:
|
|
"""Rate-limited expensive operation"""
|
|
result = await perform_expensive_calculation(data)
|
|
return {"result": result}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Graceful Errors
|
|
```python
|
|
@server.tool("process_order")
|
|
async def process_order(order_data: Dict) -> Dict:
|
|
try:
|
|
# Validate order
|
|
validation_result = validate_order(order_data)
|
|
if not validation_result.is_valid:
|
|
return {
|
|
"status": "error",
|
|
"error_code": "INVALID_ORDER",
|
|
"message": validation_result.message,
|
|
"fields": validation_result.invalid_fields
|
|
}
|
|
|
|
# Process order
|
|
result = await order_processor.process(order_data)
|
|
return {
|
|
"status": "success",
|
|
"order_id": result.order_id,
|
|
"estimated_time": result.estimated_time
|
|
}
|
|
|
|
except InventoryError as e:
|
|
return {
|
|
"status": "error",
|
|
"error_code": "INSUFFICIENT_INVENTORY",
|
|
"message": str(e),
|
|
"available_items": e.available_items
|
|
}
|
|
except Exception as e:
|
|
# Log unexpected errors
|
|
logger.error(f"Unexpected error: {e}")
|
|
return {
|
|
"status": "error",
|
|
"error_code": "INTERNAL_ERROR",
|
|
"message": "An unexpected error occurred"
|
|
}
|
|
```
|
|
|
|
### Error Recovery
|
|
```python
|
|
class ResilientMCPServer(MCPServer):
|
|
def __init__(self):
|
|
super().__init__("resilient-server")
|
|
self.db = None
|
|
self.reconnect_attempts = 0
|
|
|
|
async def ensure_connection(self):
|
|
"""Ensure database connection with retry logic"""
|
|
if self.db and self.db.is_connected():
|
|
return
|
|
|
|
for attempt in range(3):
|
|
try:
|
|
self.db = await create_db_connection()
|
|
self.reconnect_attempts = 0
|
|
return
|
|
except ConnectionError:
|
|
await asyncio.sleep(2 ** attempt)
|
|
|
|
raise ServiceUnavailableError("Cannot connect to database")
|
|
|
|
async def handle_tool_call(self, tool_name, params):
|
|
await self.ensure_connection()
|
|
return await super().handle_tool_call(tool_name, params)
|
|
```
|
|
|
|
## Testing MCP Servers
|
|
|
|
### Unit Testing
|
|
```python
|
|
import pytest
|
|
from unittest.mock import AsyncMock
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_sales_data():
|
|
server = RestaurantMCPServer()
|
|
server.db = AsyncMock()
|
|
server.db.query.return_value = [
|
|
{"date": "2024-01-15", "total": 1000}
|
|
]
|
|
|
|
result = await server.tools["get_sales_data"](
|
|
date="2024-01-15",
|
|
location="main"
|
|
)
|
|
|
|
assert result["total_sales"] == 1000
|
|
server.db.query.assert_called_once()
|
|
```
|
|
|
|
### Integration Testing
|
|
```python
|
|
async def test_mcp_server_integration():
|
|
# Start test server
|
|
server = RestaurantMCPServer()
|
|
test_port = 8765
|
|
await server.start(port=test_port)
|
|
|
|
# Create client
|
|
client = MCPClient(f"http://localhost:{test_port}")
|
|
|
|
# Test tool call
|
|
result = await client.call_tool(
|
|
"get_sales_data",
|
|
{"date": "2024-01-15"}
|
|
)
|
|
|
|
assert result["status"] == "success"
|
|
|
|
# Cleanup
|
|
await server.stop()
|
|
```
|
|
|
|
### Mock Server for Development
|
|
```javascript
|
|
// servers/mock_restaurant_mcp.js
|
|
class MockRestaurantServer extends MCPServer {
|
|
constructor() {
|
|
super('mock-restaurant-server');
|
|
this.setupMockTools();
|
|
}
|
|
|
|
setupMockTools() {
|
|
this.registerTool({
|
|
name: 'get_sales_data',
|
|
handler: async ({ date }) => ({
|
|
date,
|
|
total_sales: Math.random() * 10000,
|
|
transactions: Math.floor(Math.random() * 500)
|
|
})
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Performance Optimization
|
|
|
|
### Caching
|
|
```python
|
|
from functools import lru_cache
|
|
from cachetools import TTLCache
|
|
|
|
class CachedMCPServer(MCPServer):
|
|
def __init__(self):
|
|
super().__init__("cached-server")
|
|
self.cache = TTLCache(maxsize=100, ttl=300)
|
|
|
|
@server.tool("get_analytics")
|
|
async def get_analytics(self, date_range: str) -> Dict:
|
|
# Check cache
|
|
cache_key = f"analytics_{date_range}"
|
|
if cache_key in self.cache:
|
|
return self.cache[cache_key]
|
|
|
|
# Compute analytics
|
|
result = await self.compute_analytics(date_range)
|
|
|
|
# Store in cache
|
|
self.cache[cache_key] = result
|
|
return result
|
|
```
|
|
|
|
### Connection Pooling
|
|
```python
|
|
import asyncpg
|
|
|
|
class PooledMCPServer(MCPServer):
|
|
def __init__(self):
|
|
super().__init__("pooled-server")
|
|
self.db_pool = None
|
|
|
|
async def initialize(self):
|
|
self.db_pool = await asyncpg.create_pool(
|
|
database="restaurant_db",
|
|
user="mcp_user",
|
|
password=os.environ["DB_PASSWORD"],
|
|
host="localhost",
|
|
port=5432,
|
|
min_size=5,
|
|
max_size=20
|
|
)
|
|
|
|
async def query(self, sql, *args):
|
|
async with self.db_pool.acquire() as conn:
|
|
return await conn.fetch(sql, *args)
|
|
```
|
|
|
|
## Deployment
|
|
|
|
### Docker Configuration
|
|
```dockerfile
|
|
# Dockerfile
|
|
FROM python:3.11-slim
|
|
|
|
WORKDIR /app
|
|
|
|
COPY requirements.txt .
|
|
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
COPY servers/ ./servers/
|
|
COPY .mcp.json .
|
|
|
|
ENV PYTHONUNBUFFERED=1
|
|
|
|
CMD ["python", "servers/restaurant_mcp.py"]
|
|
```
|
|
|
|
### Systemd Service
|
|
```ini
|
|
# /etc/systemd/system/restaurant-mcp.service
|
|
[Unit]
|
|
Description=Restaurant MCP Server
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=mcp
|
|
WorkingDirectory=/opt/restaurant-mcp
|
|
Environment="DATABASE_URL=postgresql://localhost/restaurant"
|
|
ExecStart=/usr/bin/python3 /opt/restaurant-mcp/servers/restaurant_mcp.py
|
|
Restart=always
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
### Health Checks
|
|
```python
|
|
@server.tool("health_check")
|
|
async def health_check() -> Dict:
|
|
"""MCP server health check endpoint"""
|
|
checks = {
|
|
"server": "ok",
|
|
"database": "unknown",
|
|
"cache": "unknown"
|
|
}
|
|
|
|
# Check database
|
|
try:
|
|
await db.execute("SELECT 1")
|
|
checks["database"] = "ok"
|
|
except:
|
|
checks["database"] = "error"
|
|
|
|
# Check cache
|
|
try:
|
|
cache.get("test")
|
|
checks["cache"] = "ok"
|
|
except:
|
|
checks["cache"] = "error"
|
|
|
|
overall_status = "healthy" if all(
|
|
v == "ok" for v in checks.values()
|
|
) else "unhealthy"
|
|
|
|
return {
|
|
"status": overall_status,
|
|
"checks": checks,
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
``` |