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
17 KiB
🔗 MCP Middleware Architecture Pattern
Overview
The MCP Logger Middleware represents a novel architectural pattern for integrating existing command-line tools with Model Context Protocol servers. This middleware acts as a bidirectional translation layer that transforms traditional CLI tools into AI-native, interactive systems without modifying the original tool's codebase.
🏗️ Architectural Principles
Core Middleware Concept
┌─────────────────────────────────────────────────────────────────┐
│ MCP Middleware Layer │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Legacy │───▶│ Translation │───▶│ MCP │ │
│ │ Tool │ │ Engine │ │ Context │ │
│ │ API │◀───│ │◀───│ API │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Enhancement │ │
│ │ Services │ │
│ │ │ │
│ │ • Progress │ │
│ │ • Elicitation │ │
│ │ • Context │ │
│ │ • Validation │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Design Philosophy
- Non-Invasive Integration: Zero modifications to existing tools
- Protocol Translation: Seamless sync/async bridging
- Progressive Enhancement: Graceful degradation across client capabilities
- Bidirectional Communication: Interactive workflows with user feedback
- Context Awareness: Rich integration with MCP ecosystem
🔧 Middleware Implementation Patterns
1. Logger Interception Pattern
The most elegant approach for tools with pluggable logging systems:
# middleware/logger_interceptor.py
from abc import ABC, abstractmethod
from typing import Protocol, Any, Dict, Optional
from fastmcp import Context
class LoggerInterceptor(ABC):
"""Abstract base for logger interception middleware"""
def __init__(self, context: Context, operation_id: str):
self.context = context
self.operation_id = operation_id
self.capabilities = self._detect_capabilities()
@abstractmethod
def intercept_logging_calls(self) -> None:
"""Intercept and redirect tool's logging calls"""
pass
@abstractmethod
async def translate_to_mcp(self, call_type: str, *args, **kwargs) -> None:
"""Translate tool calls to MCP context methods"""
pass
def _detect_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. Context Manager Pattern
Ensures proper resource management and cleanup:
# middleware/context_manager.py
from contextlib import contextmanager
from typing import Generator, TypeVar, Generic
T = TypeVar('T')
class MiddlewareContextManager(Generic[T]):
"""Context manager for middleware lifecycle"""
def __init__(self, tool_instance: T, middleware_class):
self.tool_instance = tool_instance
self.middleware_class = middleware_class
self.original_state = None
@contextmanager
def activate(self, context: Context, operation_id: str) -> Generator[T, None, None]:
"""Activate middleware for the duration of operations"""
# Store original state
self.original_state = self._capture_original_state()
# Create and inject middleware
middleware = self.middleware_class(context, operation_id)
self._inject_middleware(middleware)
try:
yield self.tool_instance
finally:
# Restore original state
self._restore_original_state()
def _capture_original_state(self) -> Dict[str, Any]:
"""Capture tool's original configuration"""
# Implementation specific to each tool
pass
def _inject_middleware(self, middleware) -> None:
"""Inject middleware into tool's execution path"""
# Implementation specific to each tool
pass
def _restore_original_state(self) -> None:
"""Restore tool to original state"""
# Implementation specific to each tool
pass
3. Factory Pattern for Tool-Specific Middleware
# middleware/factory.py
from typing import Type, Dict, Any
from .esptool_middleware import ESPToolMiddleware
from .platformio_middleware import PlatformIOMiddleware
from .idf_middleware import IDFMiddleware
class MiddlewareFactory:
"""Factory for creating tool-specific middleware instances"""
_middleware_registry: Dict[str, Type] = {
'esptool': ESPToolMiddleware,
'platformio': PlatformIOMiddleware,
'esp-idf': IDFMiddleware,
# Add more tools as needed
}
@classmethod
def create_middleware(
cls,
tool_name: str,
context: Context,
operation_id: str,
**kwargs
) -> LoggerInterceptor:
"""Create appropriate middleware for the specified tool"""
if tool_name not in cls._middleware_registry:
raise ValueError(f"No middleware available for tool: {tool_name}")
middleware_class = cls._middleware_registry[tool_name]
return middleware_class(context, operation_id, **kwargs)
@classmethod
def register_middleware(cls, tool_name: str, middleware_class: Type) -> None:
"""Register new middleware for a tool"""
cls._middleware_registry[tool_name] = middleware_class
@classmethod
def list_supported_tools(cls) -> List[str]:
"""List all tools with available middleware"""
return list(cls._middleware_registry.keys())
🎯 ESPTool Implementation Case Study
Complete ESPTool Middleware Implementation
# middleware/esptool_middleware.py
from esptool.logger import log, TemplateLogger
from .logger_interceptor import LoggerInterceptor
import asyncio
from typing import Dict, Any, Optional
class ESPToolMiddleware(LoggerInterceptor):
"""ESPTool-specific middleware implementation"""
def __init__(self, context: Context, operation_id: str):
super().__init__(context, operation_id)
self.original_logger = None
self.active_stages = []
def intercept_logging_calls(self) -> None:
"""Replace esptool's logger with MCP-aware version"""
self.original_logger = log.get_logger()
mcp_logger = MCPESPToolLogger(self.context, self.operation_id)
log.set_logger(mcp_logger)
def restore_original_logger(self) -> None:
"""Restore esptool's original logger"""
if self.original_logger:
log.set_logger(self.original_logger)
async def translate_to_mcp(self, call_type: str, *args, **kwargs) -> None:
"""Translate esptool calls to MCP context methods"""
translation_map = {
'print': self._handle_print,
'note': self._handle_note,
'warning': self._handle_warning,
'error': self._handle_error,
'stage': self._handle_stage,
'progress_bar': self._handle_progress,
'set_verbosity': self._handle_verbosity
}
handler = translation_map.get(call_type)
if handler:
await handler(*args, **kwargs)
async def _handle_print(self, message: str, *args) -> None:
"""Handle general print messages"""
if self.capabilities['logging']:
await self.context.log(level='info', message=self._format_message(message, *args))
async def _handle_note(self, message: str) -> None:
"""Handle note messages with special formatting"""
formatted = f"📋 {message}"
if self.capabilities['logging']:
await self.context.log(level='notice', message=formatted)
async def _handle_warning(self, message: str) -> None:
"""Handle warning messages"""
formatted = f"⚠️ {message}"
if self.capabilities['logging']:
await self.context.log(level='warning', message=formatted)
async def _handle_error(self, message: str) -> None:
"""Handle error messages"""
formatted = f"❌ {message}"
if self.capabilities['logging']:
await self.context.log(level='error', message=formatted)
async def _handle_stage(self, message: str = "", finish: bool = False) -> None:
"""Handle stage transitions with potential user interaction"""
if finish and self.active_stages:
stage = self.active_stages.pop()
await self._finish_stage(stage)
elif message:
self.active_stages.append(message)
await self._start_stage(message)
async def _handle_progress(self, cur_iter: int, total_iters: int, **kwargs) -> None:
"""Handle progress updates"""
if self.capabilities['progress']:
percentage = (cur_iter / total_iters) * 100 if total_iters > 0 else 0
await self.context.progress(
operation_id=self.operation_id,
progress=percentage,
total=total_iters,
current=cur_iter,
message=kwargs.get('prefix', '') + ' ' + kwargs.get('suffix', '')
)
async def _start_stage(self, stage_message: str) -> None:
"""Start new stage with potential user interaction"""
await self.context.log(level='info', message=f"🔄 Starting: {stage_message}")
# Check if stage requires user confirmation
if self._requires_confirmation(stage_message) and self.capabilities['elicitation']:
await self._request_user_confirmation(stage_message)
async def _finish_stage(self, stage_message: str) -> None:
"""Finish stage with completion notification"""
await self.context.log(level='info', message=f"✅ Completed: {stage_message}")
def _requires_confirmation(self, stage_message: str) -> bool:
"""Determine if stage requires user confirmation"""
critical_keywords = ['erase', 'burn', 'encrypt', 'secure', 'factory']
return any(keyword in stage_message.lower() for keyword in critical_keywords)
async def _request_user_confirmation(self, stage_message: str) -> bool:
"""Request user confirmation for critical operations"""
try:
response = await self.context.request_user_input(
prompt=f"🤔 About to: {stage_message}. Continue?",
input_type="confirmation"
)
return response.get('confirmed', True)
except Exception:
return True # Default to proceed if elicitation fails
def _format_message(self, message: str, *args) -> str:
"""Format message with arguments"""
try:
return message % args if args else message
except (TypeError, ValueError):
return f"{message} {' '.join(map(str, args))}" if args else message
🔄 Middleware Usage Patterns
Simple Tool Wrapping
# Usage example with context manager
@app.tool("esp_flash_with_middleware")
async def flash_with_middleware(
context: Context,
port: str,
firmware_path: str
) -> str:
"""Flash ESP32 with full middleware integration"""
middleware = MiddlewareFactory.create_middleware('esptool', context, 'flash_operation')
with MiddlewareContextManager(middleware).activate():
# All esptool operations now use MCP integration
with detect_chip(port) as esp:
esp = run_stub(esp)
attach_flash(esp)
write_flash(esp, [(0x1000, firmware_path)])
reset_chip(esp, 'hard-reset')
return "✅ Flashing completed with user interaction"
Advanced Middleware Configuration
# Advanced middleware with custom configuration
@app.tool("esp_configure_advanced_middleware")
async def configure_advanced_middleware(
context: Context,
operation_config: Dict[str, Any]
) -> str:
"""Configure middleware with advanced options"""
middleware_config = {
'enable_progress': operation_config.get('show_progress', True),
'require_confirmations': operation_config.get('interactive', True),
'verbosity_level': operation_config.get('verbosity', 1),
'custom_translations': operation_config.get('message_translations', {})
}
middleware = MiddlewareFactory.create_middleware(
'esptool',
context,
'advanced_operation',
**middleware_config
)
# Use configured middleware for operations
return await execute_with_middleware(middleware, operation_config)
🌟 Benefits and Advantages
For Tool Integration
- Zero Code Changes: Original tools remain completely unmodified
- Preservation of Functionality: All original features remain available
- Enhanced User Experience: Interactive workflows with progress tracking
- Error Recovery: Better error handling and user guidance
- Context Awareness: Operations become aware of broader development context
For MCP Server Development
- Rapid Integration: Quick integration of existing CLI tools
- Consistent Patterns: Reusable middleware architecture
- Extensibility: Easy to add new tools and capabilities
- Maintainability: Clear separation of concerns
- Testing: Isolated middleware can be unit tested independently
For End Users
- Natural Language Interface: CLI tools become conversational
- Safety Features: Interactive confirmations for destructive operations
- Progress Visibility: Real-time feedback on long operations
- Error Guidance: Helpful error messages and recovery suggestions
- Context Integration: Tools work seamlessly with AI assistants
🚀 Broader Applications
This middleware pattern extends far beyond esptool to any CLI tool with pluggable interfaces:
- PlatformIO: Embedded development framework
- ESP-IDF: Espressif's official development framework
- Arduino CLI: Arduino command-line interface
- OpenOCD: On-chip debugging tool
- GDB: GNU Debugger
- Make/CMake: Build systems
- Git: Version control operations
- Docker: Container operations
The pattern creates a pathway for transforming the entire embedded development toolchain into AI-native, interactive systems while preserving their original capabilities and maintaining backward compatibility.
★ Insight ─────────────────────────────────────
Universal Integration Pattern: This middleware architecture represents a universal solution for modernizing CLI tools with AI interfaces. It demonstrates how existing software ecosystems can be enhanced without disruption.
Bidirectional Translation: The middleware doesn't just capture output - it enables bidirectional communication, allowing AI systems to interact with tools in real-time, creating truly collaborative development experiences.
Emergent Intelligence: By providing tools with context awareness and user interaction capabilities, the middleware enables emergent intelligent behaviors that weren't possible with traditional CLI interfaces.
─────────────────────────────────────────────────