mirror of
https://github.com/mcp-use/mcp-use.git
synced 2026-09-14 18:06:01 +08:00
4997674559
* Wrap server session from MCP * Update server and middleware * Update middleware example * Fix formatting and linting * Update docs * Fix monkeypatch for tests * Update docs and remove comment * feat(python): add tests, docs, and docstrings for on_initialize middleware - Add 13 unit tests covering dispatch, chain order, rejection, and context enrichment - Rewrite middleware.mdx documentation with Mintlify components (Tabs, Tips, Cards) - Add comprehensive docstring to MiddlewareServerSession documenting single-server-per-process limitation - Add docs/CLAUDE.md with documentation improvement guidelines - Regenerate API documentation * docs: highlight typed context feature in middleware docs * example: add ClientCapabilitiesGuard demonstrating typed context * style: fix linting in middleware example * docs(examples): add comprehensive docstrings to middleware_example.py - Add module docstring explaining key concepts and middleware hooks - Document each middleware class with typed context details - Organize code with section headers (CONNECTION, REQUEST, TOOL-SPECIFIC) - Add ClientCapabilitiesGuard example showing typed context usage --------- Co-authored-by: pietrozullo <pietro.zullo@gmail.com>
268 lines
9.1 KiB
Python
268 lines
9.1 KiB
Python
"""
|
|
MCP Server Middleware Examples
|
|
|
|
This file demonstrates the middleware system for MCP servers, showing common patterns
|
|
like logging, authentication, rate limiting, and validation.
|
|
|
|
Key concepts demonstrated:
|
|
- on_initialize: Intercept client connections during MCP handshake
|
|
- on_request: Intercept ALL requests (wraps other hooks)
|
|
- on_call_tool: Intercept only tool calls
|
|
- Typed context: Each hook gets fully-typed context.message (IDE autocomplete works!)
|
|
- Middleware order: First added = outermost (sees requests first, responses last)
|
|
|
|
Session IDs come from the SDK via the mcp-session-id header (context.session_id).
|
|
Spec: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
|
|
|
|
Run with: python middleware_example.py
|
|
"""
|
|
|
|
import time
|
|
from collections import defaultdict
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from mcp_use.server import MCPServer
|
|
from mcp_use.server.middleware import CallNext, Middleware, ServerMiddlewareContext
|
|
|
|
# =============================================================================
|
|
# CONNECTION MIDDLEWARE (on_initialize)
|
|
# These run during the MCP handshake, before any tools/resources are accessed.
|
|
# =============================================================================
|
|
|
|
|
|
class ConnectionGuard(Middleware):
|
|
"""Log incoming client connections.
|
|
|
|
Uses on_initialize to intercept the MCP handshake. The context.message is
|
|
typed as InitializeRequestParams, giving you access to:
|
|
- clientInfo.name: Client application name
|
|
- clientInfo.version: Client version
|
|
- protocolVersion: MCP protocol version
|
|
- capabilities: What the client supports
|
|
"""
|
|
|
|
async def on_initialize(self, context, call_next):
|
|
client_name = context.message.clientInfo.name
|
|
print(f"Incoming connection from: {client_name}")
|
|
|
|
return await call_next(context)
|
|
|
|
|
|
class ClientCapabilitiesGuard(Middleware):
|
|
"""Reject clients that don't support required capabilities.
|
|
|
|
Demonstrates:
|
|
- Typed context: context.message.capabilities is fully typed
|
|
- Early rejection: Raise an exception to reject the connection
|
|
"""
|
|
|
|
async def on_initialize(self, context, call_next) -> Any:
|
|
print(f"Client capabilities: {context.message.capabilities}")
|
|
|
|
capabilities = context.message.capabilities
|
|
if not capabilities.elicitation:
|
|
raise ValueError("Client must support elicitation")
|
|
if not capabilities.sampling:
|
|
raise ValueError("Client must support sampling")
|
|
if not capabilities.roots:
|
|
raise ValueError("Client must support roots")
|
|
return await call_next(context)
|
|
|
|
|
|
# =============================================================================
|
|
# REQUEST MIDDLEWARE (on_request)
|
|
# These run for EVERY request type, wrapping all other hooks.
|
|
# =============================================================================
|
|
|
|
|
|
class LoggingMiddleware(Middleware):
|
|
"""Log all requests with timing.
|
|
|
|
Demonstrates:
|
|
- on_request: Runs for ALL requests (tools, resources, prompts, etc.)
|
|
- on_call_tool: Additional logging specific to tool calls
|
|
- Hook nesting: on_request wraps on_call_tool when both are defined
|
|
- Error handling: Catch, log, and re-raise exceptions
|
|
"""
|
|
|
|
async def on_request(self, context: ServerMiddlewareContext[Any], call_next: CallNext[Any, Any]) -> Any:
|
|
start = time.time()
|
|
sid = context.session_id or "anonymous"
|
|
print(f"[{context.method}] sid={sid}")
|
|
try:
|
|
result = await call_next(context)
|
|
print(f"[{context.method}] ok ({(time.time() - start) * 1000:.1f}ms)")
|
|
return result
|
|
except Exception as e:
|
|
print(f"[{context.method}] failed: {e}")
|
|
raise
|
|
|
|
async def on_call_tool(self, context, call_next):
|
|
# This runs INSIDE on_request for tool calls
|
|
print(f" tool={context.message.name} args={context.message.arguments or {}}")
|
|
return await call_next(context)
|
|
|
|
|
|
# =============================================================================
|
|
# TOOL-SPECIFIC MIDDLEWARE (on_call_tool)
|
|
# These only run for tool calls, not for resources or prompts.
|
|
# =============================================================================
|
|
|
|
|
|
class AuthenticationMiddleware(Middleware):
|
|
"""Check x-api-key header for tool calls.
|
|
|
|
Demonstrates:
|
|
- on_call_tool: Only intercepts tool execution
|
|
- Header access: context.headers (available on HTTP transports)
|
|
- Configurable middleware: Pass valid keys via constructor
|
|
"""
|
|
|
|
def __init__(self, valid_api_keys: set[str] | None = None):
|
|
self.valid_api_keys = valid_api_keys or {"test-key-123"}
|
|
|
|
async def on_call_tool(self, context, call_next):
|
|
api_key = context.headers.get("x-api-key") if context.headers else None
|
|
if api_key and api_key not in self.valid_api_keys:
|
|
raise PermissionError("Invalid API key")
|
|
return await call_next(context)
|
|
|
|
|
|
class RateLimitingMiddleware(Middleware):
|
|
"""Limit tool calls per session.
|
|
|
|
Demonstrates:
|
|
- Session-based state: Use context.session_id to track per-client limits
|
|
- Stateful middleware: Store request history in instance variables
|
|
- Sliding window: Only count requests from last 60 seconds
|
|
"""
|
|
|
|
def __init__(self, max_requests_per_minute: int = 30):
|
|
self.max = max_requests_per_minute
|
|
self.seen: dict[str, list[datetime]] = defaultdict(list)
|
|
|
|
async def on_call_tool(self, context, call_next):
|
|
sid = context.session_id or "anonymous"
|
|
now = datetime.now()
|
|
self.seen[sid] = [t for t in self.seen[sid] if (now - t).total_seconds() < 60]
|
|
if len(self.seen[sid]) >= self.max:
|
|
raise Exception(f"Rate limit exceeded ({self.max}/min) for session {sid[:8]}")
|
|
self.seen[sid].append(now)
|
|
return await call_next(context)
|
|
|
|
|
|
class ValidationMiddleware(Middleware):
|
|
"""Validate tool arguments before execution.
|
|
|
|
Demonstrates:
|
|
- Typed context: context.message is CallToolRequestParams
|
|
- Tool-specific validation: Check message.name to apply rules per tool
|
|
- Early rejection: Raise before call_next to skip execution
|
|
"""
|
|
|
|
async def on_call_tool(self, context, call_next):
|
|
if context.message.name == "echo":
|
|
msg = (context.message.arguments or {}).get("message", "")
|
|
if len(msg) > 200:
|
|
raise ValueError("message too long")
|
|
return await call_next(context)
|
|
|
|
|
|
# =============================================================================
|
|
# SERVER SETUP
|
|
# Middleware order matters: first added = outermost (sees requests first)
|
|
# =============================================================================
|
|
|
|
server = MCPServer(
|
|
name="Middleware Demo Server",
|
|
version="1.0.0",
|
|
instructions="Minimal middleware demo",
|
|
middleware=[
|
|
# 1. Logging first - sees all requests including rejected ones
|
|
LoggingMiddleware(),
|
|
# 2. Auth early - reject unauthorized before expensive operations
|
|
AuthenticationMiddleware(),
|
|
# 3. Rate limiting - protect resources from abuse
|
|
RateLimitingMiddleware(max_requests_per_minute=10),
|
|
# 4. Validation - check data before handler runs
|
|
ValidationMiddleware(),
|
|
# 5-6. Connection guards - run during handshake (on_initialize)
|
|
ConnectionGuard(),
|
|
ClientCapabilitiesGuard(),
|
|
],
|
|
debug=True,
|
|
pretty_print_jsonrpc=True,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# TOOLS, RESOURCES, AND PROMPTS
|
|
# =============================================================================
|
|
|
|
|
|
@server.tool()
|
|
def echo(message: str) -> str:
|
|
"""Echo back a message."""
|
|
return f"Echo: {message}"
|
|
|
|
|
|
@server.tool()
|
|
def session_id() -> dict[str, Any]:
|
|
"""Return current session ID (None on stdio)."""
|
|
sid = server._get_session_id_from_request()
|
|
return {
|
|
"session_id": sid,
|
|
"session_id_short": sid[:8] if sid else None,
|
|
"is_anonymous": sid is None,
|
|
"note": "From mcp-session-id header",
|
|
}
|
|
|
|
|
|
@server.resource(uri="info://middleware", name="middleware_info", title="Middleware Info", mime_type="text/plain")
|
|
def middleware_info() -> str:
|
|
"""List active middleware."""
|
|
return f"""Active Middleware:
|
|
1. LoggingMiddleware
|
|
2. AuthenticationMiddleware
|
|
3. RateLimitingMiddleware
|
|
4. ValidationMiddleware
|
|
|
|
Generated at: {datetime.now().isoformat()}
|
|
"""
|
|
|
|
|
|
@server.resource(uri="data://config", name="config", title="Server Configuration", mime_type="application/json")
|
|
def get_config() -> str:
|
|
"""Server configuration."""
|
|
return """{
|
|
"server": "Middleware Demo",
|
|
"version": "1.0.0",
|
|
"features": ["logging", "auth", "rate-limit", "validation"]
|
|
}"""
|
|
|
|
|
|
@server.prompt(name="help")
|
|
def help_prompt() -> str:
|
|
"""Help prompt."""
|
|
return """# Middleware Demo Server
|
|
|
|
Middleware:
|
|
- Logging (request + tools)
|
|
- Auth (x-api-key)
|
|
- Rate limiting (per session)
|
|
- Validation (echo length)
|
|
|
|
Tools:
|
|
- echo(message)
|
|
- session_id()
|
|
|
|
Resources:
|
|
- info://middleware
|
|
- data://config
|
|
"""
|
|
|
|
|
|
if __name__ == "__main__":
|
|
server.run(transport="streamable-http", port=8000)
|