Skip to main content

embeddedci-mcp

An MCP server that lets an AI agent drive an EmbeddedCI BenchPod — a hardware-in-the-loop tester wired to a real board. Ask Claude (or any MCP client) to flash a build, power-cycle the target, watch its UART, pretend to be the sensor it expects, probe its I2C/CAN traffic or drive and measure analog signals, and the pod does it.

It is a thin layer over the embeddedci SDK: every tool maps to SDK calls, so anything an agent discovers interactively can become a pytest test.

Requirements

  • Python 3.10+ and uv (for uvx), or pip.
  • A BenchPod reachable over the network or the embeddedci.com cloud. (A USB connection works for status, LA voltage and power only: the STM32 pod's USB console has no JSON mode.)
  • For flash: OpenOCD with the cmsis_dap_tcp backend (newer than 0.12.0 — e.g. brew install --HEAD open-ocd, or the xPack build) 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

BENCHPOD_LA_VOLTAGE is the board's I/O voltage, configured once here — use 1.8 for a 1V8 board. Without it the agent is told to call set_la_voltage before touching the LA bank.

Claude Desktop / Cursor

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"
      }
    }
  }
}

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

Use the device name and an API key — the cloud transport is included:

"env": {
  "BENCHPOD_CONNECTION": "embeddedci:my-bench",
  "BENCHPOD_API_KEY": "eci_…",
  "BENCHPOD_LA_VOLTAGE": "3.3"
}

Or skip the API key: after benchpod login, the server reuses that session (~/.config/benchpod-cli/token.json, refreshed and written back when it has expired) for both cloud_list_devices and connect("embeddedci:<name>"). BENCHPOD_API_KEY wins when set, and inside GitHub Actions the SDK uses OIDC instead.

cloud_list_devices lists the pods on your account and which are online, without connecting to one; pass a result's connection to connect. The session token behind a cloud connection is renewed before it expires, so a long session keeps working.

A cloud pod is shared, so connect takes an exclusive lease (waiting up to --lease-wait seconds if another run holds it). The lease is released by disconnect, or after --idle-timeout seconds without a tool call — the next call reconnects transparently, so an idle chat never blocks CI on that pod.

Options

Flag Environment Default
--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 (without it, cloud tools use the benchpod login session)
— BENCHPOD_API_BASE https://www.embeddedci.com another embeddedci server
--lease-wait — 30 cloud: seconds to wait for a busy pod
--idle-timeout — 600 cloud: release the lease after this many idle seconds (0 = never)
--timeout — 30 per-command device timeout
--transport — stdio stdio or http
--host / --port — 127.0.0.1 / 8000 HTTP bind address
--auth-token EMBEDDEDCI_MCP_TOKEN — require Authorization: Bearer <token> (mandatory off loopback)
--allowed-host — — Host header(s) to accept on a network bind (DNS-rebinding protection)
--allow-unauthenticated — off serve a network address without a token (isolated networks only)

Serving a bench over HTTP

Run the server on the machine next to the pod, and point clients at it:

export EMBEDDEDCI_MCP_TOKEN=$(openssl rand -hex 32)
embeddedci-mcp --transport http --host 0.0.0.0 --connection 192.168.1.213 --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. It holds one pod connection shared by all HTTP clients, and serialises their tool calls.

Tools

Group Tools
Connection connect, disconnect, status, set_la_voltage
Cloud (embeddedci.com) cloud_list_devices
Wiring profile wiring, set_wiring
Power power_on, power_off, power_status, reset_target, 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
Pins + GPIO la_pins, gpio_mode, gpio_write, gpio_read, gpio_wait, gpio_pulse, gpio_release
Pull resistors set_pull, pull_status
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 (raw firmware escape hatch)

Resources: benchpod://wiring (the connected bench's own wiring profile, then LA channels, bias resistors and analog paths) and benchpod://help (the server instructions).

