Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Arduino Agent — repo guide for AI agents

Fork of Arduino IDE 2.x (Theia/Electron) with an embedded MCP server.
`arduino-ide-extension/` is **vendored upstream code** (synced from
arduino/arduino-ide by targeted file copies — this repo shares no git history
with upstream); `arduino-mcp-extension/` is ours; `electron-app/` packages both.

## Build

- Yarn 4 via corepack (`corepack enable`). Node 18+ works (engines say <21 but
22 is fine in practice).
- `yarn install` applies `.yarn/patches/*` — after editing ANY patch file you
MUST regenerate and commit `yarn.lock` in the same commit, or CI's immutable
install fails with YN0028.
- Workspace deps are pinned to **exact versions**. A version bump must touch
the workspace's package.json + every dependent's pin (`electron-app` pins
both extensions) + a regenerated `yarn.lock`, all in ONE commit. Use
`node scripts/update-version.js x.y.z` for IDE-version bumps (it also visits
arduino-mcp-extension's deps). Verify with
`corepack yarn install --immutable --mode=skip-build` — exactly what CI runs
— BEFORE pushing a release tag. Both failure modes have broken releases.
- Typecheck an extension: `npm run --prefix <workspace> build` (NOT
`yarn --cwd` — Yarn 4 bin-scoping falls back to a stale global tsc).

## Fast backend-only iteration (Windows dev box)

The MCP extension is bundled into `electron-app/lib/backend/main.js`. To test
a backend change against the installed app at `C:\Arduino` without full
packaging:

1. `npm run --prefix arduino-mcp-extension build`
2. In `electron-app/`: `node ../node_modules/@theia/cli/bin/theia build
--config webpack.config.js --mode production` (capture FULL output to a
file — piping through tail/grep has repeatedly swallowed the real error).
If it dies with ENOTEMPTY on `lib/backend/native-webpack-plugin`, delete
that directory and rerun (transient Windows file-lock race).
3. Kill "Arduino IDE" processes, copy `electron-app/lib/backend/main.js` over
`C:\Arduino\resources\app\lib\backend\main.js`, relaunch.
4. Confirm the copy landed by grepping the installed main.js for a distinctive
new string, and check `/health` uptime is small (a stale uptime means the
old process survived the kill).

`theia rebuild:electron` lies: it says "already rebuilt" even when
`node_modules/node-pty/build/Release/pty.node` is missing. Force it with
`rm -rf electron-app/.browser_modules node_modules/node-pty/build`. The
production webpack build needs pty.node to exist, and rebuild:electron must
run BEFORE the webpack build.

## CI / release

- `check-mcp-extension.yml` runs tsc only. The real test is
`arduino-mcp-extension/test/manual/smoke-test.js` against a RUNNING IDE.
- Release: push a `v*` tag (or create the ref via
`gh api .../git/refs -f ref=refs/tags/vX -f sha=<main tip>`) →
`release.yml` builds win/linux/mac and attaches a DRAFT release; publish
with `gh release edit vX --draft=false --latest`.
- The Windows job is pinned to `windows-2022`: the -latest image ships Visual
Studio 18, which no released node-gyp can detect. Do not "fix" this by
bumping node-gyp; it does not help.
- Release checklist: immutable-install check, smoke test (includes the
bridge-instructions parity assertion), hardware pass if tools changed.

## MCP server architecture notes

- Theia binds CoreService/BoardsService/etc. per frontend connection; the MCP
server reaches them through its own child container
(`mcp-arduino-services.ts`). A backend singleton cannot @inject them.
- Serial output flows through ONE funnel: `ws.on('message')` in
`mcp-serial-manager.ts` (cursor accounting, line scanner, crash-signature
events, wait_for waiters all hang off it).
- Server guidance for agents lives in `src/common/mcp-instructions.ts` (sent
at initialize) and `src/common/mcp-prompts.ts`. The stdio bridge
(`bridge/arduino-agent-bridge.js`) answers initialize locally, loading the
compiled instructions with an embedded fallback — the smoke test asserts
bridge/server parity.
- Serial monitor settings only apply when the monitor service is CREATED;
`connect()` stops any existing service first. The reported baud rate always
reflects the wire (ON_SETTINGS_DID_CHANGE feedback) — never "fix" it to echo
the requested value.

## Hardware testing

Dev board: ESP32-S3 N16R8 on the CH343 UART port (vid 0x1A86), FQBN
`esp32:esp32:esp32s3:FlashSize=16M,PSRAM=opi,CDCOnBoot=default`. Its USB PID
(0x4001, native port) appears in no boards.txt, so it can never be
auto-identified — always pass the FQBN explicitly (or use
`arduino_board suggest_fqbn`). Uploads via the UART port need no BOOT-button
dance; the native-USB port does.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ Unzip and run `Arduino IDE` (`Arduino IDE.exe` on Windows). Older builds are on
> and no dependencies. See [the bridge README](arduino-mcp-extension/bridge/README.md)
> for auto-launch and other options.

> **Claude Code users:** the server sends workflow guidance automatically,
> and ships three slash commands (`/bringup`, `/debug-serial`,
> `/profile-board`). For deeper hardware know-how, install the bundled
> skill: copy [`skills/arduino-agent/`](skills/arduino-agent/) into
> `~/.claude/skills/`.

3. **Talk to your board.**
- *"Create a Blink sketch and open it."*
- *"What boards are connected?"*
Expand Down
25 changes: 25 additions & 0 deletions arduino-mcp-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,31 @@ Returns current IDE state including:

Returns task status: `pending`, `running`, `completed`, `failed`, or `cancelled`.

## Guidance for agents

The server teaches connected agents how to use it:

- **Instructions at initialize** — every MCP client receives ~30 lines of
workflow guidance (explicit FQBNs for unidentifiable boards, `wait:true`,
cursor-based serial, crash events, BOOT-button recovery). Source:
`src/common/mcp-instructions.ts`; the stdio bridge serves an identical copy
when the IDE is closed (parity asserted by the smoke test).
- **MCP prompts** — `bringup`, `debug-serial`, `profile-board`; Claude Code
surfaces them as slash commands.
- **Claude Code skill** — richer judgment (port choice, CDCOnBoot, watchdog
rules, Wokwi simulation): [`skills/arduino-agent/`](../skills/arduino-agent/).

Deliberately **not** shipped: client-side hooks and custom subagents. The
instructions + prompts + skill cover the same ground without imposing
configuration on users; revisit only if a concrete need appears.

## Roadmap

- **v0.7 — pin-safety preflight**: validate pin choices against the target
board (existence, input-only pins, strapping pins, PWM/I2C conflicts)
before upload. Needs a real per-board pin database to be more than theater,
which is why it did not ship in v0.6.

## Environment Variables

Environment variables override the IDE preferences:
Expand Down
96 changes: 96 additions & 0 deletions skills/arduino-agent/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
name: arduino-agent
description: Drive Arduino-compatible hardware through the Arduino Agent MCP server - identify boards, write sketches, compile, flash, and debug over serial. Use when working with Arduino, ESP32, or embedded boards via the arduino MCP tools.
---

# Driving hardware with Arduino Agent

Arduino Agent is an Arduino IDE with an MCP server embedded in it. You share
one editor, one board and one serial monitor with the user: when you write a
sketch, their editor live-reloads; when they select a board, your tools see it.

The server's own instructions (sent at connect) cover the core workflow. This
skill adds the judgment that comes from real hardware sessions.

## Install the bridge, not the URL

If the MCP config points at `http://127.0.0.1:3847/mcp` directly, the whole
server shows "failed to connect" whenever the IDE is closed. Prefer the stdio
bridge (`arduino-mcp-extension/bridge/arduino-agent-bridge.js` in the repo or
install): it always connects, returns "launch the IDE" as a normal tool error,
and recovers without a client restart.

## Identifying boards: the decision tree

1. `arduino_board list_connected` → each port has `identified`, `vid`, `pid`.
2. `identified: true` → use the reported fqbn. Done.
3. `identified: false` → `arduino_board suggest_fqbn {port}`:
- **Native VID** (Espressif 0x303A, Arduino 0x2341, RasPi 0x2E8A): family
candidates come back; PID cannot distinguish S2/S3/C3 variants, so ask
the user or check the silkscreen if more than one fits.
- **Bridge VID** (CH34x 0x1A86, CP210x 0x10C4, FTDI 0x0403): the VID names
the *adapter*, not the MCU. Re-run with `name:` from the user/silkscreen.
- `core_to_install` set → run `install_core` (esp32 core ≈ minutes and GBs;
warn the user), then re-run suggest_fqbn if the fqbn was hidden.
4. From then on pass the fqbn EXPLICITLY to compile/upload/serial connect.
Never retry auto-identification hoping it changes - it cannot.

## Flashing: which USB port matters more than anything

Many dev boards have two USB-C ports. **Uploads via the UART/bridge port just
work** (esptool toggles DTR/RTS). Uploads via the **native USB port** often
fail with "No serial data received" and may need the user to hold BOOT, tap
RESET, release BOOT - and after flashing, the port re-enumerates and can
change its number. If an upload fails, read `result.explained` first; it
distinguishes these cases. When the user reports a two-port board, recommend
the UART port.

Match `CDCOnBoot` to the cable: on the UART port use `CDCOnBoot=default`
(Serial → UART0); `CDCOnBoot=cdc` sends Serial to the native port and the
UART port goes silent while the sketch runs fine.

## Serial: capture without loss, diagnose crashes

- Keep the `cursor` from every read; pass it as `since` next time. `dropped`
> 0 means the 512KB buffer overflowed between reads - read more often.
- `wait_for {pattern, timeout_seconds}` beats polling. A crash/reset resolves
it early with the `event` attached - that is a feature, not a failure.
- `events` tell you what a raw stream cannot: repeated `reset` events =
crash loop (compare `detail` reasons); `panic` carries the Guru Meditation
cause and backtrace; `brownout` = power supply, not code; `watchdog` =
something starved an idle task.
- Garbage output = baud mismatch. The monitor's reported baudRate is always
the true wire rate; match the sketch's `Serial.begin()`.

## Firmware the hardware will accept

- Never busy-loop: `while(true){}` or a tight compute loop starves FreeRTOS'
idle task → task watchdog → reboot. Yield ~1ms every ≤200ms
(`vTaskDelay(1)`) - >99% duty cycle, no watchdog.
- ESP32 PSRAM is ~17x slower than internal SRAM (measured); keep hot buffers
internal, bulk data in PSRAM.
- After `esp_restart()` or a panic, boot-ROM lines appear on UART (not always
on native USB). Wait ~1s after CDC re-enumeration before expecting output.

## Recipes (also available as MCP prompts)

- **/bringup** — detect → suggest_fqbn → install core → heartbeat blink →
upload wait:true → wait_for the heartbeat.
- **/debug-serial** — connect → cursor reads → interpret events → concrete fix.
- **/profile-board** — from_example 99.ArduinoAgent/ESP32_SelfProfile →
upload → parse the JSON it streams (chip, memory bandwidth, thermal curve).
Needs nothing but USB; good demo and good smoke test of a board.

## No board? Simulate

Wokwi's CLI ships an experimental MCP server that runs firmware on simulated
boards (ESP32, Uno, ...) - virtual buttons, sensors, serial assertions. Use it
alongside Arduino Agent when no hardware is attached: develop and logic-test
in the simulator, then flash the real board with these tools. See
https://docs.wokwi.com/wokwi-ci/mcp-support (needs a Wokwi CLI token).

## Timeouts

Task waits: default 60s, max 600. Serial wait_for: default 30s, max 120. Both
return progress/cursor on timeout instead of erroring - re-issue to keep
waiting. Keep requested timeouts under your client's per-call MCP timeout.