Files
esp-idf/tools/idf_py_actions
Marek Fiala 3f005c8a4d feat(tools): mcp-server added monitor-device tool
Add a `monitor_device` MCP tool that lets an AI agent run a scripted,
non-interactive `esp-idf-monitor` session against a flashed device and
get back a short status plus a log file path, instead of raw serial
output inline.

Under the hood:

- The agent supplies a plain-text command body (expect/send/sleep/reset/
  exit/comments). `assemble_monitor_script_from_agent_commands()` frames
  it into a script the monitor's non-interactive command mode can
  consume via stdin: it appends `exit` if the agent didn't already end
  with one, and rewrites every bare `expect <regex>` into `expect
  --timeout <timeout_sec> <regex>` via `_monitor_normalize_expect_line()`
  (an already-bounded `expect --timeout ...` line is left untouched so
  the monitor itself reports a bad value). A leading `reset` is not
  prepended - the monitor already resets the chip when it opens the
  port - and any `reset` the agent wrote is left in place.
  `_monitor_parse_sleep_duration()` extracts each `sleep <n>` duration.
  The effective timeout is the sum of every bounded expect duration
  plus every sleep duration. Scripts whose sum exceeds
  `MONITOR_MAX_SCRIPT_SEC` are rejected. If the script has neither
  expect nor sleep (for example only `send`), `timeout_sec` is used so
  the process still has a kill bound.

- `monitor_device()` runs `python -m esp_idf_monitor` via
  `subprocess.run(..., input=script, timeout=2 * effective_timeout)`.
  `no_reset` is forwarded as `--no-reset` so the connection reset can be
  skipped; an explicit `-p` is forwarded when a port is given. Extra
  arguments match `idf.py monitor` where a build exists: baud (`baud`
  tool arg, else `monitor_baud` from `project_description.json`),
  toolchain prefix, `--target`/`--revision`, coredump/panic decode, and
  ELF files with the app ELF first. The 2x hard timeout is a safety net
  independent of the script's own `expect --timeout`/`exit` logic; on
  `TimeoutExpired` the process is killed but any output already captured
  is preserved and logged. `decode_stream()` normalizes that captured
  output, which can be `bytes` on the timeout path even though the
  process otherwise runs in text mode.

- The monitor's exit code drives the reported status via
  `_monitor_status()`, using `EXIT_EXPECT_TIMEOUT` and
  `EXIT_SCRIPT_ERROR` from `esp_idf_monitor.base.constants`: 0 is
  success, 110 means an `expect` pattern never showed up before its
  `--timeout` elapsed, 2 means the monitor rejected the script (bad
  syntax/timeout/regex), anything else is reported generically.

- Serial output and the monitor's own messages share one pipe
  (`stderr=STDOUT`) so decoded panic backtraces stay next to the lines
  that triggered them. `_save_monitor_output()` writes the full merge to
  `<tempdir>/esp_idf_mcp_log/action_monitor/monitor_<timestamp>.log` and
  reports a dedicated `Log file:` line. On non-zero exit or process
  kill, a short tail of that same merge is also returned inline so the
  agent has some failure context without a second file read. If the log
  file can't be written, it falls back to inlining a truncated tail.

Closes https://github.com/espressif/esp-idf/issues/18757
Closes https://github.com/espressif/esp-idf/pull/18385

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 15:20:45 +02:00
..

idf.py extensions

Python modules (subdirectories and files) in this directory named [your_extension]_ext will be loaded as idf.py extensions. If you want to provide extra extensions just provide ; separated list of directories with extensions in IDF_EXTRA_ACTIONS_PATH. Extensions will be loaded in alphanumeric order. Command line arguments parsing and extension mechanism is implemented on top of Click (versions >=5.0 are supported).

They should define a function action_extensions(base_actions, project_path) where:

  • base_actions - dictionary with actions that are already available for idf.py
  • project_path - working dir, may be defaulted to os.getcwd()

This function have to return a dict with 3 possible keys:

{
    # Additional options that will be available from id
    "global_options": [{
        "names": ["--option-name"],
        "help": "Help for option --option-name.",
    }],
    # List of functions that will have access to full app context, and can mangle with arguments
    "global_action_callbacks": [global_callback],
    # Additional subcommands for idf.py
    "actions": {
        "subcommand_name": {
            "callback": subcommand_callback,
            "help": "Help for subcommand.",
        },
    },
}

Where function global_callback(ctx, global_args, tasks) accepts 3 arguments:

  • ctx - Click context
  • global_args - dictionary of all available global arguments
  • tasks - list of Task objects

And subcommand_callback(subcommand_name, ctx, args) accepts 3 arguments:

  • subcommand_name - name of subcommand
  • ctx - Click context
  • args - list of command's arguments