
Security News
Re-Enabled GitHub Actions Expose Thousands of Repositories to Mini Shai-Hulud
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.
embeddedci-mcp
Advanced tools
MCP server that lets AI agents drive an EmbeddedCI BenchPod hardware-in-the-loop bench
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.
uvx), or pip.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.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_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"
}
}
}
}
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" }
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"
}
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.
| 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 |
| — | 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) |
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.
| Group | Tools |
|---|---|
| Connection | connect, disconnect, status, set_la_voltage |
| 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).
connect and set_la_voltage before
anything else.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 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.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.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: ….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.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.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.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.status, power_status, adc_read, …) are marked so clients
can auto-approve them; tools that power, flash or drive voltages are marked destructive.Connect to the bench, flash
build/app.elfto the STM32F4 (SWCLK on LA11, SWDIO on LA12, reset wired), then power-cycle it and tell me whether it reachesAPP_OKon 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
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.
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.
pip install -e "../embeddedci[dev]" -e ".[dev]"
pytest
FAQs
MCP server that lets AI agents drive an EmbeddedCI BenchPod hardware-in-the-loop bench
We found that embeddedci-mcp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.

Research
/Security News
The compromise affects MemTensor's MemOS, an open source memory framework for large language models (LLMs) and AI agents. Both npm package @memtensor/memos-cloud-openclaw-plugin and the PyPI package MemoryOS are compromised. They drop cross-platform Go binaries that exfiltrate developer secrets.