MCP server

Connect a coding agent to espx mcp so it can build, flash and debug firmware on your board.

espx mcp serves the Model Context Protocol on stdio. A coding agent connected to it can create a project, build it, flash the board on your desk, read the boot log and fix what it sees, with nothing but espx. The agent works in a loop: build, flash (or run, which also watches the boot log), read the monitor, fix, repeat.

Add espx to a client

Run espx mcp install once per client:

espx mcp install --client claude-code     # prints: claude mcp add espx -- /path/to/espx mcp
espx mcp install --client codex           # prints: codex mcp add espx -- /path/to/espx mcp
espx mcp install --client cursor          # writes ~/.cursor/mcp.json
espx mcp install --client vscode          # writes .vscode/mcp.json in this directory
espx mcp install --client claude-desktop  # writes Claude Desktop's claude_desktop_config.json
ClientWhat install does
claude-codePrints the claude mcp add command to run. espx never edits files that Claude Code's CLI owns
codexPrints the codex mcp add command to run
cursorAdds espx to ~/.cursor/mcp.json
vscodeAdds espx to .vscode/mcp.json in the current directory
claude-desktopAdds espx to claude_desktop_config.json

For the JSON files espx merges in its own entry and keeps every other key. --print shows the config instead of writing it. Restart the client afterwards so it loads espx. The entry starts the espx binary you ran install with:

~/.cursor/mcp.json
{
  "mcpServers": {
    "espx": { "command": "/home/you/.cargo/bin/espx", "args": ["mcp"] }
  }
}

The desktop app does the same for your whole user account, with Connect and Disconnect buttons per client.

Let the agent touch a board

An agent may flash, reset or erase only a board the project allows in espx.toml. By default that is the project's bound board, [board] id, and erasing is off. Create projects with a binding:

espx new blinky --board-id 58:8C:81:53:AD:2C

An agent does the same with project_new and board_id. A call outside the policy fails with policy_denied, and the message says which key to set. Commands you run yourself are never checked. Agent policy covers the [agent] table and the audit log.

Tools

espx serves 22 tools. Every tool carries readOnlyHint and destructiveHint annotations, and every result is a one-paragraph summary plus structured data.

ToolEffectWhat it does
doctorreadChecks rustup, cargo, the tools and the Rust targets, offline
setupchanges, jobInstalls the tools and targets, and the Xtensa fork for Xtensa chips. Takes chip
chipsreadThe chip table
board_catalogreadPin facts for known dev boards, or one with board
devicesreadAttached boards: id, port, USB interface, name, open monitor session, lease holder. Touches no board
board_infochangesChip, revision, crystal, flash size and MAC. Resets the board
project_newchangesCreates a project with esp-generate and AGENTS.md. Takes name, dir, chip, board, port, board_id, options
project_inforeadName, chip, target, toolchain, release binary and size, binding, resolved crates
depsreadThe esp-hal family versions from Cargo.lock
contextreadThe AGENTS.md text plus attached boards and open monitor sessions
buildchanges, jobcargo build --release; compiler messages come back as diagnostics with file and line. Takes extra
flashdestroys, jobBuilds and flashes, then resets the board. Policy-checked. Takes port, baud, extra
erasedestroys, jobErases the whole flash. Needs erase = true in the policy
resetchangesResets the board through the serial lines. Policy-checked
rundestroys, jobBuild, flash, open a monitor session and read the boot log. Policy-checked
monitor_startchangesOpens (or returns) a monitor session on a board, or plays a recording
monitor_readreadReads a session, filtered and shaped
monitor_writechangesSends text to the board's serial input
monitor_stopchangesCloses a session and releases the port
job_statusreadA job's state, progress and last output lines
job_waitreadWaits for a job and returns its result
job_cancelchangesStops a job; a build in progress is killed

Most tools take dir, the project directory. It defaults to the directory espx mcp was started in, and relative paths resolve against it. Board tools take port: a serial port, a board id from devices, or a name from espx boards name. Leave it out when the project binds a board, pins a port in espflash_ports.toml, or exactly one board is attached.

Every call's arguments are checked strictly; an unknown argument is an error. A bad argument, a failed build or an espx error comes back as a tool result with isError: true, never as a JSON-RPC error, so the agent reads it. When an error caused it, the structured content holds error with a stable code (invalid_arguments for bad arguments) and a message that says what to do next. The codes are listed in Exit codes.

Builds