How it behaves

  • Instructions. The server sends usage instructions at initialisation — session start, typical flows, units, error contract — so the agent knows to connect and set_la_voltage before anything else.
  • Typed, structured results. Every tool has an input schema with enums and ranges (paths, sources, LA channels 1-14, eFuse 1/2, …) and an output schema; results come back as structured content. Units are volts, seconds and hertz.
  • Errors. A tool that cannot do what was asked fails with an MCP tool error whose message names the cause, e.g. FirmwareError: la voltage not set or NotConnectedError: …. A completed operation with a negative outcome is a normal result: flash returns ok: false with its logs, a UART capture matched: false.
  • Wiring profile. wiring is the bench's map of DUT signal → LA channel, plus the target-power rail, the UART baud and the SWD target. Omitted channel / baud / rail / SWD arguments come from it, and channel arguments also accept its names (trigger_la: "READY", rx: "uart_rx"), so an agent that read wiring once can call flash(), uart_open() or power_on() with no pin numbers at all. set_wiring replaces it for the connection, or stores it on embeddedci.com with save: true.
  • Agent-sized captures. capture_adc returns calibrated statistics, the dominant frequency and a min/max envelope; capture_la returns per-channel levels, edges and frequencies. The last captures stay in the session, so decode_la, replay(from_last_capture=true) and save_capture_as_recording don't capture again.
  • Triggered captures. capture_adc, capture_la and capture_correlated take trigger_la (+ trigger_edge rising/falling/high/low and trigger_timeout) so sampling starts on an event — t = 0 is the trigger — and the summary echoes it as trigger: "LA9 rising". A condition that never happens fails with TriggerTimeout: ….
  • Pins and GPIO. Each LA channel has one owner at a time: none, GPIO, a UART proxy, SWD, the emulated sensor or a step train. la_pins shows the table, gpio_mode / gpio_write / gpio_read / gpio_wait / gpio_pulse drive and watch channels, gpio_release frees them. A second claim fails with PinConflictError: pin conflict: LA5 is in use by uart_rx; … naming the owner and how to free it — so the agent releases GPIO before opening a UART session on that channel. Captures observe all 14 channels whatever owns them.
  • Power profiles. measure_power(duration) reports average, minimum and peak current, voltage, energy and charge. Every sample is timestamped and the integrals run over those timestamps, so energy is integrated rather than estimated. rate_hz (100-500, default 500) tracks the request to ~200 Hz and then flattens near 365 Hz — the result reports the rate actually delivered in rate_hz and the sensor's configured rate in adc_rate_hz. Optional downsampled trace (points). power_profile_start / power_profile_stop bracket other tool calls; the running profile lives on the session (status reports it) and is dropped on disconnect.
  • Sessions. uart_open buffers the DUT's console in the background (open it before power_on, then uart_read / uart_write); can_open keeps a CAN bus open across calls.
  • Gateware images. The pod's FPGA runs either the loop image (control_loop) or the deep_replay image (replays longer than 2048 samples). control_loop, replay and replay_waveform switch automatically (~3 s) and report it as switched_image; switch_image: false fails instead. A switch resets the FPGA — a running DAC output, UART session or I2C sensor emulation stops — and the server instructions tell the agent to start those after it. fpga_image switches explicitly.
  • Non-blocking. Tools run on worker threads under one device lock: a 5-minute flash doesn't freeze the server, sends progress notifications, and concurrent calls can't interleave commands.
  • Annotations. Read-only tools (status, power_status, adc_read, …) are marked so clients can auto-approve them; tools that power, flash or drive voltages are marked destructive.

Example

Connect to the bench, flash build/app.elf to the STM32F4 (SWCLK on LA11, SWDIO on LA12, reset wired), then power-cycle it and tell me whether it reaches APP_OK on the UART (DUT TX on LA5, RX on LA4).

connect()                                   # BENCHPOD_CONNECTION + BENCHPOD_LA_VOLTAGE
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")

With a wiring profile stored for the bench, the same run needs no pin numbers:

connect()
wiring()                                    # SWCLK on LA11, DUT TX on LA5, rail eFuse 1, …
flash(file="build/app.elf")
power_cycle_and_capture(delay=1.0, duration=5.0, until_regex="APP_OK")
measure_power(duration=2.0, points=100)     # what the firmware draws once it is up

Stability

2.x freezes the tool names, input schemas and annotations (tests/tools_surface.json; CI fails on any unreviewed change): tools and optional parameters may be added, nothing is renamed or removed within a major version. See CHANGELOG.md for migrating from 0.1.

Publishing to the MCP Registry

server.json describes this package for the MCP Registry. After the PyPI release (which is what makes uvx embeddedci-mcp work):

mcp-publisher login github
mcp-publisher publish

The registry verifies PyPI ownership through the mcp-name comment at the top of this README.

Development

pip install -e "../embeddedci[dev]" -e ".[dev]"
pytest

Metadata

Release files for embeddedci-mcp 2.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for embeddedci-mcp 2.2.0
File Size Uploaded
embeddedci_mcp-2.2.0.tar.gz 77.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for embeddedci-mcp 2.2.0
File Interpreter ABI Platform
embeddedci_mcp-2.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 128.0 kB

Release files / embeddedci_mcp-2.2.0.tar.gz

Download URL embeddedci_mcp-2.2.0.tar.gz
Size 77.3 kB
Tags Source
SHA-256 checksum
How to use checksums
2325852ab10efffe47ee7e64ef645d7ea30c9fd47f63ceb6e9cfd4e780bba01c
BLAKE2b-256 checksum
How to use checksums
c7369d3ba4dd92cd1338c871ecd5dc818fd4e3dae6aad221e3abc0bff6600895
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / embeddedci_mcp-2.2.0-py3-none-any.whl

Download URL embeddedci_mcp-2.2.0-py3-none-any.whl
Size 50.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
02447463e318a71ea64580097d9b5b5b2a8d39648cf702a75804497b4710aa23
BLAKE2b-256 checksum
How to use checksums
30a65d3d9df13524b9fc365f2813be80750829f149c73aa02e08048dab09380b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release history Release notifications | RSS feed

2.13.1

2 release files

2.13.0

2 release files

2.12.0

2 release files

2.11.0

2 release files

2.9.0

2 release files

2.8.0

2 release files

2.7.0

2 release files

2.6.0

2 release files

2.5.0

2 release files

2.4.0

2 release files

2.3.0

2 release files

This release

2.2.0 This release

2 release files

2.1.1

2 release files

2.1.0

2 release files

2.0.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page