mcesptool/MIDDLEWARE_DESIGN_PATTERNS.md
Ryan Malloy 64c1505a00 Add QEMU ESP32 emulation support
Integrate Espressif's QEMU fork for virtual ESP device management:

- QemuManager component with 5 MCP tools (start/stop/list/status/flash)
- Config auto-detects QEMU binaries from ~/.espressif/tools/
- Supports esp32, esp32s2, esp32s3, esp32c3 chip emulation
- Virtual serial over TCP (socket://localhost:PORT) transparent to esptool
- Scan integration: QEMU instances appear in esp_scan_ports results
- Blank flash images initialized to 0xFF (erased NOR flash state)
- 38 unit tests covering lifecycle, port allocation, flash writes
2026-01-28 15:35:22 -07:00

21 KiB

🎨 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.

# 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.

# 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.

# 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.

# 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.

# 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.

# 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

# 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.