build (and the build step of flash and run) returns each compiler message as a diagnostic with its level, code, file, line and column. A failed build names the first error and its location in the summary, with rustc's rendered text as the body. A successful one reports the image size against the app partition, and an image that does not fit fails before anything reaches the board.

Monitoring

run and monitor_start open a monitor session, which espx reads itself: it decodes defmt and resolves panics against the project's release binary. monitor_start resets the board when the port opens, so the log starts at boot; pass reset: false to join without resetting. Pass replay with the path of an espx monitor --record file to play a recording through the same tools.

monitor_read arguments:

ArgumentDefaultMeaning
sessionthe only open oneSession id from monitor_start or run
sincenewnew: since the last read. boot: since the board last booted. start: everything still buffered
cursorRead from an earlier read's next instead
untilKeep reading until a line matches
expectRegexes that must match in order; the read succeeds once the last one matches
rejectRegexes that fail the read on the first matching line, panic message or exception kind
wait_s25 with patterns, else 2Seconds to wait for output
max_events200Most events to return; repeated lines count once
max_bytes16384Most bytes of log text to return
levelDrop lines below trace, debug, info, warn or error. Panics, resets and warnings always pass
moduleKeep only lines whose defmt module contains this text
patternKeep only lines matching this regex

The result is shaped for a model to read. Runs of identical lines collapse into one with ×N. Each boot shows its reason and the ROM's rst: line. Every panic or exception frame inside the project carries seven numbered source lines, the frame's line marked with >. The summary lists the boots and says so when the last one was a watchdog reset, a panic or a brownout, or when the same reason repeats, which is a reset loop.

run takes until, expect, reject and timeout_s: how long to watch after flashing, 30 seconds when there are patterns to match, else 5 seconds of boot log. A run that hits a rejected pattern, or whose expectations did not all match, is an error result. The session stays open for monitor_read.

After a flash or reset with a session open, the tool waits up to 3 seconds for the new boot and names its reason.

Jobs

setup, build, flash, erase and run can take longer than a client wants to wait. Each takes wait_s (default 25). If the work is done in time the call returns its result. Otherwise it returns {job, state: "running", tail, progress} and keeps going:

  • job_status returns the state (running, done, failed or cancelled), progress and the last output lines.
  • job_wait waits up to its own wait_s and returns the same result the original call would have, or running again.
  • job_cancel stops the job.

Cancelling the starting call, or a job_wait, through MCP's notifications/cancelled cancels the job too. A cancelled build is killed with its whole process group. A flash cannot stop in the middle of a write, so a cancelled flash stops after it. A cancelled setup finishes its current download in the background.

When the request carries a progressToken, every step goes out as notifications/progress: cargo's Compiling lines, each block a flash writes, each setup step. Jobs live in memory, the newest 64, so restarting the server forgets them.

When the agent needs a person

When espx cannot get the bootloader to answer, also after one retry at 115200 baud, or the board is not on the USB bus, the result carries needs_human with an action and a message:

actionAsk the person to
download_modeHold BOOT, tap RESET, release BOOT, then flash again
replugPlug the board in, or unplug and replug it, with a data cable

Nothing else asks for a person. The server's own instructions tell the agent to ask for help only when a result carries needs_human. The desktop app reads it from the audit log and shows a banner.

Resources

URIContents
espx://boardsAttached boards with their id, port, lease holder and open monitor session
espx://boards/{id}/logThe current boot's log of a board with an open session; id is the board id, its port, or the port's file name, with slashes URL-encoded
espx://project/contextChip, board pins, esp-hal version, the examples link and attached boards for the project espx mcp was started in
espx://catalog/{board}Pin facts for one known dev board

Prompts

PromptArgumentsWhat it asks the agent to do
new-firmwaregoal (required), board, nameStart from an empty folder: check the toolchain, create a bound project, build, take the API from the esp-hal examples, run with until set to a line that proves the goal, and fix until the board's log shows it working
debug-crashsymptomRead the board's own output since boot, find the panic, exception or watchdog reset, fix the cause and confirm it is gone
port-exampleexample (required)Bring an esp-hal example from the project's exact release onto this board, adapting the pins

Sharing boards

espx mcp uses the same board hub as the CLI and the desktop app. When nothing else serves it, espx mcp serves it on ~/.espx/hub.sock, so two agents, or an agent and a terminal, share one board. Every flash, reset, erase and board-info read is leased under the client's name, espx mcp (claude-code) for example, and another holder's call fails with board_leased until it is done. If the process serving the hub quits, the server reconnects or opens the boards itself.

Every tool call is appended to ~/.espx/agent.log. espx log prints it; see Agent policy.