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
This commit is contained in:
Ryan Malloy 2026-01-28 15:35:22 -07:00
commit 64c1505a00
44 changed files with 13750 additions and 0 deletions

33
.env.example Normal file
View File

@ -0,0 +1,33 @@
# MCP ESPTool Server Configuration
# Project Configuration
COMPOSE_PROJECT_NAME=mcp-esptool
# ESPTool Configuration
ESPTOOL_PATH=esptool
ESP_DEFAULT_BAUD_RATE=460800
ESP_CONNECTION_TIMEOUT=30
ESP_ENABLE_STUB_FLASHER=true
ESP_MAX_CONCURRENT_OPERATIONS=5
ESP_OPERATION_TIMEOUT=300
# ESP-IDF Configuration (optional)
ESP_IDF_PATH=/path/to/esp-idf
# MCP Project Roots (colon-separated paths)
MCP_PROJECT_ROOTS=/home/user/esp_projects:/home/user/Arduino
# MCP Integration Settings
MCP_ENABLE_PROGRESS=true
MCP_ENABLE_ELICITATION=true
MCP_LOG_LEVEL=INFO
# Development Settings
DEV_ENABLE_HOT_RELOAD=false
DEV_MOCK_HARDWARE=false
DEV_ENABLE_TRACING=false
# Production Settings
PRODUCTION_MODE=false
PROD_ENABLE_SECURITY_AUDIT=true
PROD_REQUIRE_CONFIRMATIONS=true

90
.gitignore vendored Normal file
View File

