Skip to content

feat: add additional ESP-IDF tools - #44

Open
maitiSoutrik wants to merge 1 commit into
horw:mainfrom
maitiSoutrik:feature/additional-esp-tools
Open

feat: add additional ESP-IDF tools#44
maitiSoutrik wants to merge 1 commit into
horw:mainfrom
maitiSoutrik:feature/additional-esp-tools

Conversation

@maitiSoutrik

@maitiSoutrik maitiSoutrik commented Feb 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds 6 new MCP tools to expand ESP-IDF workflow coverage.

New Tools

Tool Description ESP-IDF Command
clean_esp_project Clean build artifacts idf.py clean / fullclean
get_esp_project_size Analyze firmware RAM/Flash usage 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 (with timeout) idf.py monitor
get_esp_app_info Get project description and app info Reads project_description.json

Details

  • clean_esp_project: Supports both regular clean and fullclean modes via boolean flag
  • monitor_esp_device: Uses timeout to prevent runaway processes (capped at 120s) since MCP tools can't run indefinitely
  • erase_esp_flash: Includes warning in docstring about destructive nature

Testing

Tested tool registration and command generation. Actual ESP-IDF execution requires IDF_PATH environment setup.

README Updates

  • Added tool reference table documenting all available MCP tools
  • Updated capabilities list in the PoC section

Summary by CodeRabbit

Release Notes

  • New Features

    • Added commands for cleaning build artifacts, analyzing firmware size (overall and per-component), erasing device flash memory, monitoring serial output, and retrieving project information.
  • Documentation

    • Updated README with new usage examples, available tools table, and illustrative examples for all new capabilities.

✏️ Tip: You can customize this high-level summary in your review settings.

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.
@coderabbitai

coderabbitai Bot commented Feb 1, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces nine new MCP tools for ESP-IDF operations including cleaning build artifacts, analyzing firmware size, erasing flash memory, monitoring serial output, and retrieving project information. Updates README documentation with tool descriptions and usage examples.

Changes

