mcesptool/src/mcp_esptool_server/middleware/middleware_factory.py
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

159 lines
5.3 KiB
Python

"""
Middleware Factory
Provides factory methods for creating appropriate middleware instances
based on target CLI tools and operation context.
"""
import logging
from typing import Any
from uuid import uuid4
from fastmcp import Context
from .esptool_middleware import ESPToolMiddleware
from .logger_interceptor import LoggerInterceptor, ToolNotFoundError
logger = logging.getLogger(__name__)
class MiddlewareFactory:
"""Factory for creating CLI tool middleware instances"""
# Registry of available middleware classes
_middleware_registry: dict[str, type[LoggerInterceptor]] = {
"esptool": ESPToolMiddleware,
}
@classmethod
def create_middleware(
cls, tool_name: str, context: Context, operation_id: str | None = None, **kwargs
) -> LoggerInterceptor:
"""
Create middleware instance for specified CLI tool
Args:
tool_name: Name of the CLI tool (e.g., 'esptool')
context: FastMCP context for logging and user interaction
operation_id: Unique identifier for this operation
**kwargs: Additional parameters for middleware initialization
Returns:
Configured middleware instance
Raises:
ToolNotFoundError: If tool is not supported
"""
if tool_name not in cls._middleware_registry:
available_tools = ", ".join(cls._middleware_registry.keys())
raise ToolNotFoundError(
f"No middleware available for tool: {tool_name}. Available tools: {available_tools}"
)
# Generate operation ID if not provided
if operation_id is None:
operation_id = f"{tool_name}_{uuid4().hex[:8]}"
# Get middleware class and create instance
middleware_class = cls._middleware_registry[tool_name]
try:
middleware = middleware_class(context, operation_id, **kwargs)
logger.info(f"Created {tool_name} middleware with operation ID: {operation_id}")
return middleware
except Exception as e:
logger.error(f"Failed to create {tool_name} middleware: {e}")
raise ToolNotFoundError(f"Failed to initialize {tool_name} middleware: {e}")
@classmethod
def register_middleware(cls, tool_name: str, middleware_class: type[LoggerInterceptor]) -> None:
"""
Register a new middleware class for a CLI tool
Args:
tool_name: Name of the CLI tool
middleware_class: Middleware class that extends LoggerInterceptor
"""
if not issubclass(middleware_class, LoggerInterceptor):
raise ValueError(f"Middleware class must extend LoggerInterceptor: {middleware_class}")
cls._middleware_registry[tool_name] = middleware_class
logger.info(f"Registered middleware for tool: {tool_name}")
@classmethod
def get_supported_tools(cls) -> dict[str, str]:
"""
Get list of supported CLI tools and their descriptions
Returns:
Dictionary mapping tool names to descriptions
"""
tool_descriptions = {
"esptool": "ESP32/ESP8266 programming and debugging tool",
}
return {
tool: tool_descriptions.get(tool, "CLI tool integration")
for tool in cls._middleware_registry.keys()
}
@classmethod
def is_tool_supported(cls, tool_name: str) -> bool:
"""Check if a CLI tool is supported by middleware"""
return tool_name in cls._middleware_registry
@classmethod
def create_esptool_middleware(
cls, context: Context, operation_id: str | None = None, **kwargs
) -> ESPToolMiddleware:
"""
Convenience method to create ESPTool middleware with proper typing
Args:
context: FastMCP context
operation_id: Optional operation identifier
**kwargs: Additional ESPTool-specific parameters
Returns:
Configured ESPToolMiddleware instance
"""
middleware = cls.create_middleware("esptool", context, operation_id, **kwargs)
return middleware # Type checker knows this is ESPToolMiddleware
@classmethod
def get_middleware_info(cls, tool_name: str) -> dict[str, Any]:
"""
Get information about a specific middleware
Args:
tool_name: Name of the CLI tool
Returns:
Dictionary with middleware information
"""
if not cls.is_tool_supported(tool_name):
return {"error": f"Tool not supported: {tool_name}"}
middleware_class = cls._middleware_registry[tool_name]
# Create temporary instance to get interaction points
# (without context, for info purposes only)
try:
# Use a dummy context for information gathering
class DummyContext:
pass
temp_instance = middleware_class(DummyContext(), "info_query")
interaction_points = temp_instance.get_interaction_points()
except Exception:
interaction_points = []
return {
"tool_name": tool_name,
"middleware_class": middleware_class.__name__,
"description": cls.get_supported_tools()[tool_name],
"interaction_points": interaction_points,
"module": middleware_class.__module__,
}