@ -0,0 +1,90 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
.nox/
coverage.xml
*.cover
*.py,cover
.hypothesis/
# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# MyPy
.mypy_cache/
.dmypy.json
dmypy.json
# Ruff
.ruff_cache/
# ESP Development
*.bin
*.elf
*.map
esp_projects/
build/
sdkconfig
sdkconfig.old
# Logs
*.log
logs/
# Docker
.dockerignore
# OS
.DS_Store
Thumbs.db
# Project specific
.env.local
*.backup
temp/
tmp/
# Vendored esptool (embedded git repo)
esptool/
# QEMU flash images (generated at runtime)
src/mcp_esptool_server/resources/qemu/*.bin

465
API_DESIGN.md Normal file
View File

@ -0,0 +1,465 @@
# 🔧 FastMCP ESPTool Server API Design
## API Philosophy & Design Principles
Following the patterns established in the Arduino MCP server, this API prioritizes:
1. **Natural Language Workflows**: Tools designed for conversational AI interaction
2. **Component Architecture**: Modular, testable, and maintainable design
3. **Production Readiness**: Enterprise-grade error handling and resource management
4. **Performance Optimization**: Leveraging esptool's advanced capabilities
5. **Security First**: Built-in security validation and eFuse management
## 🎯 Core API Structure
### FastMCP Server Foundation
```python
# server.py - Main FastMCP server following Arduino MCP patterns
from fastmcp import FastMCP
from .components import (
ChipControl,
FlashManager,
PartitionManager,
SecurityManager,
FirmwareBuilder,
OTAManager,
ProductionTools,
Diagnostics
)
from .config import ESPToolServerConfig
class ESPToolServer:
def __init__(self):
self.app = FastMCP("ESP Development Server")
self.config = ESPToolServerConfig()
self.initialize_components()
def initialize_components(self):
"""Initialize all component modules"""
self.chip_control = ChipControl(self.app, self.config)
self.flash_manager = FlashManager(self.app, self.config)
self.partition_manager = PartitionManager(self.app, self.config)
self.security_manager = SecurityManager(self.app, self.config)
self.firmware_builder = FirmwareBuilder(self.app, self.config)
self.ota_manager = OTAManager(self.app, self.config)
self.production_tools = ProductionTools(self.app, self.config)
self.diagnostics = Diagnostics(self.app, self.config)
def main():
server = ESPToolServer()
server.app.run()
```
## 🧩 Component API Specifications
### 1. ChipControl Component
```python
# components/chip_control.py
from fastmcp import FastMCP
from esptool.cmds import detect_chip, run_stub, reset_chip, load_ram, run
from typing import Dict, List, Optional
import asyncio
class ChipControl:
"""ESP chip detection, connection, and control operations"""
def __init__(self, app: FastMCP, config):
self.app = app
self.config = config
self.register_tools()
self.register_resources()
def register_tools(self):
"""Register chip control tools with FastMCP"""
@self.app.tool("esp_detect_chip")
async def detect_esp_chip(
port: str,
baud: int = 115200,
connect_attempts: int = 7
) -> Dict:
"""
Auto-detect ESP chip type and return comprehensive information
Args:
port: Serial port path (e.g., '/dev/ttyUSB0', 'COM3')
baud: Connection baud rate
connect_attempts: Number of connection retry attempts
Returns:
Dict with chip info: type, features, MAC address, flash size, etc.
"""
try:
with detect_chip(port, baud=baud, connect_attempts=connect_attempts) as esp:
return {
"success": True,
"chip_type": esp.get_chip_description(),
"chip_revision": esp.get_chip_revision(),
"features": esp.get_chip_features(),
"mac_address": esp.read_mac().hex(':'),
"flash_size": esp.flash_size,
"crystal_freq": esp.get_crystal_freq(),
"port": port,
"stub_supported": hasattr(esp, 'STUB_SUPPORTED') and esp.STUB_SUPPORTED
}
except Exception as e:
return {"success": False, "error": str(e), "port": port}
@self.app.tool("esp_connect_advanced")
async def connect_with_strategies(
port: str,
enable_stub: bool = True,
high_speed: bool = True
) -> str:
"""
Connect to ESP chip with multiple fallback strategies
Args:
port: Serial port path
enable_stub: Whether to load the flasher stub for better performance
high_speed: Attempt high-speed connection first
Returns:
Connection status and optimization details
"""
strategies = [
{'baud': 460800 if high_speed else 115200, 'connect_mode': 'default-reset'},
{'baud': 115200, 'connect_mode': 'usb-reset'},
{'baud': 115200, 'connect_mode': 'manual-reset'}
]
for i, strategy in enumerate(strategies):
try:
with detect_chip(port, **strategy) as esp:
chip_info = esp.get_chip_description()
if enable_stub and hasattr(esp, 'STUB_SUPPORTED') and esp.STUB_SUPPORTED:
esp = run_stub(esp)
return f"✓ Connected to {chip_info} with stub flasher (strategy {i+1})"
else:
return f"✓ Connected to {chip_info} (strategy {i+1})"
except Exception as e:
continue
return f"❌ Failed to connect to {port} with all strategies"
@self.app.tool("esp_load_test_firmware")
async def load_firmware_to_ram(
port: str,
firmware_path: str
) -> str:
"""
Load and execute firmware in RAM for testing (no flash wear)
Args:
port: Serial port path
firmware_path: Path to firmware binary file
Returns:
Execution status and any output
"""
try:
with detect_chip(port) as esp:
load_ram(esp, firmware_path)
run(esp)
return f"✓ Firmware loaded and executed in RAM from {firmware_path}"
except Exception as e:
return f"❌ Failed to load firmware: {e}"
@self.app.tool("esp_reset_advanced")
async def reset_chip_advanced(
port: str,
reset_mode: str = "hard-reset"
) -> str:
"""
Reset ESP chip with specific reset mode
Args:
port: Serial port path
reset_mode: Reset type ('hard-reset', 'soft-reset', 'no-reset')
Returns:
Reset operation status
"""
valid_modes = ['hard-reset', 'soft-reset', 'no-reset']
if reset_mode not in valid_modes:
return f"❌ Invalid reset mode. Use: {', '.join(valid_modes)}"
try:
with detect_chip(port) as esp:
reset_chip(esp, reset_mode)
return f"✓ Chip reset using {reset_mode} mode"
except Exception as e:
return f"❌ Reset failed: {e}"
def register_resources(self):
"""Register MCP resources for real-time chip information"""
@self.app.resource("esp://chips")
async def list_connected_chips() -> str:
"""List all connected ESP chips with basic information"""
# Implementation to scan common ports and detect chips
# Returns formatted list of connected devices
pass
```
### 2. FlashManager Component
```python
# components/flash_manager.py
from esptool.cmds import (
attach_flash, write_flash, read_flash, erase_flash,
verify_flash, flash_id, read_flash_status
)
class FlashManager:
"""Advanced flash memory operations and optimization"""
def register_tools(self):
@self.app.tool("esp_flash_firmware")
async def flash_firmware_advanced(
port: str,
firmware_files: List[Dict[str, any]],
verify: bool = True,
optimize: bool = True
) -> str:
"""
Flash multiple firmware files with verification and optimization
Args:
port: Serial port path
firmware_files: List of {address: int, file: str} mappings
verify: Verify flash after writing
optimize: Use performance optimizations
Returns:
Flashing status with timing and verification results
"""
try:
with detect_chip(port) as esp:
if optimize and hasattr(esp, 'STUB_SUPPORTED'):
esp = run_stub(esp)
attach_flash(esp)
# Convert file list to esptool format
flash_files = [(item['address'], item['file']) for item in firmware_files]
write_flash(esp, flash_files)
if verify:
verify_flash(esp, flash_files)
reset_chip(esp, 'hard-reset')
return f"✓ Flashed {len(firmware_files)} files successfully"
except Exception as e:
return f"❌ Flash operation failed: {e}"
@self.app.tool("esp_flash_analyze")
async def analyze_flash_usage(port: str) -> Dict:
"""
Analyze flash memory layout and usage statistics
Args:
port: Serial port path
Returns:
Detailed flash analysis including used/free space, partitions
"""
# Implementation for flash analysis
pass
@self.app.tool("esp_flash_backup")
async def backup_flash_contents(
port: str,
output_directory: str,
include_partitions: bool = True
) -> str:
"""
Create complete backup of ESP flash contents
Args:
port: Serial port path
output_directory: Where to save backup files
include_partitions: Whether to backup individual partitions
Returns:
Backup operation status and file locations
"""
# Implementation for flash backup
pass
```
### 3. PartitionManager Component
```python
# components/partition_manager.py
class PartitionManager:
"""ESP partition table creation and management"""
def register_tools(self):
@self.app.tool("esp_partition_create_ota")
async def create_ota_partition_table(
app_size: str = "1MB",
ota_data_size: str = "8KB",
nvs_size: str = "24KB",
output_file: Optional[str] = None
) -> str:
"""
Create optimized partition table for OTA updates
Args:
app_size: Size for each app partition (factory + OTA)
ota_data_size: Size for OTA data partition
nvs_size: Size for NVS partition
output_file: Output partition CSV file path
Returns:
Generated partition table path and layout summary
"""
# Implementation for OTA partition creation
pass
@self.app.tool("esp_partition_custom")
async def create_custom_partition_table(
partitions: List[Dict],
output_file: str
) -> str:
"""
Create custom partition table from specification
Args:
partitions: List of partition definitions
output_file: Output CSV file path
Returns:
Partition table creation status
"""
# Implementation for custom partition tables
pass
```
### 4. SecurityManager Component
```python
# components/security_manager.py
from esptool.cmds import get_security_info
class SecurityManager:
"""ESP security features and eFuse management"""
def register_tools(self):
@self.app.tool("esp_security_audit")
async def security_audit(port: str) -> Dict:
"""
Comprehensive security audit of ESP chip
Args:
port: Serial port path
Returns:
Security status including eFuses, encryption, secure boot
"""
try:
with detect_chip(port) as esp:
security_info = get_security_info(esp)
return {
"success": True,
"security_info": security_info,
"recommendations": self._generate_security_recommendations(security_info)
}
except Exception as e:
return {"success": False, "error": str(e)}
@self.app.tool("esp_enable_flash_encryption")
async def enable_flash_encryption(
port: str,
key_file: Optional[str] = None
) -> str:
"""
Enable flash encryption with optional custom key
Args:
port: Serial port path
key_file: Optional custom encryption key file
Returns:
Encryption setup status and security warnings
"""
# Implementation for flash encryption setup
pass
```
## 🔄 Asynchronous Operations & Performance
### Background Task Management
```python
# Enhanced async support for long-running operations
class AsyncFlashManager:
def __init__(self):
self.active_operations = {}
@self.app.tool("esp_flash_firmware_async")
async def flash_firmware_background(
port: str,
firmware_files: List[Dict],
operation_id: Optional[str] = None
) -> str:
"""Start firmware flashing in background with progress tracking"""
if not operation_id:
operation_id = f"flash_{port}_{int(time.time())}"
# Start background task
task = asyncio.create_task(self._flash_firmware_task(port, firmware_files))
self.active_operations[operation_id] = task
return f"✓ Flashing started (ID: {operation_id}). Use esp_operation_status to check progress."
@self.app.tool("esp_operation_status")
async def check_operation_status(operation_id: str) -> Dict:
"""Check status of background operation"""
if operation_id not in self.active_operations:
return {"error": "Operation not found"}
task = self.active_operations[operation_id]
return {
"operation_id": operation_id,
"done": task.done(),
"result": task.result() if task.done() else None
}
```
## 📋 Resource API Design
### Real-time ESP Information
```python
# MCP Resources for live ESP data
@self.app.resource("esp://chips")
async def connected_chips() -> str:
"""JSON list of all connected ESP chips"""
@self.app.resource("esp://flash/{port}")
async def flash_status(port: str) -> str:
"""Real-time flash status for specific chip"""
@self.app.resource("esp://security/{port}")
async def security_status(port: str) -> str:
"""Current security configuration"""
@self.app.resource("esp://partitions/{port}")
async def partition_info(port: str) -> str:
"""Live partition table information"""
```
This API design provides a comprehensive, production-ready foundation for ESP development workflows while maintaining the conversational AI-first approach that makes the Arduino MCP server so effective.

451
BROADER_APPLICATIONS.md Normal file
View File

@ -0,0 +1,451 @@
# 🌍 MCP Middleware: Broader Applications Beyond ESPTool
## Overview
The MCP middleware pattern established for esptool creates a universal framework for integrating any CLI tool with AI-powered interfaces. This document explores the broader ecosystem of tools that can benefit from this pattern.
## 🎯 Embedded Development Ecosystem
### ESP-IDF Framework (`idf.py`)
**Priority: HIGH** - Essential for professional ESP development
```python
# ESP-IDF middleware implementation
class IDFMiddleware(ToolAdapter):
"""ESP-IDF development framework integration"""
def get_logging_interface(self) -> Dict[str, Callable]:
return {
'info': self._handle_info,
'warning': self._handle_warning,
'error': self._handle_error,
'debug': self._handle_debug,
'success': self._handle_success
}
def get_interaction_points(self) -> List[str]:
return [
'menuconfig', # Interactive configuration
'erase-flash', # Destructive operation
'erase-otadata', # OTA data erase
'set-target', # Target chip selection
'fullclean' # Complete project clean
]
# Natural language workflows:
# "Create a new ESP-IDF project with WiFi and Bluetooth"
# "Configure the project for low power mode"
# "Build and flash with debug symbols"
# "Monitor serial output and parse crash dumps"
```
### Arduino CLI (`arduino-cli`)
**Status: EXISTING** - Already implemented in Arduino MCP server
**Enhanced Integration**: Extend existing Arduino MCP with middleware patterns
```python
# Enhanced Arduino CLI middleware
class ArduinoCLIMiddleware(ToolAdapter):
"""Enhanced Arduino CLI integration using middleware patterns"""
def get_interaction_points(self) -> List[str]:
return [
'core install', # Core installation confirmation
'lib install', # Library installation
'compile', # Compilation with progress
'upload', # Upload with verification
'board attach' # Board configuration
]
# Integration with existing Arduino MCP:
# "Compile this ESP32 sketch and flash with security enabled"
# → Arduino MCP: Compile sketch
# → ESPTool MCP: Advanced flashing with security
# → IDF MCP: Monitor and debug
```
### PlatformIO (`pio`)
**Priority: HIGH** - Popular cross-platform embedded framework
```python
class PlatformIOMiddleware(ToolAdapter):
"""PlatformIO development platform integration"""
def get_logging_interface(self) -> Dict[str, Callable]:
return {
'INFO': self._handle_info,
'WARNING': self._handle_warning,
'ERROR': self._handle_error,
'SUCCESS': self._handle_success
}
def get_interaction_points(self) -> List[str]:
return [
'run --target upload', # Upload with confirmation
'run --target erase', # Erase operations
'pkg install', # Package installation
'platform install', # Platform installation
'device monitor' # Serial monitoring
]
# Natural language workflows:
# "Create a PlatformIO project for ESP32 with OTA support"
# "Add FreeRTOS library and configure tasks"
# "Build for production and enable crash reporting"
```
### OpenOCD (On-Chip Debugger)
**Priority: MEDIUM** - Professional debugging tool
```python
class OpenOCDMiddleware(ToolAdapter):
"""OpenOCD debugging tool integration"""
def get_interaction_points(self) -> List[str]:
return [
'halt', # Halt processor
'reset', # Reset operations
'flash erase', # Flash erase
'flash write', # Flash programming
'reg', # Register access
'step', # Single step execution
]
# Natural language workflows:
# "Connect to ESP32 via JTAG and halt execution"
# "Set breakpoint at main() and examine registers"
# "Flash firmware via JTAG and verify"
```
## 🔧 Build Systems & Development Tools
### CMake
**Priority: MEDIUM** - Universal build system
```python
class CMakeMiddleware(ToolAdapter):
"""CMake build system integration"""
def get_interaction_points(self) -> List[str]:
return [
'configure', # Configuration phase
'build', # Build phase with progress
'install', # Installation
'test', # Test execution
'clean' # Clean build
]
# Natural language workflows:
# "Configure CMake for cross-compilation to ESP32"
# "Build with verbose output and parallel jobs"
# "Run tests and generate coverage report"
```
### Make/Ninja
**Priority: MEDIUM** - Traditional build tools
```python
class MakeMiddleware(ToolAdapter):
"""Make/Ninja build tool integration"""
def get_progress_interface(self) -> Optional[Callable]:
# Parse make output for progress estimation
return self._parse_make_progress
def get_interaction_points(self) -> List[str]:
return [
'clean', # Clean build
'distclean', # Complete clean
'install', # Installation
'uninstall' # Uninstall
]
```
## 🐳 Container & Infrastructure Tools
### Docker
**Priority: HIGH** - Essential for development environments
```python
class DockerMiddleware(ToolAdapter):
"""Docker container management integration"""
def get_logging_interface(self) -> Dict[str, Callable]:
return {
'build': self._handle_build_progress,
'pull': self._handle_pull_progress,
'push': self._handle_push_progress,
'run': self._handle_run_output
}
def get_interaction_points(self) -> List[str]:
return [
'system prune', # System cleanup
'image rm', # Image removal
'container rm', # Container removal
'volume rm' # Volume removal
]
# Natural language workflows:
# "Build ESP32 development environment container"
# "Run interactive shell in ESP-IDF container"
# "Clean up unused Docker resources"
```
### Kubernetes (`kubectl`)
**Priority: MEDIUM** - Cloud deployment
```python
class KubectlMiddleware(ToolAdapter):
"""Kubernetes cluster management integration"""
def get_interaction_points(self) -> List[str]:
return [
'delete', # Resource deletion
'drain', # Node draining
'cordon', # Node cordoning
'apply', # Resource application
'rollout restart' # Rolling restart
]
```
## 🔍 Version Control & DevOps
### Git
**Priority: HIGH** - Universal version control
```python
class GitMiddleware(ToolAdapter):
"""Git version control integration"""
def get_interaction_points(self) -> List[str]:
return [
'push --force', # Force push
'reset --hard', # Hard reset
'clean -fd', # Force clean
'rebase', # Interactive rebase
'merge', # Merge with conflicts
'commit --amend' # Amend commits
]
# Natural language workflows:
# "Create feature branch for ESP32 WiFi improvements"
# "Commit changes with conventional commit format"
# "Rebase feature branch onto latest main"
```
### GitHub CLI (`gh`)
**Priority: MEDIUM** - GitHub integration
```python
class GitHubCLIMiddleware(ToolAdapter):
"""GitHub CLI integration"""
def get_interaction_points(self) -> List[str]:
return [
'pr create', # Pull request creation
'pr merge', # Pull request merge
'issue close', # Issue management
'release create' # Release creation
]
```
## 🧪 Testing & Quality Assurance
### pytest
**Priority: HIGH** - Python testing framework
```python
class PytestMiddleware(ToolAdapter):
"""pytest testing framework integration"""
def get_progress_interface(self) -> Optional[Callable]:
return self._parse_pytest_progress
def get_interaction_points(self) -> List[str]:
return [
'--pdb', # Debugger on failure
'--lf', # Last failed tests
'-x', # Stop on first failure
'--tb=short' # Traceback format
]
# Natural language workflows:
# "Run tests with coverage and generate HTML report"
# "Test only ESP32-related modules with verbose output"
# "Debug failed test with interactive debugger"
```
### GDB (GNU Debugger)
**Priority: HIGH** - Essential for embedded debugging
```python
class GDBMiddleware(ToolAdapter):
"""GDB debugger integration"""
def get_interaction_points(self) -> List[str]:
return [
'continue', # Continue execution
'step', # Step execution
'next', # Next line
'finish', # Finish function
'kill', # Kill process
'quit' # Quit debugger
]
# Natural language workflows:
# "Load ESP32 ELF file and connect to remote target"
# "Set breakpoint at WiFi initialization"
# "Examine stack trace and local variables"
```
## 📊 Analysis & Monitoring Tools
### Valgrind
**Priority: MEDIUM** - Memory analysis
```python
class ValgrindMiddleware(ToolAdapter):
"""Valgrind memory analysis integration"""
def get_progress_interface(self) -> Optional[Callable]:
return self._parse_valgrind_progress
# Natural language workflows:
# "Run memory leak detection on ESP32 simulator"
# "Profile performance with callgrind"
```
### Wireshark/tshark
**Priority: LOW** - Network analysis
```python
class WiresharkMiddleware(ToolAdapter):
"""Network packet analysis integration"""
def get_interaction_points(self) -> List[str]:
return [
'capture', # Start capture
'stop', # Stop capture
'save' # Save capture
]
```
## 🎨 Integration Ecosystem Benefits
### 1. **Unified Development Experience**
```
User: "Set up complete ESP32 development environment"
Claude: I'll set up a comprehensive ESP32 development stack using multiple integrated tools.
# Docker Middleware
→ docker_create_dev_environment
✓ ESP-IDF development container ready
# Git Middleware
→ git_clone_project_template
✓ ESP32 project template cloned
# IDF Middleware
→ idf_set_target("esp32")
→ idf_install_dependencies
✓ ESP-IDF environment configured
# ESPTool Middleware
→ esp_detect_chip
✓ ESP32 detected on /dev/ttyUSB0
# PlatformIO Middleware (alternative)
→ pio_project_init
✓ Alternative PlatformIO environment ready
Your complete ESP32 development environment is ready!
Use Docker for isolation, Git for version control,
ESP-IDF for professional development, or PlatformIO for quick prototyping.
```
### 2. **Cross-Tool Workflows**
```
User: "Build ESP32 firmware, test it, and deploy to production"
Claude: I'll execute a complete CI/CD workflow across multiple tools.
# Git Middleware
→ git_ensure_clean_workspace
→ git_pull_latest_changes
# IDF Middleware
→ idf_build_project(release=True)
→ idf_analyze_binary_size
# ESPTool Middleware
→ esp_firmware_analyze(security_check=True)
→ esp_generate_production_config
# Pytest Middleware
→ pytest_run_hardware_tests
→ pytest_generate_coverage_report
# Docker Middleware
→ docker_build_production_image
→ docker_push_to_registry
# Kubernetes Middleware
→ kubectl_deploy_to_staging
→ kubectl_run_integration_tests
→ kubectl_promote_to_production
✓ Complete production deployment pipeline executed
```
### 3. **Intelligent Tool Selection**
The middleware system can intelligently select the best tool for each task:
```python
class IntelligentToolSelector:
"""Select optimal tool for each development task"""
tool_capabilities = {
'esp_flashing': ['esptool', 'idf.py', 'platformio'],
'esp_debugging': ['idf.py', 'openocd', 'gdb'],
'esp_monitoring': ['idf.py', 'platformio', 'arduino-cli'],
'build_management': ['idf.py', 'platformio', 'cmake', 'make'],
'version_control': ['git', 'gh'],
'containerization': ['docker', 'podman'],
'testing': ['pytest', 'googletest', 'unity']
}
async def select_best_tool(self, task: str, context: Dict) -> str:
"""Select optimal tool based on context and availability"""
# Consider project type, available tools, user preferences
pass
```
`★ Insight ─────────────────────────────────────`
**Universal Development Ecosystem**: The middleware pattern creates a universal integration layer that can transform any CLI tool into an AI-native, interactive system. This enables the creation of comprehensive development ecosystems where tools work seamlessly together.
**Emergent Workflows**: By providing common interfaces across different tools, the middleware enables emergent workflows that weren't possible before - AI can intelligently combine tools to accomplish complex tasks.
**Standardized Interaction Model**: All tools gain the same interaction capabilities (progress tracking, user confirmations, context awareness) regardless of their original design, creating a consistent developer experience.
`─────────────────────────────────────────────────`
This comprehensive integration approach transforms individual CLI tools into a cohesive, AI-powered development ecosystem where each tool maintains its strengths while gaining enhanced interactivity and intelligence.

93
Dockerfile Normal file
View File

@ -0,0 +1,93 @@
# Use official uv image for Python development
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim AS base
# Set working directory
WORKDIR /app
# Install system dependencies for ESP development
RUN apt-get update && apt-get install -y \
git \
curl \
build-essential \
cmake \
ninja-build \
ccache \
libffi-dev \
libssl-dev \
dfu-util \
libusb-1.0-0 \
python3-venv \
&& rm -rf /var/lib/apt/lists/*
# Development stage with source mounting and hot reload
FROM base AS development
# Set environment for development
ENV UV_COMPILE_BYTECODE=0
ENV DEV_ENABLE_HOT_RELOAD=true
# Copy dependency files
COPY pyproject.toml uv.lock ./
# Install dependencies in development mode
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --dev --frozen
# Copy source code
COPY . .
# Install project in editable mode
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install -e .
# Create non-root user for security
RUN useradd -m -u 1000 mcpuser && \
chown -R mcpuser:mcpuser /app
USER mcpuser
# Default command for development
CMD ["uv", "run", "mcp-esptool-server", "--debug"]
# Production stage with optimized build
FROM base AS production
# Set environment for production
ENV UV_COMPILE_BYTECODE=1
ENV PRODUCTION_MODE=true
# Copy dependency files
COPY pyproject.toml uv.lock ./
# Install only production dependencies
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-editable
# Copy source code
COPY src ./src
# Install project
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install --no-editable .
# Create non-root user
RUN useradd -m -u 1000 mcpuser && \
chown -R mcpuser:mcpuser /app
USER mcpuser
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD uv run python -c "import mcp_esptool_server; print('OK')" || exit 1
# Default command for production
CMD ["uv", "run", "mcp-esptool-server", "--production"]
# Testing stage for CI/CD
FROM development AS testing
# Run tests as part of build
RUN uv run pytest tests/ -v
# Default target
FROM development

332
FASTMCP_ESPTOOL_SERVER.md Normal file
View File

@ -0,0 +1,332 @@
# 🚀 FastMCP Esptool Server Architecture
## Overview
A comprehensive FastMCP server that leverages esptool's Python API to provide AI-powered ESP32/ESP8266 development workflows. This server complements the existing Arduino MCP server by offering direct ESP chip control, advanced flashing capabilities, and production-grade automation.
## 🎯 Core Value Proposition
**Problem**: Current ESP development requires manual Arduino IDE/CLI usage, lacks AI assistance, and provides limited automation for production workflows.
**Solution**: Natural language ESP development with professional-grade chip control, automated workflows, and seamless CI/CD integration.
```
User: "Flash my ESP32 with custom partitions for OTA updates"
Claude: → Detects ESP32 variant automatically
→ Creates optimal partition table
→ Configures OTA bootloader
→ Flashes with production settings
→ Validates flash integrity
✓ "ESP32 ready for OTA updates, partition table optimized"
```
## 🏗️ Architecture Design
### Core Components Structure
```python
# FastMCP server structure following Arduino MCP server patterns
src/
├── mcp_esptool_server/
│ ├── __init__.py
│ ├── server.py # Main FastMCP server
│ ├── config.py # Configuration management
│ └── components/
│ ├── __init__.py
│ ├── chip_control.py # ESP chip detection & control
│ ├── flash_manager.py # Flash operations & optimization
│ ├── partition_manager.py # Partition table management
│ ├── security_manager.py # eFuse & security operations
│ ├── firmware_builder.py # ELF→binary conversion
│ ├── ota_manager.py # OTA update workflows
│ ├── production_tools.py # Factory programming
│ └── diagnostics.py # Debug & analysis tools
```
### Component Architecture Patterns
Each component follows FastMCP best practices:
```python
# Example: chip_control.py
from fastmcp import FastMCP
from esptool.cmds import detect_chip, run_stub, reset_chip
import asyncio
from pathlib import Path
class ChipControl:
def __init__(self, app: FastMCP):
self.app = app
self.register_tools()
self.register_resources()
def register_tools(self):
"""Register chip control tools"""
@self.app.tool("esp_detect_chip")
async def detect_esp_chip(port: str) -> dict:
"""Auto-detect ESP chip and return detailed information"""
# Implementation using esptool.cmds.detect_chip
@self.app.tool("esp_connect_optimized")
async def connect_with_optimization(port: str, enable_stub: bool = True) -> str:
"""Connect to ESP with performance optimizations"""
# Multi-strategy connection with fallbacks
```
## 🔧 Tool Categories & API Design
### 1. **Chip Control & Detection (8 tools)**
```python
# Chip identification and connection management
esp_detect_chip(port: str) -> dict
esp_connect_advanced(port: str, baud: int = 460800, retries: int = 7) -> str
esp_reset_chip(port: str, reset_mode: str = "hard-reset") -> str
esp_load_ram(port: str, firmware: str) -> str # Development testing
esp_run_firmware(port: str) -> str
esp_get_chip_info(port: str) -> dict
esp_test_connection(port: str) -> dict
esp_recover_chip(port: str) -> str # Brick recovery
```
### 2. **Flash Memory Operations (12 tools)**
```python
# Advanced flash management beyond Arduino CLI
esp_flash_firmware(port: str, files: list[dict]) -> str
esp_flash_read(port: str, address: int, size: int, output_file: str) -> str
esp_flash_erase(port: str, region: str = "all") -> str
esp_flash_verify(port: str, files: list[dict]) -> dict
esp_flash_analyze(port: str) -> dict # Usage analysis
esp_flash_optimize(port: str) -> dict # Performance tuning
esp_flash_backup(port: str, output_dir: str) -> str
esp_flash_restore(port: str, backup_dir: str) -> str
esp_flash_encrypt(port: str, key_file: str) -> str
esp_flash_status(port: str) -> dict
esp_flash_sfdp_read(port: str) -> dict # Flash chip details
esp_flash_size_detect(port: str) -> dict
```
### 3. **Partition Management (6 tools)**
```python
# Custom partition table creation and management
esp_partition_create(layout: dict, output_file: str) -> str
esp_partition_flash(port: str, partition_file: str) -> str
esp_partition_read(port: str) -> dict
esp_partition_analyze(port: str) -> dict
esp_ota_partition_setup(port: str, app_size: str) -> str
esp_nvs_partition_create(data: dict, output_file: str) -> str
```
### 4. **Security & eFuse Operations (8 tools)**
```python
# Production security configuration
esp_efuse_read(port: str, efuse_name: str = None) -> dict
esp_efuse_burn(port: str, efuse_name: str, value: str) -> str
esp_security_info(port: str) -> dict
esp_encryption_enable(port: str, key_file: str) -> str
esp_secure_boot_enable(port: str, key_file: str) -> str
esp_flash_encryption_configure(port: str) -> str
esp_mac_address_read(port: str) -> str
esp_security_audit(port: str) -> dict
```
### 5. **Firmware Building & Analysis (7 tools)**
```python
# ELF processing and binary preparation
esp_elf_to_binary(elf_file: str, chip_type: str, output_file: str = None) -> str
esp_binary_merge(files: list[dict], output_file: str) -> str
esp_firmware_analyze(binary_file: str) -> dict
esp_bootloader_build(chip_type: str, config: dict) -> str
esp_app_prepare(elf_file: str, chip_type: str) -> dict
esp_binary_optimize(binary_file: str) -> str
esp_size_analysis(elf_file: str) -> dict
```
### 6. **OTA & Production Workflows (10 tools)**
```python
# Over-the-air updates and factory programming
esp_ota_package_create(firmware_file: str, version: str) -> str
esp_ota_flash_prepare(port: str) -> str
esp_factory_program(port: str, config: dict) -> str
esp_production_test(port: str, test_suite: str) -> dict
esp_batch_program(ports: list[str], firmware: str) -> dict
esp_quality_control(port: str) -> dict
esp_calibration_data_write(port: str, cal_data: dict) -> str
esp_manufacturing_data_write(port: str, mfg_data: dict) -> str
esp_provision_device(port: str, config: dict) -> str
esp_factory_reset(port: str) -> str
```
### 7. **Diagnostics & Debug (8 tools)**
```python
# Advanced debugging and analysis
esp_memory_dump(port: str, address: int, size: int) -> str
esp_register_dump(port: str) -> dict
esp_core_dump_analyze(dump_file: str) -> dict
esp_performance_profile(port: str, duration: int) -> dict
esp_power_analysis(port: str) -> dict
esp_rf_calibration_check(port: str) -> dict
esp_thermal_status(port: str) -> dict
esp_diagnostic_report(port: str) -> dict
```
## 📋 MCP Resources (6 resources)
Real-time information resources for Claude to understand ESP development environment:
```python
# Resource URLs following MCP patterns
esp://chips # Connected ESP devices with capabilities
esp://flash/status # Flash memory status and layout
esp://security/info # Security configuration and eFuse status
esp://partitions # Partition table information
esp://firmware/info # Loaded firmware details
esp://diagnostics # Real-time diagnostic information
```
## 🎨 Natural Language Workflows
### Development Workflows
```
User: "I need to test my new ESP32 firmware without wearing out the flash"
Claude: → esp_detect_chip to identify ESP32 variant
→ esp_load_ram with firmware binary
→ esp_run_firmware to execute from RAM
✓ "Firmware running from RAM, flash cycles preserved"
User: "Create an OTA-ready ESP32 with 2MB app partitions"
Claude: → esp_partition_create with OTA layout (2x2MB app + OTA data)
→ esp_bootloader_build with OTA support
→ esp_flash_firmware with bootloader + partition table
✓ "ESP32 configured for OTA updates, ready for deployment"
```
### Production Workflows
```
User: "Program 50 ESP32 devices for production with unique MAC addresses"
Claude: → esp_detect_chip for each device
→ esp_factory_program with base firmware
→ esp_manufacturing_data_write with unique identifiers
→ esp_quality_control validation
→ esp_production_test suite execution
✓ "Batch programming completed, all devices validated"
```
### Security Workflows
```
User: "Enable flash encryption and secure boot on this ESP32"
Claude: → esp_security_audit to check current state
→ esp_secure_boot_enable with key generation
→ esp_flash_encryption_configure
→ esp_efuse_burn for permanent security settings
✓ "ESP32 secured with flash encryption and secure boot"
```
## 🔗 Integration with Existing Arduino MCP Server
### Complementary Capabilities
```python
# Integration patterns
class ESPToolIntegration:
"""Bridge between Arduino MCP and ESPTool MCP servers"""
async def arduino_to_esptool_workflow(self, sketch_path: str, board_type: str):
"""Convert Arduino sketch to ESP-optimized firmware"""
# 1. Use Arduino MCP to compile sketch
# 2. Use ESPTool MCP to optimize binary
# 3. Use ESPTool MCP for advanced flashing
async def unified_esp_development(self, project_config: dict):
"""Complete ESP development pipeline"""
# 1. Arduino MCP: Sketch development and compilation
# 2. ESPTool MCP: Binary optimization and security
# 3. ESPTool MCP: Production flashing and validation
```
### Shared Resources
```python
# Shared configuration between servers
class UnifiedESPConfig:
arduino_cli_path: str
esptool_path: str
sketch_directories: list[Path]
esp_tools_directory: Path
shared_project_roots: list[Path]
```
## 🚀 Implementation Roadmap
### Phase 1: Core Infrastructure (Week 1-2)
- [ ] FastMCP server setup with modern uv/pyproject.toml
- [ ] Basic chip control component (detect, connect, reset)
- [ ] Flash operations component (read, write, erase)
- [ ] Configuration management and MCP roots integration
### Phase 2: Advanced Features (Week 3-4)
- [ ] Partition management tools
- [ ] Security and eFuse operations
- [ ] Firmware building and analysis
- [ ] Custom logger integration
### Phase 3: Production Features (Week 5-6)
- [ ] OTA workflow automation
- [ ] Factory programming tools
- [ ] Batch operations and CI/CD integration
- [ ] Comprehensive diagnostics
### Phase 4: Integration & Polish (Week 7-8)
- [ ] Arduino MCP server integration
- [ ] Documentation and examples
- [ ] Performance optimization
- [ ] Production deployment tooling
## 📚 Configuration Example
```toml
# pyproject.toml
[project]
name = "mcp-esptool-server"
version = "2025.09.27.1"
description = "FastMCP server for ESP32/ESP8266 development with esptool integration"
requires-python = ">=3.10"
dependencies = [
"fastmcp>=2.12.4",
"esptool>=5.0.0",
"pyserial>=3.5",
"pyserial-asyncio>=0.6",
"thefuzz[speedup]>=0.22.1"
]
[project.scripts]
mcp-esptool = "mcp_esptool_server.server:main"
# Docker compose integration
services:
mcp-esptool:
build: .
environment:
- ESPTOOL_PATH=/usr/local/bin/esptool
- ESP_DEVICE_TIMEOUT=30
- MCP_ESP_PROJECT_DIR=/workspace/esp_projects
volumes:
- esp-firmware:/workspace/firmware
- esp-tools:/workspace/tools
labels:
caddy: esp-tools.local
caddy.reverse_proxy: "{{upstreams 8080}}"
```
This FastMCP esptool server will provide enterprise-grade ESP development capabilities through natural language interaction, complementing the existing Arduino ecosystem while enabling advanced production workflows.

View File

@ -0,0 +1,553 @@
# 🚀 ESP-IDF Middleware Integration with Host Applications
## Overview
ESP-IDF middleware integration creates a comprehensive development environment that combines the power of ESP-IDF's professional framework with MCP's AI-powered workflows. The inclusion of ESP-IDF Host Applications enables rapid prototyping and debugging without physical hardware.
## 🎯 ESP-IDF Integration Architecture
### Core `idf.py` Middleware
```python
# middleware/idf_middleware.py
from .logger_interceptor import LoggerInterceptor
from typing import Dict, List, Optional, Any
import re
import asyncio
class IDFMiddleware(LoggerInterceptor):
"""ESP-IDF development framework middleware integration"""
def __init__(self, context: Context, operation_id: str):
super().__init__(context, operation_id)
self.project_path = None
self.target_chip = None
self.build_config = {}
self.host_mode = False
def get_logging_interface(self) -> Dict[str, Callable]:
"""Map ESP-IDF logging to MCP"""
return {
'info': self._handle_info,
'warning': self._handle_warning,
'error': self._handle_error,
'debug': self._handle_debug,
'verbose': self._handle_verbose,
'success': self._handle_success
}
def get_interaction_points(self) -> List[str]:
"""Operations requiring user interaction"""
return [
'menuconfig', # Interactive configuration
'set-target', # Target chip selection
'erase-flash', # Destructive flash operations
'erase-otadata', # OTA data erase
'fullclean', # Complete project clean
'monitor', # Serial monitoring
'gdb', # GDB debugging session
'openocd', # OpenOCD debugging
'partition-table', # Partition operations
'efuse-burn' # eFuse programming
]
def get_progress_interface(self) -> Optional[Callable]:
"""Progress tracking for build operations"""
return self._parse_idf_progress
async def _parse_idf_progress(self, output_line: str) -> Optional[Dict]:
"""Parse ESP-IDF build output for progress information"""
# Build progress patterns
patterns = {
'ninja_progress': r'\[(\d+)/(\d+)\]\s+(.+)',
'cmake_progress': r'(\d+)%\]\s+(.+)',
'component_build': r'Building (\w+)',
'linking': r'Linking (.+)',
'generating': r'Generating (.+)'
}
for pattern_name, pattern in patterns.items():
match = re.search(pattern, output_line)
if match:
return await self._format_progress_update(pattern_name, match, output_line)
return None
async def _format_progress_update(self, pattern_type: str, match: re.Match, line: str) -> Dict:
"""Format progress update based on pattern type"""
if pattern_type == 'ninja_progress':
current, total, task = match.groups()
return {
'progress': (int(current) / int(total)) * 100,
'current': int(current),
'total': int(total),
'message': f"Building: {task}",
'stage': 'build'
}
elif pattern_type == 'cmake_progress':
percentage, task = match.groups()
return {
'progress': float(percentage),
'message': f"Configuring: {task}",
'stage': 'configure'
}
return {'message': line.strip(), 'stage': 'unknown'}
async def _handle_menuconfig_interaction(self, stage_message: str) -> bool:
"""Handle menuconfig interactive sessions"""
if not self.capabilities['elicitation']:
return True
# Determine what configuration might be needed
config_suggestions = await self._analyze_project_config()
if config_suggestions:
response = await self.context.request_user_input(
prompt="🔧 Configuration options detected. Would you like me to suggest optimal settings?",
input_type="confirmation",
additional_data={
"suggestions": config_suggestions,
"action": "configure_project"
}
)
if response.get('confirmed', False):
await self._apply_suggested_config(config_suggestions)
return True
async def _analyze_project_config(self) -> List[Dict]:
"""Analyze project and suggest configuration options"""
suggestions = []
# Analyze based on detected components/libraries
if self._project_uses_wifi():
suggestions.append({
"category": "WiFi Configuration",
"options": [
"Enable WiFi power saving",
"Set optimal WiFi buffer sizes",
"Configure WiFi security settings"
]
})
if self._project_uses_bluetooth():
suggestions.append({
"category": "Bluetooth Configuration",
"options": [
"Enable BLE optimization",
"Configure BT/WiFi coexistence",
"Set BLE advertising parameters"
]
})
if self.host_mode:
suggestions.append({
"category": "Host Application Mode",
"options": [
"Enable Linux simulator",
"Configure host-specific logging",
"Enable Valgrind compatibility"
]
})
return suggestions
def _project_uses_wifi(self) -> bool:
"""Check if project uses WiFi components"""
# Implementation to scan project files/components
return True # Placeholder
def _project_uses_bluetooth(self) -> bool:
"""Check if project uses Bluetooth components"""
# Implementation to scan project files/components
return False # Placeholder
```
## 🔬 Host Applications Integration
### Rapid Prototyping Workflow
```python
# components/idf_host_applications.py
class IDFHostApplications:
"""ESP-IDF Host Applications for rapid prototyping"""
def __init__(self, app: FastMCP, config):
self.app = app
self.config = config
self.register_tools()
def register_tools(self):
"""Register host application tools"""
@self.app.tool("idf_create_host_project")
async def create_host_project(
context: Context,
project_name: str,
template: str = "basic",
enable_mocking: bool = True
) -> str:
"""
Create ESP-IDF project optimized for host development
Args:
project_name: Name of the project
template: Project template (basic, wifi, bluetooth, etc.)
enable_mocking: Enable CMock for hardware mocking
Returns:
Project creation status and next steps
"""
with IDFMiddleware(context, f"create_host_{project_name}") as middleware:
try:
# Create project structure
await self._create_project_structure(project_name, template)
# Configure for host target
await self._configure_host_target(project_name)
# Set up mocking if requested
if enable_mocking:
await self._setup_cmock_environment(project_name)
# Initialize development environment
await self._initialize_dev_environment(project_name)
return f"""✅ Host project '{project_name}' created successfully!
🔄 Next steps:
1. `cd {project_name}`
2. Configure: `idf.py menuconfig`
3. Build: `idf.py build`
4. Run: `idf.py monitor`
🏃‍♂️ Ready for rapid prototyping without hardware!"""
except Exception as e:
return f"❌ Failed to create host project: {e}"
@self.app.tool("idf_host_build_and_run")
async def host_build_and_run(
context: Context,
project_path: str = ".",
debug_mode: bool = False,
valgrind_check: bool = False
) -> str:
"""
Build and run ESP-IDF application on host
Args:
project_path: Path to ESP-IDF project
debug_mode: Enable debug symbols and logging
valgrind_check: Run with Valgrind memory checking
Returns:
Build and execution results
"""
with IDFMiddleware(context, "host_build_run") as middleware:
try:
# Ensure target is set to linux
await self._ensure_linux_target(project_path)
# Build with host-specific optimizations
build_result = await self._build_for_host(
project_path, debug_mode
)
if not build_result['success']:
return f"❌ Build failed: {build_result['error']}"
# Run application
if valgrind_check:
return await self._run_with_valgrind(project_path)
else:
return await self._run_host_application(project_path, debug_mode)
except Exception as e:
return f"❌ Host execution failed: {e}"
@self.app.tool("idf_host_debug_interactive")
async def host_debug_interactive(
context: Context,
project_path: str = ".",
debugger: str = "gdb"
) -> str:
"""
Start interactive debugging session for host application
Args:
project_path: Path to ESP-IDF project
debugger: Debugger to use (gdb, lldb, valgrind)
Returns:
Debug session status and instructions
"""
with IDFMiddleware(context, "host_debug") as middleware:
try:
# Build with debug symbols
await self._build_debug_version(project_path)
# Start debugging session
if debugger == "gdb":
return await self._start_gdb_session(project_path)
elif debugger == "valgrind":
return await self._start_valgrind_session(project_path)
elif debugger == "lldb":
return await self._start_lldb_session(project_path)
else:
return f"❌ Unsupported debugger: {debugger}"
except Exception as e:
return f"❌ Debug session failed: {e}"
@self.app.tool("idf_host_test_automation")
async def host_test_automation(
context: Context,
project_path: str = ".",
test_framework: str = "unity",
coverage: bool = True
) -> str:
"""
Run automated tests on host application
Args:
project_path: Path to ESP-IDF project
test_framework: Testing framework (unity, googletest)
coverage: Generate code coverage report
Returns:
Test results and coverage information
"""
with IDFMiddleware(context, "host_testing") as middleware:
try:
# Configure test environment
await self._configure_test_environment(project_path, test_framework)
# Run tests
test_results = await self._run_host_tests(project_path)
# Generate coverage if requested
if coverage:
coverage_results = await self._generate_coverage_report(project_path)
test_results['coverage'] = coverage_results
return await self._format_test_results(test_results)
except Exception as e:
return f"❌ Testing failed: {e}"
async def _ensure_linux_target(self, project_path: str) -> None:
"""Ensure project is configured for Linux target"""
cmd = ["idf.py", "--preview", "set-target", "linux"]
# Execute command with middleware integration
pass
async def _build_for_host(self, project_path: str, debug: bool) -> Dict:
"""Build project for host execution"""
cmd = ["idf.py", "build"]
if debug:
cmd.extend(["-DCMAKE_BUILD_TYPE=Debug"])
# Execute with progress tracking
return await self._execute_with_progress(cmd, project_path)
async def _run_with_valgrind(self, project_path: str) -> str:
"""Run application with Valgrind memory checking"""
binary_path = f"{project_path}/build/main"
cmd = [
"valgrind",
"--leak-check=full",
"--show-leak-kinds=all",
"--track-origins=yes",
"--verbose",
binary_path
]
result = await self._execute_command(cmd)
return self._format_valgrind_output(result)
async def _start_gdb_session(self, project_path: str) -> str:
"""Start GDB debugging session"""
binary_path = f"{project_path}/build/main"
# Create GDB command file for better UX
gdb_commands = [
"set confirm off",
"set height 0",
"file " + binary_path,
"break main",
"run",
"backtrace"
]
gdb_script = "\n".join(gdb_commands)
return f"""🐛 GDB Debug Session Ready
Binary: {binary_path}
🔧 Quick Commands:
- `break function_name` - Set breakpoint
- `run` - Start execution
- `continue` - Continue execution
- `step` - Step into function
- `next` - Step over function
- `print variable` - Print variable value
- `backtrace` - Show call stack
Starting GDB with automatic setup..."""
```
## 🎨 Natural Language Workflows
### Rapid Prototyping Workflow
```
User: "I want to quickly prototype an ESP32 WiFi scanner without hardware"
Claude: I'll set up a host-based ESP-IDF project for rapid WiFi scanner prototyping.
🔄 Creating host application project...
→ idf_create_host_project("wifi_scanner", template="wifi", enable_mocking=True)
✓ Project structure created with WiFi template
🔧 Configuring for host development...
→ idf.py --preview set-target linux
→ idf.py menuconfig (auto-configured for WiFi scanning)
✓ Host target configured with WiFi simulation
🏗️ Building host application...
→ idf.py build
Progress: ████████████████████████████████ 100%
✓ Built successfully with host-specific optimizations
🚀 Running prototype...
→ ./build/main
[INFO] WiFi Scanner Prototype Starting...
[INFO] Simulated WiFi networks detected:
- "TestNetwork_1" (RSSI: -45 dBm)
- "TestNetwork_2" (RSSI: -62 dBm)
✓ Prototype running successfully!
💡 Next steps:
- Modify wifi_scan.c to test different scenarios
- Use `idf_host_debug_interactive` for debugging
- Run `idf_host_test_automation` for automated testing
- Deploy to real hardware with `esp_flash_firmware`
```
### Development → Production Pipeline
```
User: "I've finished prototyping on host, now deploy to real ESP32"
Claude: I'll transition your host prototype to real hardware deployment.
🔍 Analyzing host application...
→ Project uses WiFi scanning and JSON output
→ Detected optimizations for host environment
→ Found test mocks that need hardware equivalents
🔄 Preparing for hardware deployment...
→ idf.py set-target esp32
✓ Target changed from linux to ESP32
🔧 Hardware-specific configuration...
→ Disabling host-only features
→ Enabling ESP32 WiFi drivers
→ Configuring partition table for OTA
✓ Hardware configuration optimized
🏗️ Building for ESP32...
→ idf.py build
Progress: ████████████████████████████████ 100%
✓ ESP32 firmware built successfully
📡 Deploying to hardware...
→ esp_detect_chip("/dev/ttyUSB0")
✓ ESP32 detected and ready
→ esp_flash_firmware(firmware="build/wifi_scanner.bin")
Progress: ████████████████████████████████ 100%
✓ Firmware flashed successfully
🔍 Monitoring real hardware...
→ idf.py monitor
[INFO] WiFi Scanner starting on ESP32...
[INFO] Real networks detected:
- "MyHomeWiFi" (RSSI: -35 dBm)
- "NeighborNetwork" (RSSI: -78 dBm)
✅ Successfully transitioned from host prototype to hardware!
```
### Advanced Debugging Workflow
```
User: "Debug memory issues in my ESP32 application using host tools"
Claude: I'll help debug memory issues using ESP-IDF host applications with advanced tooling.
🔧 Setting up debug environment...
→ idf_host_build_and_run(debug_mode=True, valgrind_check=True)
🔍 Building with debug symbols...
→ idf.py build -DCMAKE_BUILD_TYPE=Debug
✓ Debug build completed
🧪 Running Valgrind analysis...
→ valgrind --leak-check=full --show-leak-kinds=all ./build/main
⚠️ Memory issues detected:
==12345== 48 bytes in 1 blocks are definitely lost
==12345== at malloc (vg_replace_malloc.c:307)
==12345== at wifi_scanner_init (wifi_scanner.c:45)
🐛 Starting interactive GDB session...
→ idf_host_debug_interactive(debugger="gdb")
(gdb) break wifi_scanner_init
(gdb) run
(gdb) print scan_buffer
$1 = (void *) 0x0
💡 Issue found: Uninitialized buffer in wifi_scanner_init()
🔧 Suggested fix: Add proper buffer allocation check
Would you like me to suggest code improvements?
```
## 🚀 Advanced Integration Benefits
### Development Acceleration
1. **No Hardware Required**: Develop and test without physical ESP32
2. **Faster Iteration**: Host execution is much faster than chip debugging
3. **Rich Tooling**: Access to Valgrind, GDB, profilers
4. **Automated Testing**: Easy CI/CD integration with host tests
5. **Memory Analysis**: Detect leaks and issues before hardware deployment
### Seamless Hardware Transition
1. **Same Codebase**: Code runs on both host and hardware
2. **Configuration Management**: Easy target switching
3. **Progressive Testing**: Host → Hardware validation pipeline
4. **Production Deployment**: Smooth transition to final hardware
`★ Insight ─────────────────────────────────────`
**Revolutionary Development Model**: ESP-IDF Host Applications combined with MCP middleware creates a revolutionary development model where AI can rapidly prototype, test, and deploy ESP32 applications without physical hardware limitations.
**Debugging Superpower**: The ability to use host-based debugging tools (Valgrind, GDB) while maintaining ESP-IDF compatibility provides unprecedented debugging capabilities for embedded development.
**Continuous Integration Paradise**: Host applications enable true CI/CD for embedded projects - automated testing, memory analysis, and performance profiling without hardware in the loop.
`─────────────────────────────────────────────────`
This integration transforms ESP32 development from a hardware-dependent process into a modern, AI-assisted workflow that rivals traditional software development in speed and tooling while maintaining embedded-specific capabilities.
<function_calls>
<invoke name="TodoWrite">
<parameter name="todos">[{"content": "Document middleware architecture pattern", "status": "completed", "activeForm": "Documenting middleware architecture pattern"}, {"content": "Create reusable middleware design patterns", "status": "completed", "activeForm": "Creating reusable middleware design patterns"}, {"content": "Document broader applications beyond esptool", "status": "completed", "activeForm": "Documenting broader applications beyond esptool"}, {"content": "Design idf.py middleware integration", "status": "completed", "activeForm": "Designing idf.py middleware integration"}]

163
IMPLEMENTATION_COMPLETE.md Normal file
View File

@ -0,0 +1,163 @@
# MCP ESPTool Server - Implementation Complete
## Overview
Successfully implemented a comprehensive FastMCP server for ESP32/ESP8266 development with AI-powered natural language interfaces. The server provides production-grade ESP development workflows through Model Context Protocol integration.
## Key Features Implemented
### ✅ Core Architecture
- **FastMCP Integration**: Full MCP 2.12.4 server implementation
- **Component-Based Design**: 8 specialized components for different ESP workflows
- **Universal Middleware**: Bidirectional CLI tool integration pattern
- **Production Ready**: Docker support, configuration management, health checks
### ✅ Middleware System
- **LoggerInterceptor Base Class**: Abstract foundation for CLI tool integration
- **ESPToolMiddleware**: Specialized esptool integration with progress tracking
- **MiddlewareFactory**: Dynamic middleware creation and management
- **Bidirectional Communication**: MCP context integration with user interaction
### ✅ ESP Development Components
1. **ChipControl**: Device detection, connection, reset operations
2. **FlashManager**: Flash operations with verification and backup
3. **PartitionManager**: Partition table management and OTA support
4. **SecurityManager**: Security features and eFuse management
5. **FirmwareBuilder**: ESP-IDF integration and binary operations
6. **OTAManager**: Over-the-air update workflows
7. **ProductionTools**: Factory programming and quality control
8. **Diagnostics**: Memory dumps and performance profiling
### ✅ Production Features
- **Environment Configuration**: Comprehensive environment variable support
- **MCP Capability Detection**: Automatic detection of client features
- **Progress Tracking**: Real-time operation progress with history
- **User Interaction**: Confirmation prompts for critical operations
- **Error Handling**: Graceful error handling and recovery
- **Health Monitoring**: Component and system health checks
### ✅ Development Environment
- **Modern Python Tooling**: uv, pyproject.toml, ruff, mypy, pytest
- **Docker Support**: Multi-stage builds for development and production
- **Testing Framework**: Comprehensive test suite with mocking
- **CI/CD Ready**: Makefile for common operations
- **Documentation**: Complete README and inline documentation
## Technical Specifications
### Dependencies
- **FastMCP**: 2.12.4+ for MCP server functionality
- **ESPTool**: 5.0.0+ for ESP device programming
- **Rich**: Enhanced CLI output and logging
- **Click**: Command-line interface framework
- **PySerial**: Serial communication support
### Architecture Patterns
- **Middleware Pattern**: Universal CLI tool integration
- **Component Registry**: Dynamic component loading
- **Factory Pattern**: Middleware creation and management
- **Context Manager**: Resource lifecycle management
- **Async/Await**: Full asynchronous operation support
### Configuration Management
- **Environment Variables**: Comprehensive configuration via env vars
- **Auto-Detection**: ESP-IDF and device port auto-detection
- **MCP Integration**: Dynamic project root discovery
- **Validation**: Configuration validation with helpful error messages
## Usage Examples
### Basic Server Start
```bash
# Install with uvx
uvx mcp-esptool-server
# Add to Claude Code
claude mcp add mcp-esptool-server "uvx mcp-esptool-server"
```
### Development
```bash
# Setup development environment
make dev
# Run tests
make test
# Run development server
make run-debug
# Docker development
make docker-up
```
### Production Deployment
```bash
# Production mode
PRODUCTION_MODE=true mcp-esptool-server
# Docker production
DOCKER_TARGET=production make docker-up
```
## Tools Available
### Chip Control
- `esp_detect_chip`: Advanced chip detection with detailed information
- `esp_connect_advanced`: Robust connection with retry logic
- `esp_reset_chip`: Multiple reset types (hard, soft, bootloader)
- `esp_scan_ports`: Comprehensive port scanning
- `esp_load_test_firmware`: Test firmware loading
### Server Management
- `esp_server_info`: Comprehensive server information
- `esp_list_tools`: Tool discovery and categorization
- `esp_health_check`: Environment health monitoring
### Component Tools
- Flash operations: firmware, read, erase, backup
- Partition management: OTA, custom, analyze
- Security features: audit, encryption, eFuse
- Firmware building: ELF conversion, analysis, optimization
- OTA management: package creation, deployment, rollback
- Production tools: factory programming, batch operations, QC
- Diagnostics: memory dumps, profiling, reports
## Testing Status
### ✅ All Tests Passing
- Configuration management: 4/5 tests passing (1 skipped - esptool not available)
- Middleware system: 8/8 tests passing
- Environment variable handling: Working correctly
- Import system: All imports successful
- CLI interface: Working correctly
### ✅ Code Quality
- Linting: Ruff formatting applied
- Type checking: Modern typing with Python 3.11+ syntax
- Documentation: Comprehensive docstrings
- Error handling: Robust exception handling
## Next Steps
### Immediate Priorities
1. **Full Component Implementation**: Complete all component placeholder methods
2. **ESP-IDF Integration**: Add complete ESP-IDF host application support
3. **Real Hardware Testing**: Test with actual ESP devices
4. **Documentation**: Add comprehensive API documentation
### Future Enhancements
1. **Web Interface**: Optional HTTP interface for browser-based interaction
2. **Plugin System**: Support for custom component plugins
3. **Advanced Monitoring**: Prometheus metrics and monitoring
4. **Cloud Integration**: Cloud-based ESP development workflows
## Implementation Insights
`★ Insight ─────────────────────────────────────`
**Universal Middleware Pattern**: Created a reusable pattern for integrating any CLI tool with MCP, not just esptool
**Bidirectional Communication**: Implemented full MCP context integration allowing real-time user interaction during operations
**Production Architecture**: Component-based design allows for easy extension and testing of individual ESP workflows
`─────────────────────────────────────────────────`
This implementation provides a solid foundation for AI-powered ESP development workflows with natural language interfaces through Claude Code's MCP integration.

1013
IMPLEMENTATION_EXAMPLES.md Normal file

File diff suppressed because it is too large Load Diff

391
IMPLEMENTATION_ROADMAP.md Normal file
View File

@ -0,0 +1,391 @@
# 🚀 FastMCP ESPTool Server Implementation Roadmap
## Project Overview
**Goal**: Build a production-ready FastMCP server that provides AI-powered ESP32/ESP8266 development capabilities through esptool's Python API.
**Timeline**: 8 weeks (MVP in 4 weeks, Production-ready in 8 weeks)
**Success Metrics**:
- 60+ ESP-specific MCP tools implemented
- Natural language ESP development workflows
- Integration with existing Arduino MCP server
- Production deployment capabilities
- Comprehensive test coverage (>85%)
## 📋 Implementation Phases
### Phase 1: Foundation & Core Infrastructure (Weeks 1-2)
#### Week 1: Project Setup & Basic Structure
**Goals**: Establish modern Python project foundation and basic FastMCP server
**Tasks**:
```bash
# Day 1-2: Project initialization
- [ ] Create project structure with uv and pyproject.toml
- [ ] Set up Docker development environment with Caddy
- [ ] Configure Git repository with .gitignore
- [ ] Initialize FastMCP server skeleton
- [ ] Set up basic logging and configuration
# Day 3-4: Core components foundation
- [ ] Implement ESPToolServerConfig class
- [ ] Create component base classes
- [ ] Set up MCP roots integration
- [ ] Add environment variable management
- [ ] Basic error handling framework
# Day 5-7: First working tools
- [ ] ChipControl component with esp_detect_chip
- [ ] Basic FastMCP server running
- [ ] First integration test
- [ ] Development documentation
```
**Deliverables**:
- Working FastMCP server with 1-2 basic tools
- Docker development environment
- Project documentation started
#### Week 2: Core Chip Operations
**Goals**: Implement essential chip control and basic flash operations
**Tasks**:
```bash
# ChipControl component (8 tools)
- [ ] esp_detect_chip - Auto-detect ESP variants
- [ ] esp_connect_advanced - Multi-strategy connection
- [ ] esp_reset_chip - Various reset modes
- [ ] esp_load_test_firmware - RAM execution for testing
- [ ] esp_chip_info - Detailed chip information
- [ ] esp_test_connection - Connection validation
- [ ] esp_recover_chip - Brick recovery procedures
# FlashManager component (basic operations)
- [ ] esp_flash_firmware - Basic flashing capability
- [ ] esp_flash_read - Flash content reading
- [ ] esp_flash_erase - Selective erasing
- [ ] esp_flash_id - Flash chip identification
# Infrastructure
- [ ] Async operation support
- [ ] Error handling and recovery
- [ ] Resource cleanup patterns
- [ ] Basic MCP resources (esp://chips)
```
**Deliverables**:
- 12+ working tools for basic ESP operations
- Robust error handling
- First MCP resource implementation
### Phase 2: Advanced Features & Security (Weeks 3-4)
#### Week 3: Flash Management & Partitions
**Goals**: Complete flash operations and partition management
**Tasks**:
```bash
# Complete FlashManager (12 tools total)
- [ ] esp_flash_verify - Integrity verification
- [ ] esp_flash_analyze - Usage analysis and optimization
- [ ] esp_flash_backup - Complete flash backup
- [ ] esp_flash_restore - Flash restoration
- [ ] esp_flash_optimize - Performance tuning
- [ ] esp_flash_encrypt - Flash encryption setup
- [ ] esp_flash_status - Status monitoring
- [ ] esp_flash_sfdp_read - Advanced flash details
# PartitionManager component (6 tools)
- [ ] esp_partition_create_ota - OTA-optimized tables
- [ ] esp_partition_custom - Custom partition creation
- [ ] esp_partition_flash - Partition table flashing
- [ ] esp_partition_read - Current table reading
- [ ] esp_partition_analyze - Usage analysis
- [ ] esp_nvs_partition_create - NVS partition tools
# Enhanced resources
- [ ] esp://flash/{port} - Real-time flash status
- [ ] esp://partitions/{port} - Live partition info
```
**Deliverables**:
- Complete flash management capabilities
- Partition table creation and management
- Advanced MCP resources
#### Week 4: Security & eFuse Management
**Goals**: Implement production security features
**Tasks**:
```bash
# SecurityManager component (8 tools)
- [ ] esp_security_audit - Comprehensive security analysis
- [ ] esp_efuse_read - eFuse examination
- [ ] esp_efuse_burn - Production eFuse programming
- [ ] esp_encryption_enable - Flash encryption setup
- [ ] esp_secure_boot_enable - Secure boot configuration
- [ ] esp_security_configure - Unified security setup
- [ ] esp_mac_address_read - MAC address extraction
- [ ] esp_security_validate - Security validation
# Production security workflows
- [ ] Security template system
- [ ] Key management integration
- [ ] Security audit reporting
- [ ] Compliance validation tools
# Testing & validation
- [ ] Security test suite
- [ ] eFuse simulation for testing
- [ ] Security documentation
```
**Deliverables**:
- Production-ready security features
- eFuse management capabilities
- Security audit and compliance tools
### Phase 3: Production Features & Automation (Weeks 5-6)
#### Week 5: Firmware Building & OTA
**Goals**: Advanced firmware processing and OTA capabilities
**Tasks**:
```bash
# FirmwareBuilder component (7 tools)
- [ ] esp_elf_to_binary - ELF conversion with optimization
- [ ] esp_binary_merge - Multi-binary combination
- [ ] esp_firmware_analyze - Detailed analysis
- [ ] esp_bootloader_build - Custom bootloader creation
- [ ] esp_app_prepare - Application preparation
- [ ] esp_binary_optimize - Size and performance optimization
- [ ] esp_size_analysis - Memory usage analysis
# OTAManager component (10 tools)
- [ ] esp_ota_package_create - OTA package generation
- [ ] esp_ota_flash_prepare - OTA flash preparation
- [ ] esp_ota_partition_setup - OTA partition configuration
- [ ] esp_ota_validate - OTA package validation
- [ ] esp_ota_deploy - OTA deployment automation
- [ ] esp_ota_rollback - Rollback capabilities
- [ ] esp_ota_status - Update status monitoring
# CI/CD integration
- [ ] GitHub Actions workflows
- [ ] Docker production builds
- [ ] Automated testing pipeline
```
**Deliverables**:
- Complete firmware building pipeline
- OTA update system
- CI/CD integration
#### Week 6: Production Tools & Factory Programming
**Goals**: Enterprise-grade production tools
**Tasks**:
```bash
# ProductionTools component (10 tools)
- [ ] esp_factory_program - Automated factory programming
- [ ] esp_production_test - Comprehensive test suites
- [ ] esp_batch_program - Multi-device programming
- [ ] esp_quality_control - QC validation
- [ ] esp_calibration_data_write - Calibration data programming
- [ ] esp_manufacturing_data_write - Manufacturing data
- [ ] esp_provision_device - Device provisioning
- [ ] esp_factory_reset - Factory reset procedures
- [ ] esp_serial_number_program - Serial number assignment
- [ ] esp_final_test - Final validation
# Production infrastructure
- [ ] Batch operation queuing
- [ ] Production reporting
- [ ] Quality metrics tracking
- [ ] Failure analysis tools
# Documentation
- [ ] Production deployment guide
- [ ] Factory programming procedures
- [ ] Quality control documentation
```
**Deliverables**:
- Complete production toolchain
- Factory programming capabilities
- Quality control systems
### Phase 4: Integration, Polish & Deployment (Weeks 7-8)
#### Week 7: Arduino MCP Integration
**Goals**: Seamless integration with existing Arduino MCP server
**Tasks**:
```bash
# Integration framework
- [ ] Unified configuration system
- [ ] Cross-server communication
- [ ] Shared resource management
- [ ] Workflow coordination
# Arduino MCP enhancements
- [ ] ESP-specific Arduino tools
- [ ] Unified device detection
- [ ] Cross-server error handling
- [ ] Integrated troubleshooting
# Testing integration
- [ ] End-to-end workflow tests
- [ ] Cross-server compatibility tests
- [ ] Performance benchmarking
- [ ] Integration documentation
# Examples and templates
- [ ] Unified workflow examples
- [ ] Project templates
- [ ] Best practices guide
```
#### Week 8: Final Polish & Production Deployment
**Goals**: Production deployment and comprehensive documentation
**Tasks**:
```bash
# Production readiness
- [ ] Performance optimization
- [ ] Memory usage optimization
- [ ] Error handling refinement
- [ ] Security audit
# Documentation completion
- [ ] Complete API documentation
- [ ] User guide and tutorials
- [ ] Troubleshooting guide
- [ ] Migration guide from Arduino IDE
# Deployment
- [ ] Production Docker images
- [ ] Kubernetes deployment manifests
- [ ] Package distribution (PyPI)
- [ ] Release automation
# Community
- [ ] Example projects
- [ ] Video tutorials
- [ ] Community documentation
- [ ] Feedback collection system
```
## 🔧 Development Environment Setup
### Local Development
```bash
# Quick setup script
git clone <repository>
cd mcp-esptool-server
uv venv
uv pip install -e ".[dev]"
uv run mcp-esptool-server
# Docker development
docker compose up --build
# Access at https://esp-tools.local (via Caddy)
```
### Testing Strategy
```python
# Test structure
tests/
├── unit/ # Component unit tests
├── integration/ # Cross-component tests
├── end_to_end/ # Full workflow tests
├── performance/ # Performance benchmarks
├── security/ # Security validation
└── fixtures/ # Test data and mocks
```
### Continuous Integration
```yaml
# .github/workflows/ci.yml
name: CI/CD Pipeline
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
os: [ubuntu-latest, macos-latest, windows-latest]
security:
runs-on: ubuntu-latest
steps:
- name: Security scan
run: |
uv run bandit -r src/
uv run safety check
integration:
runs-on: ubuntu-latest
services:
arduino-mcp:
image: mcp-arduino:latest
steps:
- name: Cross-server integration tests
run: uv run pytest tests/integration/
```
## 📊 Success Metrics & Milestones
### Week 2 Milestone: Basic Operations
- ✅ 12+ working tools
- ✅ Basic ESP chip detection and flashing
- ✅ Docker development environment
### Week 4 Milestone: Advanced Features
- ✅ 25+ working tools
- ✅ Security and partition management
- ✅ MCP resources implementation
### Week 6 Milestone: Production Ready
- ✅ 45+ working tools
- ✅ OTA and production tooling
- ✅ CI/CD integration
### Week 8 Milestone: Complete System
- ✅ 60+ working tools
- ✅ Arduino MCP integration
- ✅ Production deployment ready
- ✅ Comprehensive documentation
## 🎯 Risk Mitigation
### Technical Risks
- **esptool API changes**: Pin specific esptool version, comprehensive testing
- **Hardware compatibility**: Extensive device testing, fallback strategies
- **Performance issues**: Early benchmarking, optimization focus
### Timeline Risks
- **Feature creep**: Strict scope management, MVP focus
- **Integration complexity**: Early integration testing, parallel development
- **Documentation lag**: Documentation-driven development
### Quality Risks
- **Insufficient testing**: TDD approach, automated testing
- **Security vulnerabilities**: Security-first design, regular audits
- **Poor UX**: User testing, feedback integration
This roadmap provides a clear path to building a production-ready FastMCP ESPTool server that seamlessly integrates with the existing Arduino ecosystem while providing advanced ESP development capabilities.

395
INTEGRATION_PATTERNS.md Normal file
View File

@ -0,0 +1,395 @@
# 🔗 Integration Patterns: Arduino MCP ↔ ESPTool MCP
## Overview
The ESPTool MCP server is designed to complement and enhance the existing Arduino MCP server, creating a unified ESP development ecosystem. Rather than replacing Arduino workflows, it provides specialized ESP capabilities that work seamlessly together.
## 🎯 Integration Philosophy
### Complementary Architecture
```
┌─────────────────────┐ ┌─────────────────────┐
│ Arduino MCP │ │ ESPTool MCP │
│ Server │ │ Server │
├─────────────────────┤ ├─────────────────────┤
│ • Sketch Management │ │ • Direct ESP Control│
│ • Library System │ │ • Advanced Flashing │
│ • Compilation │ │ • Security Features │
│ • Basic Upload │ │ • Production Tools │
│ • Serial Monitor │ │ • OTA Management │
│ • Cross-Platform │ │ • eFuse Programming │
└─────────────────────┘ └─────────────────────┘
│ │
└─────────┬─────────────────┘
┌─────────▼─────────┐
│ Unified ESP │
│ Development │
│ Workflow │
└───────────────────┘
```
## 🏗️ Integration Patterns
### 1. **Workflow Handoff Pattern**
The Arduino MCP server handles high-level development, while ESPTool MCP handles low-level chip operations:
```python
# Example: Advanced ESP32 Development Workflow
class UnifiedESPWorkflow:
"""Orchestrates between Arduino and ESPTool MCP servers"""
async def complete_esp_development_cycle(self, project_config: dict):
"""End-to-end ESP development using both servers"""
# Phase 1: Arduino MCP Server - High-level development
arduino_tasks = [
"arduino_create_sketch", # Create project structure
"arduino_install_library", # Install ESP32 libraries
"arduino_write_sketch", # Write application code
"arduino_compile_sketch" # Compile to binary
]
# Phase 2: ESPTool MCP Server - Low-level optimization
esptool_tasks = [
"esp_detect_chip", # Identify ESP variant
"esp_firmware_analyze", # Optimize binary
"esp_partition_create_ota", # Setup OTA partitions
"esp_flash_firmware", # Advanced flashing
"esp_security_audit" # Security validation
]
return await self._execute_unified_workflow(arduino_tasks, esptool_tasks)
```
### 2. **Shared Configuration Pattern**
Both servers share common configuration for seamless operation:
```python
# config/unified_esp_config.py
from pathlib import Path
from dataclasses import dataclass
from typing import List, Dict
@dataclass
class UnifiedESPConfig:
"""Shared configuration between Arduino and ESPTool MCP servers"""
# Shared paths
project_roots: List[Path]
sketch_directory: Path
libraries_directory: Path
tools_directory: Path
# Arduino MCP specific
arduino_cli_path: str
arduino_cores: List[str]
# ESPTool MCP specific
esptool_path: str
esp_idf_path: Path
partition_templates: Dict[str, Path]
# Shared ESP settings
default_baud_rate: int = 460800
connection_timeout: int = 30
enable_stub_flasher: bool = True
@classmethod
def from_environment(cls) -> 'UnifiedESPConfig':
"""Initialize from environment variables and MCP roots"""
return cls(
project_roots=cls._get_mcp_roots(),
sketch_directory=Path(os.getenv('MCP_SKETCH_DIR', '~/Arduino')).expanduser(),
arduino_cli_path=os.getenv('ARDUINO_CLI_PATH', 'arduino-cli'),
esptool_path=os.getenv('ESPTOOL_PATH', 'esptool'),
# ... other config
)
```
### 3. **Cross-Server Communication Pattern**
Enable servers to coordinate operations and share information:
```python
# coordination/server_bridge.py
class MCPServerBridge:
"""Facilitates communication between Arduino and ESPTool MCP servers"""
def __init__(self, arduino_server_url: str, esptool_server_url: str):
self.arduino_client = MCPClient(arduino_server_url)
self.esptool_client = MCPClient(esptool_server_url)
async def compile_and_flash_esp(self, sketch_path: str, port: str) -> dict:
"""Coordinate compilation via Arduino MCP and flashing via ESPTool MCP"""
# Step 1: Use Arduino MCP to compile
compile_result = await self.arduino_client.call_tool(
"arduino_compile_sketch",
{"sketch_path": sketch_path, "board": "esp32"}
)
if not compile_result["success"]:
return {"error": "Compilation failed", "details": compile_result}
# Step 2: Use ESPTool MCP for advanced flashing
flash_result = await self.esptool_client.call_tool(
"esp_flash_firmware",
{
"port": port,
"firmware_files": [{"address": 0x1000, "file": compile_result["binary_path"]}],
"verify": True,
"optimize": True
}
)
return {
"compile_result": compile_result,
"flash_result": flash_result,
"success": flash_result.get("success", False)
}
```
## 🎨 Natural Language Workflows
### Unified Development Commands
Claude can seamlessly use both servers to provide comprehensive ESP development:
```
User: "Create an ESP32 weather station with OTA updates"
Claude: I'll create a comprehensive ESP32 weather station with OTA capability using both Arduino and ESP-specific tools.
# Arduino MCP Server - Application Development
→ arduino_create_sketch("WeatherStation")
→ arduino_install_library("DHT sensor library")
→ arduino_install_library("WiFiManager")
→ arduino_write_sketch(weather_station_code)
→ arduino_compile_sketch()
# ESPTool MCP Server - Production Setup
→ esp_detect_chip(port="/dev/ttyUSB0")
→ esp_partition_create_ota(app_size="1MB")
→ esp_flash_firmware(with_ota_partitions=True)
→ esp_security_audit()
✓ Weather station deployed with OTA updates enabled
✓ Secure boot configured for production
✓ Ready for remote firmware updates
```
### Development vs Production Workflows
```
User: "I'm developing an ESP32 project - I want fast iteration"
Claude: I'll set up a development-optimized workflow for rapid testing.
# Development Mode - Fast Iteration
→ arduino_compile_sketch(board="esp32", debug=True)
→ esp_load_test_firmware(to_ram=True) # No flash wear
→ serial_monitor(detailed_logging=True)
# When ready for testing:
→ esp_flash_firmware(development_mode=True)
User: "Now I'm ready for production deployment"
Claude: I'll configure this for production with security and optimization.
# Production Mode - Security & Optimization
→ esp_firmware_analyze(optimize=True)
→ esp_enable_flash_encryption()
→ esp_enable_secure_boot()
→ esp_flash_firmware(production_mode=True)
→ esp_factory_test_suite()
✓ Production deployment complete with security enabled
```
## 🔄 Shared Resource Management
### Unified Device Detection
```python
# resources/unified_device_manager.py
class UnifiedDeviceManager:
"""Manages ESP devices across both MCP servers"""
async def get_all_esp_devices(self) -> List[Dict]:
"""Combine device information from both servers"""
# Get basic device info from Arduino MCP
arduino_devices = await self._get_arduino_devices()
# Get detailed ESP info from ESPTool MCP
esptool_devices = await self._get_esptool_devices()
# Merge information
unified_devices = []
for device in arduino_devices:
if device['board_type'].startswith('esp'):
esp_details = self._find_esptool_device(device['port'], esptool_devices)
unified_devices.append({
**device,
"esp_details": esp_details,
"capabilities": self._determine_capabilities(device, esp_details)
})
return unified_devices
def _determine_capabilities(self, arduino_info: dict, esp_info: dict) -> List[str]:
"""Determine what operations are available for this device"""
capabilities = ["compile", "upload", "monitor"] # Arduino basics
if esp_info:
capabilities.extend([
"direct_flash", "partition_management", "security_config",
"ota_support", "factory_programming"
])
return capabilities
```
### Resource Synchronization
```python
# Both servers expose unified resources
@arduino_mcp.resource("unified://esp_devices")
@esptool_mcp.resource("unified://esp_devices")
async def synchronized_esp_devices() -> str:
"""Synchronized device list across both servers"""
device_manager = UnifiedDeviceManager()
devices = await device_manager.get_all_esp_devices()
return json.dumps(devices, indent=2)
```
## 🚀 Advanced Integration Scenarios
### 1. **CI/CD Pipeline Integration**
```yaml
# .github/workflows/esp32-deploy.yml
name: ESP32 Production Deployment
on:
push:
tags: ['v*']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup ESP Development Environment
run: |
# Install both MCP servers
uvx mcp-arduino
uvx mcp-esptool-server
- name: Compile Firmware
run: |
claude mcp call arduino arduino_compile_sketch \
--sketch-path ./WeatherStation \
--board esp32 \
--optimize-size
- name: Security Configuration
run: |
claude mcp call esptool esp_firmware_analyze \
--input ./WeatherStation/build/WeatherStation.bin \
--security-check
- name: Factory Programming
run: |
claude mcp call esptool esp_factory_program \
--firmware ./WeatherStation/build/WeatherStation.bin \
--security-keys ./keys/ \
--partition-table ./partitions/production.csv
```
### 2. **Development Environment Setup**
```python
# setup/unified_development_environment.py
class UnifiedESPDevEnvironment:
"""Sets up complete ESP development environment"""
async def setup_project(self, project_name: str, project_type: str):
"""Initialize new ESP project with both Arduino and ESP tooling"""
setup_tasks = {
"arduino_tasks": [
("arduino_create_sketch", {"name": project_name}),
("arduino_install_core", {"core": "esp32"}),
("arduino_config_init", {})
],
"esptool_tasks": [
("esp_config_init", {}),
("esp_partition_create_ota", {"project": project_name}),
("esp_security_template_create", {"level": "development"})
]
}
# Execute tasks in parallel where possible
arduino_results = await self._execute_arduino_tasks(setup_tasks["arduino_tasks"])
esptool_results = await self._execute_esptool_tasks(setup_tasks["esptool_tasks"])
return {
"project_name": project_name,
"arduino_setup": arduino_results,
"esptool_setup": esptool_results,
"ready_for_development": all([arduino_results["success"], esptool_results["success"]])
}
```
### 3. **Error Recovery and Diagnostics**
```python
# diagnostics/unified_troubleshooting.py
class UnifiedTroubleshooting:
"""Cross-server diagnostic and recovery tools"""
async def diagnose_esp_issue(self, port: str, symptoms: List[str]) -> dict:
"""Use both servers to diagnose ESP development issues"""
diagnosis = {
"port": port,
"symptoms": symptoms,
"tests_performed": [],
"recommendations": []
}
# Arduino MCP diagnostics
arduino_tests = [
("serial_check_port", {"port": port}),
("arduino_list_boards", {}),
("serial_test_connection", {"port": port})
]
# ESPTool MCP diagnostics
esptool_tests = [
("esp_detect_chip", {"port": port}),
("esp_security_audit", {"port": port}),
("esp_flash_analyze", {"port": port})
]
# Run diagnostics and generate recommendations
arduino_results = await self._run_arduino_diagnostics(arduino_tests)
esptool_results = await self._run_esptool_diagnostics(esptool_tests)
diagnosis.update({
"arduino_diagnostics": arduino_results,
"esptool_diagnostics": esptool_results,
"recommendations": self._generate_recommendations(arduino_results, esptool_results)
})
return diagnosis
```
This integration design ensures that both MCP servers work together seamlessly, providing Claude with a comprehensive toolkit for ESP development that spans from high-level Arduino sketch development to low-level chip programming and production deployment.

444
MCP_LOGGER_INTEGRATION.md Normal file
View File

@ -0,0 +1,444 @@
# 🔗 ESPTool Custom Logger → MCP Integration
## Overview
Bridge esptool's logging system with FastMCP's advanced logging, progress tracking, and user elicitation capabilities. This creates a seamless user experience where ESP operations provide real-time feedback and can request user input when needed.
## 🎯 Logger Mapping Strategy
### ESPTool → MCP Method Mapping
```python
# Direct logging method mappings
ESPTOOL_TO_MCP_MAPPING = {
# Basic logging
"print": "log.info", # General information
"note": "log.notice", # Important notices
"warning": "log.warning", # Warning messages
"error": "log.error", # Error messages
# Advanced MCP features
"stage": "elicit_if_available", # User interaction/confirmation
"progress_bar": "progress", # Progress tracking
"set_verbosity": "configure_verbosity" # Dynamic verbosity control
}
```
## 🏗️ Custom Logger Implementation
### FastMCP-Integrated ESPTool Logger
```python
# components/mcp_esptool_logger.py
from esptool.logger import TemplateLogger
from fastmcp import Context
from typing import Optional, Any
import asyncio
import sys
class MCPESPToolLogger(TemplateLogger):
"""Custom esptool logger that integrates with FastMCP capabilities"""
def __init__(self, context: Context, operation_id: str):
"""
Initialize MCP-integrated logger
Args:
context: FastMCP context for logging and elicitation
operation_id: Unique identifier for this operation
"""
self.context = context
self.operation_id = operation_id
self.verbosity_level = 1
self.current_stage = None
self.progress_total = 0
self.progress_current = 0
# Check MCP client capabilities
self.supports_progress = self._check_progress_support()
self.supports_elicitation = self._check_elicitation_support()
def _check_progress_support(self) -> bool:
"""Check if client supports progress notifications"""
try:
return hasattr(self.context, 'progress') and callable(self.context.progress)
except:
return False
def _check_elicitation_support(self) -> bool:
"""Check if client supports user elicitation"""
try:
return hasattr(self.context, 'request_user_input') and callable(self.context.request_user_input)
except:
return False
def print(self, message: str = "", *args, **kwargs) -> None:
"""Map esptool print() to MCP info logging"""
if self.verbosity_level >= 1:
formatted_message = self._format_message(message, *args)
# Use MCP context for structured logging
asyncio.create_task(self._log_async("info", formatted_message))
def note(self, message: str) -> None:
"""Map esptool note() to MCP notice logging"""
formatted_message = f"📋 NOTE: {message}"
asyncio.create_task(self._log_async("notice", formatted_message))
def warning(self, message: str) -> None:
"""Map esptool warning() to MCP warning logging"""
formatted_message = f"⚠️ WARNING: {message}"
asyncio.create_task(self._log_async("warning", formatted_message))
def error(self, message: str) -> None:
"""Map esptool error() to MCP error logging"""
formatted_message = f"❌ ERROR: {message}"
asyncio.create_task(self._log_async("error", formatted_message))
def stage(self, message: str = "", finish: bool = False) -> None:
"""
Map esptool stage() to MCP elicitation for user interaction
This is where the magic happens - esptool "stages" become opportunities
for user interaction and confirmation
"""
if finish and self.current_stage:
# End current stage
asyncio.create_task(self._finish_stage())
self.current_stage = None
elif message and not finish:
# Start new stage - potentially with user interaction
self.current_stage = message
asyncio.create_task(self._start_stage(message))
def progress_bar(
self,
cur_iter: int,
total_iters: int,
prefix: str = "",
suffix: str = "",
bar_length: int = 30,
) -> None:
"""Map esptool progress_bar() to MCP progress notifications"""
self.progress_current = cur_iter
self.progress_total = total_iters
if self.supports_progress:
percentage = (cur_iter / total_iters) * 100 if total_iters > 0 else 0
# Send MCP progress notification
asyncio.create_task(self._update_progress(
operation_id=self.operation_id,
progress=percentage,
total=total_iters,
current=cur_iter,
message=f"{prefix} {suffix}".strip()
))
else:
# Fallback to text-based progress
percentage = f"{100 * (cur_iter / float(total_iters)):.1f}%"
self.print(f"Progress: {percentage} {prefix} {suffix}")
def set_verbosity(self, verbosity: int) -> None:
"""Dynamic verbosity control"""
self.verbosity_level = verbosity
asyncio.create_task(self._log_async("info", f"Verbosity set to level {verbosity}"))
# MCP-specific async methods
async def _log_async(self, level: str, message: str) -> None:
"""Async logging to MCP context"""
try:
if hasattr(self.context, 'log'):
await self.context.log(level=level, message=message)
else:
# Fallback to console
print(f"[{level.upper()}] {message}")
except Exception as e:
print(f"Logging error: {e}")
async def _start_stage(self, stage_message: str) -> None:
"""Start interactive stage with potential user elicitation"""
await self._log_async("info", f"🔄 Starting: {stage_message}")
# Check if this stage might need user input
if self._requires_user_interaction(stage_message):
await self._elicit_user_confirmation(stage_message)
async def _finish_stage(self) -> None:
"""Finish current stage"""
if self.current_stage:
await self._log_async("info", f"✅ Completed: {self.current_stage}")
async def _update_progress(
self,
operation_id: str,
progress: float,
total: int,
current: int,
message: str
) -> None:
"""Send progress update via MCP"""
try:
if self.supports_progress:
await self.context.progress(
operation_id=operation_id,
progress=progress,
total=total,
current=current,
message=message
)
except Exception as e:
await self._log_async("warning", f"Progress update failed: {e}")
async def _elicit_user_confirmation(self, stage_message: str) -> bool:
"""Elicit user confirmation for critical operations"""
if not self.supports_elicitation:
return True # Proceed if no elicitation support
try:
# Determine confirmation message based on stage
confirmation_message = self._generate_confirmation_message(stage_message)
response = await self.context.request_user_input(
prompt=confirmation_message,
input_type="confirmation"
)
return response.get("confirmed", True)
except Exception as e:
await self._log_async("warning", f"User elicitation failed: {e}")
return True # Default to proceed
def _requires_user_interaction(self, stage_message: str) -> bool:
"""Determine if stage requires user confirmation"""
critical_operations = [
"erasing flash",
"burning efuses",
"enabling secure boot",
"enabling flash encryption",
"factory reset"
]
message_lower = stage_message.lower()
return any(op in message_lower for op in critical_operations)
def _generate_confirmation_message(self, stage_message: str) -> str:
"""Generate appropriate confirmation message"""
confirmations = {
"erasing flash": "⚠️ This will erase all data on the ESP flash memory. Continue?",
"burning efuses": "🔥 eFuse burning is PERMANENT and cannot be undone. Continue?",
"enabling secure boot": "🔐 Secure boot will permanently change chip configuration. Continue?",
"enabling flash encryption": "🔒 Flash encryption is permanent and cannot be disabled. Continue?",
"factory reset": "🏭 This will restore factory settings, erasing all user data. Continue?"
}
message_lower = stage_message.lower()
for key, confirmation in confirmations.items():
if key in message_lower:
return confirmation
return f"🤔 About to: {stage_message}. Continue?"
def _format_message(self, message: str, *args) -> str:
"""Format message with optional arguments"""
if args:
try:
return message % args
except (TypeError, ValueError):
return f"{message} {' '.join(map(str, args))}"
return message
```
## 🔧 Integration with ESPTool Operations
### Logger Factory and Context Management
```python
# components/logger_factory.py
from contextlib import contextmanager
from esptool.logger import log
class MCPLoggerFactory:
"""Factory for creating MCP-integrated esptool loggers"""
@staticmethod
@contextmanager
def create_mcp_logger(context: Context, operation_name: str):
"""
Context manager for MCP logger integration
Usage:
with MCPLoggerFactory.create_mcp_logger(ctx, "flash_operation") as logger:
# esptool operations here will use MCP logging
pass
"""
operation_id = f"{operation_name}_{int(time.time())}"
mcp_logger = MCPESPToolLogger(context, operation_id)
# Replace esptool's default logger
original_logger = log.get_logger()
log.set_logger(mcp_logger)
try:
yield mcp_logger
finally:
# Restore original logger
log.set_logger(original_logger)
```
### Enhanced Tool Implementation
```python
# Example: Enhanced esp_flash_firmware with MCP logging
@app.tool("esp_flash_firmware_interactive")
async def flash_firmware_with_mcp_logging(
context: Context,
port: str,
firmware_files: List[Dict],
interactive: bool = True
) -> str:
"""Flash firmware with full MCP integration and user interaction"""
with MCPLoggerFactory.create_mcp_logger(context, "flash_firmware") as mcp_logger:
try:
# esptool operations now use MCP logging automatically
with detect_chip(port) as esp:
# Logger will elicit user confirmation for critical operations
esp = run_stub(esp)
attach_flash(esp)
# Progress updates go to MCP progress system
write_flash(esp, [(item['address'], item['file']) for item in firmware_files])
# User confirmation for reset
if interactive:
await mcp_logger._elicit_user_confirmation("reset chip after flashing")
reset_chip(esp, 'hard-reset')
return "✅ Firmware flashed successfully with user interaction"
except Exception as e:
await mcp_logger._log_async("error", f"Flash operation failed: {e}")
raise
```
## 🎨 Natural Language Workflow Enhancement
### Interactive ESP Development Experience
```
User: "Flash my ESP32 with the new firmware, but ask me before doing anything destructive"
Claude: I'll flash your ESP32 with interactive confirmations for safety.
🔄 Starting: Detecting ESP32 chip...
✅ Detected: ESP32-D0WD (revision 1)
🔄 Starting: Loading flasher stub...
✅ Flasher stub loaded for faster operation
🔄 Starting: Erasing flash memory...
⚠️ This will erase existing firmware on the ESP flash memory. Continue? [Y/n]
User: y
Claude: 📋 Proceeding with flash erase...
Progress: ████████████████████████████████ 100%
✅ Flash erased successfully
🔄 Starting: Writing firmware...
Progress: ████████████████████████████████ 100%
✅ Firmware written and verified
🤔 About to: reset chip after flashing. Continue? [Y/n]
User: y
Claude: ✅ ESP32 flashed successfully and reset
Your device is ready to run the new firmware!
```
## 📊 Advanced MCP Features Integration
### Progress Tracking for Long Operations
```python
# Enhanced progress tracking
class ProgressAwareESPOperation:
"""Wrapper for ESP operations with detailed progress tracking"""
async def flash_large_firmware(
self,
context: Context,
port: str,
firmware_path: str
) -> None:
"""Flash large firmware with detailed progress"""
with MCPLoggerFactory.create_mcp_logger(context, "large_firmware_flash") as logger:
# Multi-stage progress tracking
stages = [
("Connecting to chip", 10),
("Loading flasher stub", 20),
("Erasing flash", 40),
("Writing firmware", 80),
("Verifying flash", 95),
("Resetting chip", 100)
]
for stage_name, progress_percent in stages:
await logger._update_progress(
operation_id=logger.operation_id,
progress=progress_percent,
total=100,
current=progress_percent,
message=stage_name
)
# Actual esptool operation here
await self._execute_stage(stage_name, port, firmware_path)
```
### Elicitation for Configuration Decisions
```python
# Smart configuration elicitation
async def configure_esp_security_interactive(
context: Context,
port: str,
security_level: str = "auto"
) -> Dict:
"""Configure ESP security with intelligent user prompts"""
if security_level == "auto":
# Use elicitation to determine security configuration
security_config = await context.request_user_input(
prompt="🔐 Security Configuration",
input_type="selection",
options=[
{"id": "development", "label": "Development (no security)"},
{"id": "testing", "label": "Testing (basic security)"},
{"id": "production", "label": "Production (full security)"}
]
)
security_level = security_config.get("selected", "development")
# Configure based on user choice
return await apply_security_configuration(context, port, security_level)
```
This MCP logger integration transforms esptool from a silent command-line tool into an interactive, user-friendly system that provides real-time feedback and intelligently requests user input when needed.
`★ Insight ─────────────────────────────────────`
**Seamless User Experience**: By mapping esptool's logging methods directly to MCP capabilities, we create a unified experience where ESP operations feel native to the Claude environment while preserving all of esptool's functionality.
**Smart Interaction Points**: The logger intelligently identifies critical operations (eFuse burning, secure boot) that require user confirmation, transforming potentially destructive operations into safe, interactive workflows.
**Progressive Enhancement**: The system gracefully degrades when MCP features aren't available, ensuring compatibility across different client implementations while providing enhanced experiences when possible.
`─────────────────────────────────────────────────`
<function_calls>
<invoke name="TodoWrite">
<parameter name="todos">[{"content": "Create FastMCP esptool server architecture documentation", "status": "completed", "activeForm": "Creating FastMCP esptool server architecture documentation"}, {"content": "Design esptool MCP server API structure", "status": "completed", "activeForm": "Designing esptool MCP server API structure"}, {"content": "Document integration patterns with existing Arduino MCP server", "status": "completed", "activeForm": "Documenting integration patterns with existing Arduino MCP server"}, {"content": "Create implementation roadmap and examples", "status": "completed", "activeForm": "Creating implementation roadmap and examples"}, {"content": "Design esptool custom logger for MCP integration", "status": "completed", "activeForm": "Designing esptool custom logger for MCP integration"}]

410
MIDDLEWARE_ARCHITECTURE.md Normal file
View File

@ -0,0 +1,410 @@
# 🔗 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
1. **Non-Invasive Integration**: Zero modifications to existing tools
2. **Protocol Translation**: Seamless sync/async bridging
3. **Progressive Enhancement**: Graceful degradation across client capabilities
4. **Bidirectional Communication**: Interactive workflows with user feedback
5. **Context Awareness**: Rich integration with MCP ecosystem
## 🔧 Middleware Implementation Patterns
### 1. **Logger Interception Pattern**
The most elegant approach for tools with pluggable logging systems:
```python
# 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:
```python
# 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**
```python
# 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
```python
# 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
```python
# 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
```python
# 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
1. **Zero Code Changes**: Original tools remain completely unmodified
2. **Preservation of Functionality**: All original features remain available
3. **Enhanced User Experience**: Interactive workflows with progress tracking
4. **Error Recovery**: Better error handling and user guidance
5. **Context Awareness**: Operations become aware of broader development context
### For MCP Server Development
1. **Rapid Integration**: Quick integration of existing CLI tools
2. **Consistent Patterns**: Reusable middleware architecture
3. **Extensibility**: Easy to add new tools and capabilities
4. **Maintainability**: Clear separation of concerns
5. **Testing**: Isolated middleware can be unit tested independently
### For End Users
1. **Natural Language Interface**: CLI tools become conversational
2. **Safety Features**: Interactive confirmations for destructive operations
3. **Progress Visibility**: Real-time feedback on long operations
4. **Error Guidance**: Helpful error messages and recovery suggestions
5. **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.
`─────────────────────────────────────────────────`

View File

@ -0,0 +1,630 @@
# 🎨 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.

91
Makefile Normal file
View File

@ -0,0 +1,91 @@
# MCP ESPTool Server Makefile
.PHONY: help install dev test lint format clean docker-build docker-up docker-down docker-logs
# Default target
help:
@echo "MCP ESPTool Server Development Commands"
@echo ""
@echo "Setup & Installation:"
@echo " install Install project with uv"
@echo " dev Install in development mode"
@echo ""
@echo "Development:"
@echo " test Run test suite"
@echo " lint Run linting checks"
@echo " format Format code with ruff"
@echo " clean Clean build artifacts"
@echo ""
@echo "Docker Operations:"
@echo " docker-build Build development container"
@echo " docker-up Start development environment"
@echo " docker-down Stop development environment"
@echo " docker-logs View container logs"
@echo ""
@echo "MCP Integration:"
@echo " mcp-install Install server with Claude Code"
@echo " mcp-test Test MCP server integration"
# Installation
install:
uv sync
dev:
uv sync --dev
uv run pre-commit install
# Testing and Quality
test:
PYTHONPATH=src uv run pytest tests/ -v
test-watch:
PYTHONPATH=src uv run pytest-watch tests/
lint:
uv run ruff check src/ tests/
uv run mypy src/
format:
uv run ruff format src/ tests/
uv run ruff check --fix src/ tests/
clean:
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
rm -rf build/ dist/ *.egg-info/
rm -rf .pytest_cache/ .coverage .mypy_cache/
# Docker Operations
docker-build:
docker compose build
docker-up:
docker compose up -d
@echo "Development environment started"
@echo "Run 'make docker-logs' to view logs"
docker-down:
docker compose down
docker-logs:
docker compose logs -f
# MCP Integration
mcp-install:
claude mcp add mcp-esptool-server "uvx mcp-esptool-server"
@echo "MCP server installed with Claude Code"
@echo "You can now use it in Claude conversations"
mcp-test:
uvx mcp-esptool-server --help
@echo "Testing MCP server installation..."
# Development shortcuts
run:
uv run mcp-esptool-server
run-debug:
uv run mcp-esptool-server --debug
run-production:
uv run mcp-esptool-server --production

970
PRODUCTION_DEPLOYMENT.md Normal file
View File

@ -0,0 +1,970 @@
# 🚀 Production Deployment Guide
## Overview
This guide covers deploying the FastMCP ESPTool server in production environments, including containerization, scaling, monitoring, and enterprise integration patterns.
## 🏭 Production Architecture
### Deployment Topology
```
┌─────────────────────────────────────────────────────────────┐
│ Production Environment │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Load │ │ Reverse │ │ SSL/TLS │ │
│ │ Balancer │───▶│ Proxy │───▶│ Termination │ │
│ │ (HAProxy) │ │ (Caddy) │ │ (Cert) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────┤
│ │ MCP ESPTool Server Cluster │
│ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ │ Server │ │ Server │ │ Server │ │
│ │ │ Instance 1 │ │ Instance 2 │ │ Instance 3 │ │
│ │ │ │ │ │ │ │ │
│ │ │ Port: 8080 │ │ Port: 8081 │ │ Port: 8082 │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │
│ └─────────────────────────────────────────────────────────┤
│ │ │
│ ┌─────────────────────────────────────────────────────────┤
│ │ Shared Services │
│ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ │ Redis │ │ PostgreSQL │ │ Monitoring │ │
│ │ │ Cache │ │ Database │ │ (Grafana) │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │
│ └─────────────────────────────────────────────────────────┤
│ │ │
│ ┌─────────────────────────────────────────────────────────┤
│ │ Hardware Interface │
│ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ │ ESP Device │ │ ESP Device │ │ ESP Device │ │
│ │ │ Station 1 │ │ Station 2 │ │ Station N │ │
│ │ │ │ │ │ │ │ │
│ │ │/dev/ttyUSB0 │ │/dev/ttyUSB1 │ │/dev/ttyUSBN │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │
│ └─────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────┘
```
## 🐳 Container Production Setup
### Production Dockerfile
```dockerfile
# Multi-stage production Dockerfile
FROM python:3.11-slim-bookworm AS base
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
build-essential \
cmake \
ninja-build \
libusb-1.0-0-dev \
udev \
&& rm -rf /var/lib/apt/lists/*
# Install uv for fast package management
RUN pip install uv
# Create non-root user for security
RUN groupadd -r esptool && useradd -r -g esptool -d /app -s /bin/bash esptool
RUN mkdir -p /app && chown esptool:esptool /app
# Production stage
FROM base AS production
USER esptool
WORKDIR /app
# Copy project files
COPY --chown=esptool:esptool pyproject.toml ./
COPY --chown=esptool:esptool src/ ./src/
# Install production dependencies
RUN uv venv .venv && \
. .venv/bin/activate && \
uv pip install -e ".[production]" && \
uv pip install esptool esp-idf-tools
# Set up ESP-IDF (production minimal)
RUN git clone --depth 1 --branch v5.1 \
https://github.com/espressif/esp-idf.git /opt/esp-idf && \
cd /opt/esp-idf && \
./install.sh --targets esp32,esp32s3,esp32c3
# Environment setup
ENV ESP_IDF_PATH=/opt/esp-idf
ENV PATH="/opt/esp-idf/tools:/app/.venv/bin:$PATH"
ENV PYTHONPATH=/app/src
# Create directories for data persistence
RUN mkdir -p /app/data /app/logs /app/config
# Health check script
COPY --chown=esptool:esptool scripts/health-check.py ./health-check.py
RUN chmod +x health-check.py
# Security: Remove package management tools in production
USER root
RUN apt-get remove -y git curl build-essential cmake && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
USER esptool
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD python health-check.py || exit 1
# Expose port
EXPOSE 8080
# Production startup
CMD ["/app/.venv/bin/python", "-m", "mcp_esptool_server.server", "--production"]
```
### Production Docker Compose
```yaml
# docker-compose.prod.yml
version: '3.8'
services:
# Load balancer
haproxy:
image: haproxy:2.8-alpine
ports:
- "80:80"
- "443:443"
- "8404:8404" # Stats
volumes:
- ./config/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
- ./certs:/etc/ssl/certs:ro
networks:
- frontend
- backend
restart: unless-stopped
depends_on:
- esptool-server-1
- esptool-server-2
- esptool-server-3
# MCP ESPTool Server Instances
esptool-server-1: &esptool-server
build:
context: .
dockerfile: Dockerfile
target: production
image: mcp-esptool-server:${VERSION:-latest}
environment:
- SERVER_ID=1
- SERVER_PORT=8080
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- DATABASE_URL=postgresql://esptool:${DB_PASSWORD}@postgres:5432/esptool
- REDIS_URL=redis://redis:6379/0
- ESP_DEVICE_BASE_PATH=/dev/serial
- PRODUCTION_MODE=true
- MAX_CONCURRENT_OPERATIONS=10
- ENABLE_METRICS=true
- METRICS_PORT=9090
volumes:
- ./data/server-1:/app/data
- ./logs:/app/logs
- ./config:/app/config:ro
- /dev/serial:/dev/serial:rw
networks:
- backend
restart: unless-stopped
depends_on:
- postgres
- redis
labels:
- "com.docker.compose.service=esptool-server"
esptool-server-2:
<<: *esptool-server
environment:
- SERVER_ID=2
- SERVER_PORT=8080
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- DATABASE_URL=postgresql://esptool:${DB_PASSWORD}@postgres:5432/esptool
- REDIS_URL=redis://redis:6379/0
- ESP_DEVICE_BASE_PATH=/dev/serial
- PRODUCTION_MODE=true
volumes:
- ./data/server-2:/app/data
- ./logs:/app/logs
- ./config:/app/config:ro
- /dev/serial:/dev/serial:rw
esptool-server-3:
<<: *esptool-server
environment:
- SERVER_ID=3
- SERVER_PORT=8080
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- DATABASE_URL=postgresql://esptool:${DB_PASSWORD}@postgres:5432/esptool
- REDIS_URL=redis://redis:6379/0
- ESP_DEVICE_BASE_PATH=/dev/serial
- PRODUCTION_MODE=true
volumes:
- ./data/server-3:/app/data
- ./logs:/app/logs
- ./config:/app/config:ro
- /dev/serial:/dev/serial:rw
# Database
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: esptool
POSTGRES_USER: esptool
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
networks:
- backend
restart: unless-stopped
# Cache and session store
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --maxmemory 1gb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
networks:
- backend
restart: unless-stopped
# Monitoring and metrics
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./config/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
networks:
- backend
- monitoring
restart: unless-stopped
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
volumes:
- grafana-data:/var/lib/grafana
- ./config/grafana:/etc/grafana/provisioning:ro
networks:
- monitoring
restart: unless-stopped
depends_on:
- prometheus
# Log aggregation
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
volumes:
- loki-data:/tmp/loki
- ./config/loki.yml:/etc/loki/local-config.yaml:ro
networks:
- monitoring
restart: unless-stopped
volumes:
postgres-data:
redis-data:
prometheus-data:
grafana-data:
loki-data:
networks:
frontend:
driver: bridge
backend:
driver: bridge
monitoring:
driver: bridge
```
## ⚙️ Configuration Management
### Production Configuration
```python
# src/mcp_esptool_server/config/production.py
import os
from dataclasses import dataclass, field
from typing import List, Dict, Optional
from pathlib import Path
@dataclass
class ProductionConfig:
"""Production environment configuration"""
# Server settings
server_id: str = field(default_factory=lambda: os.getenv('SERVER_ID', '1'))
server_port: int = int(os.getenv('SERVER_PORT', '8080'))
max_workers: int = int(os.getenv('MAX_WORKERS', '4'))
max_concurrent_operations: int = int(os.getenv('MAX_CONCURRENT_OPERATIONS', '10'))
# Database configuration
database_url: str = os.getenv('DATABASE_URL', 'postgresql://localhost:5432/esptool')
redis_url: str = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
# Security settings
enable_auth: bool = os.getenv('ENABLE_AUTH', 'true').lower() == 'true'
jwt_secret: str = os.getenv('JWT_SECRET', 'change-me-in-production')
api_key_required: bool = os.getenv('API_KEY_REQUIRED', 'true').lower() == 'true'
# ESP device settings
esp_device_base_path: str = os.getenv('ESP_DEVICE_BASE_PATH', '/dev/serial')
max_device_connections: int = int(os.getenv('MAX_DEVICE_CONNECTIONS', '20'))
device_timeout: int = int(os.getenv('DEVICE_TIMEOUT', '30'))
# Logging and monitoring
log_level: str = os.getenv('LOG_LEVEL', 'INFO')
enable_metrics: bool = os.getenv('ENABLE_METRICS', 'true').lower() == 'true'
metrics_port: int = int(os.getenv('METRICS_PORT', '9090'))
# Performance tuning
enable_caching: bool = os.getenv('ENABLE_CACHING', 'true').lower() == 'true'
cache_ttl: int = int(os.getenv('CACHE_TTL', '300'))
# Factory programming settings
enable_factory_mode: bool = os.getenv('ENABLE_FACTORY_MODE', 'false').lower() == 'true'
factory_batch_size: int = int(os.getenv('FACTORY_BATCH_SIZE', '100'))
# Data retention
log_retention_days: int = int(os.getenv('LOG_RETENTION_DAYS', '30'))
metrics_retention_days: int = int(os.getenv('METRICS_RETENTION_DAYS', '90'))
def __post_init__(self):
"""Validate configuration after initialization"""
if self.enable_auth and self.jwt_secret == 'change-me-in-production':
raise ValueError("JWT_SECRET must be set in production with authentication enabled")
if not Path(self.esp_device_base_path).exists():
raise ValueError(f"ESP device base path does not exist: {self.esp_device_base_path}")
```
### Environment Configuration
```bash
# .env.production
# Core server settings
SERVER_ID=1
SERVER_PORT=8080
MAX_WORKERS=4
MAX_CONCURRENT_OPERATIONS=10
# Database and cache
DATABASE_URL=postgresql://esptool:secure_password@postgres:5432/esptool
REDIS_URL=redis://redis:6379/0
# Security (CHANGE THESE IN PRODUCTION)
ENABLE_AUTH=true
JWT_SECRET=your-256-bit-secret-key-here
API_KEY_REQUIRED=true
# ESP device configuration
ESP_DEVICE_BASE_PATH=/dev/serial
MAX_DEVICE_CONNECTIONS=20
DEVICE_TIMEOUT=30
# Logging and monitoring
LOG_LEVEL=INFO
ENABLE_METRICS=true
METRICS_PORT=9090
# Performance
ENABLE_CACHING=true
CACHE_TTL=300
# Factory programming
ENABLE_FACTORY_MODE=true
FACTORY_BATCH_SIZE=50
# Data retention
LOG_RETENTION_DAYS=30
METRICS_RETENTION_DAYS=90
# External services
GRAFANA_PASSWORD=secure_grafana_password
DB_PASSWORD=secure_db_password
```
## 🔐 Security Configuration
### Authentication and Authorization
```python
# src/mcp_esptool_server/security/auth.py
import jwt
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from fastapi import HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
class ProductionAuth:
"""Production authentication and authorization"""
def __init__(self, config: ProductionConfig):
self.config = config
self.security = HTTPBearer()
self.valid_api_keys: set = self._load_api_keys()
def _load_api_keys(self) -> set:
"""Load valid API keys from configuration"""
# In production, load from secure key management system
api_keys_file = Path(self.config.config_dir) / "api_keys.txt"
if api_keys_file.exists():
return set(api_keys_file.read_text().strip().split('\n'))
return set()
def create_access_token(self, data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
"""Create JWT access token"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, self.config.jwt_secret, algorithm="HS256")
return encoded_jwt
def verify_token(self, credentials: HTTPAuthorizationCredentials) -> Dict[str, Any]:
"""Verify JWT token"""
try:
payload = jwt.decode(
credentials.credentials,
self.config.jwt_secret,
algorithms=["HS256"]
)
return payload
except jwt.PyJWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials"
)
def verify_api_key(self, api_key: str) -> bool:
"""Verify API key"""
return api_key in self.valid_api_keys
# Security middleware
class SecurityMiddleware:
"""Production security middleware"""
def __init__(self, app: FastMCP, auth: ProductionAuth):
self.app = app
self.auth = auth
self._setup_security_headers()
def _setup_security_headers(self):
"""Configure security headers"""
@self.app.middleware("http")
async def add_security_headers(request, call_next):
response = await call_next(request)
# Security headers
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Content-Security-Policy"] = "default-src 'self'"
return response
```
## 📊 Monitoring and Observability
### Metrics Collection
```python
# src/mcp_esptool_server/monitoring/metrics.py
from prometheus_client import Counter, Histogram, Gauge, start_http_server
from typing import Dict, Any
import time
class ProductionMetrics:
"""Production metrics collection"""
def __init__(self, config: ProductionConfig):
self.config = config
# Operation metrics
self.operations_total = Counter(
'esptool_operations_total',
'Total number of ESPTool operations',
['operation_type', 'status', 'server_id']
)
self.operation_duration = Histogram(
'esptool_operation_duration_seconds',
'Duration of ESPTool operations',
['operation_type', 'server_id']
)
# Device metrics
self.connected_devices = Gauge(
'esptool_connected_devices',
'Number of connected ESP devices',
['server_id']
)
self.device_operations = Counter(
'esptool_device_operations_total',
'Total device operations by port',
['port', 'operation', 'status', 'server_id']
)
# System metrics
self.active_connections = Gauge(
'esptool_active_connections',
'Number of active MCP connections',
['server_id']
)
self.memory_usage = Gauge(
'esptool_memory_usage_bytes',
'Memory usage in bytes',
['server_id']
)
# Error metrics
self.errors_total = Counter(
'esptool_errors_total',
'Total number of errors',
['error_type', 'component', 'server_id']
)
if config.enable_metrics:
start_http_server(config.metrics_port)
def record_operation(self, operation_type: str, duration: float, status: str):
"""Record operation metrics"""
self.operations_total.labels(
operation_type=operation_type,
status=status,
server_id=self.config.server_id
).inc()
self.operation_duration.labels(
operation_type=operation_type,
server_id=self.config.server_id
).observe(duration)
def record_device_operation(self, port: str, operation: str, status: str):
"""Record device-specific operation"""
self.device_operations.labels(
port=port,
operation=operation,
status=status,
server_id=self.config.server_id
).inc()
def update_connected_devices(self, count: int):
"""Update connected devices count"""
self.connected_devices.labels(server_id=self.config.server_id).set(count)
def record_error(self, error_type: str, component: str):
"""Record error occurrence"""
self.errors_total.labels(
error_type=error_type,
component=component,
server_id=self.config.server_id
).inc()
```
### Logging Configuration
```python
# src/mcp_esptool_server/logging/production.py
import logging
import logging.config
from pathlib import Path
import json
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"json": {
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
"format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d"
},
"standard": {
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
}
},
"handlers": {
"console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "standard",
"stream": "ext://sys.stdout"
},
"file": {
"level": "DEBUG",
"class": "logging.handlers.RotatingFileHandler",
"formatter": "json",
"filename": "/app/logs/esptool-server.log",
"maxBytes": 10485760, # 10MB
"backupCount": 5
},
"error_file": {
"level": "ERROR",
"class": "logging.handlers.RotatingFileHandler",
"formatter": "json",
"filename": "/app/logs/esptool-errors.log",
"maxBytes": 10485760,
"backupCount": 5
}
},
"loggers": {
"mcp_esptool_server": {
"level": "DEBUG",
"handlers": ["console", "file", "error_file"],
"propagate": False
},
"esptool": {
"level": "INFO",
"handlers": ["file"],
"propagate": False
}
},
"root": {
"level": "INFO",
"handlers": ["console"]
}
}
def setup_production_logging(config: ProductionConfig):
"""Set up production logging configuration"""
# Ensure log directory exists
log_dir = Path("/app/logs")
log_dir.mkdir(exist_ok=True)
# Update logging level from configuration
LOGGING_CONFIG["handlers"]["console"]["level"] = config.log_level
LOGGING_CONFIG["loggers"]["mcp_esptool_server"]["level"] = config.log_level
# Configure logging
logging.config.dictConfig(LOGGING_CONFIG)
# Set up structured logging context
logger = logging.getLogger("mcp_esptool_server")
logger.info("Production logging initialized", extra={
"server_id": config.server_id,
"log_level": config.log_level,
"environment": "production"
})
```
## 🚀 Deployment Automation
### Kubernetes Deployment
```yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-esptool-server
labels:
app: mcp-esptool-server
spec:
replicas: 3
selector:
matchLabels:
app: mcp-esptool-server
template:
metadata:
labels:
app: mcp-esptool-server
spec:
containers:
- name: mcp-esptool-server
image: mcp-esptool-server:latest
ports:
- containerPort: 8080
- containerPort: 9090 # Metrics
env:
- name: SERVER_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: esptool-secrets
key: database-url
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: esptool-secrets
key: redis-url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: esptool-secrets
key: jwt-secret
volumeMounts:
- name: device-access
mountPath: /dev/serial
- name: config
mountPath: /app/config
readOnly: true
- name: data
mountPath: /app/data
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: device-access
hostPath:
path: /dev/serial
- name: config
configMap:
name: esptool-config
- name: data
persistentVolumeClaim:
claimName: esptool-data
---
apiVersion: v1
kind: Service
metadata:
name: mcp-esptool-service
spec:
selector:
app: mcp-esptool-server
ports:
- name: http
port: 80
targetPort: 8080
- name: metrics
port: 9090
targetPort: 9090
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mcp-esptool-ingress
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- esptool.yourdomain.com
secretName: esptool-tls
rules:
- host: esptool.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mcp-esptool-service
port:
number: 80
```
### CI/CD Pipeline
```yaml
# .github/workflows/production-deploy.yml
name: Production Deployment
on:
push:
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install uv
uv venv
uv pip install -e ".[dev,testing]"
- name: Run tests
run: |
uv run pytest tests/ --cov=src --cov-fail-under=85
- name: Security scan
run: |
uv run bandit -r src/
uv run safety check
build:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
target: production
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Deploy to production
run: |
kubectl set image deployment/mcp-esptool-server \
mcp-esptool-server=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
kubectl rollout status deployment/mcp-esptool-server --timeout=300s
- name: Verify deployment
run: |
kubectl get pods -l app=mcp-esptool-server
kubectl logs -l app=mcp-esptool-server --tail=50
```
## 📋 Production Checklist
### Pre-Deployment
- [ ] **Security Configuration**
- [ ] Change default passwords and secrets
- [ ] Configure JWT secrets
- [ ] Set up API key management
- [ ] Configure TLS certificates
- [ ] Review firewall rules
- [ ] **Infrastructure Setup**
- [ ] Database cluster configured
- [ ] Redis cache configured
- [ ] Load balancer configured
- [ ] Monitoring stack deployed
- [ ] Log aggregation configured
- [ ] **Hardware Access**
- [ ] ESP device permissions configured
- [ ] USB device access verified
- [ ] Device path mapping documented
- [ ] Hardware redundancy planned
### Post-Deployment
- [ ] **Monitoring Setup**
- [ ] Grafana dashboards configured
- [ ] Alerting rules configured
- [ ] Log analysis configured
- [ ] Performance baselines established
- [ ] **Operational Procedures**
- [ ] Backup procedures documented
- [ ] Disaster recovery tested
- [ ] Scaling procedures documented
- [ ] Maintenance windows scheduled
- [ ] **Team Training**
- [ ] Operations team trained
- [ ] Troubleshooting guides created
- [ ] Escalation procedures documented
- [ ] User documentation published
This production deployment guide ensures enterprise-grade reliability, security, and scalability for the FastMCP ESPTool server in production environments.
`★ Insight ─────────────────────────────────────`
**Enterprise-Grade Deployment**: This production guide demonstrates how to scale the FastMCP ESPTool server from development to enterprise production environments with proper security, monitoring, and operational procedures.
**Hardware Integration Challenges**: The unique aspect of this deployment is managing physical ESP device access in containerized environments, requiring careful consideration of USB device mapping and permissions across different deployment platforms.
**Observability Excellence**: The comprehensive monitoring, logging, and metrics collection ensures production visibility and enables proactive problem resolution, critical for embedded development workflows where hardware issues can be difficult to diagnose.
`─────────────────────────────────────────────────`
<function_calls>
<invoke name="TodoWrite">
<parameter name="todos">[{"content": "Create project initialization and setup documentation", "status": "completed", "activeForm": "Creating project initialization and setup documentation"}, {"content": "Design concrete implementation examples", "status": "completed", "activeForm": "Designing concrete implementation examples"}, {"content": "Create production deployment guide", "status": "completed", "activeForm": "Creating production deployment guide"}]

692
PROJECT_SETUP.md Normal file
View File

@ -0,0 +1,692 @@
# 🚀 FastMCP ESPTool Server - Project Setup & Initialization
## Quick Start
```bash
# 1. Clone and initialize project
git clone <repository-url> mcp-esptool-server
cd mcp-esptool-server
# 2. Set up Python environment with uv
uv venv
uv pip install -e ".[dev]"
# 3. Start development server
uv run mcp-esptool-server
# 4. Add to Claude Desktop
claude mcp add esptool "uv run mcp-esptool-server"
```
## 📁 Project Structure
```
mcp-esptool-server/
├── src/
│ └── mcp_esptool_server/
│ ├── __init__.py
│ ├── server.py # Main FastMCP server
│ ├── config.py # Configuration management
│ ├── middleware/ # Middleware system
│ │ ├── __init__.py
│ │ ├── logger_interceptor.py # Base middleware classes
│ │ ├── esptool_middleware.py # ESPTool-specific middleware
│ │ └── idf_middleware.py # ESP-IDF middleware
│ └── components/ # Tool components
│ ├── __init__.py
│ ├── chip_control.py # ESP chip operations
│ ├── flash_manager.py # Flash memory management
│ ├── partition_manager.py # Partition table tools
│ ├── security_manager.py # Security & eFuse tools
│ ├── firmware_builder.py # Binary processing
│ ├── ota_manager.py # OTA update tools
│ ├── production_tools.py # Factory programming
│ ├── diagnostics.py # Debug & analysis
│ └── idf_host_apps.py # ESP-IDF host applications
├── tests/
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests
│ ├── end_to_end/ # Full workflow tests
│ └── fixtures/ # Test data
├── examples/ # Usage examples
├── docs/ # Additional documentation
├── scripts/ # Development scripts
├── docker/ # Docker configuration
├── .env.example # Environment template
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── Makefile
├── pyproject.toml
└── README.md
```
## 🔧 Configuration Setup
### Environment Variables
```bash
# .env file configuration
# Copy from .env.example and customize
# Core paths
ESPTOOL_PATH=esptool # esptool binary path
ESP_IDF_PATH=/opt/esp-idf # ESP-IDF installation
MCP_PROJECT_ROOTS=/workspace/projects # Project directories
# Communication settings
ESP_DEFAULT_BAUD_RATE=460800 # Default baud rate
ESP_CONNECTION_TIMEOUT=30 # Connection timeout (seconds)
ESP_ENABLE_STUB_FLASHER=true # Enable stub flasher
# Middleware options
MCP_ENABLE_PROGRESS=true # Enable progress tracking
MCP_ENABLE_ELICITATION=true # Enable user interaction
MCP_LOG_LEVEL=INFO # Logging level
# Development settings
DEV_ENABLE_HOT_RELOAD=true # Hot reload in development
DEV_MOCK_HARDWARE=false # Mock hardware for testing
DEV_ENABLE_TRACING=false # Enable detailed tracing
# Production settings
PROD_ENABLE_SECURITY_AUDIT=true # Security auditing
PROD_REQUIRE_CONFIRMATIONS=true # User confirmations
PROD_MAX_CONCURRENT_OPERATIONS=5 # Concurrent operation limit
```
### Python Configuration
```toml
# pyproject.toml
[project]
name = "mcp-esptool-server"
version = "2025.09.28.1"
description = "FastMCP server for ESP32/ESP8266 development with esptool integration"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [
{ name = "Your Name", email = "your.email@example.com" }
]
keywords = [
"mcp", "model-context-protocol", "esp32", "esp8266",
"esptool", "esp-idf", "embedded", "iot", "fastmcp"
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Embedded Systems",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
dependencies = [
"fastmcp>=2.12.4", # FastMCP framework
"esptool>=5.0.0", # ESPTool Python API
"pyserial>=3.5", # Serial communication
"pyserial-asyncio>=0.6", # Async serial support
"thefuzz[speedup]>=0.22.1", # Fuzzy string matching
"pydantic>=2.0.0", # Data validation
"click>=8.0.0", # CLI framework
"rich>=13.0.0", # Rich console output
]
[project.optional-dependencies]
dev = [
"pytest>=8.4.2",
"pytest-asyncio>=0.21.0",
"pytest-cov>=7.0.0",
"pytest-mock>=3.12.0",
"ruff>=0.13.2",
"mypy>=1.5.0",
"black>=23.0.0",
"pre-commit>=3.0.0",
"watchdog>=3.0.0", # Hot reload support
]
idf = [
"esp-idf-tools>=2.0.0", # ESP-IDF integration
"kconfiglib>=14.1.0", # Kconfig parsing
]
testing = [
"pytest-xdist>=3.0.0", # Parallel testing
"pytest-benchmark>=4.0.0", # Performance testing
"factory-boy>=3.3.0", # Test data factories
]
docs = [
"mkdocs>=1.5.0",
"mkdocs-material>=9.0.0",
"mkdocs-mermaid2-plugin>=1.0.0",
]
[project.scripts]
mcp-esptool-server = "mcp_esptool_server.server:main"
esptool-mcp = "mcp_esptool_server.cli:cli"
[project.urls]
Homepage = "https://github.com/yourusername/mcp-esptool-server"
Repository = "https://github.com/yourusername/mcp-esptool-server"
Issues = "https://github.com/yourusername/mcp-esptool-server/issues"
Documentation = "https://yourusername.github.io/mcp-esptool-server"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/mcp_esptool_server"]
[tool.ruff]
line-length = 100
target-version = "py310"
src = ["src", "tests"]
[tool.ruff.lint]
select = ["E", "F", "W", "B", "I", "N", "UP", "ANN", "S", "C4", "DTZ", "T20"]
ignore = ["E501", "ANN101", "ANN102", "S101"]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101", "ANN"]
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
strict_optional = true
[[tool.mypy.overrides]]
module = "esptool.*"
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = [
"--cov=src/mcp_esptool_server",
"--cov-report=html",
"--cov-report=term-missing",
"--cov-fail-under=85"
]
[tool.coverage.run]
source = ["src"]
omit = ["tests/*", "*/test_*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
]
```
## 🐳 Docker Development Environment
### docker-compose.yml
```yaml
# Development environment with hot reload
services:
mcp-esptool-server:
build:
context: .
dockerfile: Dockerfile
target: ${MODE:-development}
container_name: mcp-esptool-dev
environment:
- LOG_LEVEL=${LOG_LEVEL:-DEBUG}
- MCP_PROJECT_ROOTS=/workspace/projects
- ESPTOOL_PATH=/usr/local/bin/esptool
- ESP_IDF_PATH=/opt/esp-idf
- DEV_ENABLE_HOT_RELOAD=true
volumes:
# Development volumes for hot reload
- ./src:/app/src:ro
- ./tests:/app/tests:ro
- ./examples:/app/examples:ro
# Project workspace
- esp-projects:/workspace/projects
# ESP-IDF and tools
- esp-idf:/opt/esp-idf
- esp-tools:/opt/esp-tools
# Device access (Linux)
- /dev:/dev:ro
ports:
- "${SERVER_PORT:-8080}:8080"
networks:
- default
- caddy
labels:
# Caddy reverse proxy
caddy: ${CADDY_DOMAIN:-esp-tools.local}
caddy.reverse_proxy: "{{upstreams 8080}}"
restart: unless-stopped
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: pyproject.toml
# ESP-IDF development environment
esp-idf-dev:
image: espressif/idf:latest
container_name: esp-idf-tools
volumes:
- esp-idf:/opt/esp-idf
- esp-tools:/opt/esp-tools
- esp-projects:/workspace/projects
command: tail -f /dev/null
networks:
- default
volumes:
esp-projects:
name: ${COMPOSE_PROJECT_NAME:-mcp-esptool}-projects
esp-idf:
name: ${COMPOSE_PROJECT_NAME:-mcp-esptool}-idf
esp-tools:
name: ${COMPOSE_PROJECT_NAME:-mcp-esptool}-tools
networks:
default:
name: ${COMPOSE_PROJECT_NAME:-mcp-esptool}
caddy:
external: true
```
### Multi-stage Dockerfile
```dockerfile
# Dockerfile
# Base image with Python and system dependencies
FROM python:3.11-slim-bookworm AS base
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
wget \
build-essential \
cmake \
ninja-build \
libusb-1.0-0-dev \
&& rm -rf /var/lib/apt/lists/*
# Install uv for fast Python package management
RUN pip install uv
# Create app user
RUN useradd --create-home --shell /bin/bash app
USER app
WORKDIR /app
# Development stage
FROM base AS development
# Copy project files
COPY --chown=app:app pyproject.toml ./
COPY --chown=app:app src/ ./src/
# Install development dependencies
RUN uv venv && \
uv pip install -e ".[dev,idf,testing]"
# Install esptool and ESP-IDF
RUN uv pip install esptool
ENV PATH="/home/app/.local/bin:$PATH"
# Configure ESP-IDF (development version)
RUN git clone --depth 1 --branch v5.1 https://github.com/espressif/esp-idf.git /opt/esp-idf
RUN cd /opt/esp-idf && ./install.sh esp32
# Set up environment
ENV ESP_IDF_PATH=/opt/esp-idf
ENV PATH="$ESP_IDF_PATH/tools:$PATH"
EXPOSE 8080
CMD ["uv", "run", "mcp-esptool-server"]
# Production stage
FROM base AS production
# Copy only necessary files
COPY --chown=app:app pyproject.toml ./
COPY --chown=app:app src/ ./src/
# Install production dependencies only
RUN uv venv && \
uv pip install -e ".[idf]" --no-dev
# Install production tools
RUN uv pip install esptool
ENV PATH="/home/app/.local/bin:$PATH"
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
EXPOSE 8080
CMD ["uv", "run", "mcp-esptool-server", "--production"]
```
## 🛠️ Development Tools
### Makefile
```makefile
# Makefile for development tasks
.PHONY: help install dev test lint format clean docker-build docker-up docker-down
# Default target
help:
@echo "Available targets:"
@echo " install - Install dependencies"
@echo " dev - Start development server"
@echo " test - Run tests"
@echo " lint - Run linting"
@echo " format - Format code"
@echo " clean - Clean build artifacts"
@echo " docker-up - Start Docker development environment"
@echo " docker-down - Stop Docker environment"
# Installation
install:
uv venv
uv pip install -e ".[dev,idf,testing]"
# Development server
dev:
uv run mcp-esptool-server --debug
# Testing
test:
uv run pytest tests/ -v
test-cov:
uv run pytest tests/ --cov=src --cov-report=html --cov-report=term
test-integration:
uv run pytest tests/integration/ -v
# Code quality
lint:
uv run ruff check src/ tests/
uv run mypy src/
format:
uv run ruff format src/ tests/
uv run ruff check --fix src/ tests/
# Cleanup
clean:
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
rm -rf dist/ build/ *.egg-info/
rm -rf htmlcov/ .coverage
rm -rf .pytest_cache/
# Docker operations
docker-build:
docker compose build
docker-up:
docker compose up -d
docker-down:
docker compose down
docker-logs:
docker compose logs -f mcp-esptool-server
# ESP-IDF specific
esp-idf-setup:
docker compose exec esp-idf-dev /opt/esp-idf/install.sh
# Add to Claude Desktop
claude-add:
claude mcp add esptool "uv run mcp-esptool-server"
# Remove from Claude Desktop
claude-remove:
claude mcp remove esptool
```
## 🔧 Development Scripts
### scripts/setup.py
```python
#!/usr/bin/env python3
"""Setup script for MCP ESPTool Server development environment."""
import os
import subprocess
import sys
from pathlib import Path
def run_command(cmd: str, check: bool = True) -> subprocess.CompletedProcess:
"""Run command and handle errors."""
print(f"Running: {cmd}")
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if check and result.returncode != 0:
print(f"Error running command: {cmd}")
print(f"Stdout: {result.stdout}")
print(f"Stderr: {result.stderr}")
sys.exit(1)
return result
def setup_python_environment():
"""Set up Python environment with uv."""
print("🐍 Setting up Python environment...")
# Check if uv is installed
result = run_command("uv --version", check=False)
if result.returncode != 0:
print("Installing uv...")
run_command("pip install uv")
# Create virtual environment and install dependencies
run_command("uv venv")
run_command("uv pip install -e '.[dev,idf,testing]'")
print("✅ Python environment ready!")
def setup_esptool():
"""Set up esptool."""
print("🔧 Setting up esptool...")
run_command("uv pip install esptool")
# Verify installation
result = run_command("uv run esptool version", check=False)
if result.returncode == 0:
print("✅ ESPTool ready!")
else:
print("⚠️ ESPTool installation may have issues")
def setup_esp_idf():
"""Set up ESP-IDF (optional)."""
print("🏗️ Checking ESP-IDF installation...")
esp_idf_path = os.environ.get('ESP_IDF_PATH')
if esp_idf_path and Path(esp_idf_path).exists():
print(f"✅ ESP-IDF found at {esp_idf_path}")
else:
print("⚠️ ESP-IDF not found. Install manually or use Docker environment.")
print(" Docker: make docker-up")
print(" Manual: https://docs.espressif.com/projects/esp-idf/en/latest/get-started/")
def setup_git_hooks():
"""Set up git pre-commit hooks."""
print("🪝 Setting up git hooks...")
if Path(".git").exists():
run_command("uv run pre-commit install")
print("✅ Git hooks installed!")
else:
print("⚠️ Not a git repository, skipping hooks")
def create_env_file():
"""Create .env file from template."""
print("📝 Creating environment file...")
env_file = Path(".env")
env_example = Path(".env.example")
if not env_file.exists() and env_example.exists():
env_file.write_text(env_example.read_text())
print("✅ Created .env file from template")
print(" Please review and customize .env file")
else:
print(" .env file already exists or no template found")
def verify_installation():
"""Verify the installation."""
print("🔍 Verifying installation...")
# Test import
result = run_command("uv run python -c 'import mcp_esptool_server; print(\"Import successful\")'", check=False)
if result.returncode == 0:
print("✅ Package import successful!")
else:
print("❌ Package import failed!")
return False
# Test server startup (dry run)
result = run_command("uv run mcp-esptool-server --help", check=False)
if result.returncode == 0:
print("✅ Server startup test successful!")
else:
print("❌ Server startup test failed!")
return False
return True
def main():
"""Main setup function."""
print("🚀 Setting up MCP ESPTool Server development environment...")
# Change to project directory
project_root = Path(__file__).parent.parent
os.chdir(project_root)
try:
setup_python_environment()
setup_esptool()
setup_esp_idf()
setup_git_hooks()
create_env_file()
if verify_installation():
print("\n🎉 Setup completed successfully!")
print("\nNext steps:")
print("1. Review and customize .env file")
print("2. Start development server: make dev")
print("3. Add to Claude Desktop: make claude-add")
print("4. Or use Docker: make docker-up")
else:
print("\n❌ Setup completed with errors. Check output above.")
sys.exit(1)
except KeyboardInterrupt:
print("\n⚠ Setup interrupted by user")
sys.exit(1)
except Exception as e:
print(f"\n❌ Setup failed: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
```
## 🚀 Getting Started Guide
### 1. Initial Setup
```bash
# Clone repository
git clone <repository-url> mcp-esptool-server
cd mcp-esptool-server
# Run automated setup
python scripts/setup.py
# Or manual setup
make install
```
### 2. Configuration
```bash
# Copy and customize environment
cp .env.example .env
# Edit .env with your settings
# Set up ESP-IDF (optional)
# Option A: Use Docker
make docker-up
# Option B: Install locally
# Follow: https://docs.espressif.com/projects/esp-idf/en/latest/get-started/
```
### 3. Development
```bash
# Start development server
make dev
# Run tests
make test
# Code formatting
make format
# Add to Claude Desktop
make claude-add
```
### 4. Verification
```bash
# Test basic functionality
uv run python -c "
import asyncio
from mcp_esptool_server.server import main
print('✅ Server imports successfully')
"
# Test with Claude
claude mcp list
# Should show 'esptool' server
```
This setup provides a complete development environment with modern Python tooling, Docker support, and comprehensive testing infrastructure.
`★ Insight ─────────────────────────────────────`
**Modern Python Project Structure**: This setup follows current best practices with uv for dependency management, ruff for linting/formatting, and comprehensive testing infrastructure. The multi-stage Docker setup enables both development and production deployments.
**Development Experience Focus**: The Makefile and scripts prioritize developer productivity with hot reload, automated testing, and easy Claude Desktop integration. The setup script automates the entire development environment creation.
**Production Readiness**: The configuration separates development and production concerns while maintaining compatibility with your preferred infrastructure patterns (Caddy, Docker Compose, environment variables).
`─────────────────────────────────────────────────`
<function_calls>
<invoke name="TodoWrite">
<parameter name="todos">[{"content": "Create project initialization and setup documentation", "status": "completed", "activeForm": "Creating project initialization and setup documentation"}, {"content": "Design concrete implementation examples", "status": "in_progress", "activeForm": "Designing concrete implementation examples"}, {"content": "Create production deployment guide", "status": "pending", "activeForm": "Creating production deployment guide"}]

92
README.md Normal file
View File

@ -0,0 +1,92 @@
# MCP ESPTool Server
FastMCP server providing AI-powered ESP32/ESP8266 development workflows through natural language interfaces.
## Features
- **Chip Control**: Advanced ESP device detection, connection, and control
- **Flash Operations**: Comprehensive flash memory management with safety features
- **Security Management**: ESP security features including secure boot and flash encryption
- **Production Tools**: Factory programming and batch operations
- **Middleware System**: Universal CLI tool integration with bidirectional MCP communication
- **ESP-IDF Integration**: Host application support for hardware-free development
## Quick Start
### Installation
```bash
# Install with uvx (recommended)
uvx mcp-esptool-server
# Or install in project
uv add mcp-esptool-server
```
### Claude Code Integration
```bash
# Add to Claude Code
claude mcp add mcp-esptool-server "uvx mcp-esptool-server"
```
### Development Setup
```bash
# Clone and setup
git clone <repository>
cd mcp-esptool
make dev
# Run development server
make run-debug
# Run tests
make test
```
## Architecture
The server implements a component-based architecture with middleware for CLI tool integration:
- **Components**: Specialized modules for different ESP development workflows
- **Middleware**: Universal pattern for intercepting and redirecting CLI tool output to MCP context
- **Configuration**: Environment-based configuration with auto-detection
- **Production Ready**: Docker support with development and production modes
## Components
- `ChipControl`: Device detection, connection management, reset operations
- `FlashManager`: Flash operations with verification and backup
- `PartitionManager`: Partition table management and OTA support
- `SecurityManager`: Security features and eFuse management
- `FirmwareBuilder`: ESP-IDF integration and binary operations
- `OTAManager`: Over-the-air update workflows
- `ProductionTools`: Factory programming and quality control
- `Diagnostics`: Memory dumps and performance profiling
## Configuration
Configure via environment variables or `.env` file:
```bash
ESPTOOL_PATH=esptool
ESP_DEFAULT_BAUD_RATE=460800
ESP_IDF_PATH=/path/to/esp-idf
MCP_ENABLE_PROGRESS=true
PRODUCTION_MODE=false
```
## Docker
```bash
# Development with hot reload
make docker-up
# Production deployment
DOCKER_TARGET=production make docker-up
```
## License
MIT License - see LICENSE file for details.

42
docker-compose.yml Normal file
View File

@ -0,0 +1,42 @@
services:
mcp-esptool-server:
build:
context: .
dockerfile: Dockerfile
target: ${DOCKER_TARGET:-development}
container_name: mcp-esptool-server
environment:
- ESPTOOL_PATH=${ESPTOOL_PATH:-esptool}
- ESP_DEFAULT_BAUD_RATE=${ESP_DEFAULT_BAUD_RATE:-460800}
- ESP_CONNECTION_TIMEOUT=${ESP_CONNECTION_TIMEOUT:-30}
- ESP_ENABLE_STUB_FLASHER=${ESP_ENABLE_STUB_FLASHER:-true}
- ESP_IDF_PATH=${ESP_IDF_PATH:-}
- MCP_ENABLE_PROGRESS=${MCP_ENABLE_PROGRESS:-true}
- MCP_ENABLE_ELICITATION=${MCP_ENABLE_ELICITATION:-true}
- MCP_LOG_LEVEL=${MCP_LOG_LEVEL:-INFO}
- PRODUCTION_MODE=${PRODUCTION_MODE:-false}
- DEV_ENABLE_HOT_RELOAD=${DEV_ENABLE_HOT_RELOAD:-true}
- DEV_MOCK_HARDWARE=${DEV_MOCK_HARDWARE:-false}
volumes:
# Development mode: mount source for hot reload
- .:/app:cached
# Mount ESP projects directory
- ${ESP_PROJECTS_DIR:-./esp_projects}:/workspace/esp_projects
# Mount device access for real hardware
- /dev:/dev
privileged: true # Required for USB device access
networks:
- mcp-network
ports:
- "8080:8080"
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: ./pyproject.toml
networks:
mcp-network:
external: false

132
pyproject.toml Normal file
View File

@ -0,0 +1,132 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mcp-esptool-server"
version = "2025.09.28.1"
description = "FastMCP server for ESP32/ESP8266 development with esptool integration"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [
{ name = "ESP Development Team", email = "dev@example.com" }
]
keywords = [
"mcp", "model-context-protocol", "esp32", "esp8266",
"esptool", "esp-idf", "embedded", "iot", "fastmcp"
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Embedded Systems",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
dependencies = [
"fastmcp>=2.12.4", # FastMCP framework
"esptool>=5.0.0", # ESPTool Python API
"pyserial>=3.5", # Serial communication
"pyserial-asyncio>=0.6", # Async serial support
"thefuzz[speedup]>=0.22.1", # Fuzzy string matching
"pydantic>=2.0.0", # Data validation
"click>=8.0.0", # CLI framework
"rich>=13.0.0", # Rich console output
"asyncio-mqtt>=0.16.0", # MQTT for coordination
]
[project.optional-dependencies]
dev = [
"pytest>=8.4.2",
"pytest-asyncio>=0.21.0",
"pytest-cov>=7.0.0",
"pytest-mock>=3.12.0",
"ruff>=0.13.2",
"mypy>=1.5.0",
"black>=23.0.0",
"pre-commit>=3.0.0",
"watchdog>=3.0.0", # Hot reload support
]
idf = [
"kconfiglib>=14.1.0", # Kconfig parsing
]
testing = [
"pytest-xdist>=3.0.0", # Parallel testing
"pytest-benchmark>=4.0.0", # Performance testing
"factory-boy>=3.3.0", # Test data factories
]
production = [
"prometheus-client>=0.19.0", # Metrics
"uvloop>=0.19.0", # Fast event loop
"gunicorn>=21.0.0", # WSGI server
]
[project.scripts]
mcp-esptool-server = "mcp_esptool_server.server:main"
esptool-mcp = "mcp_esptool_server.cli:cli"
[project.urls]
Homepage = "https://github.com/yourusername/mcp-esptool-server"
Repository = "https://github.com/yourusername/mcp-esptool-server"
Issues = "https://github.com/yourusername/mcp-esptool-server/issues"
Documentation = "https://yourusername.github.io/mcp-esptool-server"
[tool.hatch.build.targets.wheel]
packages = ["src/mcp_esptool_server"]
[tool.ruff]
line-length = 100
target-version = "py310"
src = ["src", "tests"]
[tool.ruff.lint]
select = ["E", "F", "W", "B", "I", "N", "UP", "ANN", "S", "C4", "DTZ", "T20"]
ignore = ["E501", "ANN101", "ANN102", "S101"]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101", "ANN"]
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
strict_optional = true
[[tool.mypy.overrides]]
module = "esptool.*"
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = [
"--cov=src/mcp_esptool_server",
"--cov-report=html",
"--cov-report=term-missing",
"--cov-fail-under=85"
]
[tool.coverage.run]
source = ["src"]
omit = ["tests/*", "*/test_*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
]

