# 🎨 Reusable MCP Middleware Design Patterns ## Overview This document establishes reusable design patterns for creating MCP middleware that can integrate any CLI tool with Model Context Protocol servers. These patterns provide tested, scalable solutions for common integration challenges. ## 🏗️ Core Design Patterns ### 1. **Adapter Pattern - Tool Interface Adaptation** Adapt different CLI tool interfaces to a common MCP integration standard. ```python # patterns/adapter.py from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional, Callable from fastmcp import Context class ToolAdapter(ABC): """Abstract adapter for CLI tool integration""" def __init__(self, context: Context, operation_id: str): self.context = context self.operation_id = operation_id self.capabilities = self._detect_mcp_capabilities() @abstractmethod def get_logging_interface(self) -> Dict[str, Callable]: """Return mapping of tool's logging methods to middleware handlers""" pass @abstractmethod def get_progress_interface(self) -> Optional[Callable]: """Return tool's progress reporting mechanism""" pass @abstractmethod def get_interaction_points(self) -> List[str]: """Return list of operations that require user interaction""" pass @abstractmethod def install_hooks(self) -> None: """Install middleware hooks into tool""" pass @abstractmethod def remove_hooks(self) -> None: """Remove middleware hooks from tool""" pass def _detect_mcp_capabilities(self) -> Dict[str, bool]: """Detect available MCP client capabilities""" return { 'progress': hasattr(self.context, 'progress'), 'elicitation': hasattr(self.context, 'request_user_input'), 'logging': hasattr(self.context, 'log'), 'sampling': hasattr(self.context, 'sample') } ``` ### 2. **Strategy Pattern - Multiple Integration Strategies** Support different integration approaches based on tool capabilities. ```python # patterns/strategy.py from enum import Enum from typing import Protocol, runtime_checkable class IntegrationStrategy(Enum): LOGGER_REPLACEMENT = "logger_replacement" OUTPUT_CAPTURE = "output_capture" SUBPROCESS_WRAPPER = "subprocess_wrapper" API_HOOKS = "api_hooks" @runtime_checkable class IntegrationHandler(Protocol): """Protocol for integration strategy handlers""" async def integrate(self, tool_instance: Any, middleware: ToolAdapter) -> None: """Integrate middleware with tool using this strategy""" ... async def cleanup(self, tool_instance: Any) -> None: """Clean up integration""" ... class LoggerReplacementStrategy: """Replace tool's logger with MCP-aware version""" async def integrate(self, tool_instance: Any, middleware: ToolAdapter) -> None: """Replace logger with middleware version""" logging_interface = middleware.get_logging_interface() # Store original logger if hasattr(tool_instance, '_original_logger'): middleware._original_logger = tool_instance._original_logger # Install MCP logger mcp_logger = self._create_mcp_logger(middleware, logging_interface) self._install_logger(tool_instance, mcp_logger) async def cleanup(self, tool_instance: Any) -> None: """Restore original logger""" if hasattr(tool_instance, '_original_logger'): self._install_logger(tool_instance, tool_instance._original_logger) def _create_mcp_logger(self, middleware: ToolAdapter, interface: Dict) -> Any: """Create MCP-integrated logger for tool""" # Implementation specific to tool's logger interface pass def _install_logger(self, tool_instance: Any, logger: Any) -> None: """Install logger in tool""" # Implementation specific to tool's logger mechanism pass class OutputCaptureStrategy: """Capture tool's stdout/stderr and translate to MCP""" async def integrate(self, tool_instance: Any, middleware: ToolAdapter) -> None: """Set up output capture""" import sys from io import StringIO # Capture stdout/stderr middleware._original_stdout = sys.stdout middleware._original_stderr = sys.stderr # Install capturing streams middleware._stdout_capture = MCPOutputStream(middleware.context, 'stdout') middleware._stderr_capture = MCPOutputStream(middleware.context, 'stderr') sys.stdout = middleware._stdout_capture sys.stderr = middleware._stderr_capture async def cleanup(self, tool_instance: Any) -> None: """Restore original streams""" import sys if hasattr(middleware, '_original_stdout'): sys.stdout = middleware._original_stdout sys.stderr = middleware._original_stderr class SubprocessWrapperStrategy: """Wrap tool as subprocess and capture communication""" async def integrate(self, tool_instance: Any, middleware: ToolAdapter) -> None: """Set up subprocess wrapper""" # Implementation for subprocess-based tools pass async def cleanup(self, tool_instance: Any) -> None: """Clean up subprocess""" pass ``` ### 3. **Factory Pattern - Strategy Selection** Automatically select the best integration strategy for each tool. ```python # patterns/factory.py from typing import Type, Dict, List import inspect class MiddlewareStrategyFactory: """Factory for selecting optimal integration strategy""" strategy_registry: Dict[IntegrationStrategy, Type[IntegrationHandler]] = { IntegrationStrategy.LOGGER_REPLACEMENT: LoggerReplacementStrategy, IntegrationStrategy.OUTPUT_CAPTURE: OutputCaptureStrategy, IntegrationStrategy.SUBPROCESS_WRAPPER: SubprocessWrapperStrategy, } @classmethod def select_strategy(cls, tool_instance: Any) -> IntegrationStrategy: """Automatically select best strategy for tool""" # Check for pluggable logger interface if cls._has_logger_interface(tool_instance): return IntegrationStrategy.LOGGER_REPLACEMENT # Check for direct API hooks if cls._has_api_hooks(tool_instance): return IntegrationStrategy.API_HOOKS # Check if tool is a module vs executable if cls._is_subprocess_tool(tool_instance): return IntegrationStrategy.SUBPROCESS_WRAPPER # Default to output capture return IntegrationStrategy.OUTPUT_CAPTURE @classmethod def create_handler(cls, strategy: IntegrationStrategy) -> IntegrationHandler: """Create handler for selected strategy""" handler_class = cls.strategy_registry[strategy] return handler_class() @classmethod def _has_logger_interface(cls, tool_instance: Any) -> bool: """Check if tool has replaceable logger""" # Look for common logger patterns logger_indicators = [ 'logger', 'log', 'set_logger', 'get_logger', '_logger', 'logging', 'verbose', 'quiet' ] for attr in logger_indicators: if hasattr(tool_instance, attr): return True # Check for logging module usage if hasattr(tool_instance, '__module__'): try: module = inspect.getmodule(tool_instance) return 'logging' in str(module.__dict__) except: pass return False @classmethod def _has_api_hooks(cls, tool_instance: Any) -> bool: """Check if tool provides direct API hooks""" hook_indicators = [ 'add_hook', 'register_callback', 'set_callback', 'on_progress', 'on_complete', 'on_error' ] return any(hasattr(tool_instance, attr) for attr in hook_indicators) @classmethod def _is_subprocess_tool(cls, tool_instance: Any) -> bool: """Check if tool should be wrapped as subprocess""" # Check if it's a string (command name) or Path return isinstance(tool_instance, (str, Path)) ``` ### 4. **Observer Pattern - Event Broadcasting** Broadcast tool events to multiple MCP contexts or handlers. ```python # patterns/observer.py from typing import List, Set, Callable, Any from dataclasses import dataclass from enum import Enum class ToolEvent(Enum): OPERATION_START = "operation_start" OPERATION_COMPLETE = "operation_complete" OPERATION_ERROR = "operation_error" PROGRESS_UPDATE = "progress_update" USER_INTERACTION = "user_interaction" LOG_MESSAGE = "log_message" @dataclass class ToolEventData: event_type: ToolEvent operation_id: str data: Dict[str, Any] timestamp: float context: Optional[Any] = None class ToolEventObserver(ABC): """Abstract observer for tool events""" @abstractmethod async def handle_event(self, event: ToolEventData) -> None: """Handle tool event""" pass class MCPEventObserver(ToolEventObserver): """MCP-specific event observer""" def __init__(self, context: Context): self.context = context async def handle_event(self, event: ToolEventData) -> None: """Translate tool events to MCP context calls""" event_handlers = { ToolEvent.OPERATION_START: self._handle_operation_start, ToolEvent.OPERATION_COMPLETE: self._handle_operation_complete, ToolEvent.OPERATION_ERROR: self._handle_operation_error, ToolEvent.PROGRESS_UPDATE: self._handle_progress_update, ToolEvent.USER_INTERACTION: self._handle_user_interaction, ToolEvent.LOG_MESSAGE: self._handle_log_message, } handler = event_handlers.get(event.event_type) if handler: await handler(event) async def _handle_operation_start(self, event: ToolEventData) -> None: await self.context.log( level='info', message=f"🔄 Started: {event.data.get('operation_name', 'Unknown operation')}" ) async def _handle_operation_complete(self, event: ToolEventData) -> None: await self.context.log( level='info', message=f"✅ Completed: {event.data.get('operation_name', 'Operation')}" ) async def _handle_operation_error(self, event: ToolEventData) -> None: error_msg = event.data.get('error_message', 'Unknown error') await self.context.log(level='error', message=f"❌ Error: {error_msg}") async def _handle_progress_update(self, event: ToolEventData) -> None: if hasattr(self.context, 'progress'): await self.context.progress( operation_id=event.operation_id, progress=event.data.get('progress', 0), total=event.data.get('total', 100), current=event.data.get('current', 0), message=event.data.get('message', '') ) async def _handle_user_interaction(self, event: ToolEventData) -> None: if hasattr(self.context, 'request_user_input'): response = await self.context.request_user_input( prompt=event.data.get('prompt', 'Confirmation required'), input_type=event.data.get('input_type', 'confirmation') ) # Store response in event for tool to access event.data['user_response'] = response async def _handle_log_message(self, event: ToolEventData) -> None: await self.context.log( level=event.data.get('level', 'info'), message=event.data.get('message', '') ) class ToolEventBroadcaster: """Broadcasts tool events to registered observers""" def __init__(self): self.observers: Set[ToolEventObserver] = set() def add_observer(self, observer: ToolEventObserver) -> None: """Add event observer""" self.observers.add(observer) def remove_observer(self, observer: ToolEventObserver) -> None: """Remove event observer""" self.observers.discard(observer) async def broadcast_event(self, event: ToolEventData) -> None: """Broadcast event to all observers""" tasks = [observer.handle_event(event) for observer in self.observers] await asyncio.gather(*tasks, return_exceptions=True) async def emit_operation_start(self, operation_id: str, operation_name: str) -> None: """Emit operation start event""" event = ToolEventData( event_type=ToolEvent.OPERATION_START, operation_id=operation_id, data={'operation_name': operation_name}, timestamp=time.time() ) await self.broadcast_event(event) async def emit_progress( self, operation_id: str, progress: float, total: int, current: int, message: str = "" ) -> None: """Emit progress update event""" event = ToolEventData( event_type=ToolEvent.PROGRESS_UPDATE, operation_id=operation_id, data={ 'progress': progress, 'total': total, 'current': current, 'message': message }, timestamp=time.time() ) await self.broadcast_event(event) ``` ### 5. **Decorator Pattern - Middleware Application** Apply middleware through decorators for clean integration. ```python # patterns/decorator.py from functools import wraps from typing import Callable, Any, TypeVar, ParamSpec P = ParamSpec('P') R = TypeVar('R') def with_mcp_middleware( tool_name: str, integration_strategy: Optional[IntegrationStrategy] = None, require_confirmation: bool = False, enable_progress: bool = True ): """Decorator to apply MCP middleware to tool operations""" def decorator(func: Callable[P, R]) -> Callable[P, R]: @wraps(func) async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # Extract context from args/kwargs context = kwargs.get('context') or args[0] if args else None if not isinstance(context, Context): raise ValueError("MCP context required for middleware") # Create operation ID operation_id = f"{func.__name__}_{int(time.time())}" # Create middleware middleware = MiddlewareFactory.create_middleware( tool_name, context, operation_id ) # Select integration strategy if integration_strategy is None: strategy = MiddlewareStrategyFactory.select_strategy(tool_name) else: strategy = integration_strategy # Create handler handler = MiddlewareStrategyFactory.create_handler(strategy) # Apply middleware try: await handler.integrate(tool_name, middleware) # Configure middleware options if hasattr(middleware, 'set_require_confirmation'): middleware.set_require_confirmation(require_confirmation) if hasattr(middleware, 'set_enable_progress'): middleware.set_enable_progress(enable_progress) # Execute original function result = await func(*args, **kwargs) return result finally: # Clean up middleware await handler.cleanup(tool_name) return wrapper return decorator # Usage example: @with_mcp_middleware('esptool', require_confirmation=True) async def flash_esp32(context: Context, port: str, firmware: str) -> str: """Flash ESP32 with automatic middleware integration""" # All esptool operations in this function now use MCP middleware with detect_chip(port) as esp: write_flash(esp, [(0x1000, firmware)]) return "Flashing completed" ``` ### 6. **Template Method Pattern - Common Integration Flow** Define standard integration workflow with customizable steps. ```python # patterns/template_method.py class MiddlewareIntegrationTemplate: """Template for middleware integration workflow""" async def integrate_tool( self, tool_instance: Any, context: Context, operation_id: str ) -> Any: """Template method for tool integration""" # Step 1: Analyze tool capabilities capabilities = await self.analyze_tool(tool_instance) # Step 2: Select integration strategy strategy = await self.select_strategy(tool_instance, capabilities) # Step 3: Create middleware middleware = await self.create_middleware(context, operation_id, capabilities) # Step 4: Install hooks await self.install_hooks(tool_instance, middleware, strategy) # Step 5: Configure options await self.configure_middleware(middleware, capabilities) # Step 6: Start monitoring await self.start_monitoring(tool_instance, middleware) return middleware @abstractmethod async def analyze_tool(self, tool_instance: Any) -> Dict[str, Any]: """Analyze tool's capabilities and interfaces""" pass @abstractmethod async def select_strategy( self, tool_instance: Any, capabilities: Dict[str, Any] ) -> IntegrationStrategy: """Select appropriate integration strategy""" pass async def create_middleware( self, context: Context, operation_id: str, capabilities: Dict[str, Any] ) -> ToolAdapter: """Create middleware instance (default implementation)""" return MiddlewareFactory.create_middleware( self.tool_name, context, operation_id ) @abstractmethod async def install_hooks( self, tool_instance: Any, middleware: ToolAdapter, strategy: IntegrationStrategy ) -> None: """Install middleware hooks""" pass async def configure_middleware( self, middleware: ToolAdapter, capabilities: Dict[str, Any] ) -> None: """Configure middleware options (default implementation)""" # Configure based on detected capabilities if capabilities.get('supports_progress', False): middleware.enable_progress_tracking() if capabilities.get('has_interactive_operations', False): middleware.enable_user_confirmations() async def start_monitoring( self, tool_instance: Any, middleware: ToolAdapter ) -> None: """Start monitoring tool operations (default implementation)""" # Set up event monitoring if supported if hasattr(middleware, 'start_event_monitoring'): await middleware.start_event_monitoring() ``` ## 🎯 Pattern Composition Example ### Complete ESPTool Middleware Using All Patterns ```python # esptool_complete_middleware.py class ESPToolCompleteMiddleware(MiddlewareIntegrationTemplate): """Complete ESPTool middleware using all design patterns""" def __init__(self): self.tool_name = 'esptool' self.event_broadcaster = ToolEventBroadcaster() async def analyze_tool(self, tool_instance: Any) -> Dict[str, Any]: """Analyze esptool capabilities""" return { 'has_logger_interface': True, 'supports_progress': True, 'has_interactive_operations': True, 'logger_module': 'esptool.logger', 'progress_method': 'progress_bar', 'critical_operations': [ 'erase_flash', 'burn_efuse', 'enable_secure_boot' ] } async def select_strategy( self, tool_instance: Any, capabilities: Dict[str, Any] ) -> IntegrationStrategy: """Select logger replacement strategy for esptool""" return IntegrationStrategy.LOGGER_REPLACEMENT async def install_hooks( self, tool_instance: Any, middleware: ToolAdapter, strategy: IntegrationStrategy ) -> None: """Install esptool-specific hooks""" # Add MCP observer to event broadcaster mcp_observer = MCPEventObserver(middleware.context) self.event_broadcaster.add_observer(mcp_observer) # Install logger replacement handler = MiddlewareStrategyFactory.create_handler(strategy) await handler.integrate(tool_instance, middleware) # Set up event broadcasting in middleware middleware.set_event_broadcaster(self.event_broadcaster) # Usage with decorator: @with_mcp_middleware('esptool') async def advanced_esp_operation(context: Context, config: Dict) -> str: """Advanced ESP operation with complete middleware""" # All patterns working together: # - Adapter pattern handles esptool interface # - Strategy pattern selects logger replacement # - Observer pattern broadcasts events # - Factory pattern creates appropriate handlers # - Template method ensures consistent integration # - Decorator pattern applies everything transparently result = await perform_complex_esp_operation(config) return result ``` These reusable patterns provide a comprehensive framework for integrating any CLI tool with MCP servers, ensuring consistent, maintainable, and extensible middleware implementations.