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

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

444 lines
17 KiB
Markdown

# 🔗 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"}]