17
pytest.ini Normal file
View File

@ -0,0 +1,17 @@
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
pythonpath = src
addopts =
-v
--tb=short
--strict-markers
--disable-warnings
--color=yes
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
hardware: marks tests that require hardware
mock: marks tests that use mocking

View File

@ -0,0 +1,21 @@
"""
FastMCP ESPTool Server
A comprehensive FastMCP server for ESP32/ESP8266 development with esptool integration.
Provides AI-powered ESP development workflows with production-grade capabilities.
"""
__version__ = "2025.09.28.1"
__author__ = "ESP Development Team"
__description__ = "FastMCP server for ESP32/ESP8266 development with esptool integration"
# Package-level imports for easy access
from .config import ESPToolServerConfig
from .server import ESPToolServer, main
__all__ = [
"ESPToolServer",
"ESPToolServerConfig",
"main",
"__version__",
]

View File

@ -0,0 +1,42 @@
"""
ESP Development Components
Modular components for ESP32/ESP8266 development workflows.
Each component provides specialized functionality while maintaining clean interfaces.
"""
from .chip_control import ChipControl
from .diagnostics import Diagnostics
from .firmware_builder import FirmwareBuilder
from .flash_manager import FlashManager
from .ota_manager import OTAManager
from .partition_manager import PartitionManager
from .production_tools import ProductionTools
from .qemu_manager import QemuManager
from .security_manager import SecurityManager
# Component registry for dynamic loading
COMPONENT_REGISTRY = {
"chip_control": ChipControl,
"flash_manager": FlashManager,
"partition_manager": PartitionManager,
"security_manager": SecurityManager,
"firmware_builder": FirmwareBuilder,
"ota_manager": OTAManager,
"production_tools": ProductionTools,
"diagnostics": Diagnostics,
"qemu_manager": QemuManager,
}
__all__ = [
"ChipControl",
"FlashManager",
"PartitionManager",
"SecurityManager",
"FirmwareBuilder",
"OTAManager",
"ProductionTools",
"Diagnostics",
"QemuManager",
"COMPONENT_REGISTRY",
]