Cohort / File(s) Summary
Documentation
README.md
Added tool reference table and usage examples for nine new ESP-IDF operations (clean, size analysis, flash erase, monitoring, project info).
MCP Tool Implementation
main.py
Added six new public functions: clean_esp_project(), get_esp_project_size(), get_esp_component_size(), erase_esp_flash(), monitor_esp_device(), and get_esp_app_info(). Each follows a consistent pattern: directory navigation, ESP-IDF command execution, output logging, and result return.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 Nine fresh tools in our hutch today,
Cleaning, sizing, erasing their way—
Flash goes whoosh, logs spring alive,
Watch your projects monitor and thrive! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding new ESP-IDF tools to the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@main.py`:
- Around line 215-245: In monitor_esp_device, validate and normalize
timeout_seconds before building the shell command: convert to int, ensure it's
at least 1 (or set to default 30) and then cap to 120 to prevent negative/zero
values from being used in the shell timeout; update the timeout_seconds
normalization logic near where timeout_seconds is set and before monitor_cmd is
constructed (function monitor_esp_device). Also sanitize or safely quote the
port value passed into monitor_cmd (the port parameter used in monitor_cmd) to
prevent shell injection—either validate against an allowlist/regex for expected
port names or apply proper shell quoting before interpolating into the command;
keep run_command_async usage unchanged. Ensure any log/write to mcp-monitor.log
still uses the validated values.
- Around line 186-212: The erase_esp_flash function currently interpolates the
port directly into a shell string (erase_cmd) causing command injection; update
erase_esp_flash (and similarly flash_esp_project) to validate/sanitize the port
and avoid using a shell string: either build the command as a list and call
run_command_async without shell=True or safely quote the port using shlex.quote
before insertion; add a small validator helper (e.g., is_valid_serial_port or
sanitize_port) to allow only expected characters/patterns (e.g., /^\/dev\/\w+$/
or COM port patterns) and reject/raise on invalid input; also change the
annotation from port: str = None to port: str | None = None (or Optional[str])
to match PEP 484.
🧹 Nitpick comments (2)
main.py (2)

248-268: Stderr suppression may hide useful diagnostic information.

The command uses 2>/dev/null for both reconfigure and cat, which suppresses all error output. This means if something goes wrong (e.g., permission errors, corrupted JSON), the user only sees "Project not built yet" regardless of the actual cause.

Consider capturing stderr for logging purposes while still providing a user-friendly fallback message.

💡 Alternative approach
     # 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\"'"
+        f"bash -c 'source {export_script} && idf.py reconfigure && cat build/project_description.json || echo \"Project not built yet. Error: check mcp-app-info.log\"'"
     )

129-129: Note: os.chdir() is process-global state (pre-existing pattern).

All tool functions use os.chdir(project_path) which changes the working directory for the entire process. In an async context with concurrent tool invocations, this could cause race conditions where one tool's os.chdir() affects another's execution.

This is a pre-existing pattern in the codebase (seen in build_esp_related_project, flash_esp_project, etc.), so not blocking this PR. However, for future robustness, consider using subprocess.run(..., cwd=project_path) or similar approaches that don't mutate global state.

Comment thread main.py
Comment on lines +186 to +212
@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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Command injection risk via unsanitized port parameter.

The port parameter is directly interpolated into the shell command without validation. A malicious or malformed port value like "/dev/tty; rm -rf /" could execute arbitrary shell commands.

This issue also exists in the pre-existing flash_esp_project function (line 87), but new code should not propagate the vulnerability.

🛡️ Proposed fix: validate port parameter

Add validation at the start of the function (and consider extracting to a helper for reuse):

+import re
+
+def _validate_port(port: str | None) -> None:
+    """Validate serial port to prevent command injection."""
+    if port is not None and not re.match(r'^[/\w.-]+$', port):
+        raise ValueError(f"Invalid port format: {port}")
+

 `@mcp.tool`()
-async def erase_esp_flash(project_path: str, port: str = None) -> (str, str):
+async def erase_esp_flash(project_path: str, port: str | None = None) -> (str, str):
     """Erase the entire flash memory of a connected ESP device.
     ...
     """
+    _validate_port(port)
     os.chdir(project_path)

Additionally, static analysis correctly notes that port: str = None should be port: str | None = None per PEP 484.

🧰 Tools
🪛 Ruff (0.14.14)

[warning] 187-187: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


[warning] 208-208: Unpacked variable returncode is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

🤖 Prompt for AI Agents
In `@main.py` around lines 186 - 212, The erase_esp_flash function currently
interpolates the port directly into a shell string (erase_cmd) causing command
injection; update erase_esp_flash (and similarly flash_esp_project) to
validate/sanitize the port and avoid using a shell string: either build the
command as a list and call run_command_async without shell=True or safely quote
the port using shlex.quote before insertion; add a small validator helper (e.g.,
is_valid_serial_port or sanitize_port) to allow only expected
characters/patterns (e.g., /^\/dev\/\w+$/ or COM port patterns) and reject/raise
on invalid input; also change the annotation from port: str = None to port: str
| None = None (or Optional[str]) to match PEP 484.

Comment thread main.py
Comment on lines +215 to +245
@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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Negative timeout_seconds values are not validated.

The code caps the maximum at 120, but negative or zero values would pass through. A negative timeout could cause timeout -5 idf.py monitor to behave unexpectedly (timeout command typically treats negative values as errors).

🛡️ Proposed fix
     # Cap timeout to prevent runaway processes
-    timeout_seconds = min(timeout_seconds, 120)
+    timeout_seconds = max(1, min(timeout_seconds, 120))

Also note: the same command injection risk via port applies here as flagged for erase_esp_flash.

🧰 Tools
🪛 Ruff (0.14.14)

[warning] 216-216: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


[warning] 241-241: Unpacked variable returncode is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

🤖 Prompt for AI Agents
In `@main.py` around lines 215 - 245, In monitor_esp_device, validate and
normalize timeout_seconds before building the shell command: convert to int,
ensure it's at least 1 (or set to default 30) and then cap to 120 to prevent
negative/zero values from being used in the shell timeout; update the
timeout_seconds normalization logic near where timeout_seconds is set and before
monitor_cmd is constructed (function monitor_esp_device). Also sanitize or
safely quote the port value passed into monitor_cmd (the port parameter used in
monitor_cmd) to prevent shell injection—either validate against an
allowlist/regex for expected port names or apply proper shell quoting before
interpolating into the command; keep run_command_async usage unchanged. Ensure
any log/write to mcp-monitor.log still uses the validated values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant