mcuport
v0.2.0
Published
MCUPort desktop MCP server: enumerate, flash, build, monitor, and drive GPIO/ADC/PWM/I2C/SPI on microcontrollers over USB serial.
Maintainers
Readme
The MCP for microcontrollers (working name MCUPort), desktop server
An MCP (Model Context Protocol) server that gives Claude Code, Claude
Desktop, Cursor, or any other MCP-speaking agent real hands on a real
microcontroller over USB serial: find the board, flash it, build for it,
watch its serial output, and read/write GPIO, ADC, PWM, I2C and SPI on it.
Free tier: the full server, unlimited local boards, every supported family.
Pro ($20 one-time) unlocks the phone bridge: the companion app relays a
Bluetooth LE board's serial port to this server over your local network
(family: "bridge"). The phone app is not in the stores yet, so Pro is not
on sale yet; the product's /pricing page says when it is.
What is proven where (read this before trusting anything below)
Honesty first, because this customer reads READMEs. As of 2026-09-19:
| Tool group | Proven on | Never yet run on |
|---|---|---|
| list_boards, flash_firmware, serial_* | mocked serialport + the phone-bridge fixture; the ESP32 flash command line was handed to a real esptool v4.12.0 and v5.5.0 on 2026-10-13 (ghost port, so the tool answered "could not open port", not "invalid choice"), and the image classifier is calibrated against esptool image_info on the real MicroPython ESP32/S3/C3 v1.29.0 images | real silicon by the studio |
| the phone bridge (family: "bridge"), desktop side through the app's own bridge server | test/bridge/relay_through_phone.test.ts: the pin, bus, board_info, mpy_exec and gpio_audit tools through src/boards/bridge.ts (real fetch, real licenseIsValid() against the license fixture) into apps/mcuport/src/bridge/server.ts on a loopback socket, whose BLE side is the real MicroPython interpreter; a wrong pairing token comes back as the app's 401 in the tool result | a real radio, a real phone OS, real silicon; the app has not been paired with a desktop over a real network |
| gpio_*, adc_read, pwm_write, i2c_*, spi_transfer, mpy_exec, board_info (MicroPython path) | the real MicroPython interpreter (its WebAssembly build) speaking the real raw-REPL protocol, with only the machine module faked | a physical ESP32 or Pico |
| the same tools on the Arduino bridge-sketch path | a TypeScript emulator of the sketch's line protocol | the sketch has not been compiled or run on a board |
| gpio_audit and the pin-safety refusals in every pin tool | the tables against the vendors' pinouts (unit) and the refusals end to end on the simulated boards | a real board; no pin has ever been refused on silicon |
| install_ble_repl + firmware/mcuport_ble_repl/mcuport_ble_repl.py | the chunked write, boot hook and the firmware's own Python (advertisement bytes, MTU-sized notifications, retry, re-advertise) on the real MicroPython interpreter with a fake bluetooth module | a real radio: no ESP32 or Pico W has advertised it, and no phone has connected to it |
| board_reset | sequence data asserted against esptool's documented DTR/RTS order and the 1200-baud touch; applied to simulated ports | a real auto-reset circuit |
| compile_sketch, build_project, install_bridge_sketch | fixture output shaped like arduino-cli --format json and pio run logs; the missing-tool path for real | an installed arduino-cli / PlatformIO |
No board was attached to the machine that built this, and neither CLI was
installed: nothing in this package has been run on a real board by the
studio. Everything above the line is "logic complete"; the first hardware
run will find something, and it is owed before any public claim of
hardware support. That run is one command: npm run hardware-smoke
(scripts/hardware_smoke.ts) drives the built server over real stdio,
finds the attached board, checks that pin safety refuses a flash pin on it
and reads a real level from an allowed one, optionally installs the BLE
REPL (--ble), and writes build/hardware_smoke.json with the board's USB
id and every step's real result. With no board attached it exits 3 with
verdict UNKNOWN, never a pass. On 2026-10-13 it ran on a Mac with no board:
UNKNOWN, 2 ports without a USB id, nothing attached. --simulated runs the
same steps against the simulator for calibration and is capped at
SIMULATED; the claims registry refuses a "hardware" row that does not name
this runner.
Tools
Board discovery and firmware:
list_boards: every ESP32 / RP2040 / Arduino board on USB by VID:PID, plus the phone bridge (Pro) and, withMCUPORT_SIMULATOR=1, the simulated boards. Every attached USB serial device whose id is NOT in the tables is listed too (unrecognized_usb_serial, each with the next step:board_infoon that path), and the ports ignored for having no USB id are counted, so an emptyboardslist never hides a plugged-in board behind an unlisted bridge chip.board_info: what a port is running (MicroPython version, or the bridge sketch) and its family; the first call when a tool fails.flash_firmware: esptool / picotool (or UF2 copy) / arduino-cli. For the Pico the argv ispicotool load -f -x <file>(the-fresets a board running MicroPython into BOOTSEL; without it picotool v2.3.1 only sees boards already in BOOTSEL); when picotool finds no board the server falls back to a mounted RPI-RP2/RP2350 drive and otherwise names the BOOTSEL step. For ESP32 the offset is read off the image header (src/boards/esp_image.ts: 0x1000 for a bootloader-first image on ESP32/S2, 0x0 on S3/C3/C6/H2, 0x10000 for an app image, 0x0 for a merge-bin output;flashOffsetoverrides; anything else is refused, not guessed) and the verb follows the installed esptool (v4write_flash, v5write-flash).board_reset: run-mode reset or bootloader entry via DTR/RTS (ESP32) or the 1200-baud touch (RP2040, Arduino R4).install_bridge_sketch: put the bridge sketch on an Arduino so the hardware tools work there.install_ble_repl: put a MicroPython board's REPL on Bluetooth LE (Nordic UART Service) so the phone app can reach it and relay it to this server; writesfirmware/mcuport_ble_repl/to the board and hooksboot.py.
Serial monitor (always-on ring buffer, nothing lost between calls):
serial_open,serial_read(text + base64,droppedBytes, optionaluntilregex),serial_write,serial_close.
Hardware I/O (MicroPython boards need no custom firmware; Arduino boards need the bridge sketch):
gpio_mode,gpio_read,gpio_writeadc_read(raw + estimated volts, optional averaging)pwm_writei2c_scan,i2c_read,i2c_writespi_transfermpy_exec: run any MicroPython on the board and get its output.gpio_audit: the pin-safety map for the board (blocked / needs force / warn / ok per pin, for a stated intent).
Pin safety is code, not advice (src/pin_safety.ts): every tool above that
names a pin audits it against the chip's own table BEFORE any generated code
reaches the board. SPI flash pins, pins that do not exist and output on an
input-only pad are refused outright; the console UART the link runs over,
native USB pads and a Pico W's wireless lines are refused unless the call
passes force: true after reading the reason; strapping pins go through with
the warning in the result. The chip is read from the board itself
(MicroPython os.uname().machine, the bridge sketch's hello), falling back to
the USB family; a chip with no table is reported as UNCHECKED, never as safe.
Tables: ESP32, ESP32-S2/S3/C3/C6, Pico, Pico W, Pico 2, Uno R3, Uno R4.
Build:
compile_sketch: arduino-cli compile with structured diagnostics.build_project: PlatformIOpio run(optionally upload) with structured diagnostics. PlatformIO knows STM32 and nRF52 targets, but this server does not enumerate those boards and has never built for one.
Every write tool says in its description that it changes physical state. An LLM driving GPIO can spin a motor or close a relay; wire accordingly.
How the hardware tools work (and why)
ESP32 and RP2040 boards running MicroPython expose a "raw REPL" over the
same USB serial port: the server sends Ctrl-C, Ctrl-A, a few lines of
Python, Ctrl-D, and reads back stdout / traceback framed by 0x04 bytes,
exactly as mpremote does. That gives GPIO/ADC/PWM/I2C/SPI with no
firmware of ours on the board; the only side effect is that a running
main.py is interrupted. Arduino-class boards have no interpreter, so the
server ships a small sketch (firmware/mcuport_bridge/) with a one-line
request/reply protocol and a tool to upload it.
The phone bridge needs the board's REPL on the radio. install_ble_repl
writes firmware/mcuport_ble_repl/mcuport_ble_repl.py to an ESP32-family
board or a Pico W over USB and adds import mcuport_ble_repl to its
boot.py; from then on the board advertises as MCUPort-XXXX on the Nordic
UART Service and its REPL is on BLE as well as USB (os.dupterm). The phone
app connects to it and relays the bytes; the desktop's bridge board module
(family: "bridge") speaks the same raw REPL over that link, so the pin and
bus tools work on the board through the phone. A plain Pico has no radio and
an Arduino has no interpreter; the tool refuses both with that sentence.
Supported boards: ESP32 family, Raspberry Pi RP2040/RP2350 (Pico, Pico 2,
Pico W; the MicroPython USB id 2E8A:0005 included), Arduino Uno R4
(Minima, WiFi), Nano R4 and Uno R3. Arduino uploads compile for the board
detected on the port (its FQBN comes from the USB VID/PID). STM32 Nucleo
and nRF52 are reachable through build_project (PlatformIO) but are not
yet enumerated by USB id.
Simulator
Set MCUPORT_SIMULATOR=1 and list_boards adds three boards at sim://
paths: an ESP32 and a Pico running the real MicroPython interpreter
(WebAssembly build; only the machine module is an in-memory model), and an
Arduino Uno R4 running a protocol emulator of the bridge sketch. Every tool
works on them, so an agent can be exercised end to end with no hardware.
Their labels say SIMULATED. Any other value than 1/0/true/false for the
variable fails at boot, as does any misspelled MCUPORT_* key.
Install
curl -fsSL https://mcuport.com/install.sh | sh # macOS / Linux
irm https://mcuport.com/install.ps1 | iex # Windows (PowerShell)Needs Node.js 20 or newer. The package is not on the npm registry; the
script installs a checksum-verified tarball from mcuport.com under ~/.mcuport.
Flashing and building require the relevant family's own CLI on PATH; this
package never bundles them:
- ESP32:
pip install esptool - RP2040/RP2350:
brew install picotool(or just copy the.uf2to the board's BOOTSEL mass-storage volume; no tool needed for that path) - Arduino:
brew install arduino-cli - PlatformIO:
pip install platformio
Configure Claude Code / Claude Desktop
Add this to your MCP client's config (Claude Code: claude mcp add, or
edit the config file directly; Claude Desktop:
~/Library/Application Support/Claude/claude_desktop_config.json on
macOS):
{
"mcpServers": {
"mcuport": {
"command": "mcuport",
"env": {
"MCUPORT_LICENSE_KEY": "MCUP-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}MCUPORT_LICENSE_KEY is only needed for Pro (the phone bridge); every
other tool works with no key set.
If you installed from source instead of npm, point command at
node and args at the built entry point:
{
"mcpServers": {
"mcuport": {
"command": "node",
"args": ["/path/to/mcu-mcp/server/dist/index.js"],
"env": { "MCUPORT_SIMULATOR": "1" }
}
}
}Development
npm ci
npm test # vitest: unit + simulator suites
npm run build
npm run conformance # real MCP client over stdio against dist/, incl. the simulator
npm run typecheck
npm run pack-check
node scripts/embed_sketch.mjs # after editing firmware/mcuport_bridge/test/tool_surface.test.ts fails the build if a tool exists that this
README does not name, or if the repo's CLAUDE.md promises a capability no
tool provides. Keep both honest.