View File

@ -0,0 +1,756 @@
"""
Chip Control Component
Provides comprehensive ESP32/ESP8266 chip detection, connection management,
and basic control operations with production-grade reliability features.
"""
import asyncio
import logging
import time
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from typing import Any, TypeVar
import esptool
from fastmcp import Context, FastMCP
from ..config import ESPToolServerConfig
from ..middleware import MiddlewareFactory
logger = logging.getLogger(__name__)
# Type variable for generic return type
T = TypeVar("T")
@dataclass
class ChipInfo:
"""Information about detected ESP chip"""
chip_type: str
chip_revision: str | None = None
mac_address: str | None = None
flash_size: str | None = None
crystal_frequency: str | None = None
features: list[str] = None
efuse_info: dict[str, Any] = None
def __post_init__(self):
if self.features is None:
self.features = []
if self.efuse_info is None:
self.efuse_info = {}
@dataclass
class ConnectionInfo:
"""Information about ESP device connection"""
port: str
baud_rate: int
connected: bool = False
connection_time: float | None = None
stub_loaded: bool = False
chip_info: ChipInfo | None = None
class ChipControl:
"""ESP32/ESP8266 chip control and management"""
def __init__(self, app: FastMCP, config: ESPToolServerConfig):
self.app = app
self.config = config
self.connections: dict[str, ConnectionInfo] = {}
# Set by server after QemuManager initialization (avoids circular import)
self.qemu_manager = None
# Register tools
self._register_tools()
def _register_tools(self) -> None:
"""Register chip control tools with FastMCP"""
@self.app.tool("esp_detect_chip")
async def detect_chip(
context: Context,
port: str | None = None,
baud_rate: int | None = None,
detailed: bool = False,
) -> dict[str, Any]:
"""
Detect ESP chip type and gather comprehensive information
Args:
port: Serial port (auto-detect if not specified)
baud_rate: Connection baud rate (use config default if not specified)
detailed: Include detailed chip information and eFuse data
"""
return await self._detect_chip_impl(context, port, baud_rate, detailed)
@self.app.tool("esp_connect_advanced")
async def connect_advanced(
context: Context,
port: str | None = None,
baud_rate: int | None = None,
timeout: int | None = None,
use_stub: bool = True,
retry_count: int = 3,
) -> dict[str, Any]:
"""
Advanced ESP device connection with retry logic and stub loading
Args:
port: Serial port (auto-detect if not specified)
baud_rate: Connection baud rate
timeout: Connection timeout in seconds
use_stub: Load ROM bootloader stub for faster operations
retry_count: Number of connection attempts
"""
return await self._connect_advanced_impl(
context, port, baud_rate, timeout, use_stub, retry_count
)
@self.app.tool("esp_reset_chip")
async def reset_chip(
context: Context, port: str | None = None, reset_type: str = "hard"
) -> dict[str, Any]:
"""
Reset ESP chip using various methods
Args:
port: Serial port (use active connection if not specified)
reset_type: Type of reset (hard, soft, bootloader)
"""
return await self._reset_chip_impl(context, port, reset_type)
@self.app.tool("esp_scan_ports")
async def scan_ports(context: Context, detailed: bool = False) -> dict[str, Any]:
"""
Scan for available ESP devices on all ports
Args:
detailed: Include detailed information about each detected device
"""
return await self._scan_ports_impl(context, detailed)
@self.app.tool("esp_load_test_firmware")
async def load_test_firmware(
context: Context, port: str | None = None, firmware_type: str = "blink"
) -> dict[str, Any]:
"""
Load test firmware for chip validation
Args:
port: Serial port (auto-detect if not specified)
firmware_type: Type of test firmware (blink, hello_world, wifi_scan)
"""
return await self._load_test_firmware_impl(context, port, firmware_type)
async def _detect_chip_impl(
self, context: Context, port: str | None, baud_rate: int | None, detailed: bool
) -> dict[str, Any]:
"""Implementation of chip detection"""
# Use middleware for operation tracking
middleware = MiddlewareFactory.create_esptool_middleware(
context, f"detect_chip_{int(time.time())}"
)
async with middleware.activate():
try:
# Auto-detect port if not specified
if not port:
await middleware._log_info("🔍 Auto-detecting ESP device port...")
port = await self._auto_detect_port(context)
if not port:
return {
"success": False,
"error": "No ESP devices found on available ports",
"scanned_ports": self.config.get_common_ports(),
}
# Use provided baud rate or config default
baud_rate = baud_rate or self.config.default_baud_rate
await middleware._log_info(
f"🔌 Connecting to ESP device on {port} at {baud_rate} baud..."
)
start_time = time.time()
try:
# Use subprocess for reliable timeout (threads can't be killed)
probe_result = await self._probe_port_subprocess(port, detailed)
if not probe_result.get("available"):
await middleware._log_error(
f"Chip detection failed: {probe_result.get('error', 'Unknown')}"
)
return {
"success": False,
"error": probe_result.get("error", "Detection failed"),
"port": port,
"baud_rate": baud_rate,
}
connection_time = time.time() - start_time
# Create ChipInfo from probe result
chip_info = ChipInfo(
chip_type=probe_result.get("chip_type", "Unknown"),
mac_address=probe_result.get("mac_address"),
flash_size=probe_result.get("flash_size"),
crystal_frequency=probe_result.get("crystal_freq"),
features=probe_result.get("features"),
)
# Store connection info
self.connections[port] = ConnectionInfo(
port=port,
baud_rate=baud_rate,
connected=True,
connection_time=connection_time,
chip_info=chip_info,
)
await middleware._log_success(f"Successfully detected {chip_info.chip_type}")
return {
"success": True,
"port": port,
"baud_rate": baud_rate,
"connection_time_seconds": round(connection_time, 2),
"chip_info": {
"chip_type": chip_info.chip_type,
"mac_address": chip_info.mac_address,
"flash_size": chip_info.flash_size,
"crystal_frequency": chip_info.crystal_frequency,
"features": chip_info.features,
}
if detailed
else {
"chip_type": chip_info.chip_type,
"mac_address": chip_info.mac_address,
},
}
except Exception as e:
await middleware._log_error(f"Chip detection failed: {e}")
return {"success": False, "error": str(e), "port": port, "baud_rate": baud_rate}
except Exception as e:
await middleware._log_error(f"Detection operation failed: {e}")
return {"success": False, "error": f"Operation failed: {e}"}
async def _connect_advanced_impl(
self,
context: Context,
port: str | None,
baud_rate: int | None,
timeout: int | None,
use_stub: bool,
retry_count: int,
) -> dict[str, Any]:
"""Implementation of advanced connection"""
middleware = MiddlewareFactory.create_esptool_middleware(
context, f"connect_advanced_{int(time.time())}"
)
async with middleware.activate():
# Auto-detect port if needed
if not port:
port = await self._auto_detect_port(context)
if not port:
return {"success": False, "error": "No ESP devices found"}
baud_rate = baud_rate or self.config.default_baud_rate
connection_timeout = float(timeout or self.config.connection_timeout)
last_error = None
for attempt in range(retry_count):
await middleware._log_info(f"🔄 Connection attempt {attempt + 1}/{retry_count}")
# Capture variables for closure
target_port = port
target_baud = baud_rate
load_stub = use_stub and self.config.enable_stub_flasher
def connect_blocking() -> dict[str, Any]:
"""Blocking function to connect and get chip info"""
esp = self._connect_to_chip(target_port, target_baud)
# Load stub if requested
stub_loaded = False
if load_stub:
esp.run_stub()
stub_loaded = True
# Test connection
chip_type = esp.get_chip_description()
mac_address = ":".join(f"{b:02x}" for b in esp.read_mac())
return {
"chip_type": chip_type,
"mac_address": mac_address,
"stub_loaded": stub_loaded,
}
try:
result = await self._run_blocking_with_timeout(
connect_blocking, timeout=connection_timeout
)
# Store successful connection
self.connections[port] = ConnectionInfo(
port=port,
baud_rate=baud_rate,
connected=True,
connection_time=time.time(),
stub_loaded=result["stub_loaded"],
chip_info=ChipInfo(
chip_type=result["chip_type"],
mac_address=result["mac_address"],
),
)
await middleware._log_success(f"Connected to {result['chip_type']} on {port}")
return {
"success": True,
"port": port,
"baud_rate": baud_rate,
"attempt": attempt + 1,
"stub_loaded": result["stub_loaded"],
"chip_type": result["chip_type"],
"mac_address": result["mac_address"],
}
except asyncio.TimeoutError:
last_error = f"Connection timeout ({connection_timeout}s)"
await middleware._log_warning(f"Attempt {attempt + 1} timed out")
except Exception as e:
last_error = str(e)
await middleware._log_warning(f"Attempt {attempt + 1} failed: {e}")
if attempt < retry_count - 1:
await asyncio.sleep(1) # Brief delay between attempts
await middleware._log_error(f"All connection attempts failed. Last error: {last_error}")
return {"success": False, "error": last_error, "attempts": retry_count, "port": port}
async def _reset_chip_impl(
self, context: Context, port: str | None, reset_type: str
) -> dict[str, Any]:
"""Implementation of chip reset"""
middleware = MiddlewareFactory.create_esptool_middleware(
context, f"reset_chip_{int(time.time())}"
)
async with middleware.activate():
try:
# Find active connection or specified port
if not port:
active_connections = [
conn for conn in self.connections.values() if conn.connected
]
if not active_connections:
return {"success": False, "error": "No active connections found"}
port = active_connections[0].port
connection = self.connections.get(port)
baud_rate = connection.baud_rate if connection else self.config.default_baud_rate
# Validate reset type
if reset_type not in ("hard", "soft", "bootloader"):
return {
"success": False,
"error": f"Unknown reset type: {reset_type}",
"available_types": ["hard", "soft", "bootloader"],
}
await middleware._log_info(f"🔄 Performing {reset_type} reset on {port}")
# Capture variables for closure
target_port = port
target_baud = baud_rate
target_reset_type = reset_type
def perform_reset_blocking() -> bool:
"""Blocking function to perform reset"""
esp = self._connect_to_chip(target_port, target_baud)
if target_reset_type == "hard":
esp.hard_reset()
elif target_reset_type == "soft":
esp.soft_reset()
elif target_reset_type == "bootloader":
# Just connecting puts it in bootloader mode
pass
return True
try:
# Use timeout wrapper - 10 seconds for reset
await self._run_blocking_with_timeout(perform_reset_blocking, timeout=10.0)
# Update connection status
if port in self.connections:
self.connections[port].connected = False
await middleware._log_success(f"Reset completed: {reset_type}")
return {
"success": True,
"port": port,
"reset_type": reset_type,
"timestamp": time.time(),
}
except asyncio.TimeoutError:
await middleware._log_error("Reset operation timed out (10s)")
return {
"success": False,
"error": "Reset timeout (10s)",
"port": port,
"reset_type": reset_type,
}
except Exception as e:
await middleware._log_error(f"Reset failed: {e}")
return {"success": False, "error": str(e), "port": port, "reset_type": reset_type}
async def _scan_ports_impl(self, context: Context, detailed: bool) -> dict[str, Any]:
"""Implementation of port scanning using subprocess for reliable timeout."""
import os
import re
import subprocess
# Check common ESP device ports directly (more reliable than enumeration)
common_esp_ports = [
"/dev/ttyUSB0",
"/dev/ttyUSB1",
"/dev/ttyUSB2",
"/dev/ttyUSB3",
"/dev/ttyACM0",
"/dev/ttyACM1",
"/dev/ttyACM2",
"/dev/ttyACM3",
]
# Filter to only existing ports
usb_ports = [p for p in common_esp_ports if os.path.exists(p)]
detected_devices = []
scan_results = {}
if not usb_ports:
return {
"success": True,
"detected_devices": [],
"total_scanned": len(common_esp_ports),
"checked_ports": common_esp_ports,
"scan_results": {"note": "No USB/ACM ports found on system"},
"timestamp": time.time(),
}
for port in usb_ports:
device_info = await self._probe_port_subprocess(port, detailed)
if device_info.get("available"):
detected_devices.append(device_info)
scan_results[port] = device_info
# Include running QEMU instances
qemu_devices = []
if self.qemu_manager:
for qemu_info in self.qemu_manager.get_running_ports():
qemu_info["available"] = True
qemu_devices.append(qemu_info)
detected_devices.append(qemu_info)
return {
"success": True,
"detected_devices": detected_devices,
"total_scanned": len(usb_ports) + len(qemu_devices),
"checked_ports": common_esp_ports,
"available_ports": usb_ports,
"qemu_devices": qemu_devices if qemu_devices else None,
"scan_results": scan_results if detailed else None,
"timestamp": time.time(),
}
async def _probe_port_subprocess(self, port: str, detailed: bool = False) -> dict[str, Any]:
"""
Probe a single port using esptool as an async subprocess.
Uses asyncio.create_subprocess_exec() so it never blocks the event loop.
The subprocess can be killed on timeout, unlike Python threads.
"""
import re
try:
# Use 1 connect attempt for scanning (fast probe)
info = await self._run_esptool_async(port, "chip-id", connect_attempts=1)
if not info["success"]:
return {"port": port, "available": False, "error": info["error"]}
output = info["output"]
# Parse esptool chip-id output
result: dict[str, Any] = {"port": port, "available": True}
# Extract chip type - multiple formats:
# "Chip type: ESP32-D0WD-V3 (revision v3.1)"
# "Chip is ESP32-S3 (QFN56) (revision v0.2)"
chip_match = re.search(r"Chip type:\s*(.+?)(?:\n|$)", output)
if not chip_match:
chip_match = re.search(r"Chip is\s+(.+?)(?:\n|$)", output)
if not chip_match:
chip_match = re.search(r"Detecting chip type[.…]+\s*(\S+)", output)
if chip_match:
result["chip_type"] = chip_match.group(1).strip()
# Extract MAC address
mac_match = re.search(r"MAC:\s*([0-9a-f:]+)", output, re.IGNORECASE)
if mac_match:
result["mac_address"] = mac_match.group(1)
# Extract features if present
features_match = re.search(r"Features:\s*(.+?)(?:\n|$)", output)
if features_match:
result["features"] = [f.strip() for f in features_match.group(1).split(",")]
# Extract crystal frequency - formats:
# "Crystal frequency: 40MHz"
# "Crystal is 40MHz"
crystal_match = re.search(r"Crystal\s+(?:frequency:\s*|is\s+)(\d+)\s*MHz", output)
if crystal_match:
result["crystal_freq"] = f"{crystal_match.group(1)}MHz"
if detailed:
# Run flash-id for additional info
try:
flash_info = await self._run_esptool_async(port, "flash-id")
if flash_info["success"]:
flash_output = flash_info["output"]
flash_size_match = re.search(r"Detected flash size:\s*(\S+)", flash_output)
if flash_size_match:
result["flash_size"] = flash_size_match.group(1)
flash_mfr_match = re.search(r"Manufacturer:\s*(\S+)", flash_output)
if flash_mfr_match:
result["flash_manufacturer"] = flash_mfr_match.group(1)
else:
result["flash_info_error"] = flash_info["error"]
except Exception as e:
result["flash_info_error"] = str(e)
return result
except Exception as e:
return {"port": port, "available": False, "error": str(e)}
async def _run_esptool_async(
self,
port: str,
command: str,
timeout: float = 10.0,
connect_attempts: int = 3,
) -> dict[str, Any]:
"""
Run an esptool command as a fully async subprocess.
This is the ONLY safe way to call esptool from an async event loop:
- asyncio.create_subprocess_exec() never blocks the event loop
- asyncio.wait_for() can cancel and kill the process on timeout
- The OS sends SIGKILL if the process doesn't respond
Args:
port: Serial port
command: esptool command (e.g. "chip-id", "flash-id")
timeout: Timeout in seconds
connect_attempts: Number of connection attempts (default: 3)
Returns:
dict with "success", "output", and optionally "error"
"""
proc = None
try:
proc = await asyncio.create_subprocess_exec(
self.config.esptool_path,
"--port",
port,
"--connect-attempts",
str(connect_attempts),
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
# wait_for will cancel the coroutine on timeout
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
output = (stdout or b"").decode() + (stderr or b"").decode()
if proc.returncode != 0:
return {"success": False, "error": output.strip()[:200]}
return {"success": True, "output": output}
except asyncio.TimeoutError:
# Kill the hung process
if proc and proc.returncode is None:
proc.kill()
await proc.wait()
return {"success": False, "error": f"Timeout ({timeout}s)"}
except FileNotFoundError:
return {
"success": False,
"error": f"esptool not found at {self.config.esptool_path}",
}
except Exception as e:
if proc and proc.returncode is None:
proc.kill()
await proc.wait()
return {"success": False, "error": str(e)}
async def _load_test_firmware_impl(
self, context: Context, port: str | None, firmware_type: str
) -> dict[str, Any]:
"""Implementation of test firmware loading"""
middleware = MiddlewareFactory.create_esptool_middleware(
context, f"load_test_firmware_{int(time.time())}"
)
async with middleware.activate():
# This would integrate with ESP-IDF to build and flash test firmware
# For now, return a placeholder that shows the architecture
await middleware._log_info(f"🧪 Loading test firmware: {firmware_type}")
# Auto-detect port if needed
if not port:
port = await self._auto_detect_port(context)
if not port:
return {"success": False, "error": "No ESP devices found"}
# Check if we have test firmware available
test_firmwares = {
"blink": "Simple LED blink test",
"hello_world": "Serial output hello world",
"wifi_scan": "WiFi network scanner",
}
if firmware_type not in test_firmwares:
return {
"success": False,
"error": f"Unknown firmware type: {firmware_type}",
"available_types": list(test_firmwares.keys()),
}
await middleware._log_info(f"📦 Test firmware: {test_firmwares[firmware_type]}")
# This is where we would integrate with ESP-IDF or pre-built binaries
# For demonstration, we'll simulate the process
return {
"success": True,
"port": port,
"firmware_type": firmware_type,
"description": test_firmwares[firmware_type],
"note": "Test firmware loading requires ESP-IDF integration (coming soon)",
"timestamp": time.time(),
}
def _connect_to_chip(self, port: str, baud_rate: int, connect_attempts: int = 3):
"""
Helper method to connect to ESP chip using correct esptool API
Args:
port: Serial port
baud_rate: Connection baud rate
connect_attempts: Number of connection attempts (default: 3)
Returns:
Connected ESP device object
"""
return esptool.get_default_connected_device(
serial_list=[port],
port=port,
connect_attempts=connect_attempts,
initial_baud=baud_rate,
chip="auto",
trace=False,
before="default_reset",
)
async def _run_blocking_with_timeout(self, func: Callable[[], T], timeout: float = 5.0) -> T:
"""
Run a blocking function in a thread pool with proper timeout handling.
This solves the issue where asyncio.wait_for() times out but the
ThreadPoolExecutor context manager blocks waiting for the thread to finish.
Args:
func: Blocking function to run
timeout: Timeout in seconds (default: 5.0)
Returns:
Result of the function
Raises:
asyncio.TimeoutError: If the operation times out
Exception: Any exception from the function
"""
loop = asyncio.get_event_loop()
executor = ThreadPoolExecutor(max_workers=1)
try:
future = loop.run_in_executor(executor, func)
result = await asyncio.wait_for(future, timeout=timeout)
return result
except asyncio.TimeoutError:
# Critical: shutdown WITHOUT waiting - abandon the hung thread
# cancel_futures=True requires Python 3.9+
executor.shutdown(wait=False, cancel_futures=True)
raise
finally:
# Always try to shutdown, but don't wait
try:
executor.shutdown(wait=False, cancel_futures=True)
except Exception:
pass # Already shutdown or other error
async def _auto_detect_port(self, context: Context) -> str | None:
"""Auto-detect ESP device port using subprocess for reliable timeout."""
import os
ports = self.config.get_common_ports()
for port in ports:
if not os.path.exists(port):
continue
# Use subprocess probe - guaranteed to not hang
result = await self._probe_port_subprocess(port, detailed=False)
if result.get("available"):
return port
return None
async def health_check(self) -> dict[str, Any]:
"""Component health check"""
return {
"status": "healthy",
"active_connections": len([c for c in self.connections.values() if c.connected]),
"total_connections": len(self.connections),
"esptool_available": True, # We imported successfully
}

View File

@ -0,0 +1,55 @@
"""
Diagnostics Component
Provides comprehensive ESP device diagnostics including memory dumps,
performance profiling, and diagnostic reporting.
"""
import logging
from typing import Any
from fastmcp import Context, FastMCP
from ..config import ESPToolServerConfig
logger = logging.getLogger(__name__)
class Diagnostics:
"""ESP device diagnostics and analysis"""
def __init__(self, app: FastMCP, config: ESPToolServerConfig):
self.app = app
self.config = config
self._register_tools()
def _register_tools(self) -> None:
"""Register diagnostic tools"""
@self.app.tool("esp_memory_dump")
async def memory_dump(
context: Context,
port: str | None = None,
start_address: str = "0x0",
size: str = "1KB",
) -> dict[str, Any]:
"""Dump device memory for analysis"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_performance_profile")
async def performance_profile(
context: Context, port: str | None = None, duration: int = 30
) -> dict[str, Any]:
"""Profile device performance"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_diagnostic_report")
async def diagnostic_report(
context: Context, port: str | None = None, include_memory: bool = False
) -> dict[str, Any]:
"""Generate comprehensive diagnostic report"""
return {"success": True, "note": "Implementation coming soon"}
async def health_check(self) -> dict[str, Any]:
"""Component health check"""
return {"status": "healthy", "note": "Diagnostics ready"}

View File

@ -0,0 +1,50 @@
"""
Firmware Builder Component
Provides ESP-IDF integration for building, compiling, and managing
firmware projects with host application support.
"""
import logging
from typing import Any
from fastmcp import Context, FastMCP
from ..config import ESPToolServerConfig
logger = logging.getLogger(__name__)
class FirmwareBuilder:
"""ESP firmware building and compilation"""
def __init__(self, app: FastMCP, config: ESPToolServerConfig):
self.app = app
self.config = config
self._register_tools()
def _register_tools(self) -> None:
"""Register firmware building tools"""
@self.app.tool("esp_elf_to_binary")
async def elf_to_binary(
context: Context, elf_path: str, output_path: str | None = None
) -> dict[str, Any]:
"""Convert ELF file to flashable binary"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_firmware_analyze")
async def analyze_firmware(context: Context, firmware_path: str) -> dict[str, Any]:
"""Analyze firmware binary structure"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_binary_optimize")
async def optimize_binary(
context: Context, input_path: str, output_path: str
) -> dict[str, Any]:
"""Optimize firmware binary for size/performance"""
return {"success": True, "note": "Implementation coming soon"}
async def health_check(self) -> dict[str, Any]:
"""Component health check"""
return {"status": "healthy", "note": "Firmware builder ready"}

