From caab7d882c26cc99418ea4617bf14e726d3833ae Mon Sep 17 00:00:00 2001 From: maitiSoutrik Date: Sat, 31 Jan 2026 21:53:58 -0800 Subject: [PATCH] feat: add additional ESP-IDF tools New MCP tools added: - clean_esp_project: Clean build artifacts (clean/fullclean) - get_esp_project_size: Analyze firmware RAM/Flash usage - get_esp_component_size: Per-component size breakdown - erase_esp_flash: Erase device flash memory - monitor_esp_device: Capture serial output with timeout - get_esp_app_info: Get project description and app info Updated README with tool reference table. --- README.md | 25 +++++++++ main.py | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/README.md b/README.md index 590175b..542305b 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,11 @@ This project is currently a **Proof of Concept (PoC)** for an MCP server tailore **Current Capabilities:** * Supports basic ESP-IDF project build commands. * Flash built firmware to connected ESP devices with optional port specification. +* Clean build artifacts (clean or fullclean). +* Analyze firmware size and per-component breakdown. +* Erase device flash memory. +* Monitor serial output from connected devices. +* Get project and app information. * Includes experimental support for automatic issue fixing based on build logs. **Vision & Future Work:** @@ -77,9 +82,29 @@ Once the `esp-mcp` server is configured and running, your LLM or chatbot can int * "Build the project located at `/path/to/my/esp-project` using the `esp-mcp`." * "Clean the build files for the ESP32 project in the `examples/hello_world` directory." * "Flash the firmware to my connected ESP32 device for the project in `my_app`." +* "Clean the build artifacts for the project at `/path/to/project`." +* "Show me the size breakdown of my ESP32 firmware." +* "Erase the flash on my connected ESP32." +* "Monitor the serial output from my ESP32 for 30 seconds." The MCP server will then execute the corresponding ESP-IDF commands (like `idf.py build`, `idf.py fullclean`, `idf.py flash`) based on the tools implemented in `main.py`. +### Available Tools + +| Tool | Description | ESP-IDF Command | +|------|-------------|-----------------| +| `build_esp_related_project` | Build an ESP-IDF project | `idf.py build` | +| `flash_esp_project` | Flash firmware to device | `idf.py flash` | +| `clean_esp_project` | Clean build artifacts | `idf.py clean` / `fullclean` | +| `get_esp_project_size` | Analyze firmware size | `idf.py size` | +| `get_esp_component_size` | Per-component size breakdown | `idf.py size-components` | +| `erase_esp_flash` | Erase device flash memory | `idf.py erase-flash` | +| `monitor_esp_device` | Capture serial output | `idf.py monitor` | +| `setup_project_esp_target` | Set target chip | `idf.py set-target` | +| `create_esp_project` | Create new project | `idf.py create-project` | +| `list_esp_serial_ports` | List available ports | `python -m serial.tools.list_ports` | +| `get_esp_app_info` | Get app/project info | Reads `project_description.json` | + The `result.gif` below shows an example interaction: ![Result](./result.gif) diff --git a/main.py b/main.py index f8661c8..f2a9747 100644 --- a/main.py +++ b/main.py @@ -113,5 +113,160 @@ async def list_esp_serial_ports() -> (str, str): return stdout, stderr + +@mcp.tool() +async def clean_esp_project(project_path: str, full_clean: bool = False) -> (str, str): + """Clean build artifacts from an ESP-IDF project. + + Args: + project_path: Path to the ESP-IDF project + full_clean: If True, performs fullclean (removes build dir entirely). + If False, performs regular clean. + + Returns: + tuple: (stdout, stderr) - Clean operation logs + """ + os.chdir(project_path) + export_script = get_export_script() + + clean_cmd = "fullclean" if full_clean else "clean" + returncode, stdout, stderr = await run_command_async( + f"bash -c 'source {export_script} && idf.py {clean_cmd}'" + ) + + open('mcp-clean.log', 'w+').write(str((stdout, stderr))) + logging.warning(f"clean result {stdout} {stderr}") + return stdout, stderr + + +@mcp.tool() +async def get_esp_project_size(project_path: str) -> (str, str): + """Analyze the size of a built ESP-IDF project firmware. + + Args: + project_path: Path to the ESP-IDF project (must be built first) + + Returns: + tuple: (stdout, stderr) - Size analysis output showing RAM/Flash usage + """ + os.chdir(project_path) + export_script = get_export_script() + + returncode, stdout, stderr = await run_command_async( + f"bash -c 'source {export_script} && idf.py size'" + ) + + open('mcp-size.log', 'w+').write(str((stdout, stderr))) + logging.warning(f"size result {stdout} {stderr}") + return stdout, stderr + + +@mcp.tool() +async def get_esp_component_size(project_path: str) -> (str, str): + """Get detailed per-component size breakdown of an ESP-IDF project. + + Args: + project_path: Path to the ESP-IDF project (must be built first) + + Returns: + tuple: (stdout, stderr) - Detailed component size breakdown + """ + os.chdir(project_path) + export_script = get_export_script() + + returncode, stdout, stderr = await run_command_async( + f"bash -c 'source {export_script} && idf.py size-components'" + ) + + open('mcp-size-components.log', 'w+').write(str((stdout, stderr))) + logging.warning(f"size-components result {stdout} {stderr}") + return stdout, stderr + + +@mcp.tool() +async def erase_esp_flash(project_path: str, port: str = None) -> (str, str): + """Erase the entire flash memory of a connected ESP device. + + WARNING: This will erase all data on the device including firmware, + NVS storage, and any other flash contents. + + Args: + project_path: Path to any ESP-IDF project (used for idf.py context) + port: Serial port for the ESP device (optional, auto-detect if not provided) + + Returns: + tuple: (stdout, stderr) - Erase operation logs + """ + os.chdir(project_path) + export_script = get_export_script() + + if port: + erase_cmd = f"bash -c 'source {export_script} && idf.py -p {port} erase-flash'" + else: + erase_cmd = f"bash -c 'source {export_script} && idf.py erase-flash'" + + returncode, stdout, stderr = await run_command_async(erase_cmd) + + open('mcp-erase.log', 'w+').write(str((stdout, stderr))) + logging.warning(f"erase-flash result {stdout} {stderr}") + return stdout, stderr + + +@mcp.tool() +async def monitor_esp_device(project_path: str, port: str = None, timeout_seconds: int = 30) -> (str, str): + """Monitor serial output from a connected ESP device. + + Note: This captures output for a limited time since MCP tools can't run indefinitely. + For interactive monitoring, use idf.py monitor directly in a terminal. + + Args: + project_path: Path to the ESP-IDF project + port: Serial port for the ESP device (optional, auto-detect if not provided) + timeout_seconds: How long to capture output (default: 30 seconds, max: 120) + + Returns: + tuple: (stdout, stderr) - Captured serial output + """ + os.chdir(project_path) + export_script = get_export_script() + + # Cap timeout to prevent runaway processes + timeout_seconds = min(timeout_seconds, 120) + + if port: + monitor_cmd = f"bash -c 'source {export_script} && timeout {timeout_seconds} idf.py -p {port} monitor || true'" + else: + monitor_cmd = f"bash -c 'source {export_script} && timeout {timeout_seconds} idf.py monitor || true'" + + returncode, stdout, stderr = await run_command_async(monitor_cmd) + + open('mcp-monitor.log', 'w+').write(str((stdout, stderr))) + logging.warning(f"monitor result (captured {timeout_seconds}s) {stdout} {stderr}") + return stdout, stderr + + +@mcp.tool() +async def get_esp_app_info(project_path: str) -> (str, str): + """Get information about the built ESP-IDF application. + + Args: + project_path: Path to the ESP-IDF project (must be built first) + + Returns: + tuple: (stdout, stderr) - App information including version, IDF version, etc. + """ + os.chdir(project_path) + export_script = get_export_script() + + # Get project description from build + returncode, stdout, stderr = await run_command_async( + f"bash -c 'source {export_script} && idf.py reconfigure 2>/dev/null; cat build/project_description.json 2>/dev/null || echo \"Project not built yet\"'" + ) + + open('mcp-app-info.log', 'w+').write(str((stdout, stderr))) + logging.warning(f"app-info result {stdout} {stderr}") + return stdout, stderr + + if __name__ == '__main__': mcp.run(transport='stdio')