BenchPod MCP server
embeddedci-mcp is an MCP server that lets an AI agent drive a real hardware-in-the-loop bench: flash a build, power-cycle the target, watch and type into its UART, emulate the sensor it expects, probe its I2C and CAN traffic, and drive and measure analog signals. It is a thin layer over the BenchPod SDK, so what an agent finds interactively can become a pytest test.
Requirements
- Python 3.10+ and uv (for
uvx), orpip install embeddedci-mcp. - A BenchPod reachable over the network, USB, or embeddedci.com.
- For
flash: OpenOCD with thecmsis_dap_tcpbackend (newer than 0.12.0, e.g.brew install --HEAD open-ocd) on the machine running the server. The firmware file is read from that machine too.
Set up your client
Claude Code
claude mcp add benchpod \ -e BENCHPOD_CONNECTION=192.168.1.213 -e BENCHPOD_LA_VOLTAGE=3.3 \ -- uvx embeddedci-mcp
Claude Desktop / Cursor
Add the server to claude_desktop_config.json (Claude Desktop) or .cursor/mcp.json (Cursor):
{
"mcpServers": {
"benchpod": {
"command": "uvx",
"args": ["embeddedci-mcp"],
"env": {
"BENCHPOD_CONNECTION": "192.168.1.213",
"BENCHPOD_LA_VOLTAGE": "3.3"
}
}
}
}BENCHPOD_LA_VOLTAGE must match the target's I/O voltage (1.8 for a 1V8 board) — set it once here; the pod refuses flashing, UART, logic capture, pull resistors and sensor emulation until it is set. Without it the agent is told to call set_la_voltage first.
Codex
The Codex CLI keeps its MCP servers in ~/.codex/config.toml. Add it with the CLI:
codex mcp add benchpod \ --env BENCHPOD_CONNECTION=192.168.1.213 --env BENCHPOD_LA_VOLTAGE=3.3 \ -- uvx embeddedci-mcp
…or write the entry yourself — codex mcp list shows what is configured:
# ~/.codex/config.toml
[mcp_servers.benchpod]
command = "uvx"
args = ["embeddedci-mcp"]
env = { BENCHPOD_CONNECTION = "192.168.1.213", BENCHPOD_LA_VOLTAGE = "3.3" }A pod in the cloud
"env": {
"BENCHPOD_CONNECTION": "embeddedci:my-bench",
"BENCHPOD_API_KEY": "eci_…",
"BENCHPOD_LA_VOLTAGE": "3.3"
}A cloud pod is shared, so connect takes an exclusive lease. It is released by disconnect, or after the idle timeout (10 minutes by default) — the next tool call reconnects transparently, so an idle chat never blocks CI on that pod.
Options
| Flag | Environment | Default | Purpose |
|---|---|---|---|
--connection | BENCHPOD_CONNECTION | — | host[:port], serial device, usb, discover, or embeddedci:<device> |
--la-voltage | BENCHPOD_LA_VOLTAGE | — | LA I/O voltage (1.8 or 3.3) applied on connect |
— | BENCHPOD_API_KEY | — | cloud pods and the waveform library |
--lease-wait | — | 30 | cloud: seconds connect waits for a busy pod |
--idle-timeout | — | 600 | cloud: release the lease after this many idle seconds (0 = never) |
--transport | — | stdio | stdio or http |
--host / --port | — | 127.0.0.1 / 8000 | HTTP bind address |
--auth-token | EMBEDDEDCI_MCP_TOKEN | — | require a bearer token (mandatory off loopback) |
--allowed-host | — | — | Host header(s) to accept on a network bind |
Serving a bench over HTTP
Run the server next to the pod, then point clients at it:
export EMBEDDEDCI_MCP_TOKEN=$(openssl rand -hex 32) embeddedci-mcp --transport http --host 0.0.0.0 --connection usb --la-voltage 3.3
claude mcp add --transport http benchpod http://bench-host:8000/mcp \ --header "Authorization: Bearer $EMBEDDEDCI_MCP_TOKEN"
The server drives real hardware, so it refuses a non-loopback bind without a token. All HTTP clients share one pod connection and their tool calls are serialised.
Tools
| Group | Tools |
|---|---|
| Connection | connect, disconnect, status, set_la_voltage |
| Wiring | wiring, set_wiring |
| Power | power_on, power_off, power_status, reset_target |
| Power profiles | measure_power, power_profile_start, power_profile_stop |
| Flash | flash |
| UART | capture_uart, power_cycle_and_capture, uart_open, uart_write, uart_read, uart_close |
| Emulated I2C sensor | enable_i2c_sensor, set_i2c_sensor, disable_i2c_sensor, i2c_sensor_status, i2c_sensor_regs, i2c_sensor_capture |
| Pull resistors | set_pull, pull_status |
| GPIO on the LA pins | la_pins, gpio_mode, gpio_write, gpio_read, gpio_wait, gpio_pulse, gpio_release |
| Analog | analog_path, dac_output, adc_read |
| Capture + decode | capture_adc, capture_la, capture_correlated, decode_la, la_timing |
| DAC | generate, dac_stop, replay, list_waveforms, replay_waveform, save_capture_as_recording |
| Control loop | control_loop, loop_input, loop_probe, fpga_image |
| CAN | can_open, can_write, can_read, can_respond, can_status, can_close |
| Other | la_step, command |
Two resources are also exposed: benchpod://wiring (the connected device's effective wiring profile — which role or named signal is on each LA channel) and benchpod://help (the server instructions). The wiring tool returns the same profile and set_wiring changes it; it is the bench description the web app's Wiring tab edits and the SDK reads as bp.wiring.
How it behaves
- Instructions. The server sends usage instructions when a client connects — session start, typical flows, units — so the agent knows to connect and set the LA voltage before anything else.
- Typed, structured results. Every tool has an input schema with enums and ranges (paths, sources, LA channels 1-12, eFuse 1/2) and an output schema. Units are volts, seconds and hertz.
- Errors. A tool that cannot do what was asked fails with an error that names the cause, such as
FirmwareError: la voltage not set. A completed operation with a negative outcome is a normal result:flashreturnsok: falsewith its logs, a UART capturematched: false. - Agent-sized captures.
capture_adcreturns calibrated statistics, the dominant frequency and a min/max envelope;capture_lareturns per-channel levels, edges and frequencies. The last captures stay in the session fordecode_la,replayand saving to the waveform library. - Sessions.
uart_openbuffers the console in the background (open it beforepower_on, thenuart_read/uart_write);can_openkeeps a CAN bus open across calls. - Non-blocking. Long operations such as a flash run in the background, send progress, and never interleave commands with another call.
- Annotations. Read-only tools are marked so clients can auto-approve them; tools that power, flash or drive voltages are marked destructive.
Example agent flow
connect() # BENCHPOD_CONNECTION + BENCHPOD_LA_VOLTAGE
wiring() # which DUT signal is on which LA channel on this bench
flash(swclk=11, swdio=12, nreset=true, target="target/stm32f4x.cfg",
file="build/app.elf", target_power=1)
power_cycle_and_capture(rx=5, tx=4, delay=1.0, duration=5.0, until_regex="APP_OK")
set_pull(las=[1, 2], enabled=true)
enable_i2c_sensor(sda=2, scl=1, temperature_c=22.5, pressure_pa=101000)
power_cycle_and_capture(rx=5, tx=4, delay=1.0, duration=5.0)
i2c_sensor_capture(address=0x76, register=0xD0) # did the firmware read the chip id?