View File

@ -0,0 +1,70 @@
"""
Flash Manager Component
Provides comprehensive ESP flash memory operations including reading, writing,
erasing, verification, and backup with production-grade safety features.
"""
import logging
from typing import Any
from fastmcp import Context, FastMCP
from ..config import ESPToolServerConfig
logger = logging.getLogger(__name__)
class FlashManager:
"""ESP flash memory management and operations"""
def __init__(self, app: FastMCP, config: ESPToolServerConfig):
self.app = app
self.config = config
self._register_tools()
def _register_tools(self) -> None:
"""Register flash management tools"""
@self.app.tool("esp_flash_firmware")
async def flash_firmware(
context: Context, firmware_path: str, port: str | None = None, verify: bool = True
) -> dict[str, Any]:
"""Flash firmware to ESP device"""
# Implementation placeholder
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_flash_read")
async def flash_read(
context: Context,
output_path: str,
port: str | None = None,
start_address: str = "0x0",
size: str | None = None,
) -> dict[str, Any]:
"""Read flash memory contents"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_flash_erase")
async def flash_erase(
context: Context,
port: str | None = None,
start_address: str = "0x0",
size: str | None = None,
) -> dict[str, Any]:
"""Erase flash memory regions"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_flash_backup")
async def flash_backup(
context: Context,
backup_path: str,
port: str | None = None,
include_bootloader: bool = True,
) -> dict[str, Any]:
"""Create complete flash backup"""
return {"success": True, "note": "Implementation coming soon"}
async def health_check(self) -> dict[str, Any]:
"""Component health check"""
return {"status": "healthy", "note": "Flash manager ready"}

View File

@ -0,0 +1,50 @@
"""
OTA Manager Component
Handles Over-The-Air update operations including package creation,
deployment, rollback, and update management.
"""
import logging
from typing import Any
from fastmcp import Context, FastMCP
from ..config import ESPToolServerConfig
logger = logging.getLogger(__name__)
class OTAManager:
"""ESP Over-The-Air update management"""
def __init__(self, app: FastMCP, config: ESPToolServerConfig):
self.app = app
self.config = config
self._register_tools()
def _register_tools(self) -> None:
"""Register OTA management tools"""
@self.app.tool("esp_ota_package_create")
async def create_ota_package(
context: Context, firmware_path: str, version: str, output_path: str
) -> dict[str, Any]:
"""Create OTA update package"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_ota_deploy")
async def deploy_ota_update(
context: Context, package_path: str, target_url: str
) -> dict[str, Any]:
"""Deploy OTA update to device"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_ota_rollback")
async def rollback_ota(context: Context, port: str | None = None) -> dict[str, Any]:
"""Rollback to previous firmware version"""
return {"success": True, "note": "Implementation coming soon"}
async def health_check(self) -> dict[str, Any]:
"""Component health check"""
return {"status": "healthy", "note": "OTA manager ready"}

View File

@ -0,0 +1,52 @@
"""
Partition Manager Component
Handles ESP partition table operations, OTA partition management,
and custom partition configurations.
"""
import logging
from typing import Any
from fastmcp import Context, FastMCP
from ..config import ESPToolServerConfig
logger = logging.getLogger(__name__)
class PartitionManager:
"""ESP partition table management"""
def __init__(self, app: FastMCP, config: ESPToolServerConfig):
self.app = app
self.config = config
self._register_tools()
def _register_tools(self) -> None:
"""Register partition management tools"""
@self.app.tool("esp_partition_create_ota")
async def create_ota_partition(
context: Context, flash_size: str = "4MB", app_size: str = "1MB"
) -> dict[str, Any]:
"""Create OTA-enabled partition table"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_partition_custom")
async def create_custom_partition(
context: Context, partition_config: dict[str, Any]
) -> dict[str, Any]:
"""Create custom partition table"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_partition_analyze")
async def analyze_partitions(
context: Context, port: str | None = None
) -> dict[str, Any]:
"""Analyze current partition table"""
return {"success": True, "note": "Implementation coming soon"}
async def health_check(self) -> dict[str, Any]:
"""Component health check"""
return {"status": "healthy", "note": "Partition manager ready"}

View File

@ -0,0 +1,52 @@
"""
Production Tools Component
Provides factory programming, batch operations, quality control,
and production line integration tools.
"""
import logging
from typing import Any
from fastmcp import Context, FastMCP
from ..config import ESPToolServerConfig
logger = logging.getLogger(__name__)
class ProductionTools:
"""ESP production and factory programming tools"""
def __init__(self, app: FastMCP, config: ESPToolServerConfig):
self.app = app
self.config = config
self._register_tools()
def _register_tools(self) -> None:
"""Register production tools"""
@self.app.tool("esp_factory_program")
async def factory_program(
context: Context, program_config: dict[str, Any], port: str | None = None
) -> dict[str, Any]:
"""Program device for factory deployment"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_batch_program")
async def batch_program(
context: Context, device_list: list[str], firmware_path: str
) -> dict[str, Any]:
"""Program multiple devices in batch"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_quality_control")
async def quality_control(
context: Context, port: str | None = None, test_suite: str = "basic"
) -> dict[str, Any]:
"""Run quality control tests"""
return {"success": True, "note": "Implementation coming soon"}
async def health_check(self) -> dict[str, Any]:
"""Component health check"""
return {"status": "healthy", "note": "Production tools ready"}

View File

@ -0,0 +1,484 @@
"""
QEMU Emulation Manager Component
Manages Espressif QEMU fork instances for virtual ESP32 device emulation.
Each instance exposes a virtual serial port over TCP that esptool can connect
to via socket://localhost:PORT, making QEMU devices transparent to all
existing flash/chip operations.
"""
import asyncio
import logging
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from fastmcp import Context, FastMCP
from ..config import ESPToolServerConfig
logger = logging.getLogger(__name__)
# Chip type to QEMU machine/binary mapping
CHIP_MACHINES: dict[str, dict[str, str]] = {
"esp32": {"machine": "esp32", "arch": "xtensa"},
"esp32s2": {"machine": "esp32s2", "arch": "xtensa"},
"esp32s3": {"machine": "esp32s3", "arch": "xtensa"},
"esp32c3": {"machine": "esp32c3", "arch": "riscv"},
}
@dataclass
class QemuInstance:
"""Tracks a running QEMU process"""
instance_id: str
chip_type: str
tcp_port: int
flash_image: Path
flash_size_mb: int
process: asyncio.subprocess.Process | None = None
started_at: float = 0.0
pid: int | None = None
extra_args: list[str] = field(default_factory=list)
@property
def socket_uri(self) -> str:
return f"socket://localhost:{self.tcp_port}"
@property
def is_running(self) -> bool:
return self.process is not None and self.process.returncode is None
class QemuManager:
"""Manages QEMU ESP32 emulation instances"""
def __init__(self, app: FastMCP, config: ESPToolServerConfig) -> None:
self.app = app
self.config = config
self.instances: dict[str, QemuInstance] = {}
self._next_id = 1
self._register_tools()
def _get_qemu_binary(self, arch: str) -> str | None:
"""Get the QEMU binary path for the given architecture"""
if arch == "xtensa":
return self.config.qemu_xtensa_path
elif arch == "riscv":
return self.config.qemu_riscv_path
return None
def _allocate_port(self) -> int | None:
"""Find the next available TCP port for a QEMU instance"""
used_ports = {inst.tcp_port for inst in self.instances.values()}
for offset in range(self.config.qemu_max_instances):
port = self.config.qemu_base_port + offset
if port not in used_ports:
return port
return None
def _generate_id(self) -> str:
instance_id = f"qemu-{self._next_id}"
self._next_id += 1
return instance_id
def _register_tools(self) -> None:
"""Register QEMU management tools with FastMCP"""
@self.app.tool("esp_qemu_start")
async def qemu_start(
context: Context,
chip_type: str = "esp32",
flash_image: str | None = None,
flash_size_mb: int = 4,
tcp_port: int | None = None,
extra_args: list[str] | None = None,
) -> dict[str, Any]:
"""
Start a QEMU ESP32 emulation instance
Args:
chip_type: Target chip (esp32, esp32s2, esp32s3, esp32c3)
flash_image: Path to flash image file (creates blank if not specified)
flash_size_mb: Flash size in MB for blank images (default: 4)
tcp_port: TCP port for virtual serial (auto-assigned if not specified)
extra_args: Additional QEMU command-line arguments
"""
return await self._start_impl(
context, chip_type, flash_image, flash_size_mb, tcp_port, extra_args
)
@self.app.tool("esp_qemu_stop")
async def qemu_stop(
context: Context, instance_id: str | None = None
) -> dict[str, Any]:
"""
Stop a running QEMU instance
Args:
instance_id: Instance ID to stop (stops all if not specified)
"""
return await self._stop_impl(context, instance_id)
@self.app.tool("esp_qemu_list")
async def qemu_list(context: Context) -> dict[str, Any]:
"""List all QEMU instances with status"""
return await self._list_impl(context)
@self.app.tool("esp_qemu_status")
async def qemu_status(
context: Context, instance_id: str | None = None
) -> dict[str, Any]:
"""
Get detailed status of a QEMU instance
Args:
instance_id: Instance to inspect (first running if not specified)
"""
return await self._status_impl(context, instance_id)
@self.app.tool("esp_qemu_flash")
async def qemu_flash(
context: Context,
instance_id: str,
firmware_path: str,
address: str = "0x0",
) -> dict[str, Any]:
"""
Flash a firmware binary to a QEMU instance's flash image
The instance must be stopped first. This writes the binary at the
given offset into the raw flash image file, then you can restart
the instance.
Args:
instance_id: Target QEMU instance
firmware_path: Path to firmware binary to write
address: Flash address offset (hex string, default: 0x0)
"""
return await self._flash_impl(context, instance_id, firmware_path, address)
async def _start_impl(
self,
context: Context,
chip_type: str,
flash_image: str | None,
flash_size_mb: int,
tcp_port: int | None,
extra_args: list[str] | None,
) -> dict[str, Any]:
"""Start a QEMU instance"""
# Validate chip type
chip_key = chip_type.lower().replace("-", "").replace("_", "")
if chip_key not in CHIP_MACHINES:
return {
"success": False,
"error": f"Unsupported chip type: {chip_type}",
"supported_chips": list(CHIP_MACHINES.keys()),
}
machine_info = CHIP_MACHINES[chip_key]
qemu_binary = self._get_qemu_binary(machine_info["arch"])
if not qemu_binary or not Path(qemu_binary).exists():
return {
"success": False,
"error": f"QEMU binary not found for {machine_info['arch']} architecture",
"hint": "Install via: python3 $IDF_PATH/tools/idf_tools.py install qemu-xtensa qemu-riscv32",
}
# Check instance limit
running = sum(1 for inst in self.instances.values() if inst.is_running)
if running >= self.config.qemu_max_instances:
return {
"success": False,
"error": f"Maximum QEMU instances reached ({self.config.qemu_max_instances})",
"running_instances": running,
}
# Allocate port
if tcp_port is None:
tcp_port = self._allocate_port()
if tcp_port is None:
return {"success": False, "error": "No available TCP ports"}
else:
# Check port not already in use by us
used_ports = {inst.tcp_port for inst in self.instances.values() if inst.is_running}
if tcp_port in used_ports:
return {"success": False, "error": f"Port {tcp_port} already in use"}
# Prepare flash image
if flash_image:
flash_path = Path(flash_image)
if not flash_path.exists():
return {"success": False, "error": f"Flash image not found: {flash_image}"}
else:
# Create a blank flash image in a temp-like location
resources_dir = Path(__file__).parent.parent / "resources" / "qemu"
resources_dir.mkdir(parents=True, exist_ok=True)
flash_path = resources_dir / f"flash_{chip_key}_{tcp_port}.bin"
if not flash_path.exists():
_create_blank_flash(flash_path, flash_size_mb)
instance_id = self._generate_id()
# Build QEMU command
cmd = [
qemu_binary,
"-nographic",
"-machine", machine_info["machine"],
"-drive", f"file={flash_path},if=mtd,format=raw",
"-serial", f"tcp::{tcp_port},server,nowait",
]
if extra_args:
cmd.extend(extra_args)
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
# Brief pause to let QEMU bind the TCP port
await asyncio.sleep(0.5)
if proc.returncode is not None:
stderr = (await proc.stderr.read()).decode() if proc.stderr else ""
return {
"success": False,
"error": f"QEMU exited immediately (code {proc.returncode})",
"stderr": stderr[:500],
}
instance = QemuInstance(
instance_id=instance_id,
chip_type=chip_key,
tcp_port=tcp_port,
flash_image=flash_path,
flash_size_mb=flash_size_mb,
process=proc,
started_at=time.time(),
pid=proc.pid,
extra_args=extra_args or [],
)
self.instances[instance_id] = instance
logger.info(
f"Started QEMU {chip_key} instance {instance_id} on port {tcp_port} (PID {proc.pid})"
)
return {
"success": True,
"instance_id": instance_id,
"chip_type": chip_key,
"tcp_port": tcp_port,
"socket_uri": instance.socket_uri,
"flash_image": str(flash_path),
"pid": proc.pid,
"hint": f"Use port='{instance.socket_uri}' with other esp_ tools to interact with this virtual device",
}
except FileNotFoundError:
return {"success": False, "error": f"QEMU binary not found: {qemu_binary}"}
except Exception as e:
return {"success": False, "error": f"Failed to start QEMU: {e}"}
async def _stop_impl(
self, context: Context, instance_id: str | None
) -> dict[str, Any]:
"""Stop one or all QEMU instances"""
if instance_id:
instance = self.instances.get(instance_id)
if not instance:
return {
"success": False,
"error": f"Instance not found: {instance_id}",
"available": list(self.instances.keys()),
}
stopped = [await self._kill_instance(instance)]
else:
stopped = []
for inst in list(self.instances.values()):
stopped.append(await self._kill_instance(inst))
return {
"success": True,
"stopped": [s for s in stopped if s],
"remaining": sum(1 for inst in self.instances.values() if inst.is_running),
}
async def _kill_instance(self, instance: QemuInstance) -> str | None:
"""Kill a single QEMU process, return its ID if it was running"""
if instance.process and instance.process.returncode is None:
instance.process.terminate()
try:
await asyncio.wait_for(instance.process.wait(), timeout=5.0)
except asyncio.TimeoutError:
instance.process.kill()
await instance.process.wait()
logger.info(f"Stopped QEMU instance {instance.instance_id}")
return instance.instance_id
return None
async def _list_impl(self, context: Context) -> dict[str, Any]:
"""List all instances"""
instances_info = []
for inst in self.instances.values():
instances_info.append({
"instance_id": inst.instance_id,
"chip_type": inst.chip_type,
"tcp_port": inst.tcp_port,
"socket_uri": inst.socket_uri,
"running": inst.is_running,
"pid": inst.pid,
"uptime_seconds": round(time.time() - inst.started_at, 1) if inst.is_running else 0,
})
return {
"success": True,
"instances": instances_info,
"total": len(instances_info),
"running": sum(1 for i in instances_info if i["running"]),
"max_instances": self.config.qemu_max_instances,
}
async def _status_impl(
self, context: Context, instance_id: str | None
) -> dict[str, Any]:
"""Detailed status of one instance"""
if instance_id:
instance = self.instances.get(instance_id)
else:
# Pick first running instance
running = [i for i in self.instances.values() if i.is_running]
instance = running[0] if running else None
if not instance:
return {
"success": False,
"error": "No instance found" if not instance_id else f"Instance not found: {instance_id}",
"available": list(self.instances.keys()),
}
return {
"success": True,
"instance_id": instance.instance_id,
"chip_type": instance.chip_type,
"machine": CHIP_MACHINES.get(instance.chip_type, {}).get("machine"),
"tcp_port": instance.tcp_port,
"socket_uri": instance.socket_uri,
"flash_image": str(instance.flash_image),
"flash_size_mb": instance.flash_size_mb,
"running": instance.is_running,
"pid": instance.pid,
"started_at": instance.started_at,
"uptime_seconds": round(time.time() - instance.started_at, 1) if instance.is_running else 0,
"extra_args": instance.extra_args,
}
async def _flash_impl(
self,
context: Context,
instance_id: str,
firmware_path: str,
address: str,
) -> dict[str, Any]:
"""Write a firmware binary into a QEMU instance's flash image"""
instance = self.instances.get(instance_id)
if not instance:
return {"success": False, "error": f"Instance not found: {instance_id}"}
if instance.is_running:
return {
"success": False,
"error": "Instance must be stopped before flashing. Use esp_qemu_stop first.",
}
fw_path = Path(firmware_path)
if not fw_path.exists():
return {"success": False, "error": f"Firmware not found: {firmware_path}"}
try:
offset = int(address, 16) if address.startswith("0x") else int(address)
except ValueError:
return {"success": False, "error": f"Invalid address: {address}"}
flash_path = instance.flash_image
if not flash_path.exists():
return {"success": False, "error": f"Flash image missing: {flash_path}"}
try:
firmware_data = fw_path.read_bytes()
flash_data = bytearray(flash_path.read_bytes())
end = offset + len(firmware_data)
if end > len(flash_data):
return {
"success": False,
"error": f"Firmware ({len(firmware_data)} bytes at offset {offset:#x}) exceeds flash size ({len(flash_data)} bytes)",
}
flash_data[offset:end] = firmware_data
flash_path.write_bytes(bytes(flash_data))
return {
"success": True,
"instance_id": instance_id,
"firmware_path": firmware_path,
"address": f"0x{offset:08x}",
"bytes_written": len(firmware_data),
"flash_image": str(flash_path),
"hint": "Use esp_qemu_start to restart the instance with the new firmware",
}
except Exception as e:
return {"success": False, "error": f"Flash write failed: {e}"}
def get_running_ports(self) -> list[dict[str, Any]]:
"""Return socket URIs of running instances for scan integration"""
return [
{
"port": inst.socket_uri,
"chip_type": inst.chip_type,
"instance_id": inst.instance_id,
"source": "qemu",
}
for inst in self.instances.values()
if inst.is_running
]
async def health_check(self) -> dict[str, Any]:
"""Component health check"""
return {
"status": "healthy",
"qemu_xtensa_available": bool(
self.config.qemu_xtensa_path and Path(self.config.qemu_xtensa_path).exists()
),
"qemu_riscv_available": bool(
self.config.qemu_riscv_path and Path(self.config.qemu_riscv_path).exists()
),
"running_instances": sum(1 for i in self.instances.values() if i.is_running),
"max_instances": self.config.qemu_max_instances,
}
async def shutdown(self) -> None:
"""Gracefully stop all instances on server shutdown"""
for inst in list(self.instances.values()):
await self._kill_instance(inst)
self.instances.clear()
def _create_blank_flash(path: Path, size_mb: int) -> None:
"""Create a blank (all 0xFF) flash image, matching erased NOR flash state"""
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "wb") as f:
# Write in 1MB chunks to avoid huge memory allocation
chunk = b"\xff" * (1024 * 1024)
for _ in range(size_mb):
f.write(chunk)

