-
Notifications
You must be signed in to change notification settings - Fork 19
feat: add additional ESP-IDF tools #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Comment on lines
+215
to
+245
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Negative The code caps the maximum at 120, but negative or zero values would pass through. A negative timeout could cause 🛡️ 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 🧰 Tools🪛 Ruff (0.14.14)[warning] 216-216: PEP 484 prohibits implicit Convert to (RUF013) [warning] 241-241: Unpacked variable Prefix it with an underscore or any other dummy variable pattern (RUF059) 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| @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') | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Command injection risk via unsanitized
portparameter.The
portparameter 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_projectfunction (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):
Additionally, static analysis correctly notes that
port: str = Noneshould beport: str | None = Noneper PEP 484.🧰 Tools
🪛 Ruff (0.14.14)
[warning] 187-187: PEP 484 prohibits implicit
OptionalConvert to
T | None(RUF013)
[warning] 208-208: Unpacked variable
returncodeis never usedPrefix it with an underscore or any other dummy variable pattern
(RUF059)
🤖 Prompt for AI Agents