Build Lifecycle
Six tools manage a firmware build, from “what would this do?” to “go” to “stop now”.
| Tool | What it does |
|---|---|
build (alias build.start) | Start a build. Returns a task_id right away. |
build.status | Check on a task_id: pending, running, succeeded, failed, canceled. |
build.cancel | Stop a running or queued build. |
set_target.run | Run idf.py set-target on the build machine. |
generate_build_plan | Tell you what a build would do, without running it. |
get_clean_plan | Tell you what idf.py clean or fullclean would delete. |
build / build.start
Starts a firmware build on the build server and returns right away. The
build itself runs in the background inside a sandbox; you follow along
with build.status or by reading build://log/{task_id}.
Typical inputs:
| Field | Type | Notes |
|---|---|---|
target | string | ESP chip — esp32, esp32s3, esp32c6, etc. |
profile | string | debug (default) or release. |
idf_version | string (optional) | Pin a specific IDF version. Defaults to the project’s .idf-version or the build server’s default. |
clean | bool (optional) | If true, do a clean build instead of incremental. |
params | object (optional) | Recipe-specific overrides. |
Returns:
{
"task_id": "0abf...e2",
"status": "pending"
}
The task_id is what you’ll use to follow the build. Save it.
Example dialogue:
Build the firmware for
esp32s3in release mode.
Your assistant calls build with
{"target": "esp32s3", "profile": "release"}, then watches
build.status until it finishes.
Plan-only mode: In the CLI, build goes remote by default. Pass
--local to get a build plan without compilation. In the MCP server,
if CONTROL_BASE_URL or MCP_AUTH_SECRET is missing, build returns
a plan with "status": "planning". Use generate_build_plan to
explicitly get a plan without side effects in either mode.
CLI: espctl build
When you’d rather drive a build by hand instead of through MCP. Same build server, same sandbox — just a CLI in front instead of your AI assistant.
espctl build [path] [--target <chip>] [--clean] \
[--remote <url> | --local] \
[--git-url <url> [--git-ref <ref>]] \
[--idf-version <ver>] [--sbom]
Remote build is the default. See Plan-only vs Remote Build for the long form.
Flag matrix
| Flag | Default | Notes |
|---|---|---|
path (positional) | . | Project directory. .espctl.toml and .idf-version are read from this path. |
--target | default_target from .espctl.toml | Chip — esp32, esp32s3, esp32c3, esp32c6, etc. |
--clean | false | Clean build directory first. Local-only; ignored in remote mode. |
--remote <url> | from ~/.config/espctl/credentials.json, then https://esphome.cloud | Override the build server URL. Conflicts with --local. |
--local | false | Generate a build plan without compiling. Conflicts with --remote. |
--git-url <url> | — | Have the agent clone this repo instead of receiving a project bundle. Remote mode only. |
--git-ref <ref> | (default branch) | Branch, tag, or commit to check out. Used with --git-url. |
--idf-version <ver> | .idf-version → [idf_version] in .espctl.toml → server default | Pin a specific IDF version. Written to .idf-version if the file does not exist. |
--sbom | false | Generate an SPDX SBOM at build/sbom.spdx. Remote only. |
Mode resolution
The CLI picks a mode in this order:
--local→ plan-only, no compilation.--remote <url>→ remote build to that URL.- Otherwise: the server saved by
espctl login. - Otherwise:
https://esphome.cloud(the built-in default).
Common invocations
# Default: remote build using saved credentials
espctl build . --target esp32s3
# Remote build with SPDX SBOM
espctl build . --target esp32s3 --sbom
# One-shot server override (no login persisted)
espctl build . --target esp32 --remote https://staging.example.com
# Build directly from a git ref (agent clones — no project upload)
espctl build --remote https://esphome.cloud \
--git-url https://github.com/ff4415/aegis-examples \
--git-ref v0.4.2 --target esp32c3
# Pin an IDF version explicitly
espctl build --target esp32s3 --idf-version v5.3.1
# Plan-only (offline / pre-flight)
espctl build --local --target esp32s3
# Local clean rebuild
espctl build --local --target esp32s3 --clean
Output and exit codes
Human mode prints staged progress (clone, configure, compile, link) and
a manifest summary. --json emits a stream of PipelineEvent JSON
objects, one per line, ending with the manifest.
On success: exit 0 and build/flash_bundle.tar.gz in the project
directory. With --sbom, also writes build/sbom.spdx.
On compile or runtime failure: exit 1.
On config or invalid-target error: exit 2.
Related MCP tools
build/build.start— same build, started programmatically.generate_build_plan— what--localends up doing internally.sbom.create— SBOM-only over an existingtask_id, useful when adding an SBOM after the fact.
build.status
Checks the state of a previously-started build.
Input:
{ "task_id": "0abf...e2" }
Returns:
{
"task_id": "0abf...e2",
"status": "running",
"progress": 0.42,
"started_at": 1712340000,
"updated_at": 1712340060,
"phase": "compiling"
}
status is one of pending, running, succeeded, failed, or
canceled. Some assistants also show progress (0.0–1.0) and a
free-form phase (e.g. cmake-configure, compiling, linking,
flashing).
Common pattern: Most assistants check every 1–3 seconds with a
timeout. Don’t hammer the server — there’s a build://log/{task_id}
resource that pushes new lines as they happen, which is more efficient
than asking over and over.
build.cancel
Stops a pending or running build. Doesn’t error if the build has
already finished — it’s a no-op in that case.
Input:
{ "task_id": "0abf...e2" }
Returns:
{ "task_id": "0abf...e2", "status": "canceled" }
The cancel is best-effort — the server asks the build to stop, then forces it after a short wait. Compile steps already in progress may take a few seconds to wind down.
set_target.run
Runs idf.py set-target on the build machine for a project. Creates
a pending task that the build agent picks up and executes.
Unlike set_target (which updates local config only), this tool
actually runs the set-target command on the remote build machine.
Input:
{ "target": "esp32c3" }
| Field | Required | Notes |
|---|---|---|
target | Yes | Chip — esp32, esp32s3, esp32c3, esp32c6, etc. |
Returns:
{
"task_id": "d1e2...f3",
"target": "esp32c3",
"recipe_id": "idf_set_target"
}
generate_build_plan
Tells you what a build would do, without running it. Useful for:
- Reviewing what’s about to happen before you click “go”.
- Plan-only mode (no build server set).
- Capturing a reproducible build description for CI or audit.
Input: Same as build (target, profile, etc.).
Returns: A structured plan. Exact fields depend on the recipe, but typically include:
recipe_ididf_version_resolvedtargetprofilecommand_pipeline— the ordered list of build stepsexpected_artifacts— what files the build will produceestimated_duration_secs— best-effort guess from past runs
No side effects. Safe to call as many times as you want.
get_clean_plan
Tells you what idf.py clean (incremental clean) or idf.py fullclean
(full wipe) would delete from the build directory, without actually
deleting anything.
Input:
{ "scope": "clean" } // or "fullclean"
Returns: A list of files and directories that would be removed, plus totals.
{
"scope": "clean",
"would_delete": [
"build/esp-idf/main/...",
"build/esp-idf/CMakeFiles/...",
"build/.../*.o"
],
"total_files": 1342,
"total_bytes": 187654321
}
Useful before doing a destructive cleanup, especially in CI.
See also
- Logs & Artifacts — once a build finishes, read its output files.
- Typical Workflow — end-to-end script that uses most of these tools.
- Troubleshooting — when builds fail.