Generic MCP Server Development
This skill provides comprehensive patterns and reusable code for building MCP (Model Context Protocol) servers that can expose any domain operations as tools for AI agents. Follows 2025 best practices for performance, security, and maintainability.
When to Use This Skill
Use this skill when you need to:
- Build an MCP server for any domain (not just todos)
- Expose database operations as MCP tools
- Create AI-agent accessible APIs
- Implement async MCP tool handlers
- Add proper error handling and validation
- Support rate limiting and caching
- Build enterprise-grade MCP servers
- Integrate with multiple storage backends
1. Generic MCP Server Architecture
# mcp_server/core.py
#!/usr/bin/env python3
"""
Generic MCP Server Base Architecture
Provides reusable patterns for any MCP server implementation
"""
import asyncio
import json
import logging
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Sequence, Union, Callable
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from enum import Enum
import redis.asyncio as redis
from mcp.server import Server, NotificationOptions, stdio
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from mcp.types import (
Resource, Tool, TextContent, ImageContent, EmbeddedResource,
LoggingLevel, CallToolRequest, EmptyResult,
ListResourcesRequest, ListToolsRequest, ReadResourceRequest,
GetPromptRequest, ListPromptsRequest
)
from pydantic import BaseModel, Field, validator
import aiofiles
import yaml
from pathlib import Path
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("mcp_server")
class ServerConfig(BaseModel):
"""MCP Server configuration"""
name: str
version: str = "1.0.0"
description: str
debug: bool = False
redis_url: Optional[str] = None
rate_limit_requests: int = 100
rate_limit_window: int = 60
cache_ttl: int = 300
max_retries: int = 3
timeout: int = 30
class Config:
extra = "allow"
@dataclass
class RequestContext:
"""Request context for tool calls"""
user_id: str
session_id: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
timestamp: datetime = field(default_factory=datetime.utcnow)
class RateLimiter:
"""Redis-based rate limiter for MCP operations"""
def __init__(self, redis_url: str, requests: int, window: int):
self.redis_url = redis_url
self.requests = requests
self.window = window
self._redis = None
async def _get_redis(self):
if not self._redis:
self._redis = await redis.from_url(self.redis_url)
return self._redis
async def is_allowed(self, key: str) -> bool:
"""Check if request is allowed"""
r = await self._get_redis()
current = await r.incr(f"rate_limit:{key}")
if current == 1:
await r.expire(f"rate_limit:{key}", self.window)
return current <= self.requests
async def get_remaining(self, key: str) -> int:
"""Get remaining requests"""
r = await self._get_redis()
current = await r.get(f"rate_limit:{key}")
return max(0, self.requests - int(current or 0))
class CacheManager:
"""Redis-based caching for MCP responses"""
def __init__(self, redis_url: str, ttl: int = 300):
self.redis_url = redis_url
self.ttl = ttl
self._redis = None
async def _get_redis(self):
if not self._redis:
self._redis = await redis.from_url(self.redis_url)
return self._redis
def _make_key(self, tool_name: str, args: Dict[str, Any]) -> str:
"""Generate cache key from tool name and arguments"""
import hashlib
args_str = json.dumps(args, sort_keys=True)
return f"cache:{tool_name}:{hashlib.md5(args_str.encode()).hexdigest()}"
async def get(self, tool_name: str, args: Dict[str, Any]) -> Optional[Any]:
"""Get cached result"""
r = await self._get_redis()
key = self._make_key(tool_name, args)
result = await r.get(key)
return json.loads(result) if result else None
async def set(self, tool_name: str, args: Dict[str, Any], value: Any):
"""Cache result"""
r = await self._get_redis()
key = self._make_key(tool_name, args)
await r.setex(key, self.ttl, json.dumps(value))
class BaseMCPServer:
"""Base MCP Server with common functionality"""
def __init__(self, config: ServerConfig):
self.config = config
self.server = Server(config.name)
self.tools: Dict[str, Callable] = {}
self.rate_limiter: Optional[RateLimiter] = None
self.cache: Optional[CacheManager] = None
# Setup optional components
if config.redis_url:
self.rate_limiter = RateLimiter(
config.redis_url,
config.rate_limit_requests,
config.rate_limit_window
)
self.cache = CacheManager(
config.redis_url,
config.cache_ttl
)
# Register handlers
self._register_handlers()
logger.info(f"MCP Server '{config.name}' initialized")
def _register_handlers(self):
"""Register MCP handlers"""
@self.server.list_tools()
async def handle_list_tools() -> List[Tool]:
"""Return list of available tools"""
return await self.list_tools()
@self.server.call_tool()
async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
"""Handle tool call with rate limiting and caching"""
return await self.call_tool(name, arguments)
def register_tool(self, name: str, handler: Callable, schema: Dict[str, Any]):
"""Register a new tool"""
self.tools[name] = {
"handler": handler,
"schema": schema
}
logger.info(f"Registered tool: {name}")
async def list_tools(self) -> List[Tool]:
"""List all available tools"""
tools = []
for name, tool_info in self.tools.items():
tools.append(Tool(
name=name,
description=tool_info["schema"].get("description", ""),
inputSchema=tool_info["schema"].get("inputSchema", {})
))
return tools
async def call_tool(self, name: str, arguments: Dict[str, Any]) -> List[TextContent]:
"""Execute a tool call with full middleware pipeline"""
start_time = datetime.utcnow()
try:
# Extract context from arguments
context = self._extract_context(arguments)
# Rate limiting check
if self.rate_limiter:
rate_key = f"{context.user_id}:{name}"
if not await self.rate_limiter.is_allowed(rate_key):
return [TextContent(
type="text",
text=json.dumps({
"status": "error",
"error": "Rate limit exceeded",
"remaining": await self.rate_limiter.get_remaining(rate_key)
})
)]
# Check cache
if self.cache and self._is_cacheable(name):
cached_result = await self.cache.get(name, arguments)
if cached_result:
logger.info(f"Cache hit for tool: {name}")
return [TextContent(
type="text",
text=json.dumps(cached_result)
)]
# Validate tool exists
if name not in self.tools:
raise ValueError(f"Unknown tool: {name}")
# Validate arguments
schema = self.tools[name]["schema"]
self._validate_arguments(arguments, schema)
# Execute tool
handler = self.tools[name]["handler"]
result = await self._execute_tool(handler, arguments, context)
# Cache result if applicable
if self.cache and self._is_cacheable(name) and result.get("status") != "error":
await self.cache.set(name, arguments, result)
# Log execution
duration = (datetime.utcnow() - start_time).total_seconds()
logger.info(f"Tool {name} executed in {duration:.2f}s for user {context.user_id}")
return [TextContent(
type="text",
text=json.dumps(result, default=str)
)]
except Exception as e:
logger.error(f"Error executing tool {name}: {str(e)}", exc_info=True)
duration = (datetime.utcnow() - start_time).total_seconds()
logger.error(f"Tool {name} failed after {duration:.2f}s")
return [TextContent(
type="text",
text=json.dumps({
"status": "error",
"error": str(e),
"tool": name,
"timestamp": datetime.utcnow().isoformat()
})
)]
def _extract_context(self, arguments: Dict[str, Any]) -> RequestContext:
"""Extract request context from arguments"""
user_id = arguments.pop("_user_id", "anonymous")
session_id = arguments.pop("_session_id", None)
metadata = arguments.pop("_metadata", {})
return RequestContext(
user_id=user_id,
session_id=session_id,
metadata=metadata
)
def _validate_arguments(self, arguments: Dict[str, Any], schema: Dict[str, Any]):
"""Validate tool arguments against schema"""
# Basic validation - can be extended with pydantic
input_schema = schema.get("inputSchema", {})
required = input_schema.get("required", [])
properties = input_schema.get("properties", {})
# Check required fields
for field in required:
if field not in arguments:
raise ValueError(f"Missing required field: {field}")
# Validate field types
for field, value in arguments.items():
if field in properties:
field_schema = properties[field]
expected_type = field_schema.get("type")
if expected_type == "string" and not isinstance(value, str):
raise ValueError(f"Field {field} must be a string")
elif expected_type == "integer" and not isinstance(value, int):
raise ValueError(f"Field {field} must be an integer")
elif expected_type == "array" and not isinstance(value, list):
raise ValueError(f"Field {field} must be an array")
# Check enum values
if "enum" in field_schema and value not in field_schema["enum"]:
raise ValueError(f"Field {field} must be one of {field_schema['enum']}")
def _is_cacheable(self, tool_name: str) -> bool:
"""Determine if tool result should be cached"""
# Non-mutating operations are cacheable
non_mutating = ["get", "list", "search", "find", "read"]
return any(op in tool_name.lower() for op in non_mutating)
async def _execute_tool(self, handler: Callable, arguments: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:
"""Execute tool handler with error handling"""
try:
# Pass context to handler if it accepts it
import inspect
sig = inspect.signature(handler)
if 'context' in sig.parameters:
result = await handler(arguments, context=context)
else:
result = await handler(arguments)
return result
except Exception as e:
logger.error(f"Tool handler failed: {str(e)}")
return {
"status": "error",
"error": str(e),
"timestamp": datetime.utcnow().isoformat()
}
async def run(self):
"""Start the MCP server"""
logger.info(f"Starting MCP server: {self.config.name}")
async with stdio_server() as (read_stream, write_stream):
await self.server.run(
read_stream,
write_stream,
InitializationOptions(
server_name=self.config.name,
server_version=self.config.version,
capabilities=self.server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
)
)
)
def tool(
name: Optional[str] = None,
description: str = "",
input_schema: Optional[Dict[str, Any]] = None
):
"""Decorator for registering MCP tools"""
def decorator(func):
tool_name = name or func.__name__
schema = {
"description": description or func.__doc__ or "",
"inputSchema": input_schema or {}
}
# Store schema on function for later registration
func._mcp_tool_schema = schema
func._mcp_tool_name = tool_name
return func
return decorator
2. Database Integration Patterns
# mcp_server/database.py
"""
Generic Database Integration for MCP Servers
Supports multiple ORMs and connection patterns
"""
import asyncio
from abc import ABC, abstractmethod
from contextlib import asynccontextmanager
from typing import Any, Dict, List, Optional, TypeVar, Generic, Union
from datetime import datetime
import json
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, DateTime, Text, Boolean, select, update, delete, insert
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy.pool import NullPool
import asyncpg
import motor.motor_asyncio
from redis.asyncio import Redis
# Type variables
T = TypeVar('T')
class DatabaseBackend(ABC):
"""Abstract base for database backends"""
@abstractmethod
async def connect(self):
"""Establish connection"""
pass
@abstractmethod
async def disconnect(self):
"""Close connection"""
pass
@abstractmethod
async def execute_query(self, query: str, params: Dict[str, Any] = None) -> List[Dict[str, Any]]:
"""Execute a query and return results"""
pass
@abstractmethod
async def execute_command(self, command: str, params: Dict[str, Any] = None) -> Any:
"""Execute a command (INSERT, UPDATE, DELETE)"""
pass
class PostgresBackend(DatabaseBackend):
"""PostgreSQL backend using asyncpg"""
def __init__(self, connection_string: str):
self.connection_string = connection_string
self.pool: Optional[asyncpg.Pool] = None
async def connect(self):
self.pool = await asyncpg.create_pool(
self.connection_string,
min_size=5,
max_size=20,
command_timeout=60
)
async def disconnect(self):
if self.pool:
await self.pool.close()
async def execute_query(self, query: str, params: Dict[str, Any] = None) -> List[Dict[str, Any]]:
async with self.pool.acquire() as conn:
rows = await conn.fetch(query, *params.values() if params else [])
return [dict(row) for row in rows]
async def execute_command(self, command: str, params: Dict[str, Any] = None) -> Any:
async with self.pool.acquire() as conn:
return await conn.execute(command, *params.values() if params else [])
class SQLAlchemyBackend(DatabaseBackend):
"""SQLAlchemy backend for multiple databases"""
def __init__(self, database_url: str, async_mode: bool = True):
self.database_url = database_url
self.async_mode = async_mode
self.engine = None
self.session_factory = None
async def connect(self):
if self.async_mode:
self.engine = create_async_engine(
self.database_url,
pool_pre_ping=True,
pool_recycle=300,
echo=False
)
self.session_factory = async_sessionmaker(
self.engine,
class_=AsyncSession,
expire_on_commit=False
)
else:
self.engine = create_engine(
self.database_url,
pool_pre_ping=True,
pool_recycle=300,
echo=False
)
self.session_factory = sessionmaker(
bind=self.engine,
expire_on_commit=False
)
async def disconnect(self):
if self.engine:
await self.engine.dispose()
@asynccontextmanager
async def get_session(self):
"""Get database session"""
async with self.session_factory() as session:
try:
yield session
if self.async_mode:
await session.commit()
else:
session.commit()
except Exception:
if self.async_mode:
await session.rollback()
else:
session.rollback()
raise
finally:
if self.async_mode:
await session.close()
else:
session.close()
async def execute_query(self, query: Any, params: Dict[str, Any] = None) -> List[Dict[str, Any]]:
"""Execute SQLAlchemy query"""
async with self.get_session() as session:
if isinstance(query, str):
# Raw SQL query
result = await session.execute(query, params or {})
rows = result.fetchall()
return [dict(row._mapping) for row in rows]
else:
# SQLAlchemy ORM query
result = await session.execute(query)
rows = result.fetchall()
return [dict(row._mapping) for row in rows]
async def execute_command(self, command: Any, params: Dict[str, Any] = None) -> Any:
"""Execute SQLAlchemy command"""
async with self.get_session() as session:
if isinstance(command, str):
# Raw SQL command
result = await session.execute(command, params or {})
await session.commit()
return result
else:
# SQLAlchemy ORM command
await session.execute(command, params or {})
await session.commit()
return None
class MongoBackend(DatabaseBackend):
"""MongoDB backend using motor"""
def __init__(self, connection_string: str, database_name: str):
self.connection_string = connection_string
self.database_name = database_name
self.client = None
self.db = None
async def connect(self):
self.client = motor.motor_asyncio.AsyncIOMotorClient(self.connection_string)
self.db = self.client[self.database_name]
async def disconnect(self):
if self.client:
self.client.close()
async def execute_query(self, collection: str, query: Dict[str, Any] = None) -> List[Dict[str, Any]]:
"""Execute MongoDB find query"""
cursor = self.db[collection].find(query or {})
results = []
async for document in cursor:
# Convert ObjectId to string
if '_id' in document:
document['_id'] = str(document['_id'])
results.append(document)
return results
async def execute_command(self, operation: str, collection: str, data: Dict[str, Any]) -> Any:
"""Execute MongoDB command"""
if operation == "insert":
result = await self.db[collection].insert_one(data)
return str(result.inserted_id)
elif operation == "update":
filter_ = data.pop("_filter")
update_data = {"$set": data}
result = await self.db[collection].update_one(filter_, update_data)
return result.modified_count
elif operation == "delete":
result = await self.db[collection].delete_one(data)
return result.deleted_count
class DatabaseManager(Generic[T]):
"""Generic database manager for MCP servers"""
def __init__(self, backend: DatabaseBackend):
self.backend = backend
self._connected = False
async def connect(self):
"""Connect to database"""
if not self._connected:
await self.backend.connect()
self._connected = True
async def disconnect(self):
"""Disconnect from database"""
if self._connected:
await self.backend.disconnect()
self._connected = False
@asynccontextmanager
async def transaction(self):
"""Database transaction context manager"""
if hasattr(self.backend, 'get_session'):
async with self.backend.get_session() as session:
yield session
else:
# For backends that don't support transactions
yield self.backend
async def find_one(self, table_or_collection: str, query: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Find a single record"""
if isinstance(self.backend, MongoBackend):
results = await self.backend.execute_query(table_or_collection, query)
return results[0] if results else None
else:
# SQL implementation
where_clause = " AND ".join([f"{k} = :{k}" for k in query.keys()])
sql = f"SELECT * FROM {table_or_collection} WHERE {where_clause} LIMIT 1"
results = await self.backend.execute_query(sql, query)
return results[0] if results else None
async def find_many(
self,
table_or_collection: str,
query: Dict[str, Any] = None,
limit: int = None,
offset: int = None,
order_by: str = None
) -> List[Dict[str, Any]]:
"""Find multiple records"""
query = query or {}
if isinstance(self.backend, MongoBackend):
cursor = self.backend.db[table_or_collection].find(query)
if limit:
cursor = cursor.limit(limit)
if offset:
cursor = cursor.skip(offset)
if order_by:
# MongoDB sort format
sort_field, sort_dir = order_by.split()
cursor = cursor.sort([(sort_field, 1 if sort_dir == 'ASC' else -1)])
results = []
async for document in cursor:
if '_id' in document:
document['_id'] = str(document['_id'])
results.append(document)
return results
else:
# SQL implementation
where_clause = ""
if query:
where_clause = "WHERE " + " AND ".join([f"{k} = :{k}" for k in query.keys()])
sql = f"SELECT * FROM {table_or_collection} {where_clause}"
if order_by:
sql += f" ORDER BY {order_by}"
if limit:
sql += f" LIMIT {limit}"
if offset:
sql += f" OFFSET {offset}"
return await self.backend.execute_query(sql, query)
async def create(self, table_or_collection: str, data: Dict[str, Any]) -> Any:
"""Create a new record"""
data = data.copy()
# Add timestamps
data['created_at'] = datetime.utcnow()
data['updated_at'] = datetime.utcnow()
if isinstance(self.backend, MongoBackend):
return await self.backend.execute_command("insert", table_or_collection, data)
else:
# SQL implementation
columns = list(data.keys())
placeholders = [f":{col}" for col in columns]
sql = f"INSERT INTO {table_or_collection} ({', '.join(columns)}) VALUES ({', '.join(placeholders)})"
return await self.backend.execute_command(sql, data)
async def update(self, table_or_collection: str, query: Dict[str, Any], data: Dict[str, Any]) -> int:
"""Update records"""
data = data.copy()
data['updated_at'] = datetime.utcnow()
if isinstance(self.backend, MongoBackend):
data['_filter'] = query
return await self.backend.execute_command("update", table_or_collection, data)
else:
# SQL implementation
where_clause = " AND ".join([f"{k} = :{k}" for k in query.keys()])
set_clause = ", ".join([f"{k} = :update_{k}" for k in data.keys()])
# Prefix update params to avoid conflicts
update_params = {f"update_{k}": v for k, v in data.items()}
params = {**query, **update_params}
sql = f"UPDATE {table_or_collection} SET {set_clause} WHERE {where_clause}"
result = await self.backend.execute_command(sql, params)
return result.rowcount if hasattr(result, 'rowcount') else 0
async def delete(self, table_or_collection: str, query: Dict[str, Any]) -> int:
"""Delete records"""
if isinstance(self.backend, MongoBackend):
return await self.backend.execute_command("delete", table_or_collection, query)
else:
# SQL implementation
where_clause = " AND ".join([f"{k} = :{k}" for k in query.keys()])
sql = f"DELETE FROM {table_or_collection} WHERE {where_clause}"
result = await self.backend.execute_command(sql, query)
return result.rowcount if hasattr(result, 'rowcount') else 0
async def count(self, table_or_collection: str, query: Dict[str, Any] = None) -> int:
"""Count records"""
query = query or {}
if isinstance(self.backend, MongoBackend):
return await self.backend.db[table_or_collection].count_documents(query)
else:
# SQL implementation
where_clause = ""
if query:
where_clause = "WHERE " + " AND ".join([f"{k} = :{k}" for k in query.keys()])
sql = f"SELECT COUNT(*) as count FROM {table_or_collection} {where_clause}"
results = await self.backend.execute_query(sql, query)
return results[0]['count'] if results else 0
# Factory function for creating database managers
def create_database_manager(database_url: str, backend_type: str = "auto") -> DatabaseManager:
"""Create database manager based on URL or backend type"""
if backend_type == "auto":
if database_url.startswith("postgresql+asyncpg://"):
backend = SQLAlchemyBackend(database_url, async_mode=True)
elif database_url.startswith("mongodb://"):
import re
match = re.match(r'mongodb://[^/]+/([^?]*)', database_url)
db_name = match.group(1) if match else "default"
backend = MongoBackend(database_url, db_name)
elif database_url.startswith("postgresql://"):
backend = PostgresBackend(database_url)
else:
backend = SQLAlchemyBackend(database_url, async_mode=True)
else:
if backend_type == "postgres":
backend = PostgresBackend(database_url)
elif backend_type == "mongodb":
db_name = database_url.split("/")[-1].split("?")[0]
backend = MongoBackend(database_url, db_name)
elif backend_type == "sqlalchemy":
backend = SQLAlchemyBackend(database_url)
else:
raise ValueError(f"Unknown backend type: {backend_type}")
return DatabaseManager(backend)
3. Tool Implementation Patterns
# mcp_server/tools.py
"""
Generic MCP Tool Implementation Patterns
"""
from typing import Any, Dict, List, Optional, Union, Callable
from datetime import datetime, timedelta
import json
import uuid
from dataclasses import dataclass, field
from .core import BaseMCPServer, tool, RequestContext
from .database import DatabaseManager
class BaseTool:
"""Base class for MCP tools"""
def __init__(self, db_manager: DatabaseManager, cache=None):
self.db_manager = db_manager
self.cache = cache
async def execute(self, args: Dict[str, Any], context: RequestContext = None) -> Dict[str, Any]:
"""Execute the tool logic"""
raise NotImplementedError
def _validate_permissions(self, context: RequestContext, required_permission: str = None) -> bool:
"""Validate user permissions"""
# Implement permission checking logic
return True
class CRUDBaseTool(BaseTool):
"""Base CRUD tool for any entity"""
def __init__(self, table_name: str, db_manager: DatabaseManager, schema: Dict[str, Any]):
super().__init__(db_manager)
self.table_name = table_name
self.schema = schema
self.entity_name = table_name.rstrip('s') # Remove plural 's'
async def create(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:
"""Create entity"""
try:
# Validate against schema
validated_data = self._validate_data(args, for_create=True)
# Add user context
if context:
validated_data['user_id'] = context.user_id
if context.session_id:
validated_data['session_id'] = context.session_id
# Insert into database
result = await self.db_manager.create(self.table_name, validated_data)
return {
"status": "created",
"id": result,
"entity": self.entity_name,
"timestamp": datetime.utcnow().isoformat()
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"entity": self.entity_name,
"operation": "create"
}
async def get(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:
"""Get entity by ID"""
try:
entity_id = args.get("id")
if not entity_id:
raise ValueError("Missing required field: id")
# Add user filter for security
query = {"id": entity_id}
if context and not self._validate_permissions(context, "read_all"):
query["user_id"] = context.user_id
result = await self.db_manager.find_one(self.table_name, query)
if not result:
return {
"status": "not_found",
"entity": self.entity_name,
"id": entity_id
}
return {
"status": "success",
"entity": self.entity_name,
"data": self._serialize_data(result)
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"entity": self.entity_name,
"operation": "get"
}
async def list(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:
"""List entities with filtering"""
try:
# Build query from args
query = {}
filters = args.get("filters", {})
limit = args.get("limit", 20)
offset = args.get("offset", 0)
order_by = args.get("order_by", "created_at DESC")
# Add user filter for security
if context and not self._validate_permissions(context, "read_all"):
query["user_id"] = context.user_id
# Apply additional filters
query.update(filters)
# Fetch from database
results = await self.db_manager.find_many(
self.table_name,
query=query,
limit=limit,
offset=offset,
order_by=order_by
)
# Get total count
total = await self.db_manager.count(self.table_name, query)
return {
"status": "success",
"entity": self.entity_name,
"data": [self._serialize_data(r) for r in results],
"pagination": {
"total": total,
"limit": limit,
"offset": offset,
"has_more": offset + limit < total
}
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"entity": self.entity_name,
"operation": "list"
}
async def update(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:
"""Update entity"""
try:
entity_id = args.pop("id", None)
if not entity_id:
raise ValueError("Missing required field: id")
# Validate update data
update_data = self._validate_data(args, for_create=False)
# Build query filter
query = {"id": entity_id}
if context and not self._validate_permissions(context, "update_all"):
query["user_id"] = context.user_id
# Update in database
affected = await self.db_manager.update(self.table_name, query, update_data)
if affected == 0:
return {
"status": "not_found",
"entity": self.entity_name,
"id": entity_id
}
return {
"status": "updated",
"entity": self.entity_name,
"id": entity_id,
"affected_rows": affected,
"timestamp": datetime.utcnow().isoformat()
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"entity": self.entity_name,
"operation": "update"
}
async def delete(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:
"""Delete entity"""
try:
entity_id = args.get("id")
if not entity_id:
raise ValueError("Missing required field: id")
# Build query filter
query = {"id": entity_id}
if context and not self._validate_permissions(context, "delete_all"):
query["user_id"] = context.user_id
# Delete from database
affected = await self.db_manager.delete(self.table_name, query)
if affected == 0:
return {
"status": "not_found",
"entity": self.entity_name,
"id": entity_id
}
return {
"status": "deleted",
"entity": self.entity_name,
"id": entity_id,
"affected_rows": affected,
"timestamp": datetime.utcnow().isoformat()
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"entity": self.entity_name,
"operation": "delete"
}
def _validate_data(self, data: Dict[str, Any], for_create: bool = False) -> Dict[str, Any]:
"""Validate data against schema"""
validated = {}
schema_fields = self.schema.get("properties", {})
required_fields = self.schema.get("required", [])
# Check required fields for create
if for_create:
for field in required_fields:
if field not in data:
raise ValueError(f"Missing required field: {field}")
# Validate each field
for field, value in data.items():
if field not in schema_fields:
continue # Skip unknown fields or raise error based on strictness
field_schema = schema_fields[field]
field_type = field_schema.get("type")
# Type validation
if field_type == "string":
if not isinstance(value, str):
raise ValueError(f"Field {field} must be a string")
# Check min/max length
if "minLength" in field_schema and len(value) < field_schema["minLength"]:
raise ValueError(f"Field {field} is too short")
if "maxLength" in field_schema and len(value) > field_schema["maxLength"]:
raise ValueError(f"Field {field} is too long")
elif field_type == "integer":
if not isinstance(value, int):
raise ValueError(f"Field {field} must be an integer")
# Check min/max value
if "minimum" in field_schema and value < field_schema["minimum"]:
raise ValueError(f"Field {field} is too small")
if "maximum" in field_schema and value > field_schema["maximum"]:
raise ValueError(f"Field {field} is too large")
elif field_type == "array":
if not isinstance(value, list):
raise ValueError(f"Field {field} must be an array")
# Check enum values
if "enum" in field_schema and value not in field_schema["enum"]:
raise ValueError(f"Field {field} must be one of {field_schema['enum']}")
validated[field] = value
return validated
def _serialize_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Serialize data for output"""
serialized = data.copy()
# Handle datetime serialization
for key, value in serialized.items():
if isinstance(value, datetime):
serialized[key] = value.isoformat()
elif isinstance(value, dict):
# Convert complex types to JSON string
try:
json.dumps(value)
except TypeError:
serialized[key] = str(value)
return serialized
class BulkOperationTool(BaseTool):
"""Tool for bulk operations on entities"""
def __init__(self, table_name: str, db_manager: DatabaseManager, schema: Dict[str, Any]):
super().__init__(db_manager)
self.table_name = table_name
self.schema = schema
self.entity_name = table_name.rstrip('s')
async def bulk_create(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:
"""Bulk create entities"""
try:
items = args.get("items", [])
if not items:
raise ValueError("No items provided for bulk create")
# Validate all items
validated_items = []
for item in items:
validated = self._validate_item(item)
if context:
validated["user_id"] = context.user_id
validated_items.append(validated)
# Insert all items
results = []
for item in validated_items:
result = await self.db_manager.create(self.table_name, item)
results.append(result)
return {
"status": "created",
"entity": self.entity_name,
"count": len(results),
…(truncated)
1---2name: mcp-server3description: Generic MCP (Model Context Protocol) server development patterns. Provides reusable architecture and best practices for building MCP servers that expose any domain-specific operations as tools for AI agents. Framework-agnostic implementation supporting async operations, error handling, and enterprise-grade features.4license: MIT5---67# Generic MCP Server Development89This skill provides comprehensive patterns and reusable code for building MCP (Model Context Protocol) servers that can expose any domain operations as tools for AI agents. Follows 2025 best practices for performance, security, and maintainability.1011## When to Use This Skill1213Use this skill when you need to:14- Build an MCP server for any domain (not just todos)15- Expose database operations as MCP tools16- Create AI-agent accessible APIs17- Implement async MCP tool handlers18- Add proper error handling and validation19- Support rate limiting and caching20- Build enterprise-grade MCP servers21- Integrate with multiple storage backends2223## 1. Generic MCP Server Architecture2425```python26# mcp_server/core.py27#!/usr/bin/env python328"""29Generic MCP Server Base Architecture30Provides reusable patterns for any MCP server implementation31"""3233import asyncio34import json35import logging36from abc import ABC, abstractmethod37from datetime import datetime, timedelta38from typing import Any, Dict, List, Optional, Sequence, Union, Callable39from contextlib import asynccontextmanager40from dataclasses import dataclass, field41from enum import Enum4243import redis.asyncio as redis44from mcp.server import Server, NotificationOptions, stdio45from mcp.server.models import InitializationOptions46from mcp.server.stdio import stdio_server47from mcp.types import (48 Resource, Tool, TextContent, ImageContent, EmbeddedResource,49 LoggingLevel, CallToolRequest, EmptyResult,50 ListResourcesRequest, ListToolsRequest, ReadResourceRequest,51 GetPromptRequest, ListPromptsRequest52)53from pydantic import BaseModel, Field, validator54import aiofiles55import yaml56from pathlib import Path5758# Configure logging59logging.basicConfig(60 level=logging.INFO,61 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"62)63logger = logging.getLogger("mcp_server")6465class ServerConfig(BaseModel):66 """MCP Server configuration"""67 name: str68 version: str = "1.0.0"69 description: str70 debug: bool = False71 redis_url: Optional[str] = None72 rate_limit_requests: int = 10073 rate_limit_window: int = 6074 cache_ttl: int = 30075 max_retries: int = 376 timeout: int = 307778 class Config:79 extra = "allow"8081@dataclass82class RequestContext:83 """Request context for tool calls"""84 user_id: str85 session_id: Optional[str] = None86 metadata: Dict[str, Any] = field(default_factory=dict)87 timestamp: datetime = field(default_factory=datetime.utcnow)8889class RateLimiter:90 """Redis-based rate limiter for MCP operations"""9192 def __init__(self, redis_url: str, requests: int, window: int):93 self.redis_url = redis_url94 self.requests = requests95 self.window = window96 self._redis = None9798 async def _get_redis(self):99 if not self._redis:100 self._redis = await redis.from_url(self.redis_url)101 return self._redis102103 async def is_allowed(self, key: str) -> bool:104 """Check if request is allowed"""105 r = await self._get_redis()106 current = await r.incr(f"rate_limit:{key}")107108 if current == 1:109 await r.expire(f"rate_limit:{key}", self.window)110111 return current <= self.requests112113 async def get_remaining(self, key: str) -> int:114 """Get remaining requests"""115 r = await self._get_redis()116 current = await r.get(f"rate_limit:{key}")117 return max(0, self.requests - int(current or 0))118119class CacheManager:120 """Redis-based caching for MCP responses"""121122 def __init__(self, redis_url: str, ttl: int = 300):123 self.redis_url = redis_url124 self.ttl = ttl125 self._redis = None126127 async def _get_redis(self):128 if not self._redis:129 self._redis = await redis.from_url(self.redis_url)130 return self._redis131132 def _make_key(self, tool_name: str, args: Dict[str, Any]) -> str:133 """Generate cache key from tool name and arguments"""134 import hashlib135 args_str = json.dumps(args, sort_keys=True)136 return f"cache:{tool_name}:{hashlib.md5(args_str.encode()).hexdigest()}"137138 async def get(self, tool_name: str, args: Dict[str, Any]) -> Optional[Any]:139 """Get cached result"""140 r = await self._get_redis()141 key = self._make_key(tool_name, args)142 result = await r.get(key)143 return json.loads(result) if result else None144145 async def set(self, tool_name: str, args: Dict[str, Any], value: Any):146 """Cache result"""147 r = await self._get_redis()148 key = self._make_key(tool_name, args)149 await r.setex(key, self.ttl, json.dumps(value))150151class BaseMCPServer:152 """Base MCP Server with common functionality"""153154 def __init__(self, config: ServerConfig):155 self.config = config156 self.server = Server(config.name)157 self.tools: Dict[str, Callable] = {}158 self.rate_limiter: Optional[RateLimiter] = None159 self.cache: Optional[CacheManager] = None160161 # Setup optional components162 if config.redis_url:163 self.rate_limiter = RateLimiter(164 config.redis_url,165 config.rate_limit_requests,166 config.rate_limit_window167 )168 self.cache = CacheManager(169 config.redis_url,170 config.cache_ttl171 )172173 # Register handlers174 self._register_handlers()175176 logger.info(f"MCP Server '{config.name}' initialized")177178 def _register_handlers(self):179 """Register MCP handlers"""180 @self.server.list_tools()181 async def handle_list_tools() -> List[Tool]:182 """Return list of available tools"""183 return await self.list_tools()184185 @self.server.call_tool()186 async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:187 """Handle tool call with rate limiting and caching"""188 return await self.call_tool(name, arguments)189190 def register_tool(self, name: str, handler: Callable, schema: Dict[str, Any]):191 """Register a new tool"""192 self.tools[name] = {193 "handler": handler,194 "schema": schema195 }196 logger.info(f"Registered tool: {name}")197198 async def list_tools(self) -> List[Tool]:199 """List all available tools"""200 tools = []201 for name, tool_info in self.tools.items():202 tools.append(Tool(203 name=name,204 description=tool_info["schema"].get("description", ""),205 inputSchema=tool_info["schema"].get("inputSchema", {})206 ))207 return tools208209 async def call_tool(self, name: str, arguments: Dict[str, Any]) -> List[TextContent]:210 """Execute a tool call with full middleware pipeline"""211 start_time = datetime.utcnow()212213 try:214 # Extract context from arguments215 context = self._extract_context(arguments)216217 # Rate limiting check218 if self.rate_limiter:219 rate_key = f"{context.user_id}:{name}"220 if not await self.rate_limiter.is_allowed(rate_key):221 return [TextContent(222 type="text",223 text=json.dumps({224 "status": "error",225 "error": "Rate limit exceeded",226 "remaining": await self.rate_limiter.get_remaining(rate_key)227 })228 )]229230 # Check cache231 if self.cache and self._is_cacheable(name):232 cached_result = await self.cache.get(name, arguments)233 if cached_result:234 logger.info(f"Cache hit for tool: {name}")235 return [TextContent(236 type="text",237 text=json.dumps(cached_result)238 )]239240 # Validate tool exists241 if name not in self.tools:242 raise ValueError(f"Unknown tool: {name}")243244 # Validate arguments245 schema = self.tools[name]["schema"]246 self._validate_arguments(arguments, schema)247248 # Execute tool249 handler = self.tools[name]["handler"]250 result = await self._execute_tool(handler, arguments, context)251252 # Cache result if applicable253 if self.cache and self._is_cacheable(name) and result.get("status") != "error":254 await self.cache.set(name, arguments, result)255256 # Log execution257 duration = (datetime.utcnow() - start_time).total_seconds()258 logger.info(f"Tool {name} executed in {duration:.2f}s for user {context.user_id}")259260 return [TextContent(261 type="text",262 text=json.dumps(result, default=str)263 )]264265 except Exception as e:266 logger.error(f"Error executing tool {name}: {str(e)}", exc_info=True)267 duration = (datetime.utcnow() - start_time).total_seconds()268 logger.error(f"Tool {name} failed after {duration:.2f}s")269270 return [TextContent(271 type="text",272 text=json.dumps({273 "status": "error",274 "error": str(e),275 "tool": name,276 "timestamp": datetime.utcnow().isoformat()277 })278 )]279280 def _extract_context(self, arguments: Dict[str, Any]) -> RequestContext:281 """Extract request context from arguments"""282 user_id = arguments.pop("_user_id", "anonymous")283 session_id = arguments.pop("_session_id", None)284 metadata = arguments.pop("_metadata", {})285286 return RequestContext(287 user_id=user_id,288 session_id=session_id,289 metadata=metadata290 )291292 def _validate_arguments(self, arguments: Dict[str, Any], schema: Dict[str, Any]):293 """Validate tool arguments against schema"""294 # Basic validation - can be extended with pydantic295 input_schema = schema.get("inputSchema", {})296 required = input_schema.get("required", [])297 properties = input_schema.get("properties", {})298299 # Check required fields300 for field in required:301 if field not in arguments:302 raise ValueError(f"Missing required field: {field}")303304 # Validate field types305 for field, value in arguments.items():306 if field in properties:307 field_schema = properties[field]308 expected_type = field_schema.get("type")309310 if expected_type == "string" and not isinstance(value, str):311 raise ValueError(f"Field {field} must be a string")312 elif expected_type == "integer" and not isinstance(value, int):313 raise ValueError(f"Field {field} must be an integer")314 elif expected_type == "array" and not isinstance(value, list):315 raise ValueError(f"Field {field} must be an array")316317 # Check enum values318 if "enum" in field_schema and value not in field_schema["enum"]:319 raise ValueError(f"Field {field} must be one of {field_schema['enum']}")320321 def _is_cacheable(self, tool_name: str) -> bool:322 """Determine if tool result should be cached"""323 # Non-mutating operations are cacheable324 non_mutating = ["get", "list", "search", "find", "read"]325 return any(op in tool_name.lower() for op in non_mutating)326327 async def _execute_tool(self, handler: Callable, arguments: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:328 """Execute tool handler with error handling"""329 try:330 # Pass context to handler if it accepts it331 import inspect332 sig = inspect.signature(handler)333334 if 'context' in sig.parameters:335 result = await handler(arguments, context=context)336 else:337 result = await handler(arguments)338339 return result340341 except Exception as e:342 logger.error(f"Tool handler failed: {str(e)}")343 return {344 "status": "error",345 "error": str(e),346 "timestamp": datetime.utcnow().isoformat()347 }348349 async def run(self):350 """Start the MCP server"""351 logger.info(f"Starting MCP server: {self.config.name}")352 async with stdio_server() as (read_stream, write_stream):353 await self.server.run(354 read_stream,355 write_stream,356 InitializationOptions(357 server_name=self.config.name,358 server_version=self.config.version,359 capabilities=self.server.get_capabilities(360 notification_options=NotificationOptions(),361 experimental_capabilities={},362 )363 )364 )365366def tool(367 name: Optional[str] = None,368 description: str = "",369 input_schema: Optional[Dict[str, Any]] = None370):371 """Decorator for registering MCP tools"""372 def decorator(func):373 tool_name = name or func.__name__374 schema = {375 "description": description or func.__doc__ or "",376 "inputSchema": input_schema or {}377 }378379 # Store schema on function for later registration380 func._mcp_tool_schema = schema381 func._mcp_tool_name = tool_name382383 return func384 return decorator385```386387## 2. Database Integration Patterns388389```python390# mcp_server/database.py391"""392Generic Database Integration for MCP Servers393Supports multiple ORMs and connection patterns394"""395396import asyncio397from abc import ABC, abstractmethod398from contextlib import asynccontextmanager399from typing import Any, Dict, List, Optional, TypeVar, Generic, Union400from datetime import datetime401import json402403from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, DateTime, Text, Boolean, select, update, delete, insert404from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker405from sqlalchemy.orm import sessionmaker, declarative_base406from sqlalchemy.pool import NullPool407import asyncpg408import motor.motor_asyncio409from redis.asyncio import Redis410411# Type variables412T = TypeVar('T')413414class DatabaseBackend(ABC):415 """Abstract base for database backends"""416417 @abstractmethod418 async def connect(self):419 """Establish connection"""420 pass421422 @abstractmethod423 async def disconnect(self):424 """Close connection"""425 pass426427 @abstractmethod428 async def execute_query(self, query: str, params: Dict[str, Any] = None) -> List[Dict[str, Any]]:429 """Execute a query and return results"""430 pass431432 @abstractmethod433 async def execute_command(self, command: str, params: Dict[str, Any] = None) -> Any:434 """Execute a command (INSERT, UPDATE, DELETE)"""435 pass436437class PostgresBackend(DatabaseBackend):438 """PostgreSQL backend using asyncpg"""439440 def __init__(self, connection_string: str):441 self.connection_string = connection_string442 self.pool: Optional[asyncpg.Pool] = None443444 async def connect(self):445 self.pool = await asyncpg.create_pool(446 self.connection_string,447 min_size=5,448 max_size=20,449 command_timeout=60450 )451452 async def disconnect(self):453 if self.pool:454 await self.pool.close()455456 async def execute_query(self, query: str, params: Dict[str, Any] = None) -> List[Dict[str, Any]]:457 async with self.pool.acquire() as conn:458 rows = await conn.fetch(query, *params.values() if params else [])459 return [dict(row) for row in rows]460461 async def execute_command(self, command: str, params: Dict[str, Any] = None) -> Any:462 async with self.pool.acquire() as conn:463 return await conn.execute(command, *params.values() if params else [])464465class SQLAlchemyBackend(DatabaseBackend):466 """SQLAlchemy backend for multiple databases"""467468 def __init__(self, database_url: str, async_mode: bool = True):469 self.database_url = database_url470 self.async_mode = async_mode471 self.engine = None472 self.session_factory = None473474 async def connect(self):475 if self.async_mode:476 self.engine = create_async_engine(477 self.database_url,478 pool_pre_ping=True,479 pool_recycle=300,480 echo=False481 )482 self.session_factory = async_sessionmaker(483 self.engine,484 class_=AsyncSession,485 expire_on_commit=False486 )487 else:488 self.engine = create_engine(489 self.database_url,490 pool_pre_ping=True,491 pool_recycle=300,492 echo=False493 )494 self.session_factory = sessionmaker(495 bind=self.engine,496 expire_on_commit=False497 )498499 async def disconnect(self):500 if self.engine:501 await self.engine.dispose()502503 @asynccontextmanager504 async def get_session(self):505 """Get database session"""506 async with self.session_factory() as session:507 try:508 yield session509 if self.async_mode:510 await session.commit()511 else:512 session.commit()513 except Exception:514 if self.async_mode:515 await session.rollback()516 else:517 session.rollback()518 raise519 finally:520 if self.async_mode:521 await session.close()522 else:523 session.close()524525 async def execute_query(self, query: Any, params: Dict[str, Any] = None) -> List[Dict[str, Any]]:526 """Execute SQLAlchemy query"""527 async with self.get_session() as session:528 if isinstance(query, str):529 # Raw SQL query530 result = await session.execute(query, params or {})531 rows = result.fetchall()532 return [dict(row._mapping) for row in rows]533 else:534 # SQLAlchemy ORM query535 result = await session.execute(query)536 rows = result.fetchall()537 return [dict(row._mapping) for row in rows]538539 async def execute_command(self, command: Any, params: Dict[str, Any] = None) -> Any:540 """Execute SQLAlchemy command"""541 async with self.get_session() as session:542 if isinstance(command, str):543 # Raw SQL command544 result = await session.execute(command, params or {})545 await session.commit()546 return result547 else:548 # SQLAlchemy ORM command549 await session.execute(command, params or {})550 await session.commit()551 return None552553class MongoBackend(DatabaseBackend):554 """MongoDB backend using motor"""555556 def __init__(self, connection_string: str, database_name: str):557 self.connection_string = connection_string558 self.database_name = database_name559 self.client = None560 self.db = None561562 async def connect(self):563 self.client = motor.motor_asyncio.AsyncIOMotorClient(self.connection_string)564 self.db = self.client[self.database_name]565566 async def disconnect(self):567 if self.client:568 self.client.close()569570 async def execute_query(self, collection: str, query: Dict[str, Any] = None) -> List[Dict[str, Any]]:571 """Execute MongoDB find query"""572 cursor = self.db[collection].find(query or {})573 results = []574 async for document in cursor:575 # Convert ObjectId to string576 if '_id' in document:577 document['_id'] = str(document['_id'])578 results.append(document)579 return results580581 async def execute_command(self, operation: str, collection: str, data: Dict[str, Any]) -> Any:582 """Execute MongoDB command"""583 if operation == "insert":584 result = await self.db[collection].insert_one(data)585 return str(result.inserted_id)586 elif operation == "update":587 filter_ = data.pop("_filter")588 update_data = {"$set": data}589 result = await self.db[collection].update_one(filter_, update_data)590 return result.modified_count591 elif operation == "delete":592 result = await self.db[collection].delete_one(data)593 return result.deleted_count594595class DatabaseManager(Generic[T]):596 """Generic database manager for MCP servers"""597598 def __init__(self, backend: DatabaseBackend):599 self.backend = backend600 self._connected = False601602 async def connect(self):603 """Connect to database"""604 if not self._connected:605 await self.backend.connect()606 self._connected = True607608 async def disconnect(self):609 """Disconnect from database"""610 if self._connected:611 await self.backend.disconnect()612 self._connected = False613614 @asynccontextmanager615 async def transaction(self):616 """Database transaction context manager"""617 if hasattr(self.backend, 'get_session'):618 async with self.backend.get_session() as session:619 yield session620 else:621 # For backends that don't support transactions622 yield self.backend623624 async def find_one(self, table_or_collection: str, query: Dict[str, Any]) -> Optional[Dict[str, Any]]:625 """Find a single record"""626 if isinstance(self.backend, MongoBackend):627 results = await self.backend.execute_query(table_or_collection, query)628 return results[0] if results else None629 else:630 # SQL implementation631 where_clause = " AND ".join([f"{k} = :{k}" for k in query.keys()])632 sql = f"SELECT * FROM {table_or_collection} WHERE {where_clause} LIMIT 1"633 results = await self.backend.execute_query(sql, query)634 return results[0] if results else None635636 async def find_many(637 self,638 table_or_collection: str,639 query: Dict[str, Any] = None,640 limit: int = None,641 offset: int = None,642 order_by: str = None643 ) -> List[Dict[str, Any]]:644 """Find multiple records"""645 query = query or {}646647 if isinstance(self.backend, MongoBackend):648 cursor = self.backend.db[table_or_collection].find(query)649 if limit:650 cursor = cursor.limit(limit)651 if offset:652 cursor = cursor.skip(offset)653 if order_by:654 # MongoDB sort format655 sort_field, sort_dir = order_by.split()656 cursor = cursor.sort([(sort_field, 1 if sort_dir == 'ASC' else -1)])657658 results = []659 async for document in cursor:660 if '_id' in document:661 document['_id'] = str(document['_id'])662 results.append(document)663 return results664 else:665 # SQL implementation666 where_clause = ""667 if query:668 where_clause = "WHERE " + " AND ".join([f"{k} = :{k}" for k in query.keys()])669670 sql = f"SELECT * FROM {table_or_collection} {where_clause}"671672 if order_by:673 sql += f" ORDER BY {order_by}"674675 if limit:676 sql += f" LIMIT {limit}"677678 if offset:679 sql += f" OFFSET {offset}"680681 return await self.backend.execute_query(sql, query)682683 async def create(self, table_or_collection: str, data: Dict[str, Any]) -> Any:684 """Create a new record"""685 data = data.copy()686687 # Add timestamps688 data['created_at'] = datetime.utcnow()689 data['updated_at'] = datetime.utcnow()690691 if isinstance(self.backend, MongoBackend):692 return await self.backend.execute_command("insert", table_or_collection, data)693 else:694 # SQL implementation695 columns = list(data.keys())696 placeholders = [f":{col}" for col in columns]697 sql = f"INSERT INTO {table_or_collection} ({', '.join(columns)}) VALUES ({', '.join(placeholders)})"698 return await self.backend.execute_command(sql, data)699700 async def update(self, table_or_collection: str, query: Dict[str, Any], data: Dict[str, Any]) -> int:701 """Update records"""702 data = data.copy()703 data['updated_at'] = datetime.utcnow()704705 if isinstance(self.backend, MongoBackend):706 data['_filter'] = query707 return await self.backend.execute_command("update", table_or_collection, data)708 else:709 # SQL implementation710 where_clause = " AND ".join([f"{k} = :{k}" for k in query.keys()])711 set_clause = ", ".join([f"{k} = :update_{k}" for k in data.keys()])712713 # Prefix update params to avoid conflicts714 update_params = {f"update_{k}": v for k, v in data.items()}715 params = {**query, **update_params}716717 sql = f"UPDATE {table_or_collection} SET {set_clause} WHERE {where_clause}"718 result = await self.backend.execute_command(sql, params)719 return result.rowcount if hasattr(result, 'rowcount') else 0720721 async def delete(self, table_or_collection: str, query: Dict[str, Any]) -> int:722 """Delete records"""723 if isinstance(self.backend, MongoBackend):724 return await self.backend.execute_command("delete", table_or_collection, query)725 else:726 # SQL implementation727 where_clause = " AND ".join([f"{k} = :{k}" for k in query.keys()])728 sql = f"DELETE FROM {table_or_collection} WHERE {where_clause}"729 result = await self.backend.execute_command(sql, query)730 return result.rowcount if hasattr(result, 'rowcount') else 0731732 async def count(self, table_or_collection: str, query: Dict[str, Any] = None) -> int:733 """Count records"""734 query = query or {}735736 if isinstance(self.backend, MongoBackend):737 return await self.backend.db[table_or_collection].count_documents(query)738 else:739 # SQL implementation740 where_clause = ""741 if query:742 where_clause = "WHERE " + " AND ".join([f"{k} = :{k}" for k in query.keys()])743744 sql = f"SELECT COUNT(*) as count FROM {table_or_collection} {where_clause}"745 results = await self.backend.execute_query(sql, query)746 return results[0]['count'] if results else 0747748# Factory function for creating database managers749def create_database_manager(database_url: str, backend_type: str = "auto") -> DatabaseManager:750 """Create database manager based on URL or backend type"""751752 if backend_type == "auto":753 if database_url.startswith("postgresql+asyncpg://"):754 backend = SQLAlchemyBackend(database_url, async_mode=True)755 elif database_url.startswith("mongodb://"):756 import re757 match = re.match(r'mongodb://[^/]+/([^?]*)', database_url)758 db_name = match.group(1) if match else "default"759 backend = MongoBackend(database_url, db_name)760 elif database_url.startswith("postgresql://"):761 backend = PostgresBackend(database_url)762 else:763 backend = SQLAlchemyBackend(database_url, async_mode=True)764 else:765 if backend_type == "postgres":766 backend = PostgresBackend(database_url)767 elif backend_type == "mongodb":768 db_name = database_url.split("/")[-1].split("?")[0]769 backend = MongoBackend(database_url, db_name)770 elif backend_type == "sqlalchemy":771 backend = SQLAlchemyBackend(database_url)772 else:773 raise ValueError(f"Unknown backend type: {backend_type}")774775 return DatabaseManager(backend)776```777778## 3. Tool Implementation Patterns779780```python781# mcp_server/tools.py782"""783Generic MCP Tool Implementation Patterns784"""785786from typing import Any, Dict, List, Optional, Union, Callable787from datetime import datetime, timedelta788import json789import uuid790from dataclasses import dataclass, field791792from .core import BaseMCPServer, tool, RequestContext793from .database import DatabaseManager794795class BaseTool:796 """Base class for MCP tools"""797798 def __init__(self, db_manager: DatabaseManager, cache=None):799 self.db_manager = db_manager800 self.cache = cache801802 async def execute(self, args: Dict[str, Any], context: RequestContext = None) -> Dict[str, Any]:803 """Execute the tool logic"""804 raise NotImplementedError805806 def _validate_permissions(self, context: RequestContext, required_permission: str = None) -> bool:807 """Validate user permissions"""808 # Implement permission checking logic809 return True810811class CRUDBaseTool(BaseTool):812 """Base CRUD tool for any entity"""813814 def __init__(self, table_name: str, db_manager: DatabaseManager, schema: Dict[str, Any]):815 super().__init__(db_manager)816 self.table_name = table_name817 self.schema = schema818 self.entity_name = table_name.rstrip('s') # Remove plural 's'819820 async def create(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:821 """Create entity"""822 try:823 # Validate against schema824 validated_data = self._validate_data(args, for_create=True)825826 # Add user context827 if context:828 validated_data['user_id'] = context.user_id829 if context.session_id:830 validated_data['session_id'] = context.session_id831832 # Insert into database833 result = await self.db_manager.create(self.table_name, validated_data)834835 return {836 "status": "created",837 "id": result,838 "entity": self.entity_name,839 "timestamp": datetime.utcnow().isoformat()840 }841842 except Exception as e:843 return {844 "status": "error",845 "error": str(e),846 "entity": self.entity_name,847 "operation": "create"848 }849850 async def get(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:851 """Get entity by ID"""852 try:853 entity_id = args.get("id")854 if not entity_id:855 raise ValueError("Missing required field: id")856857 # Add user filter for security858 query = {"id": entity_id}859 if context and not self._validate_permissions(context, "read_all"):860 query["user_id"] = context.user_id861862 result = await self.db_manager.find_one(self.table_name, query)863864 if not result:865 return {866 "status": "not_found",867 "entity": self.entity_name,868 "id": entity_id869 }870871 return {872 "status": "success",873 "entity": self.entity_name,874 "data": self._serialize_data(result)875 }876877 except Exception as e:878 return {879 "status": "error",880 "error": str(e),881 "entity": self.entity_name,882 "operation": "get"883 }884885 async def list(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:886 """List entities with filtering"""887 try:888 # Build query from args889 query = {}890 filters = args.get("filters", {})891 limit = args.get("limit", 20)892 offset = args.get("offset", 0)893 order_by = args.get("order_by", "created_at DESC")894895 # Add user filter for security896 if context and not self._validate_permissions(context, "read_all"):897 query["user_id"] = context.user_id898899 # Apply additional filters900 query.update(filters)901902 # Fetch from database903 results = await self.db_manager.find_many(904 self.table_name,905 query=query,906 limit=limit,907 offset=offset,908 order_by=order_by909 )910911 # Get total count912 total = await self.db_manager.count(self.table_name, query)913914 return {915 "status": "success",916 "entity": self.entity_name,917 "data": [self._serialize_data(r) for r in results],918 "pagination": {919 "total": total,920 "limit": limit,921 "offset": offset,922 "has_more": offset + limit < total923 }924 }925926 except Exception as e:927 return {928 "status": "error",929 "error": str(e),930 "entity": self.entity_name,931 "operation": "list"932 }933934 async def update(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:935 """Update entity"""936 try:937 entity_id = args.pop("id", None)938 if not entity_id:939 raise ValueError("Missing required field: id")940941 # Validate update data942 update_data = self._validate_data(args, for_create=False)943944 # Build query filter945 query = {"id": entity_id}946 if context and not self._validate_permissions(context, "update_all"):947 query["user_id"] = context.user_id948949 # Update in database950 affected = await self.db_manager.update(self.table_name, query, update_data)951952 if affected == 0:953 return {954 "status": "not_found",955 "entity": self.entity_name,956 "id": entity_id957 }958959 return {960 "status": "updated",961 "entity": self.entity_name,962 "id": entity_id,963 "affected_rows": affected,964 "timestamp": datetime.utcnow().isoformat()965 }966967 except Exception as e:968 return {969 "status": "error",970 "error": str(e),971 "entity": self.entity_name,972 "operation": "update"973 }974975 async def delete(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:976 """Delete entity"""977 try:978 entity_id = args.get("id")979 if not entity_id:980 raise ValueError("Missing required field: id")981982 # Build query filter983 query = {"id": entity_id}984 if context and not self._validate_permissions(context, "delete_all"):985 query["user_id"] = context.user_id986987 # Delete from database988 affected = await self.db_manager.delete(self.table_name, query)989990 if affected == 0:991 return {992 "status": "not_found",993 "entity": self.entity_name,994 "id": entity_id995 }996997 return {998 "status": "deleted",999 "entity": self.entity_name,1000 "id": entity_id,1001 "affected_rows": affected,1002 "timestamp": datetime.utcnow().isoformat()1003 }10041005 except Exception as e:1006 return {1007 "status": "error",1008 "error": str(e),1009 "entity": self.entity_name,1010 "operation": "delete"1011 }10121013 def _validate_data(self, data: Dict[str, Any], for_create: bool = False) -> Dict[str, Any]:1014 """Validate data against schema"""1015 validated = {}1016 schema_fields = self.schema.get("properties", {})1017 required_fields = self.schema.get("required", [])10181019 # Check required fields for create1020 if for_create:1021 for field in required_fields:1022 if field not in data:1023 raise ValueError(f"Missing required field: {field}")10241025 # Validate each field1026 for field, value in data.items():1027 if field not in schema_fields:1028 continue # Skip unknown fields or raise error based on strictness10291030 field_schema = schema_fields[field]1031 field_type = field_schema.get("type")10321033 # Type validation1034 if field_type == "string":1035 if not isinstance(value, str):1036 raise ValueError(f"Field {field} must be a string")1037 # Check min/max length1038 if "minLength" in field_schema and len(value) < field_schema["minLength"]:1039 raise ValueError(f"Field {field} is too short")1040 if "maxLength" in field_schema and len(value) > field_schema["maxLength"]:1041 raise ValueError(f"Field {field} is too long")1042 elif field_type == "integer":1043 if not isinstance(value, int):1044 raise ValueError(f"Field {field} must be an integer")1045 # Check min/max value1046 if "minimum" in field_schema and value < field_schema["minimum"]:1047 raise ValueError(f"Field {field} is too small")1048 if "maximum" in field_schema and value > field_schema["maximum"]:1049 raise ValueError(f"Field {field} is too large")1050 elif field_type == "array":1051 if not isinstance(value, list):1052 raise ValueError(f"Field {field} must be an array")10531054 # Check enum values1055 if "enum" in field_schema and value not in field_schema["enum"]:1056 raise ValueError(f"Field {field} must be one of {field_schema['enum']}")10571058 validated[field] = value10591060 return validated10611062 def _serialize_data(self, data: Dict[str, Any]) -> Dict[str, Any]:1063 """Serialize data for output"""1064 serialized = data.copy()10651066 # Handle datetime serialization1067 for key, value in serialized.items():1068 if isinstance(value, datetime):1069 serialized[key] = value.isoformat()1070 elif isinstance(value, dict):1071 # Convert complex types to JSON string1072 try:1073 json.dumps(value)1074 except TypeError:1075 serialized[key] = str(value)10761077 return serialized10781079class BulkOperationTool(BaseTool):1080 """Tool for bulk operations on entities"""10811082 def __init__(self, table_name: str, db_manager: DatabaseManager, schema: Dict[str, Any]):1083 super().__init__(db_manager)1084 self.table_name = table_name1085 self.schema = schema1086 self.entity_name = table_name.rstrip('s')10871088 async def bulk_create(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:1089 """Bulk create entities"""1090 try:1091 items = args.get("items", [])1092 if not items:1093 raise ValueError("No items provided for bulk create")10941095 # Validate all items1096 validated_items = []1097 for item in items:1098 validated = self._validate_item(item)1099 if context:1100 validated["user_id"] = context.user_id1101 validated_items.append(validated)11021103 # Insert all items1104 results = []1105 for item in validated_items:1106 result = await self.db_manager.create(self.table_name, item)1107 results.append(result)11081109 return {1110 "status": "created",1111 "entity": self.entity_name,1112 "count": len(results),1113 "ids": results,1114 "timestamp": datetime.utcnow().isoformat()1115 }11161117 except Exception as e:1118 return {1119 "status": "error",1120 "error": str(e),1121 "entity": self.entity_name,1122 "operation": "bulk_create"1123 }11241125 async def bulk_update(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:1126 """Bulk update entities"""1127 try:1128 updates = args.get("updates", [])1129 if not updates:1130 raise ValueError("No updates provided")11311132 total_affected = 01133 for update in updates:1134 entity_id = update.get("id")1135 update_data = update.get("data", {})11361137 if not entity_id:1138 continue11391140 # Build query1141 query = {"id": entity_id}1142 if context:1143 query["user_id"] = context.user_id11441145 # Update1146 affected = await self.db_manager.update(self.table_name, query, update_data)1147 total_affected += affected11481149 return {1150 "status": "updated",1151 "entity": self.entity_name,1152 "affected_rows": total_affected,1153 "updates_processed": len(updates),1154 "timestamp": datetime.utcnow().isoformat()1155 }11561157 except Exception as e:1158 return {1159 "status": "error",1160 "error": str(e),1161 "entity": self.entity_name,1162 "operation": "bulk_update"1163 }11641165 async def bulk_delete(self, args: Dict[str, Any], context: RequestContext) -> Dict[str, Any]:1166 """Bulk delete entities"""1167 try:1168 ids = args.get("ids", [])1169 if not ids:1170 raise ValueError("No IDs provided for bulk delete")11711172 total_affected = 01173 for entity_id in ids:1174 # Build query1175 query = {"id": entity_id}1176 if context:1177 query["user_id"] = context.user_id11781179 # Delete1180 affected = await self.db_manager.delete(self.table_name, query)1181 total_affected += affected11821183 return {1184 "status": "deleted",1185 "entity": self.entity_name,1186 "affected_rows": total_affected,1187 "ids_processed": len(ids),1188 "timestamp": datetime.utcnow().isoformat()1189 }11901191 except Exception as e:1192 return {1193 "status": "error",1194 "error": str(e),1195 "entity": self.entity_name,1196 "operation": "bulk_delete"1197 }11981199 def _validate_item(self, item: Dict[str, Any]) -> Dict[str, Any]:1200 """Validate a single item"""1201 # Use CRUD base tool validation1202 crud_tool = CRUDBaseTool(self.table_name, self.db_manager, self.schema)1203 return crud_tool._validate_data(item, for_create=True)1204```12051206## 4. Example: Building a Generic Task Management MCP Server12071208```python1209# examples/task_mcp_server.py1210"""1211Example: Task Management MCP Server using generic patterns1212"""12131214import os1215from typing import Dict, Any12161217from mcp_server.core import BaseMCPServer, ServerConfig, tool1218from mcp_server.database import create_database_manager1219from mcp_server.tools import CRUDBaseTool, BulkOperationTool12201221# Server configuration1222config = ServerConfig(1223 name="task-manager",1224 version="1.0.0",1225 description="Generic task management MCP server",1226 redis_url=os.getenv("REDIS_URL", "redis://localhost:6379"),1227 database_url=os.getenv("DATABASE_URL", "postgresql+asyncpg://user:pass@localhost/tasks")1228)12291230# Task entity schema1231TASK_SCHEMA = {1232 "type": "object",1233 "properties": {1234 "title": {1235 "type": "string",1236 "minLength": 1,1237 "maxLength": 200,1238 "description": "Task title"1239 },1240 "description": {1241 "type": "string",1242 "maxLength": 1000,1243 "description": "Task description"1244 },1245 "priority": {1246 "type": "string",1247 "enum": ["low", "medium", "high"],1248 "default": "medium",1249 "description": "Task priority"1250 },1251 "status": {1252 "type": "string",1253 "enum": ["todo", "in_progress", "completed"],1254 "default": "todo",1255 "description": "Task status"1256 },1257 "due_date": {1258 "type": "string",1259 "format": "date-time",1260 "description": "Optional due date"1261 },1262 "tags": {1263 "type": "array",1264 "items": {"type": "string"},1265 "description": "Task tags"1266 }1267 },1268 "required": ["title"]1269}12701271class TaskMCPServer(BaseMCPServer):1272 """Task Management MCP Server"""12731274 def __init__(self, config: ServerConfig):1275 super().__init__(config)12761277 # Initialize database1278 self.db_manager = create_database_manager(config.database_url)12791280 # Initialize tools1281 self.task_tool = CRUDBaseTool("tasks", self.db_manager, TASK_SCHEMA)1282 self.bulk_tool = BulkOperationTool("tasks", self.db_manager, TASK_SCHEMA)12831284 # Register tools1285 self._register_task_tools()12861287 def _register_task_tools(self):1288 """Register all task-related tools"""12891290 # Create task1291 self.register_tool(1292 "create_task",1293 self.task_tool.create,1294 {1295 "description": "Create a new task",1296 "inputSchema": {1297 "type": "object",1298 "properties": {1299 "title": {"type": "string", "description": "Task title"},1300 "description": {"type": "string", "description": "Optional description"},1301 "priority": {"type": "string", "enum": ["low", "medium", "high"]},1302 "due_date": {"type": "string", "format": "date-time"},1303 "tags": {"type": "array", "items": {"type": "string"}}1304 },1305 "required": ["title"]1306 }1307 }1308 )13091310 # Get task1311 self.register_tool(1312 "get_task",1313 self.task_tool.get,1314 {1315 "description": "Get a task by ID",1316 "inputSchema": {1317 "type": "object",1318 "properties": {1319 "id": {"type": "integer", "description": "Task ID"}1320 },1321 "required": ["id"]1322 }1323 }1324 )13251326 # List tasks1327 self.register_tool(1328 "list_tasks",1329 self.task_tool.list,1330 {1331 "description": "List tasks with optional filtering",1332 "inputSchema": {1333 "type": "object",1334 "properties": {1335 "filters": {"type": "object", "description": "Filter criteria"},1336 "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 20},1337 "offset": {"type": "integer", "minimum": 0, "default": 0},1338 "order_by": {"type": "string", "description": "Order by field (e.g., 'created_at DESC')"}1339 }1340 }1341 }1342 )13431344 # Update task1345 self.register_tool(1346 "update_task",1347 self.task_tool.update,1348 {1349 "description": "Update a task",1350 "inputSchema": {1351 "type": "object",1352 "properties": {1353 "id": {"type": "integer", "description": "Task ID"},1354 "title": {"type": "string", "description": "New title"},1355 "description": {"type": "string", "description": "New description"},1356 "priority": {"type": "string", "enum": ["low", "medium", "high"]},1357 "status": {"type": "string", "enum": ["todo", "in_progress", "completed"]},1358 "due_date": {"type": "string", "format": "date-time"},1359 "tags": {"type": "array", "items": {"type": "string"}}1360 },1361 "required": ["id"]1362 }1363 }1364 )13651366 # Delete task1367 self.register_tool(1368 "delete_task",1369 self.task_tool.delete,1370 {1371 "description": "Delete a task",1372 "inputSchema": {1373 "type": "object",1374 "properties": {1375 "id": {"type": "integer", "description": "Task ID"}1376 },1377 "required": ["id"]1378 }1379 }1380 )13811382 # Bulk create1383 self.register_tool(1384 "bulk_create_tasks",1385 self.bulk_tool.bulk_create,1386 {1387 "description": "Create multiple tasks at once",1388 "inputSchema": {1389 "type": "object",1390 "properties": {1391 "items": {1392 "type": "array",1393 "items": {1394 "type": "object",1395 "properties": {1396 "title": {"type": "string"},1397 "description": {"type": "string"},1398 "priority": {"type": "string", "enum": ["low", "medium", "high"]},1399 "tags": {"type": "array", "items": {"type": "string"}}1400 },1401 "required": ["title"]1402 }1403 }1404 },1405 "required": ["items"]1406 }1407 }1408 )14091410 # Search tasks1411 self.register_tool(1412 "search_tasks",1413 self._search_tasks,1414 {1415 "description": "Search tasks by text query",1416 "inputSchema": {1417 "type": "object",1418 "properties": {1419 "query": {"type": "string", "description": "Search query"},1420 "limit": {"type": "integer", "minimum": 1, "maximum": 50, "default": 20}1421 },1422 "required": ["query"]1423 }1424 }1425 )14261427 async def _search_tasks(self, args: Dict[str, Any], context) -> Dict[str, Any]:1428 """Search tasks by text"""1429 try:1430 query = args.get("query", "")1431 limit = args.get("limit", 20)14321433 # Build search query1434 if isinstance(self.db_manager.backend, MongoBackend):1435 # MongoDB text search1436 search_query = {1437 "$text": {"$search": query}1438 }1439 if context and not self.task_tool._validate_permissions(context, "read_all"):1440 search_query["user_id"] = context.user_id14411442 results = await self.db_manager.find_many("tasks", search_query, limit=limit)1443 else:1444 # PostgreSQL full-text search1445 sql = """1446 SELECT * FROM tasks1447 WHERE to_tsvector('english', title || ' ' || COALESCE(description, '')) @@ plainto_tsquery('english', :query)1448 """1449 params = {"query": query}14501451 if context and not self.task_tool._validate_permissions(context, "read_all"):1452 sql += " AND user_id = :user_id"1453 params["user_id"] = context.user_id14541455 sql += f" LIMIT {limit}"14561457 results = await self.db_manager.execute_query(sql, params)14581459 return {1460 "status": "success",1461 "entity": "task",1462 "data": [self.task_tool._serialize_data(r) for r in results],1463 "query": query,1464 "count": len(results)1465 }14661467 except Exception as e:1468 return {1469 "status": "error",1470 "error": str(e),1471 "operation": "search_tasks"1472 }14731474# Main execution1475async def main():1476 """Start the Task MCP Server"""1477 server = TaskMCPServer(config)1478 await server.run()14791480if __name__ == "__main__":1481 import asyncio1482 asyncio.run(main())1483```14841485## 5. Testing Patterns14861487```python1488# tests/test_mcp_server.py1489"""1490Generic MCP Server Testing Patterns1491"""14921493import pytest1494import asyncio1495from typing import Dict, Any, List1496from unittest.mock import Mock, AsyncMock14971498from mcp_server.core import BaseMCPServer, ServerConfig, RequestContext1499from mcp_server.database import DatabaseManager, create_database_manager1500from mcp_server.tools import CRUDBaseTool15011502class MockDatabaseManager:1503 """Mock database manager for testing"""15041505 def __init__(self):1506 self.data = {}1507 self.next_id = 115081509 async def create(self, table: str, data: Dict[str, Any]) -> int:1510 """Mock create"""1511 entity_id = self.next_id1512 data['id'] = entity_id1513 self.data[f"{table}:{entity_id}"] = data1514 self.next_id += 11515 return entity_id15161517 async def find_one(self, table: str, query: Dict[str, Any]) -> Dict[str, Any]:1518 """Mock find one"""1519 for key, value in self.data.items():1520 table_name, entity_id = key.split(":")1521 if table_name == table:1522 match = True1523 for k, v in query.items():1524 if value.get(k) != v:1525 match = False1526 break1527 if match:1528 return value1529 return None15301531 async def find_many(self, table: str, query: Dict[str, Any], limit: int = None) -> List[Dict[str, Any]]:1532 """Mock find many"""1533 results = []1534 for key, value in self.data.items():1535 table_name, entity_id = key.split(":")1536 if table_name == table:1537 match = True1538 for k, v in query.items():1539 if value.get(k) != v:1540 match = False1541 break1542 if match:1543 results.append(value)1544 if limit and len(results) >= limit:1545 break1546 return results15471548 async def update(self, table: str, query: Dict[str, Any], data: Dict[str, Any]) -> int:1549 """Mock update"""1550 count = 01551 for key, value in self.data.items():1552 table_name, entity_id = key.split(":")1553 if table_name == table:1554 match = True1555 for k, v in query.items():1556 if value.get(k) != v:1557 match = False1558 break1559 if match:1560 value.update(data)1561 count += 11562 return count15631564 async def delete(self, table: str, query: Dict[str, Any]) -> int:1565 """Mock delete"""1566 to_delete = []1567 for key, value in self.data.items():1568 table_name, entity_id = key.split(":")1569 if table_name == table:1570 match = True1571 for k, v in query.items():1572 if value.get(k) != v:1573 match = False1574 break1575 if match:1576 to_delete.append(key)15771578 for key in to_delete:1579 del self.data[key]15801581 return len(to_delete)15821583@pytest.fixture1584async def mock_db():1585 """Mock database manager fixture"""1586 return MockDatabaseManager()15871588@pytest.fixture1589def test_schema():1590 """Test entity schema"""1591 return {1592 "type": "object",1593 "properties": {1594 "name": {"type": "string", "minLength": 1},1595 "value": {"type": "integer", "minimum": 0},1596 "status": {"type": "string", "enum": ["active", "inactive"]},1597 "tags": {"type": "array", "items": {"type": "string"}}1598 },1599 "required": ["name"]1600 }16011602@pytest.fixture1603def crud_tool(mock_db, test_schema):1604 """CRUD tool fixture"""1605 return CRUDBaseTool("test_entities", mock_db, test_schema)16061607@pytest.fixture1608def user_context():1609 """User context fixture"""1610 return RequestContext(user_id="test_user", session_id="test_session")16111612class TestCRUDTool:1613 """Test CRUD tool operations"""16141615 @pytest.mark.asyncio1616 async def test_create_entity(self, crud_tool, user_context):1617 """Test entity creation"""1618 args = {1619 "name": "Test Entity",1620 "value": 100,1621 "status": "active",1622 "tags": ["test", "example"]1623 }16241625 result = await crud_tool.create(args, user_context)16261627 assert result["status"] == "created"1628 assert "id" in result1629 assert result["entity"] == "test_entity"1630 assert "timestamp" in result16311632 @pytest.mark.asyncio1633 async def test_create_missing_required(self, crud_tool, user_context):1634 """Test creation with missing required field"""1635 args = {"value": 100} # Missing 'name'16361637 result = await crud_tool.create(args, user_context)16381639 assert result["status"] == "error"1640 assert "Missing required field" in result["error"]16411642 @pytest.mark.asyncio1643 async def test_get_entity(self, crud_tool, user_context):1644 """Test getting an entity"""1645 # First create an entity1646 create_args = {"name": "Test Get"}1647 create_result = await crud_tool.create(create_args, user_context)1648 entity_id = create_result["id"]16491650 # Get the entity1651 get_args = {"id": entity_id}1652 result = await crud_tool.get(get_args, user_context)16531654 assert result["status"] == "success"1655 assert result["data"]["name"] == "Test Get"1656 assert result["data"]["id"] == entity_id16571658 @pytest.mark.asyncio1659 async def test_get_not_found(self, crud_tool, user_context):1660 """Test getting non-existent entity"""1661 args = {"id": 99999}1662 result = await crud_tool.get(args, user_context)16631664 assert result["status"] == "not_found"16651666 @pytest.mark.asyncio1667 async def test_list_entities(self, crud_tool, user_context):1668 """Test listing entities"""1669 # Create a few entities1670 for i in range(3):1671 args = {"name": f"Entity {i}"}1672 await crud_tool.create(args, user_context)16731674 # List entities1675 result = await crud_tool.list({}, user_context)16761677 assert result["status"] == "success"1678 assert len(result["data"]) == 31679 assert "pagination" in result1680 assert result["pagination"]["total"] == 316811682 @pytest.mark.asyncio1683 async def test_list_with_filters(self, crud_tool, user_context):1684 """Test listing with filters"""1685 # Create entities with different statuses1686 await crud_tool.create({"name": "Active 1", "status": "active"}, user_context)1687 await crud_tool.create({"name": "Inactive 1", "status": "inactive"}, user_context)1688 await crud_tool.create({"name": "Active 2", "status": "active"}, user_context)16891690 # Filter by status1691 result = await crud_tool.list(1692 {"filters": {"status": "active"}},1693 user_context1694 )16951696 assert result["status"] == "success"1697 assert all(entity["status"] == "active" for entity in result["data"])16981699 @pytest.mark.asyncio1700 async def test_update_entity(self, crud_tool, user_context):1701 """Test updating an entity"""1702 # Create entity1703 create_result = await crud_tool.create({"name": "Original"}, user_context)1704 entity_id = create_result["id"]17051706 # Update entity1707 update_args = {1708 "id": entity_id,1709 "name": "Updated",1710 "value": 2001711 }1712 result = await crud_tool.update(update_args, user_context)17131714 assert result["status"] == "updated"1715 assert result["affected_rows"] == 117161717 @pytest.mark.asyncio1718 async def test_delete_entity(self, crud_tool, user_context):1719 """Test deleting an entity"""1720 # Create entity1721 create_result = await crud_tool.create({"name": "To Delete"}, user_context)1722 entity_id = create_result["id"]17231724 # Delete entity1725 delete_args = {"id": entity_id}1726 result = await crud_tool.delete(delete_args, user_context)17271728 assert result["status"] == "deleted"1729 assert result["affected_rows"] == 117301731 # Verify deletion1732 get_result = await crud_tool.get({"id": entity_id}, user_context)1733 assert get_result["status"] == "not_found"17341735class TestMCPServer:1736 """Test MCP Server functionality"""17371738 @pytest.mark.asyncio1739 async def test_server_initialization(self):1740 """Test server initialization"""1741 config = ServerConfig(1742 name="test-server",1743 description="Test MCP Server"1744 )17451746 server = BaseMCPServer(config)17471748 assert server.config.name == "test-server"1749 assert server.server.name == "test-server"1750 assert len(server.tools) == 017511752 @pytest.mark.asyncio1753 async def test_tool_registration(self):1754 """Test tool registration"""1755 config = ServerConfig(name="test-server", description="Test")1756 server = BaseMCPServer(config)17571758 # Register a test tool1759 async def test_tool(args: Dict[str, Any]) -> Dict[str, Any]:1760 return {"status": "success", "data": args}17611762 schema = {1763 "description": "Test tool",1764 "inputSchema": {1765 "type": "object",1766 "properties": {1767 "message": {"type": "string"}1768 }1769 }1770 }17711772 server.register_tool("test_tool", test_tool, schema)17731774 assert "test_tool" in server.tools1775 assert server.tools["test_tool"]["schema"] == schema17761777 @pytest.mark.asyncio1778 async def test_rate_limiting(self):1779 """Test rate limiting functionality"""1780 # This would require mocking Redis or using a test instance1781 # Implementation would depend on your rate limiting strategy1782 pass17831784 @pytest.mark.asyncio1785 async def test_caching(self):1786 """Test caching functionality"""1787 # This would require mocking Redis or using a test instance1788 # Implementation would depend on your caching strategy1789 pass17901791# Integration test example1792class TestMCPServerIntegration:1793 """Integration tests for MCP Server"""17941795 @pytest.mark.asyncio1796 async def test_full_crud_workflow(self):1797 """Test full CRUD workflow through MCP interface"""1798 # Create mock server1799 config = ServerConfig(1800 name="test-integration",1801 description="Integration test server"1802 )18031804 # Use in-memory database1805 mock_db = MockDatabaseManager()18061807 # Create CRUD tool1808 test_schema = {1809 "type": "object",1810 "properties": {1811 "name": {"type": "string"},1812 "value": {"type": "integer"}1813 },1814 "required": ["name"]1815 }18161817 crud_tool = CRUDBaseTool("items", mock_db, test_schema)18181819 # Register tools1820 server = BaseMCPServer(config)1821 server.register_tool(1822 "create_item",1823 crud_tool.create,1824 {1825 "description": "Create item",1826 "inputSchema": {1827 "type": "object",1828 "properties": {1829 "name": {"type": "string"},1830 "value": {"type": "integer"}1831 },1832 "required": ["name"]1833 }1834 }1835 )18361837 # Test create1838 result = await server.call_tool(1839 "create_item",1840 {"name": "Test Item", "value": 123}1841 )18421843 response = json.loads(result[0].text)1844 assert response["status"] == "created"18451846 # Test get (would need get_item tool)1847 # Test list1848 # Test update1849 # Test delete1850```18511852This generic MCP server skill provides a reusable foundation for building any MCP server, not just for todos. It includes:185318541. **Core Server Architecture** - Base class with rate limiting, caching, and error handling18552. **Database Integration** - Support for PostgreSQL, MongoDB, and SQLAlchemy with async operations18563. **Tool Patterns** - Generic CRUD and bulk operation patterns that work with any entity18574. **Example Implementation** - Shows how to build a task management server using the generic patterns18585. **Testing Framework** - Comprehensive testing patterns and mocks18591860`★ Insight ─────────────────────────────────────`1861The key architectural pattern here is the separation of concerns between:1862- The MCP protocol handling (BaseMCPServer)1863- The data access layer (DatabaseManager with multiple backends)1864- The business logic layer (CRUDBaseTool and BulkOperationTool)1865- The specific implementation (TaskMCPServer combining the components)18661867This makes the system highly reusable and maintainable. Any developer can quickly build a new MCP server by defining their entity schema and combining the generic tools.1868`─────────────────────────────────────────────────`