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 Val