View File

@ -0,0 +1,57 @@
"""
Security Manager Component
Handles ESP security features including secure boot, flash encryption,
eFuse management, and security auditing.
"""
import logging
from typing import Any
from fastmcp import Context, FastMCP
from ..config import ESPToolServerConfig
logger = logging.getLogger(__name__)
class SecurityManager:
"""ESP security features management"""
def __init__(self, app: FastMCP, config: ESPToolServerConfig):
self.app = app
self.config = config
self._register_tools()
def _register_tools(self) -> None:
"""Register security management tools"""
@self.app.tool("esp_security_audit")
async def security_audit(context: Context, port: str | None = None) -> dict[str, Any]:
"""Perform comprehensive security audit"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_enable_flash_encryption")
async def enable_flash_encryption(
context: Context, port: str | None = None, key_file: str | None = None
) -> dict[str, Any]:
"""Enable flash encryption with optional key"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_efuse_read")
async def read_efuse(
context: Context, port: str | None = None, efuse_name: str | None = None
) -> dict[str, Any]:
"""Read eFuse values"""
return {"success": True, "note": "Implementation coming soon"}
@self.app.tool("esp_efuse_burn")
async def burn_efuse(
context: Context, efuse_name: str, value: str, port: str | None = None
) -> dict[str, Any]:
"""Burn eFuse (DANGEROUS - requires confirmation)"""
return {"success": True, "note": "Implementation coming soon"}
async def health_check(self) -> dict[str, Any]:
"""Component health check"""
return {"status": "healthy", "note": "Security manager ready"}

View File

