# 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](https://modelcontextprotocol.io) 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:

```sh
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
```

| Client           | What `install` does                                                                            |
| ---------------- | ---------------------------------------------------------------------------------------------- |
| `claude-code`    | Prints the `claude mcp add` command to run. espx never edits files that Claude Code's CLI owns |
| `codex`          | Prints the `codex mcp add` command to run                                                      |
| `cursor`         | Adds espx to `~/.cursor/mcp.json`                                                              |
| `vscode`         | Adds espx to `.vscode/mcp.json` in the current directory                                       |
| `claude-desktop` | Adds 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:

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

The [desktop app](https://www.espx.dev/docs/desktop#ai-agents) 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:

```sh
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](https://www.espx.dev/docs/agents/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.

| Tool            | Effect        | What it does                                                                                                             |
| --------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `doctor`        | read          | Checks rustup, cargo, the tools and the Rust targets, offline                                                            |
| `setup`         | changes, job  | Installs the tools and targets, and the Xtensa fork for Xtensa chips. Takes `chip`                                       |
| `chips`         | read          | The chip table                                                                                                           |
| `board_catalog` | read          | Pin facts for known dev boards, or one with `board`                                                                      |
| `devices`       | read          | Attached boards: id, port, USB interface, name, open monitor session, lease holder. Touches no board                     |
| `board_info`    | changes       | Chip, revision, crystal, flash size and MAC. Resets the board                                                            |
| `project_new`   | changes       | Creates a project with esp-generate and `AGENTS.md`. Takes `name`, `dir`, `chip`, `board`, `port`, `board_id`, `options` |
| `project_info`  | read          | Name, chip, target, toolchain, release binary and size, binding, resolved crates                                         |
| `deps`          | read          | The esp-hal family versions from `Cargo.lock`                                                                            |
| `context`       | read          | The `AGENTS.md` text plus attached boards and open monitor sessions                                                      |
| `build`         | changes, job  | `cargo build --release`; compiler messages come back as diagnostics with file and line. Takes `extra`                    |
| `flash`         | destroys, job | Builds and flashes, then resets the board. Policy-checked. Takes `port`, `baud`, `extra`                                 |
| `erase`         | destroys, job | Erases the whole flash. Needs `erase = true` in the policy                                                               |
| `reset`         | changes       | Resets the board through the serial lines. Policy-checked                                                                |
| `run`           | destroys, job | Build, flash, open a monitor session and read the boot log. Policy-checked                                               |
| `monitor_start` | changes       | Opens (or returns) a monitor session on a board, or plays a recording                                                    |
| `monitor_read`  | read          | Reads a session, filtered and shaped                                                                                     |
| `monitor_write` | changes       | Sends text to the board's serial input                                                                                   |
| `monitor_stop`  | changes       | Closes a session and releases the port                                                                                   |
| `job_status`    | read          | A job's state, progress and last output lines                                                                            |
| `job_wait`      | read          | Waits for a job and returns its result                                                                                   |
| `job_cancel`    | changes       | Stops 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](https://www.espx.dev/docs/reference/exit-codes#error-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:

| Argument     | Default                  | Meaning                                                                                               |
| ------------ | ------------------------ | ----------------------------------------------------------------------------------------------------- |
| `session`    | the only open one        | Session id from `monitor_start` or `run`                                                              |
| `since`      | `new`                    | `new`: since the last read. `boot`: since the board last booted. `start`: everything still buffered   |
| `cursor`     |                          | Read from an earlier read's `next` instead                                                            |
| `until`      |                          | Keep reading until a line matches                                                                     |
| `expect`     |                          | Regexes that must match in order; the read succeeds once the last one matches                         |
| `reject`     |                          | Regexes that fail the read on the first matching line, panic message or exception kind                |
| `wait_s`     | 25 with patterns, else 2 | Seconds to wait for output                                                                            |
| `max_events` | 200                      | Most events to return; repeated lines count once                                                      |
| `max_bytes`  | 16384                    | Most bytes of log text to return                                                                      |
| `level`      |                          | Drop lines below `trace`, `debug`, `info`, `warn` or `error`. Panics, resets and warnings always pass |
| `module`     |                          | Keep only lines whose defmt module contains this text                                                 |
| `pattern`    |                          | Keep 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`:

| `action`        | Ask the person to                                             |
| --------------- | ------------------------------------------------------------- |
| `download_mode` | Hold BOOT, tap RESET, release BOOT, then flash again          |
| `replug`        | Plug 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

| URI                      | Contents                                                                                                                                  |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `espx://boards`          | Attached boards with their id, port, lease holder and open monitor session                                                                |
| `espx://boards/{id}/log` | The 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/context` | Chip, 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

| Prompt         | Arguments                          | What it asks the agent to do                                                                                                                                                                                                |
| -------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `new-firmware` | `goal` (required), `board`, `name` | Start 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-crash`  | `symptom`                          | Read the board's own output since boot, find the panic, exception or watchdog reset, fix the cause and confirm it is gone                                                                                                   |
| `port-example` | `example` (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](https://www.espx.dev/docs/agents/policy#the-audit-log).

## Related

- [Agent policy](https://www.espx.dev/docs/agents/policy)
- [Agent context](https://www.espx.dev/docs/agents/context)
- [Monitor and replay](https://www.espx.dev/docs/cli/monitor)
