initial project setup: added plugin skill
This commit is contained in:
@@ -0,0 +1,631 @@
|
||||
# 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()
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user