@ -0,0 +1,350 @@
"""
Configuration management for MCP ESPTool Server
Handles environment variables, MCP roots detection, and configuration validation.
"""
import logging
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from fastmcp import Context
logger = logging.getLogger(__name__)
@dataclass
class ESPToolServerConfig:
"""Configuration for MCP ESPTool Server"""
# Core paths and tools
esptool_path: str = field(default="esptool")
esp_idf_path: Path | None = field(default=None)
project_roots: list[Path] = field(default_factory=list)
# Communication settings
default_baud_rate: int = field(default=460800)
connection_timeout: int = field(default=30)
enable_stub_flasher: bool = field(default=True)
# MCP integration settings
enable_progress: bool = field(default=True)
enable_elicitation: bool = field(default=True)
log_level: str = field(default="INFO")
# Performance settings
max_concurrent_operations: int = field(default=5)
operation_timeout: int = field(default=300)
# Development settings
dev_enable_hot_reload: bool = field(default=False)
dev_mock_hardware: bool = field(default=False)
dev_enable_tracing: bool = field(default=False)
# Production settings
production_mode: bool = field(default=False)
enable_security_audit: bool = field(default=True)
require_confirmations: bool = field(default=True)
# QEMU emulation settings
qemu_xtensa_path: str | None = field(default=None)
qemu_riscv_path: str | None = field(default=None)
qemu_base_port: int = field(default=5555)
qemu_max_instances: int = field(default=4)
def __post_init__(self):
"""Post-initialization setup and validation"""
self._load_environment_variables()
self._setup_esp_idf_path()
self._setup_qemu_paths()
self._setup_project_roots()
self._validate_configuration()
self._setup_logging()
def _load_environment_variables(self) -> None:
"""Load configuration from environment variables"""
self.esptool_path = os.getenv("ESPTOOL_PATH", self.esptool_path)
self.default_baud_rate = int(
os.getenv("ESP_DEFAULT_BAUD_RATE", str(self.default_baud_rate))
)
self.connection_timeout = int(
os.getenv("ESP_CONNECTION_TIMEOUT", str(self.connection_timeout))
)
self.enable_stub_flasher = (
os.getenv("ESP_ENABLE_STUB_FLASHER", str(self.enable_stub_flasher)).lower() == "true"
)
self.enable_progress = (
os.getenv("MCP_ENABLE_PROGRESS", str(self.enable_progress)).lower() == "true"
)
self.enable_elicitation = (
os.getenv("MCP_ENABLE_ELICITATION", str(self.enable_elicitation)).lower() == "true"
)
self.log_level = os.getenv("MCP_LOG_LEVEL", self.log_level)
self.max_concurrent_operations = int(
os.getenv("ESP_MAX_CONCURRENT_OPERATIONS", str(self.max_concurrent_operations))
)
self.operation_timeout = int(
os.getenv("ESP_OPERATION_TIMEOUT", str(self.operation_timeout))
)
self.dev_enable_hot_reload = (
os.getenv("DEV_ENABLE_HOT_RELOAD", str(self.dev_enable_hot_reload)).lower() == "true"
)
self.dev_mock_hardware = (
os.getenv("DEV_MOCK_HARDWARE", str(self.dev_mock_hardware)).lower() == "true"
)
self.dev_enable_tracing = (
os.getenv("DEV_ENABLE_TRACING", str(self.dev_enable_tracing)).lower() == "true"
)
self.production_mode = (
os.getenv("PRODUCTION_MODE", str(self.production_mode)).lower() == "true"
)
self.enable_security_audit = (
os.getenv("PROD_ENABLE_SECURITY_AUDIT", str(self.enable_security_audit)).lower()
== "true"
)
self.require_confirmations = (
os.getenv("PROD_REQUIRE_CONFIRMATIONS", str(self.require_confirmations)).lower()
== "true"
)
# QEMU settings
self.qemu_xtensa_path = os.getenv("QEMU_XTENSA_PATH", self.qemu_xtensa_path)
self.qemu_riscv_path = os.getenv("QEMU_RISCV_PATH", self.qemu_riscv_path)
self.qemu_base_port = int(os.getenv("QEMU_BASE_PORT", str(self.qemu_base_port)))
self.qemu_max_instances = int(
os.getenv("QEMU_MAX_INSTANCES", str(self.qemu_max_instances))
)
def _setup_esp_idf_path(self) -> None:
"""Set up ESP-IDF path from environment or auto-detect"""
idf_path_env = os.getenv("ESP_IDF_PATH")
if idf_path_env:
self.esp_idf_path = Path(idf_path_env)
else:
# Try common ESP-IDF locations
common_paths = [
Path.home() / "esp" / "esp-idf",
Path("/opt/esp-idf"),
Path("/usr/local/esp-idf"),
]
for path in common_paths:
if path.exists() and (path / "idf.py").exists():
self.esp_idf_path = path
logger.info(f"Auto-detected ESP-IDF at: {path}")
break
def _setup_qemu_paths(self) -> None:
"""Auto-detect Espressif QEMU fork binaries from ~/.espressif/tools/"""
import glob
if not self.qemu_xtensa_path:
matches = glob.glob(
str(Path.home() / ".espressif/tools/qemu-xtensa/*/qemu/bin/qemu-system-xtensa")
)
if matches:
self.qemu_xtensa_path = matches[-1] # latest version
logger.info(f"Auto-detected QEMU Xtensa: {self.qemu_xtensa_path}")
if not self.qemu_riscv_path:
matches = glob.glob(
str(Path.home() / ".espressif/tools/qemu-riscv32/*/qemu/bin/qemu-system-riscv32")
)
if matches:
self.qemu_riscv_path = matches[-1]
logger.info(f"Auto-detected QEMU RISC-V: {self.qemu_riscv_path}")
def _setup_project_roots(self) -> None:
"""Set up project roots from environment or defaults"""
# Check for explicit project roots
project_roots_env = os.getenv("MCP_PROJECT_ROOTS")
if project_roots_env:
self.project_roots = [Path(p.strip()) for p in project_roots_env.split(":")]
else:
# Default project locations
default_roots = [
Path.home() / "esp_projects",
Path.home() / "Arduino",
Path.home() / "Documents" / "Arduino",
Path("/workspace/projects"), # Docker environment
]
self.project_roots = [p for p in default_roots if p.exists()]
logger.info(f"Project roots: {[str(p) for p in self.project_roots]}")
def _validate_configuration(self) -> None:
"""Validate configuration settings"""
errors = []
# Validate esptool availability
if not self._check_tool_availability(self.esptool_path):
errors.append(f"esptool not found at: {self.esptool_path}")
# Validate numeric ranges
if self.default_baud_rate not in [9600, 57600, 115200, 230400, 460800, 921600]:
logger.warning(f"Unusual baud rate: {self.default_baud_rate}")
if self.connection_timeout < 5 or self.connection_timeout > 300:
errors.append(f"Connection timeout must be between 5-300s: {self.connection_timeout}")
if self.max_concurrent_operations < 1 or self.max_concurrent_operations > 20:
errors.append(
f"Max concurrent operations must be 1-20: {self.max_concurrent_operations}"
)
if errors:
raise ValueError(f"Configuration validation failed: {'; '.join(errors)}")
def _check_tool_availability(self, tool_path: str) -> bool:
"""Check if a tool is available in PATH or at specified path"""
import shutil
return shutil.which(tool_path) is not None
def _setup_logging(self) -> None:
"""Set up logging configuration"""
log_level = getattr(logging, self.log_level.upper(), logging.INFO)
logging.basicConfig(
level=log_level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
async def initialize_with_context(self, context: Context) -> bool:
"""Initialize configuration with MCP context to get roots"""
try:
# Try to get roots from MCP context
mcp_roots = await context.list_roots()
if mcp_roots:
logger.info(f"Found {len(mcp_roots)} MCP roots")
# Add MCP roots to project roots, preferring ESP-specific directories
for root in mcp_roots:
root_path = Path(root.get("uri", "").replace("file://", ""))
if root_path.exists():
self.project_roots.append(root_path)
# Look for ESP-specific subdirectories
esp_subdirs = ["esp_projects", "arduino", "esp32", "esp8266"]
for subdir in esp_subdirs:
esp_path = root_path / subdir
if esp_path.exists():
self.project_roots.append(esp_path)
# Remove duplicates while preserving order
seen = set()
self.project_roots = [
p for p in self.project_roots if not (str(p) in seen or seen.add(str(p)))
]
logger.info(
f"Updated project roots with MCP context: {len(self.project_roots)} total"
)
return True
except Exception as e:
logger.warning(f"Could not initialize with MCP context: {e}")
return False
def get_sketch_directory(self) -> Path:
"""Get the primary sketch directory"""
# Prefer MCP roots if available
for root in self.project_roots:
if "esp" in str(root).lower() or "arduino" in str(root).lower():
return root
# Fall back to first available root
if self.project_roots:
return self.project_roots[0]
# Create default if none exist
default_dir = Path.home() / "esp_projects"
default_dir.mkdir(exist_ok=True)
return default_dir
def get_idf_available(self) -> bool:
"""Check if ESP-IDF is available"""
if not self.esp_idf_path:
return False
return self.esp_idf_path.exists() and (self.esp_idf_path / "idf.py").exists()
def get_qemu_available(self) -> bool:
"""Check if at least one QEMU binary is available"""
if self.qemu_xtensa_path and Path(self.qemu_xtensa_path).exists():
return True
if self.qemu_riscv_path and Path(self.qemu_riscv_path).exists():
return True
return False
def get_common_ports(self) -> list[str]:
"""Get list of common ESP device ports for scanning"""
import platform
system = platform.system().lower()
if system == "linux":
return [f"/dev/ttyUSB{i}" for i in range(4)] + [f"/dev/ttyACM{i}" for i in range(4)]
elif system == "darwin": # macOS
return [f"/dev/cu.usbserial-{i:04x}" for i in range(16)] + ["/dev/cu.wchusbserial*"]
elif system == "windows":
return [f"COM{i}" for i in range(1, 21)]
else:
return []
def to_dict(self) -> dict[str, Any]:
"""Convert configuration to dictionary for serialization"""
return {
"esptool_path": self.esptool_path,
"esp_idf_path": str(self.esp_idf_path) if self.esp_idf_path else None,
"project_roots": [str(p) for p in self.project_roots],
"default_baud_rate": self.default_baud_rate,
"connection_timeout": self.connection_timeout,
"enable_stub_flasher": self.enable_stub_flasher,
"max_concurrent_operations": self.max_concurrent_operations,
"production_mode": self.production_mode,
"idf_available": self.get_idf_available(),
"qemu_available": self.get_qemu_available(),
"qemu_xtensa_path": self.qemu_xtensa_path,
"qemu_riscv_path": self.qemu_riscv_path,
}
@classmethod
def from_environment(cls) -> "ESPToolServerConfig":
"""Create configuration from environment variables"""
return cls()
def __repr__(self) -> str:
"""String representation of configuration"""
return (
f"ESPToolServerConfig("
f"esptool_path='{self.esptool_path}', "
f"esp_idf_available={self.get_idf_available()}, "
f"project_roots={len(self.project_roots)}, "
f"production_mode={self.production_mode})"
)
# Global configuration instance
_config: ESPToolServerConfig | None = None
def get_config() -> ESPToolServerConfig:
"""Get the global configuration instance"""
global _config
if _config is None:
_config = ESPToolServerConfig.from_environment()
return _config
def set_config(config: ESPToolServerConfig) -> None:
"""Set the global configuration instance"""
global _config
_config = config

View File

@ -0,0 +1,16 @@
"""
MCP Middleware System
Universal middleware for integrating CLI tools with FastMCP servers.
Provides bidirectional communication, progress tracking, and user interaction.
"""
from .esptool_middleware import ESPToolMiddleware
from .logger_interceptor import LoggerInterceptor
from .middleware_factory import MiddlewareFactory
__all__ = [
"LoggerInterceptor",
"ESPToolMiddleware",
"MiddlewareFactory",
]

View File

@ -0,0 +1,362 @@
"""
ESPTool-specific middleware implementation
Provides specialized middleware for intercepting esptool operations and redirecting
output to MCP context with intelligent progress tracking and user interaction.
"""
import asyncio
import io
import logging
import re
import sys
from re import Pattern
from typing import Any
from fastmcp import Context
from .logger_interceptor import LoggerInterceptor, MiddlewareError
class ESPToolMiddleware(LoggerInterceptor):
"""ESPTool-specific middleware for MCP integration"""
def __init__(self, context: Context, operation_id: str):
super().__init__(context, operation_id)
# ESPTool-specific state
self.original_stdout = None
self.original_stderr = None
self.captured_output = io.StringIO()
self.captured_errors = io.StringIO()
# Progress tracking patterns
self.progress_patterns = self._setup_progress_patterns()
self.stage_patterns = self._setup_stage_patterns()
# Operation tracking
self.current_operation = None
self.chip_info = {}
self.flash_info = {}
def _setup_progress_patterns(self) -> dict[str, Pattern]:
"""Set up regex patterns for progress detection"""
return {
"flash_progress": re.compile(r"Writing at 0x[0-9a-f]+\.\.\. \((\d+) %\)"),
"read_progress": re.compile(r"Reading memory at 0x[0-9a-f]+\.\.\. \((\d+) %\)"),
"erase_progress": re.compile(
r"Erasing flash \(this may take a while\)\.\.\. \((\d+) %\)"
),
"verify_progress": re.compile(r"Verifying \((\d+) %\)"),
"compress_progress": re.compile(r"Compressed (\d+) bytes to (\d+)\.\.\. \((\d+) %\)"),
}
def _setup_stage_patterns(self) -> dict[str, Pattern]:
"""Set up regex patterns for stage detection"""
return {
"chip_detection": re.compile(r"Detecting chip type\.\.\. (.+)"),
"connecting": re.compile(r"Connecting\.\.\."),
"stub_loading": re.compile(r"Running stub\.\.\."),
"flash_begin": re.compile(r"Changing baud rate to (\d+)"),
"configuring_flash": re.compile(r"Configuring flash size\.\.\."),
"erasing_flash": re.compile(r"Erasing flash \(this may take a while\)\.\.\."),
"writing_flash": re.compile(r"Writing .+ bytes at 0x[0-9a-f]+\.\.\."),
"verifying": re.compile(r"Verifying\.\.\."),
"hard_reset": re.compile(r"Hard resetting via RTS pin\.\.\."),
"leaving_download": re.compile(r"Leaving\.\.\."),
}
async def install_hooks(self) -> None:
"""Install middleware hooks into esptool"""
try:
# Create custom logger that redirects to MCP
mcp_logger = self._create_mcp_logger()
# Patch esptool's logging
self.original_stdout = sys.stdout
self.original_stderr = sys.stderr
# Install our custom output capture
sys.stdout = MCPOutputCapture(self, "stdout")
sys.stderr = MCPOutputCapture(self, "stderr")
# Override esptool's main logger
esptool_logger = logging.getLogger("esptool")
esptool_logger.handlers.clear()
esptool_logger.addHandler(mcp_logger)
esptool_logger.setLevel(logging.DEBUG)
await self._log_info("🔌 ESPTool middleware hooks installed")
except Exception as e:
await self._log_error(f"Failed to install ESPTool hooks: {e}")
raise MiddlewareError(f"Hook installation failed: {e}")
async def remove_hooks(self) -> None:
"""Remove middleware hooks from esptool"""
try:
# Restore original streams
if self.original_stdout:
sys.stdout = self.original_stdout
if self.original_stderr:
sys.stderr = self.original_stderr
# Restore esptool logging
esptool_logger = logging.getLogger("esptool")
esptool_logger.handlers.clear()
await self._log_info("🔌 ESPTool middleware hooks removed")
except Exception as e:
await self._log_warning(f"Error removing ESPTool hooks: {e}")
def get_interaction_points(self) -> list[str]:
"""Return ESPTool operations that require user interaction"""
return [
"erase_flash",
"write_flash_encrypt",
"burn_efuse",
"secure_boot_signing_key",
"flash_encryption_key_generate",
"reset_to_factory",
]
def _create_mcp_logger(self) -> logging.Handler:
"""Create a logging handler that forwards to MCP context"""
class MCPLogHandler(logging.Handler):
def __init__(self, middleware):
super().__init__()
self.middleware = middleware
def emit(self, record):
try:
message = self.format(record)
# Run async logging in event loop
loop = asyncio.get_event_loop()
if record.levelno >= logging.ERROR:
loop.create_task(self.middleware._log_error(message))
elif record.levelno >= logging.WARNING:
loop.create_task(self.middleware._log_warning(message))
else:
loop.create_task(self.middleware._log_info(message))
except Exception:
pass # Prevent logging errors from breaking operations
return MCPLogHandler(self)
async def process_output_line(self, line: str, stream_type: str) -> None:
"""Process a line of output from esptool"""
if not line.strip():
return
# Check for progress updates
await self._check_progress_patterns(line)
# Check for stage changes
await self._check_stage_patterns(line)
# Check for chip information
await self._extract_chip_info(line)
# Check for flash information
await self._extract_flash_info(line)
# Check for errors
await self._check_error_patterns(line)
# Log the line if it contains useful information
if self._is_useful_output(line):
await self._log_info(f"📟 {line.strip()}")
async def _check_progress_patterns(self, line: str) -> None:
"""Check line against progress patterns and update progress"""
for operation, pattern in self.progress_patterns.items():
match = pattern.search(line)
if match:
if operation == "compress_progress":
# Special handling for compression progress
original_size = int(match.group(1))
compressed_size = int(match.group(2))
percentage = int(match.group(3))
await self._update_progress(
percentage,
f"Compressing: {original_size}{compressed_size} bytes",
current=compressed_size,
total=original_size,
)
else:
percentage = int(match.group(1))
operation_name = operation.replace("_", " ").title()
await self._update_progress(percentage, f"{operation_name}: {percentage}%")
break
async def _check_stage_patterns(self, line: str) -> None:
"""Check line against stage patterns and handle stage changes"""
for stage, pattern in self.stage_patterns.items():
match = pattern.search(line)
if match:
stage_message = self._format_stage_message(stage, match)
await self._handle_stage_start(stage_message)
# Store current operation context
self.current_operation = stage
break
def _format_stage_message(self, stage: str, match) -> str:
"""Format stage message for user display"""
stage_messages = {
"chip_detection": f"Detecting chip type: {match.group(1)}",
"connecting": "Connecting to ESP device",
"stub_loading": "Loading ROM bootloader stub",
"flash_begin": f"Setting baud rate to {match.group(1)}",
"configuring_flash": "Configuring flash parameters",
"erasing_flash": "Erasing flash memory",
"writing_flash": "Writing firmware to flash",
"verifying": "Verifying flash contents",
"hard_reset": "Performing hard reset",
"leaving_download": "Exiting download mode",
}
return stage_messages.get(stage, stage.replace("_", " ").title())
async def _extract_chip_info(self, line: str) -> None:
"""Extract chip information from esptool output"""
patterns = {
"chip_type": re.compile(r"Chip is (.+)"),
"mac_address": re.compile(r"MAC: ([0-9a-f:]{17})"),
"flash_id": re.compile(r"Detected flash size: (.+)"),
"crystal_freq": re.compile(r"Crystal is (.+)MHz"),
}
for info_type, pattern in patterns.items():
match = pattern.search(line)
if match:
self.chip_info[info_type] = match.group(1)
await self._log_info(f"📋 {info_type.replace('_', ' ').title()}: {match.group(1)}")
async def _extract_flash_info(self, line: str) -> None:
"""Extract flash information from esptool output"""
patterns = {
"flash_size": re.compile(r"Auto-detected Flash size: (.+)"),
"flash_frequency": re.compile(r"Flash frequency: (.+)"),
"flash_mode": re.compile(r"Flash mode: (.+)"),
}
for info_type, pattern in patterns.items():
match = pattern.search(line)
if match:
self.flash_info[info_type] = match.group(1)
await self._log_info(f"💾 {info_type.replace('_', ' ').title()}: {match.group(1)}")
async def _check_error_patterns(self, line: str) -> None:
"""Check for error patterns in output"""
error_patterns = [
r"Error:? (.+)",
r"Failed to (.+)",
r"Could not (.+)",
r"No such file or directory: (.+)",
r"Permission denied: (.+)",
r"Serial exception: (.+)",
]
for pattern in error_patterns:
match = re.search(pattern, line, re.IGNORECASE)
if match:
await self._log_error(f"ESPTool error: {match.group(1)}")
break
def _is_useful_output(self, line: str) -> bool:
"""Determine if output line contains useful information"""
# Skip common noise patterns
noise_patterns = [
r"^\s*$", # Empty lines
r"^Uploading stub\.\.\.",
r"^Running stub\.\.\.",
r"^Stub running\.\.\.",
r"^\.", # Progress dots
]
for pattern in noise_patterns:
if re.match(pattern, line):
return False
# Include lines with useful keywords
useful_keywords = [
"chip",
"flash",
"mac",
"crystal",
"baud",
"size",
"error",
"warning",
"failed",
"success",
"complete",
"writing",
"reading",
"erasing",
"verifying",
]
line_lower = line.lower()
return any(keyword in line_lower for keyword in useful_keywords)
async def get_operation_summary(self) -> dict[str, Any]:
"""Get summary of current operation"""
return {
"operation_id": self.operation_id,
"current_operation": self.current_operation,
"chip_info": self.chip_info,
"flash_info": self.flash_info,
"progress_history": self.progress_history[-5:], # Last 5 progress updates
"statistics": self.get_operation_statistics(),
}
class MCPOutputCapture:
"""Custom output capture that forwards to middleware"""
def __init__(self, middleware: ESPToolMiddleware, stream_type: str):
self.middleware = middleware
self.stream_type = stream_type
self.buffer = ""
def write(self, text: str) -> int:
"""Write text and process for MCP forwarding"""
self.buffer += text
# Process complete lines
while "\n" in self.buffer:
line, self.buffer = self.buffer.split("\n", 1)
# Forward to middleware async
loop = asyncio.get_event_loop()
loop.create_task(self.middleware.process_output_line(line, self.stream_type))
return len(text)
def flush(self):
"""Flush any remaining buffer content"""
if self.buffer.strip():
loop = asyncio.get_event_loop()
loop.create_task(self.middleware.process_output_line(self.buffer, self.stream_type))
self.buffer = ""
def isatty(self) -> bool:
return False
class ESPToolOperationError(MiddlewareError):
"""Raised when ESPTool operation fails"""
pass
class ESPToolConnectionError(MiddlewareError):
"""Raised when connection to ESP device fails"""
pass

View File

@ -0,0 +1,290 @@
"""
Logger Interceptor Base Class
Abstract base class for intercepting and redirecting CLI tool logging to MCP context.
Provides the foundation for bidirectional communication with any CLI tool.
"""
import logging
import time
from abc import ABC, abstractmethod
from contextlib import asynccontextmanager
from typing import Any
from fastmcp import Context
logger = logging.getLogger(__name__)
class LoggerInterceptor(ABC):
"""Abstract base class for CLI tool logger interception"""
def __init__(self, context: Context, operation_id: str):
"""
Initialize logger interceptor
Args:
context: FastMCP context for logging and user interaction
operation_id: Unique identifier for this operation
"""
self.context = context
self.operation_id = operation_id
self.operation_start_time = time.time()
# Detect MCP client capabilities
self.capabilities = self._detect_mcp_capabilities()
# Operation state
self.progress_history: list[dict[str, Any]] = []
self.user_confirmations: dict[str, bool] = {}
self.active_stages: list[str] = []
logger.debug(f"Logger interceptor initialized for operation: {operation_id}")
def _detect_mcp_capabilities(self) -> dict[str, bool]:
"""Detect available MCP client capabilities"""
capabilities = {
"logging": hasattr(self.context, "log") and callable(self.context.log),
"progress": hasattr(self.context, "progress") and callable(self.context.progress),
"elicitation": hasattr(self.context, "request_user_input")
and callable(self.context.request_user_input),
"sampling": hasattr(self.context, "sample") and callable(self.context.sample),
}
logger.debug(f"Detected MCP capabilities: {capabilities}")
return capabilities
@abstractmethod
async def install_hooks(self) -> None:
"""Install middleware hooks into the target tool"""
pass
@abstractmethod
async def remove_hooks(self) -> None:
"""Remove middleware hooks from the target tool"""
pass
@abstractmethod
def get_interaction_points(self) -> list[str]:
"""Return list of operations that require user interaction"""
pass
@asynccontextmanager
async def activate(self):
"""Context manager for middleware lifecycle"""
try:
await self.install_hooks()
await self._log_operation_start()
yield self
except Exception as e:
await self._log_error(f"Middleware activation failed: {e}")
raise
finally:
await self._log_operation_end()
await self.remove_hooks()
# Enhanced logging methods
async def _log_info(self, message: str, **kwargs) -> None:
"""Log informational message to MCP context"""
if self.capabilities["logging"]:
try:
await self.context.log(level="info", message=message, **kwargs)
except Exception as e:
logger.warning(f"Failed to log info message: {e}")
async def _log_warning(self, message: str, **kwargs) -> None:
"""Log warning message to MCP context"""
if self.capabilities["logging"]:
try:
await self.context.log(level="warning", message=f"⚠️ {message}", **kwargs)
except Exception as e:
logger.warning(f"Failed to log warning message: {e}")
async def _log_error(self, message: str, **kwargs) -> None:
"""Log error message to MCP context"""
if self.capabilities["logging"]:
try:
await self.context.log(level="error", message=f"{message}", **kwargs)
except Exception as e:
logger.error(f"Failed to log error message: {e}")
async def _log_success(self, message: str, **kwargs) -> None:
"""Log success message to MCP context"""
if self.capabilities["logging"]:
try:
await self.context.log(level="info", message=f"{message}", **kwargs)
except Exception as e:
logger.warning(f"Failed to log success message: {e}")
async def _update_progress(
self,
percentage: float,
message: str = "",
current: int | None = None,
total: int | None = None,
) -> None:
"""Update operation progress"""
if self.capabilities["progress"]:
try:
await self.context.progress(
operation_id=self.operation_id,
progress=percentage,
total=total or 100,
current=current or int(percentage),
message=message,
)
# Store progress history
self.progress_history.append(
{
"timestamp": time.time(),
"percentage": percentage,
"message": message,
"current": current,
"total": total,
}
)
except Exception as e:
logger.warning(f"Failed to update progress: {e}")
async def _request_user_confirmation(
self, prompt: str, default: bool = True, cache_key: str | None = None
) -> bool:
"""Request user confirmation with optional caching"""
# Use cache key or prompt as key
confirmation_key = cache_key or prompt
# Check cache first
if confirmation_key in self.user_confirmations:
logger.debug(f"Using cached confirmation for: {confirmation_key}")
return self.user_confirmations[confirmation_key]
if self.capabilities["elicitation"]:
try:
response = await self.context.request_user_input(
prompt=prompt, input_type="confirmation", additional_data={"default": default}
)
confirmed = response.get("confirmed", default)
self.user_confirmations[confirmation_key] = confirmed
await self._log_info(
f"User confirmation: {prompt} -> {'Yes' if confirmed else 'No'}"
)
return confirmed
except Exception as e:
await self._log_warning(f"User confirmation failed: {e}")
return default
else:
# No elicitation support, use default
await self._log_info(
f"Auto-confirming (no elicitation): {prompt} -> {'Yes' if default else 'No'}"
)
return default
async def _handle_stage_start(self, stage_message: str) -> None:
"""Handle stage start with potential user interaction"""
self.active_stages.append(stage_message)
await self._log_info(f"🔄 Starting: {stage_message}")
# Check if this stage requires user confirmation
if self._requires_user_interaction(stage_message):
confirmed = await self._request_user_confirmation(
f"🤔 About to: {stage_message}. Continue?",
default=True,
cache_key=f"stage_{stage_message}",
)
if not confirmed:
await self._log_error(f"Operation cancelled by user: {stage_message}")
raise RuntimeError(f"User cancelled operation: {stage_message}")
async def _handle_stage_end(self, stage_message: str | None = None) -> None:
"""Handle stage completion"""
if self.active_stages:
completed_stage = stage_message or self.active_stages.pop()
await self._log_success(f"Completed: {completed_stage}")
elif stage_message:
await self._log_success(f"Completed: {stage_message}")
def _requires_user_interaction(self, operation: str) -> bool:
"""Determine if operation requires user confirmation"""
critical_keywords = [
"erase",
"burn",
"encrypt",
"secure",
"factory",
"reset",
"delete",
"remove",
"clear",
"format",
"destroy",
]
operation_lower = operation.lower()
return any(keyword in operation_lower for keyword in critical_keywords)
def _format_message(self, message: str, *args) -> str:
"""Format message with optional arguments"""
try:
return message % args if args else message
except (TypeError, ValueError):
return f"{message} {' '.join(map(str, args))}" if args else message
async def _log_operation_start(self) -> None:
"""Log operation start"""
await self._log_info(f"🔧 Operation started: {self.operation_id}")
async def _log_operation_end(self) -> None:
"""Log operation completion with statistics"""
duration = time.time() - self.operation_start_time
await self._log_info(
f"⏱️ Operation completed: {self.operation_id} "
f"(duration: {duration:.2f}s, "
f"progress_updates: {len(self.progress_history)}, "
f"confirmations: {len(self.user_confirmations)})"
)
def get_operation_statistics(self) -> dict[str, Any]:
"""Get operation statistics for analysis"""
duration = time.time() - self.operation_start_time
return {
"operation_id": self.operation_id,
"duration_seconds": round(duration, 2),
"progress_updates": len(self.progress_history),
"user_confirmations": len(self.user_confirmations),
"stages_completed": len(self.active_stages),
"capabilities_used": [cap for cap, available in self.capabilities.items() if available],
"start_time": self.operation_start_time,
"end_time": time.time(),
}
class MiddlewareError(Exception):
"""Base exception for middleware-related errors"""
pass
class ToolNotFoundError(MiddlewareError):
"""Raised when target CLI tool is not found or available"""
pass
class HookInstallationError(MiddlewareError):
"""Raised when middleware hooks cannot be installed"""
pass
class UserCancellationError(MiddlewareError):
"""Raised when user cancels an operation"""
pass

View File

@ -0,0 +1,158 @@
"""
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__,
}

View File

@ -0,0 +1,421 @@
"""
Main FastMCP ESPTool Server
This is the core server that orchestrates all ESP development components using FastMCP.
Provides AI-powered ESP32/ESP8266 development workflows with production-grade capabilities.
"""
import asyncio
import logging
import signal
import sys
import time
from typing import Any
import click
from fastmcp import Context, FastMCP
from rich.console import Console
from rich.logging import RichHandler
from .components import (
ChipControl,
Diagnostics,
FirmwareBuilder,
FlashManager,
OTAManager,
PartitionManager,
ProductionTools,
QemuManager,
SecurityManager,
)
from .config import ESPToolServerConfig, get_config, set_config
# Set up rich logging
console = Console()
logging.basicConfig(
level=logging.INFO,
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(console=console, rich_tracebacks=True)],
)
logger = logging.getLogger(__name__)
class ESPToolServer:
"""FastMCP ESPTool Server - AI-powered ESP development"""
def __init__(self, config: ESPToolServerConfig | None = None):
"""Initialize the ESP Tool Server"""
self.config = config or get_config()
set_config(self.config)
# Initialize FastMCP app
self.app = FastMCP("ESP Development Server")
# Component instances
self.components: dict[str, Any] = {}
# Server state
self.startup_time = time.time()
# Initialize components
self._initialize_components()
self._setup_server_info()
self._setup_resources()
def _initialize_components(self) -> None:
"""Initialize all ESP development components"""
logger.info("🔧 Initializing ESP development components...")
try:
# Core ESP operations
self.components["chip_control"] = ChipControl(self.app, self.config)
self.components["flash_manager"] = FlashManager(self.app, self.config)
self.components["partition_manager"] = PartitionManager(self.app, self.config)
# Advanced features
self.components["security_manager"] = SecurityManager(self.app, self.config)
self.components["firmware_builder"] = FirmwareBuilder(self.app, self.config)
self.components["ota_manager"] = OTAManager(self.app, self.config)
# Production tools
self.components["production_tools"] = ProductionTools(self.app, self.config)
self.components["diagnostics"] = Diagnostics(self.app, self.config)
# QEMU emulation (if available)
if self.config.get_qemu_available():
self.components["qemu_manager"] = QemuManager(self.app, self.config)
logger.info("✅ QEMU emulation enabled")
# ESP-IDF integration (if available)
if self.config.get_idf_available():
from .components.idf_integration import IDFIntegration
self.components["idf_integration"] = IDFIntegration(self.app, self.config)
logger.info("✅ ESP-IDF integration enabled")
# Cross-wire: let ChipControl see QEMU instances for scan integration
if "qemu_manager" in self.components:
self.components["chip_control"].qemu_manager = self.components["qemu_manager"]
logger.info(f"✅ Initialized {len(self.components)} components")
except Exception as e:
logger.error(f"❌ Failed to initialize components: {e}")
raise
def _setup_server_info(self) -> None:
"""Set up server information and metadata tools"""
@self.app.tool("esp_server_info")
async def server_info(context: Context) -> dict[str, Any]:
"""Get comprehensive ESP development server information"""
uptime = time.time() - self.startup_time
# Get tool and resource counts via public API
tools = await self.app.get_tools()
# Note: FastMCP doesn't expose get_resources(), so we count our components
resource_count = 3 # esp://server/status, esp://config, esp://capabilities
return {
"server_name": "MCP ESPTool Server",
"version": "2025.09.28.1",
"uptime_seconds": round(uptime, 2),
"configuration": self.config.to_dict(),
"components": list(self.components.keys()),
"total_tools": len(tools),
"total_resources": resource_count,
"esp_idf_available": self.config.get_idf_available(),
"production_mode": self.config.production_mode,
"capabilities": {
"chip_detection": True,
"flash_operations": True,
"partition_management": True,
"security_features": True,
"ota_updates": True,
"factory_programming": True,
"host_applications": self.config.get_idf_available(),
"qemu_emulation": self.config.get_qemu_available(),
},
}
@self.app.tool("esp_list_tools")
async def list_esp_tools(context: Context, category: str | None = None) -> dict[str, Any]:
"""List all available ESP development tools, optionally filtered by category"""
# Categorize tools by component
tool_categories = {
"chip_control": [
"esp_detect_chip",
"esp_connect_advanced",
"esp_reset_chip",
"esp_load_test_firmware",
],
"flash_operations": [
"esp_flash_firmware",
"esp_flash_read",
"esp_flash_erase",
"esp_flash_backup",
],
"partition_management": [
"esp_partition_create_ota",
"esp_partition_custom",
"esp_partition_analyze",
],
"security": [
"esp_security_audit",
"esp_enable_flash_encryption",
"esp_efuse_read",
"esp_efuse_burn",
],
"firmware": ["esp_elf_to_binary", "esp_firmware_analyze", "esp_binary_optimize"],
"ota": ["esp_ota_package_create", "esp_ota_deploy", "esp_ota_rollback"],
"production": ["esp_factory_program", "esp_batch_program", "esp_quality_control"],
"diagnostics": [
"esp_memory_dump",
"esp_performance_profile",
"esp_diagnostic_report",
],
}
if self.config.get_qemu_available():
tool_categories["qemu_emulation"] = [
"esp_qemu_start",
"esp_qemu_stop",
"esp_qemu_list",
"esp_qemu_status",
"esp_qemu_flash",
]
if self.config.get_idf_available():
tool_categories["esp_idf"] = [
"idf_create_host_project",
"idf_build_project",
"idf_flash_project",
"idf_monitor",
]
if category:
if category not in tool_categories:
return {
"error": f"Unknown category: {category}",
"available_categories": list(tool_categories.keys()),
}
return {"category": category, "tools": tool_categories[category]}
return {
"total_categories": len(tool_categories),
"categories": tool_categories,
"total_tools": sum(len(tools) for tools in tool_categories.values()),
}
@self.app.tool("esp_health_check")
async def health_check(context: Context, detailed: bool = False) -> dict[str, Any]:
"""Perform health check of ESP development environment"""
health_status = {"status": "healthy", "timestamp": time.time(), "checks": {}}
# Check esptool availability
try:
import subprocess
result = subprocess.run(
[self.config.esptool_path, "version"], capture_output=True, text=True, timeout=5
)
health_status["checks"]["esptool"] = {
"available": result.returncode == 0,
"version": result.stdout.strip() if result.returncode == 0 else None,
}
except Exception as e:
health_status["checks"]["esptool"] = {"available": False, "error": str(e)}
# Check ESP-IDF availability
if self.config.esp_idf_path:
health_status["checks"]["esp_idf"] = {
"available": self.config.get_idf_available(),
"path": str(self.config.esp_idf_path),
}
# Check project directories
health_status["checks"]["project_roots"] = {
"configured": len(self.config.project_roots),
"accessible": sum(1 for root in self.config.project_roots if root.exists()),
}
# Check component health
if detailed:
component_health = {}
for component_name, component in self.components.items():
if hasattr(component, "health_check"):
try:
component_health[component_name] = await component.health_check()
except Exception as e:
component_health[component_name] = {"status": "error", "error": str(e)}
else:
component_health[component_name] = {
"status": "ok",
"note": "No health check available",
}
health_status["components"] = component_health
# Determine overall health
failed_checks = [
name
for name, check in health_status["checks"].items()
if not check.get("available", True)
]
if failed_checks:
health_status["status"] = "degraded"
health_status["failed_checks"] = failed_checks
return health_status
def _setup_resources(self) -> None:
"""Set up MCP resources for real-time information"""
@self.app.resource("esp://server/status")
async def server_status() -> str:
"""Real-time server status information"""
uptime = time.time() - self.startup_time
status = {
"status": "running",
"uptime_seconds": round(uptime, 2),
"components_loaded": len(self.components),
"production_mode": self.config.production_mode,
"last_updated": time.time(),
}
return status
@self.app.resource("esp://config")
async def current_config() -> str:
"""Current server configuration"""
return self.config.to_dict()
@self.app.resource("esp://capabilities")
async def server_capabilities() -> str:
"""Server capabilities and feature availability"""
return {
"esp_chip_support": [
"ESP32",
"ESP32-S2",
"ESP32-S3",
"ESP32-C3",
"ESP32-C6",
"ESP8266",
],
"flash_operations": ["read", "write", "erase", "verify", "encrypt"],
"partition_features": ["custom_tables", "ota_support", "nvs_management"],
"security_features": ["efuse_management", "secure_boot", "flash_encryption"],
"production_features": [
"factory_programming",
"batch_operations",
"quality_control",
],
"debugging_features": [
"memory_dump",
"performance_profiling",
"diagnostic_reports",
],
"esp_idf_integration": self.config.get_idf_available(),
"host_applications": self.config.get_idf_available(),
"qemu_emulation": self.config.get_qemu_available(),
}
def run(self, transport: str = "stdio") -> None:
"""
Run the FastMCP server
Args:
transport: Transport type (stdio, sse, http)
"""
# Log startup information
logger.info("🚀 Starting ESP Development Server...")
logger.info(f"📊 Loaded {len(self.components)} components")
# Check esptool availability
import subprocess
try:
result = subprocess.run(
[self.config.esptool_path, "version"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0:
logger.info(f"✅ esptool available: {result.stdout.strip().split()[0]}")
else:
logger.warning("⚠️ esptool not available - some features may be limited")
except (subprocess.TimeoutExpired, FileNotFoundError):
logger.warning("⚠️ esptool not found - some features may be limited")
if self.config.get_qemu_available():
logger.info("🖥️ QEMU emulation available")
if self.config.get_idf_available():
logger.info("🏗️ ESP-IDF integration available")
if self.config.production_mode:
logger.info("🏭 Running in production mode")
else:
logger.info("🛠️ Running in development mode")
logger.info("✅ Server ready - waiting for MCP connections...")
# Use FastMCP's built-in run() method
self.app.run(transport=transport)
# CLI interface
@click.command()
@click.option("--config", "-c", help="Configuration file path")
@click.option("--debug", "-d", is_flag=True, help="Enable debug logging")
@click.option("--production", "-p", is_flag=True, help="Run in production mode")
@click.option("--port", default=8080, help="Server port (for future HTTP interface)")
@click.version_option(version="2025.09.28.1")
def main(config: str | None, debug: bool, production: bool, port: int) -> None:
"""
FastMCP ESP Development Server
Provides AI-powered ESP32/ESP8266 development workflows through natural language.
"""
# Configure logging level
if debug:
logging.getLogger().setLevel(logging.DEBUG)
logger.info("🐛 Debug logging enabled")
# Load configuration
if config:
logger.info(f"📁 Loading configuration from: {config}")
# TODO: Implement configuration file loading
server_config = ESPToolServerConfig.from_environment()
else:
server_config = ESPToolServerConfig.from_environment()
# Override production mode if specified
if production:
server_config.production_mode = True
logger.info("🏭 Production mode enabled via CLI")
# Display startup banner
console.print("\n[bold blue]🚀 FastMCP ESP Development Server[/bold blue]")
console.print("[dim]AI-powered ESP32/ESP8266 development workflows[/dim]")
console.print("[dim]Version: 2025.09.28.1[/dim]")
console.print()
# Create and run server
try:
server = ESPToolServer(server_config)
server.run()
except Exception as e:
logger.error(f"❌ Failed to start server: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

1
tests/__init__.py Normal file
View File

@ -0,0 +1 @@
# Test package initialization

84
tests/test_config.py Normal file
View File

@ -0,0 +1,84 @@
"""
Test configuration management
"""
import os
from pathlib import Path
import pytest
from mcp_esptool_server.config import ESPToolServerConfig
def test_config_from_environment():
"""Test configuration creation from environment variables"""
config = ESPToolServerConfig.from_environment()
assert config.esptool_path == "esptool" # default value
assert config.default_baud_rate == 460800
assert config.connection_timeout == 30
assert config.enable_stub_flasher is True
assert config.production_mode is False
def test_config_environment_override():
"""Test environment variable override"""
from unittest.mock import patch
# Set test environment variables
test_env = {
"ESPTOOL_PATH": "/custom/esptool",
"ESP_DEFAULT_BAUD_RATE": "115200",
"PRODUCTION_MODE": "true",
}
# Temporarily override environment
original_env = {}
for key, value in test_env.items():
original_env[key] = os.getenv(key)
os.environ[key] = value
try:
# Mock tool availability check to always return True
with patch.object(ESPToolServerConfig, "_check_tool_availability", return_value=True):
config = ESPToolServerConfig.from_environment()
assert config.esptool_path == "/custom/esptool"
assert config.default_baud_rate == 115200
assert config.production_mode is True
finally:
# Restore original environment
for key, value in original_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
def test_config_to_dict():
"""Test configuration serialization"""
config = ESPToolServerConfig()
config_dict = config.to_dict()
assert isinstance(config_dict, dict)
assert "esptool_path" in config_dict
assert "default_baud_rate" in config_dict
assert "production_mode" in config_dict
def test_common_ports():
"""Test common port detection"""
config = ESPToolServerConfig()
ports = config.get_common_ports()
assert isinstance(ports, list)
assert len(ports) > 0 # Should return some ports for any platform
@pytest.mark.skipif(not Path("/usr/bin/esptool").exists(), reason="esptool not found")
def test_tool_availability():
"""Test tool availability check"""
config = ESPToolServerConfig()
# This should work if esptool is in PATH
available = config._check_tool_availability("esptool")
assert isinstance(available, bool)

140
tests/test_middleware.py Normal file
View File

@ -0,0 +1,140 @@
"""
Test middleware system
"""
from unittest.mock import AsyncMock
import pytest
from mcp_esptool_server.middleware import LoggerInterceptor, MiddlewareFactory
class MockContext:
"""Mock FastMCP context for testing"""
def __init__(self):
self.log = AsyncMock()
self.progress = AsyncMock()
self.request_user_input = AsyncMock()
self.sample = AsyncMock()
def test_middleware_factory_supported_tools():
"""Test middleware factory tool support"""
supported = MiddlewareFactory.get_supported_tools()
assert isinstance(supported, dict)
assert "esptool" in supported
assert isinstance(supported["esptool"], str)
def test_middleware_factory_tool_support_check():
"""Test tool support checking"""
assert MiddlewareFactory.is_tool_supported("esptool")
assert not MiddlewareFactory.is_tool_supported("nonexistent_tool")
def test_middleware_factory_create_esptool():
"""Test ESPTool middleware creation"""
context = MockContext()
middleware = MiddlewareFactory.create_esptool_middleware(context)
assert middleware is not None
assert middleware.context == context
assert middleware.operation_id.startswith("esptool_")
def test_middleware_factory_unsupported_tool():
"""Test error handling for unsupported tools"""
context = MockContext()
with pytest.raises(Exception): # Should raise ToolNotFoundError
MiddlewareFactory.create_middleware("unsupported_tool", context)
def test_middleware_info():
"""Test middleware information retrieval"""
info = MiddlewareFactory.get_middleware_info("esptool")
assert isinstance(info, dict)
assert info["tool_name"] == "esptool"
assert "middleware_class" in info
assert "description" in info
def test_logger_interceptor_capabilities():
"""Test logger interceptor capability detection"""
context = MockContext()
# Create a concrete implementation for testing
class TestInterceptor(LoggerInterceptor):
async def install_hooks(self):
pass
async def remove_hooks(self):
pass
def get_interaction_points(self):
return ["test_operation"]
interceptor = TestInterceptor(context, "test_op")
assert interceptor.capabilities["logging"] is True
assert interceptor.capabilities["progress"] is True
assert interceptor.capabilities["elicitation"] is True
@pytest.mark.asyncio
async def test_logger_interceptor_logging():
"""Test logger interceptor logging methods"""
context = MockContext()
class TestInterceptor(LoggerInterceptor):
async def install_hooks(self):
pass
async def remove_hooks(self):
pass
def get_interaction_points(self):
return []
interceptor = TestInterceptor(context, "test_op")
# Test logging methods
await interceptor._log_info("Test info message")
await interceptor._log_warning("Test warning")
await interceptor._log_error("Test error")
await interceptor._log_success("Test success")
# Verify context.log was called
assert context.log.call_count == 4
@pytest.mark.asyncio
async def test_logger_interceptor_progress():
"""Test logger interceptor progress tracking"""
context = MockContext()
class TestInterceptor(LoggerInterceptor):
async def install_hooks(self):
pass
async def remove_hooks(self):
pass
def get_interaction_points(self):
return []
interceptor = TestInterceptor(context, "test_op")
# Test progress update
await interceptor._update_progress(50, "Half complete")
# Verify context.progress was called
context.progress.assert_called_once()
# Check progress history
assert len(interceptor.progress_history) == 1
assert interceptor.progress_history[0]["percentage"] == 50

461
tests/test_qemu_manager.py Normal file
View File

@ -0,0 +1,461 @@
"""
Test QEMU Manager component
Tests lifecycle management, port allocation, flash image handling,
and integration with the scan system.
"""
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
import pytest
from mcp_esptool_server.components.qemu_manager import (
CHIP_MACHINES,
QemuInstance,
QemuManager,
_create_blank_flash,
)
from mcp_esptool_server.config import ESPToolServerConfig
@pytest.fixture
def config():
"""Config with QEMU paths pointing to real Espressif binaries (if installed)"""
return ESPToolServerConfig()
@pytest.fixture
def mock_app():
"""Mock FastMCP app that captures registered tools"""
app = MagicMock()
registered_tools = {}
def tool_decorator(name):
def decorator(func):
registered_tools[name] = func
return func
return decorator
app.tool = tool_decorator
app._registered_tools = registered_tools
return app
@pytest.fixture
def manager(mock_app, config):
return QemuManager(mock_app, config)
@pytest.fixture
def mock_context():
ctx = MagicMock()
ctx.log = AsyncMock()
ctx.progress = AsyncMock()
return ctx
class TestChipMachines:
def test_supported_chips(self):
assert "esp32" in CHIP_MACHINES
assert "esp32s3" in CHIP_MACHINES
assert "esp32c3" in CHIP_MACHINES
def test_xtensa_arch(self):
assert CHIP_MACHINES["esp32"]["arch"] == "xtensa"
assert CHIP_MACHINES["esp32s3"]["arch"] == "xtensa"
def test_riscv_arch(self):
assert CHIP_MACHINES["esp32c3"]["arch"] == "riscv"
def test_machine_names(self):
for _key, info in CHIP_MACHINES.items():
assert "machine" in info
assert "arch" in info
class TestQemuInstance:
def test_socket_uri(self):
inst = QemuInstance(
instance_id="qemu-1",
chip_type="esp32",
tcp_port=5555,
flash_image=Path("/tmp/flash.bin"),
flash_size_mb=4,
)
assert inst.socket_uri == "socket://localhost:5555"
def test_is_running_no_process(self):
inst = QemuInstance(
instance_id="qemu-1",
chip_type="esp32",
tcp_port=5555,
flash_image=Path("/tmp/flash.bin"),
flash_size_mb=4,
)
assert not inst.is_running
def test_is_running_with_active_process(self):
proc = MagicMock()
proc.returncode = None # still running
inst = QemuInstance(
instance_id="qemu-1",
chip_type="esp32",
tcp_port=5555,
flash_image=Path("/tmp/flash.bin"),
flash_size_mb=4,
process=proc,
)
assert inst.is_running
def test_is_running_with_exited_process(self):
proc = MagicMock()
proc.returncode = 0 # exited
inst = QemuInstance(
instance_id="qemu-1",
chip_type="esp32",
tcp_port=5555,
flash_image=Path("/tmp/flash.bin"),
flash_size_mb=4,
process=proc,
)
assert not inst.is_running
class TestQemuManagerInit:
def test_registers_tools(self, mock_app, config):
QemuManager(mock_app, config)
tools = mock_app._registered_tools
assert "esp_qemu_start" in tools
assert "esp_qemu_stop" in tools
assert "esp_qemu_list" in tools
assert "esp_qemu_status" in tools
assert "esp_qemu_flash" in tools
def test_port_allocation(self, manager):
port = manager._allocate_port()
assert port == manager.config.qemu_base_port
def test_port_allocation_skips_used(self, manager):
# Simulate an occupied port
proc = MagicMock()
proc.returncode = None
manager.instances["qemu-1"] = QemuInstance(
instance_id="qemu-1",
chip_type="esp32",
tcp_port=manager.config.qemu_base_port,
flash_image=Path("/tmp/f.bin"),
flash_size_mb=4,
process=proc,
)
port = manager._allocate_port()
assert port == manager.config.qemu_base_port + 1
def test_port_allocation_exhausted(self, manager):
proc = MagicMock()
proc.returncode = None
for i in range(manager.config.qemu_max_instances):
manager.instances[f"qemu-{i}"] = QemuInstance(
instance_id=f"qemu-{i}",
chip_type="esp32",
tcp_port=manager.config.qemu_base_port + i,
flash_image=Path(f"/tmp/f{i}.bin"),
flash_size_mb=4,
process=proc,
)
assert manager._allocate_port() is None
def test_id_generation(self, manager):
assert manager._generate_id() == "qemu-1"
assert manager._generate_id() == "qemu-2"
def test_get_qemu_binary_xtensa(self, manager):
binary = manager._get_qemu_binary("xtensa")
assert binary == manager.config.qemu_xtensa_path
def test_get_qemu_binary_riscv(self, manager):
binary = manager._get_qemu_binary("riscv")
assert binary == manager.config.qemu_riscv_path
def test_get_qemu_binary_unknown(self, manager):
assert manager._get_qemu_binary("arm") is None
class TestStartImpl:
@pytest.mark.asyncio
async def test_unsupported_chip(self, manager, mock_context):
result = await manager._start_impl(mock_context, "esp8266", None, 4, None, None)
assert not result["success"]
assert "Unsupported chip" in result["error"]
@pytest.mark.asyncio
async def test_missing_binary(self, manager, mock_context):
manager.config.qemu_xtensa_path = "/nonexistent/qemu"
result = await manager._start_impl(mock_context, "esp32", None, 4, None, None)
assert not result["success"]
assert "not found" in result["error"]
@pytest.mark.asyncio
async def test_max_instances_reached(self, manager, mock_context):
proc = MagicMock()
proc.returncode = None
for i in range(manager.config.qemu_max_instances):
manager.instances[f"qemu-{i}"] = QemuInstance(
instance_id=f"qemu-{i}",
chip_type="esp32",
tcp_port=5555 + i,
flash_image=Path(f"/tmp/f{i}.bin"),
flash_size_mb=4,
process=proc,
)
result = await manager._start_impl(mock_context, "esp32", None, 4, None, None)
assert not result["success"]
assert "Maximum" in result["error"]
@pytest.mark.asyncio
async def test_missing_flash_image(self, manager, mock_context):
manager.config.qemu_xtensa_path = "/bin/true" # exists but not real qemu
result = await manager._start_impl(
mock_context, "esp32", "/nonexistent/flash.bin", 4, None, None
)
assert not result["success"]
assert "not found" in result["error"]
@pytest.mark.asyncio
async def test_port_conflict(self, manager, mock_context):
proc = MagicMock()
proc.returncode = None
manager.instances["qemu-0"] = QemuInstance(
instance_id="qemu-0",
chip_type="esp32",
tcp_port=5555,
flash_image=Path("/tmp/f.bin"),
flash_size_mb=4,
process=proc,
)
manager.config.qemu_xtensa_path = "/bin/true"
result = await manager._start_impl(mock_context, "esp32", None, 4, 5555, None)
assert not result["success"]
assert "already in use" in result["error"]
class TestStopImpl:
@pytest.mark.asyncio
async def test_stop_nonexistent(self, manager, mock_context):
result = await manager._stop_impl(mock_context, "qemu-999")
assert not result["success"]
assert "not found" in result["error"]
@pytest.mark.asyncio
async def test_stop_running_instance(self, manager, mock_context):
proc = AsyncMock()
proc.returncode = None
proc.terminate = MagicMock()
proc.kill = MagicMock()
proc.wait = AsyncMock()
manager.instances["qemu-1"] = QemuInstance(
instance_id="qemu-1",
chip_type="esp32",
tcp_port=5555,
flash_image=Path("/tmp/f.bin"),
flash_size_mb=4,
process=proc,
)
result = await manager._stop_impl(mock_context, "qemu-1")
assert result["success"]
assert "qemu-1" in result["stopped"]
proc.terminate.assert_called_once()
@pytest.mark.asyncio
async def test_stop_all(self, manager, mock_context):
for i in range(2):
proc = AsyncMock()
proc.returncode = None
proc.terminate = MagicMock()
proc.kill = MagicMock()
proc.wait = AsyncMock()
manager.instances[f"qemu-{i}"] = QemuInstance(
instance_id=f"qemu-{i}",
chip_type="esp32",
tcp_port=5555 + i,
flash_image=Path(f"/tmp/f{i}.bin"),
flash_size_mb=4,
process=proc,
)
result = await manager._stop_impl(mock_context, None)
assert result["success"]
assert len(result["stopped"]) == 2
class TestListImpl:
@pytest.mark.asyncio
async def test_list_empty(self, manager, mock_context):
result = await manager._list_impl(mock_context)
assert result["success"]
assert result["total"] == 0
@pytest.mark.asyncio
async def test_list_with_instances(self, manager, mock_context):
proc = MagicMock()
proc.returncode = None
manager.instances["qemu-1"] = QemuInstance(
instance_id="qemu-1",
chip_type="esp32",
tcp_port=5555,
flash_image=Path("/tmp/f.bin"),
flash_size_mb=4,
process=proc,
started_at=1000.0,
)
result = await manager._list_impl(mock_context)
assert result["total"] == 1
assert result["running"] == 1
assert result["instances"][0]["socket_uri"] == "socket://localhost:5555"
class TestFlashImpl:
@pytest.mark.asyncio
async def test_flash_nonexistent_instance(self, manager, mock_context):
result = await manager._flash_impl(mock_context, "qemu-999", "/tmp/fw.bin", "0x0")
assert not result["success"]
@pytest.mark.asyncio
async def test_flash_running_instance(self, manager, mock_context):
proc = MagicMock()
proc.returncode = None
manager.instances["qemu-1"] = QemuInstance(
instance_id="qemu-1",
chip_type="esp32",
tcp_port=5555,
flash_image=Path("/tmp/f.bin"),
flash_size_mb=4,
process=proc,
)
result = await manager._flash_impl(mock_context, "qemu-1", "/tmp/fw.bin", "0x0")
assert not result["success"]
assert "stopped" in result["error"].lower()
@pytest.mark.asyncio
async def test_flash_writes_data(self, manager, mock_context, tmp_path):
flash_file = tmp_path / "flash.bin"
flash_file.write_bytes(b"\xff" * 1024)
fw_file = tmp_path / "firmware.bin"
fw_file.write_bytes(b"\xde\xad\xbe\xef")
manager.instances["qemu-1"] = QemuInstance(
instance_id="qemu-1",
chip_type="esp32",
tcp_port=5555,
flash_image=flash_file,
flash_size_mb=1,
process=None, # stopped
)
result = await manager._flash_impl(
mock_context, "qemu-1", str(fw_file), "0x100"
)
assert result["success"]
assert result["bytes_written"] == 4
# Verify data was written at correct offset
data = flash_file.read_bytes()
assert data[0x100:0x104] == b"\xde\xad\xbe\xef"
assert data[0x0FF] == 0xFF # byte before is unchanged
class TestBlankFlash:
def test_creates_correct_size(self, tmp_path):
path = tmp_path / "flash.bin"
_create_blank_flash(path, 2)
assert path.stat().st_size == 2 * 1024 * 1024
def test_all_ff(self, tmp_path):
path = tmp_path / "flash.bin"
_create_blank_flash(path, 1)
data = path.read_bytes()
assert all(b == 0xFF for b in data)
def test_creates_parent_dirs(self, tmp_path):
path = tmp_path / "nested" / "dir" / "flash.bin"
_create_blank_flash(path, 1)
assert path.exists()
class TestGetRunningPorts:
def test_empty(self, manager):
assert manager.get_running_ports() == []
def test_returns_running_only(self, manager):
running_proc = MagicMock()
running_proc.returncode = None
stopped_proc = MagicMock()
stopped_proc.returncode = 0
manager.instances["qemu-1"] = QemuInstance(
instance_id="qemu-1",
chip_type="esp32",
tcp_port=5555,
flash_image=Path("/tmp/f.bin"),
flash_size_mb=4,
process=running_proc,
)
manager.instances["qemu-2"] = QemuInstance(
instance_id="qemu-2",
chip_type="esp32c3",
tcp_port=5556,
flash_image=Path("/tmp/f2.bin"),
flash_size_mb=4,
process=stopped_proc,
)
ports = manager.get_running_ports()
assert len(ports) == 1
assert ports[0]["port"] == "socket://localhost:5555"
assert ports[0]["source"] == "qemu"
assert ports[0]["instance_id"] == "qemu-1"
class TestHealthCheck:
@pytest.mark.asyncio
async def test_health_check(self, manager):
result = await manager.health_check()
assert result["status"] == "healthy"
assert "running_instances" in result
assert "max_instances" in result
class TestShutdown:
@pytest.mark.asyncio
async def test_shutdown_kills_all(self, manager):
for i in range(2):
proc = AsyncMock()
proc.returncode = None
proc.terminate = MagicMock()
proc.kill = MagicMock()
proc.wait = AsyncMock()
manager.instances[f"qemu-{i}"] = QemuInstance(
instance_id=f"qemu-{i}",
chip_type="esp32",
tcp_port=5555 + i,
flash_image=Path(f"/tmp/f{i}.bin"),
flash_size_mb=4,
process=proc,
)
await manager.shutdown()
assert len(manager.instances) == 0
class TestConfigQemu:
def test_qemu_available(self):
config = ESPToolServerConfig()
# Should return True if auto-detected
result = config.get_qemu_available()
assert isinstance(result, bool)
def test_to_dict_includes_qemu(self):
config = ESPToolServerConfig()
d = config.to_dict()
assert "qemu_available" in d
assert "qemu_xtensa_path" in d
assert "qemu_riscv_path" in d

2279
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff