Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2d502d8
docs(assistant): MCP × Claude Code review and hardening architecture
claude Sep 25, 2026
3ee05eb
feat(assistant/mcp): server correctness for Claude Code
claude Sep 25, 2026
e432d01
feat(electron/mcp): resilient stdio bridge for Claude Code
claude Sep 25, 2026
b289ca7
feat(assistant): approval round-trip for MCP clients + live panel upd…
claude Sep 25, 2026
6afbfab
feat(designer): live design change stream; shared undo across writers
claude Sep 25, 2026
08acdf3
feat(assistant/mcp): parity tools — build verification and Docs pages
claude Sep 25, 2026
bdae810
feat(assistant/mcp): PCB, board, rules and design-management tools fo…
claude Sep 25, 2026
2c66ada
feat(electron/mcp): stable launcher, one-click Claude Code connect, l…
claude Sep 25, 2026
91dc437
feat(assistant/mcp): graduate mcp.server to release builds; docs and …
claude Sep 25, 2026
c74e228
fix(assistant/mcp): isolate concurrent sessions — actor provenance, p…
claude Sep 25, 2026
fa4b635
fix(assistant): scope write idempotency to the issuer; never apply on…
claude Sep 25, 2026
c7ce438
fix(assistant): refuse stale approvals — design delete checks its rev…
claude Sep 25, 2026
87b739c
fix(assistant/mcp): DRC waivers and rule-class ignores need the user;…
claude Sep 25, 2026
bf92ce8
fix(assistant/mcp): validate PCB writes against the design; one risk …
claude Sep 25, 2026
2393299
fix(electron/mcp): bridge re-lists on contract or endpoint changes; s…
claude Sep 25, 2026
7d3c4f5
fix(assistant/mcp): bound result text and audit storage; page the PCB…
claude Sep 25, 2026
347c727
fix(electron/mcp): Windows without cmd.exe in the MCP path; exact set…
claude Sep 25, 2026
7e13544
docs(assistant/mcp): agent guidance asks before assuming; review-roun…
claude Sep 25, 2026
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
59 changes: 59 additions & 0 deletions .github/release-notes/next-mcp-claude-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!--
Draft section for the next release's notes. Fold it into
`.github/release-notes/v<version>.md` (from RELEASE_TEMPLATE.md) at tag time,
then delete this file. Written when the `mcp.server` feature flag was
graduated to "all" — the flip is the release event (CLAUDE.md → Feature flags).
-->

## Highlights — use Claude Code as your OpenPCB agent

- **Claude Code integration (MCP).** Claude Code — using the Claude subscription you already
have — can now drive OpenPCB: everything the built-in Assistant does (find parts, build and wire
schematics, review, ERC, DRC, BOM), plus work the built-in Assistant cannot do yet: **PCB
footprint placement, trace and via routing, board outline, design rules and net classes, copper
zones, keepouts, DRC waivers, renaming, deleting and focusing designs, and undo/redo.** Claude
Desktop and other MCP clients work too.
- **One-click setup.** Settings → Assistant → *MCP server · Claude Code* → **Connect Claude Code**
installs an OpenPCB plugin for Claude Code (the tools plus workflow skills: build a circuit,
review a schematic, DRC triage, BOM check, PCB layout, connection help). Commands for setting it
up by hand are in the same panel.
- **You stay in control.**
- The MCP server and "Allow writes" are both **off** until you turn them on.
- With writes on, edits apply immediately and land in your normal undo history (Ctrl+Z).
- Deletions, design-rule and net-class changes, DRC waivers, ignoring DRC rule classes and
deleting a design wait for your approval in the assistant panel; Claude waits for your
decision. An approval card for a design that has changed since is refused, never applied.
- DRC results Claude reports always include waived and hidden violations — it cannot present a
board with suppressed problems as clean.
- Each Claude Code session is kept apart: it can undo only its own most recent change (never
yours or another session's), and only it can follow its proposals.
- Every call Claude makes is logged in a per-session "MCP · Claude Code" chat on the design, and
the canvas updates live as it works.
- OpenPCB only ever updates or removes the Claude Code registration it created itself.
- **Robust connection.** Claude Code connects through a launcher OpenPCB keeps up to date, so the
setup survives app updates, moves and restarts; while OpenPCB is closed, Claude Code is told to
ask you to start it instead of failing.

## Fixes

- Pressing Ctrl+Z right after the Assistant edited a design could undo one of your own earlier
changes instead of the Assistant's edit. Undo now always reverses the most recent change,
whoever made it.

## Known issues (MCP)

- On Windows, Claude Code runs OpenPCB's own executable for the connection. If you move or
reinstall OpenPCB elsewhere, Settings → Assistant → MCP shows **Update connection** (or **Update
plugin**); click it once.
- The connection runs OpenPCB's own binary as Node. On the Linux **AppImage** and the Windows
**portable** build this was not verified on real hardware before release. If Claude Code cannot
connect: on Linux, installing Node.js is a workaround (the launcher falls back to it); on Windows,
use the installer build.
- On macOS, run OpenPCB from the Applications folder. Opened straight from the disk image or
Downloads, macOS gives it a temporary path; Settings shows a warning and Claude Code may lose it
after you quit.
- After updating OpenPCB, click **Update plugin** in Settings, then run `/reload-plugins` in open
Claude Code sessions, so Claude Code picks up new tools and skills.

<!-- Maintainer note, not for the published notes: the launcher depends on
Electron's RunAsNode fuse staying enabled (see electron/AGENTS.md). -->
121 changes: 91 additions & 30 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,37 +323,98 @@ OpenPCB is a **single-user desktop app with no auth layer**. Loopback is the sec

## MCP server

OpenPCB exposes its assistant tool registry over **MCP** so external agents (Claude Code, Claude
Desktop, Codex) can drive the design the user has open. It lives **inside the assistant module**,
which owns the registry, `ContextResolver`, proposals and the write policy.

- **Endpoint:** Streamable HTTP at `/api/modules/assistant/mcp` (POST/GET/DELETE), built on
`@modelcontextprotocol/server` v2's `createMcpHandler`, whose `fetch(Request) → Response` matches
the module router natively. Code under `src/modules/assistant/backend/mcp/`.
- **Sessions are backed by a real assistant chat**, one per client, matched on
`metadata.mcp.clientKey` derived from the `X-OpenPCB-MCP-Client` header or User-Agent — it must be
**header-stable, never the display name**. This is what lets every existing designer tool work
unchanged: they resolve their design through `contextResolver.getPrimaryDesign(chatId)`. It also
means MCP tool calls and pending proposals render in the assistant panel.
- **Tools:** the 15 in-app `AiTool`s projected 1:1 (`AiToolDefinition` is already MCP-shaped;
`fromJsonSchema` takes `inputSchema` verbatim), plus MCP-only extended reads in
`tools/read-tools.ts` (`designer_list_designs`, `get_pcb_state`, `run_erc`, `run_drc`, `get_bom`,
`export_manufacturing`) and the session-scoped `designer_use_design`. **Do not add the extended
reads to the in-app registry** — its prompt and DoD harness are tuned against the current 15.
- **Design targeting:** explicit `designId` → session pin (`designer_use_design`) → UI-active
design. The frontend pushes the focused tab to `PUT /api/modules/designer/active-design`
(in-memory).
OpenPCB exposes its tools over **MCP** so external agents — above all **Claude Code**, on the user's
own Claude subscription — can drive the designs in the running app. It lives **inside the assistant
module**, which owns the registry, `ContextResolver`, proposals and the write policy. Long-form
review, architecture and user guide: `docs/assistant/mcp-claude-code.md`.

- **Endpoint:** Streamable HTTP at `/api/modules/assistant/mcp`, built on
`@modelcontextprotocol/server` v2's `createMcpHandler`. It is **stateless per request** for
2025-era clients (fresh server per POST, no `Mcp-Session-Id`, GET/DELETE → 405), so nothing about a
client survives inside the SDK. Code under `src/modules/assistant/backend/mcp/`.
- **Identity comes from headers**, handed to the server factory as `authInfo`:
`X-OpenPCB-MCP-Client` (client key — **header-stable, never the display name**; falls back to
User-Agent), `X-OpenPCB-MCP-Client-Name` (display), `X-OpenPCB-MCP-Instance` (the **session**: the
bridge sends a hash of Claude Code's `CLAUDE_CODE_SESSION_ID`, stable across `/mcp` reconnects;
`OPENPCB_MCP_INSTANCE` overrides; else random per process). **The actor is `clientKey +
instanceId`** — two Claude Code sessions of one client are two actors. `McpConnectionRegistry`
holds pins and last-used designs per actor, evicted after 30 min idle.
- **Ownership is the actor, never the chat.** Proposals persist `actor_client_key` /
`actor_instance_id` (migration 0015); `assistant_{get,list_pending,await}_proposal` and
`designer_undo`/`redo` (`commandIdsAppliedByActor`) compare them. Chats are presentation:
**one unbound home chat per session plus one chat per session per design**, bound once and never
rebound (metadata `{scope:"designer", designId, mcp:{clientKey, instanceId, role}}` — the shape
`listChatsForDesign` filters on). Because chats are per session, chat-keyed session allowances
cannot cross sessions. A tool that binds the home chat (create/resolve design) turns it into that
design's chat; writes and binding tools are serialized per session (`serialize`), and every
auto-bind path uses `ContextResolver.bindDesignIfUnbound` (synchronous check+insert). Every call is
recorded (`call-recorder.ts`) as a tool event on a visible activity message — that is what renders
proposal cards. **Never** write MCP calls through the run-service path: its `role:"tool"` replay
messages would corrupt in-app history.
- **Idempotency** (migration 0016): `UNIQUE(design_id, idempotency_scope, action_id)` where the scope
is `mcp:<client>:<instance>` or, in-app, `chat:<chatId>`; the lookup uses the same key. When the
store returns an existing proposal, callers must **not** apply (`finalizeAndMaybeApply` / the
placement tool check the returned id). A rejected or failed `action_id` is blocked on re-send.
- **Staleness:** approving a proposal whose `baseRevision` is no longer the head throws
`ProposalStaleError` (`STALE_PROPOSAL`), persisted as the failed apply result. Design deletion
checks it too. There is no apply-anyway for stale proposals.
- **Results** (`result-envelope.ts`): `structuredContent` = `{ok, status, summary, warnings, error,
proposal, data}` and the text block repeats it, data capped at 24k chars (cut on a code point).
Clients disagree on which half reaches the model (Claude Code: structuredContent only; Claude
Desktop: content only) — keep readable text in both. The recorder stores full results only where
`MessageCard` renders them (library search, BOM, placement) or when ≤ 8k chars; proposal results as
`{id, kind, designId, baseRevision}`; the rest as a digest. Big reads page
(`designer_get_pcb_layout` offset/limit).
- **Tools:** the 15 in-app `AiTool`s projected 1:1, plus **MCP-only** tools — extended reads
(`tools/read-tools.ts`), Docs pages (`tools/knowledge-tools.ts`, via the core `MentionRegistry`),
PCB/board/rules (`tools/mcp-pcb-tools.ts`: one risk per tool — add/update and delete are separate
tools), design management + history (`tools/mcp-design-tools.ts`), and connection-scoped
`designer_use_design`, `designer_verify_build`, `assistant_{get,list_pending,await}_proposal`.
**Do not add MCP-only tools to the in-app registry** — its prompt and DoD harness are tuned against
the current 15. Every write tool needs an entry in `mcp/tool-policy.ts` (a test enforces it;
`uiSideEffect` marks read tools that change only what the UI shows); descriptions and
`mcp/instructions.ts` must stay ≤ 2,000 chars (Claude Code truncates at 2,048). MCP writes bypass
the HTTP route parsers, so the tools validate against the design themselves (layers vs the real
stack, sizes > 0, drill < pad, unique net-class ids/names — `tools/rules-validation.ts`).
- **Writes** go through the proposal system (persisted, carded, `action_id` dedupe). Undoable edits
auto-apply; destructive ones and `APPROVAL_REQUIRED_KINDS` (non-undoable rule changes, DRC waivers,
rule-class ignores, design deletion) wait for the user's approval in the panel;
`NEVER_SESSION_ALLOWED_KINDS` (rule-class ignores, design deletion) are never covered by "allow this
tool this session". `assistant_await_proposal` lets the agent wait for the decision.
`designer_undo`/`redo` share the UI's `designer-ui-session` and only act on an entry this actor
landed (apply results record `commandIds`; history exposes `nextUndo`).
- **DRC suppression is always reported:** `DrcReport.suppressed` counts violations hidden by ignored
rule classes / "ignore" overrides (absent when none); MCP summaries give active, waived, hidden and
raw counts (`tools/drc-counts.ts`) and never call a board clean while anything is suppressed.
- **Live UI:** `GET /api/modules/assistant/events` and `GET /api/modules/designer/events` (SSE, ids
only) let the panel and canvas refresh on MCP activity. The designer's undo histories are shared
per database across its two store instances (routes + SDK).
- **Design targeting:** explicit `designId` → session pin → UI-active design (pushed by the frontend
to `PUT /api/modules/designer/active-design`, cleared when the Designer screen unmounts) →
the connection's last design (with a warning).
- **Two settings, both default off** (`assistant_settings.mcp_enabled` / `mcp_allow_writes`). Writes
are forced off whenever the server is off; when writes are off, write tools are **not registered
at all**. Otherwise the in-app policy applies — non-destructive edits auto-apply, deletions pend
for approval.
- **Security:** bearer token from `OPENPCB_MCP_TOKEN` (generated per launch by Electron main) plus a
loopback-only Origin check. Discovery via `<APP_DATA_DIR>/mcp.json` at mode 0600; the backend port
is ephemeral, so there is nothing to hardcode.
- **stdio clients** use the bundled shim (`electron/src/mcp-shim/`), launched by
`build/mcp/openpcb-mcp{,.cmd}` via `ELECTRON_RUN_AS_NODE` on the app's own Electron binary — no
system Node needed. It ships through `extraResources` because nothing inside `app.asar` is
spawnable. The app must be running; there is no headless fallback (one SQLite writer).
at all**. Toggling notifies 2026-era clients (`handler.notify`); the bridge covers 2025-era ones.
- **Security:** bearer token from `OPENPCB_MCP_TOKEN` (generated per launch by Electron main,
compared by SHA-256 digest) plus a loopback-only Origin check. Discovery via
`<APP_DATA_DIR>/mcp.json` at mode 0600, written atomically, never over a live instance's file.
`/mcp-state` (bearer) is the bridge's cheap state probe: a SHA-256 of the full tool contracts plus a
per-boot `generation`.
- **stdio bridge** (`electron/src/mcp-shim/`, Electron-free modules): answers `initialize`/`ping`
itself (works while the app is closed), re-reads the portfile on failure (survives restarts),
synthesizes `list_changed` (state fingerprint or endpoint epoch changed), frames SSE with
`eventsource-parser`, aborts on `notifications/cancelled`, and answers `server/discover` with
method-not-found so 2026-era clients fall back to the 2025 handshake it speaks. Electron main
installs a **stable launcher** at `<APP_DATA_DIR>/mcp/openpcb-mcp{,.cmd}` on every launch
(`ELECTRON_RUN_AS_NODE` on the current binary; AppImage / portable / translocation aware). macOS /
Linux clients point at the launcher; **Windows clients point at the app exe + the copied shim with
`ELECTRON_RUN_AS_NODE` in their env** (no cmd.exe in the transport). Main also generates a **local
Claude Code plugin marketplace** at `<APP_DATA_DIR>/claude-code/marketplace` (MCP server + skills
from `electron/resources/claude-plugin/`). Settings → Assistant → MCP can install it via the user's
`claude` CLI; `<APP_DATA_DIR>/claude-code/registration.json` records exactly what was registered,
and only registrations matching it (or what the app would register now) are ever updated or
removed. The app must be running for tool calls; there is no headless fallback (one SQLite
writer).

## Commands — agent-relevant deltas

Expand Down Expand Up @@ -414,7 +475,7 @@ any id below.** The exact ids were not confirmed when this file was written.
| `pcb.lengthTuning` | the length-tuning (Tune) tool |
| `pcb.bundleRouting` | the Bundle tool (toolbar-only surface) |
| `dataset.capture` | designer dataset capture — see `src/modules/designer/AGENTS.md` |
| `mcp.server` | the MCP endpoint route |
| `mcp.server` | the MCP endpoint route and the Settings MCP section (graduated to `all`) |
| `cloud.auth` | cloud foundation, wired at the `readCloudConfig().enabled` chokepoint |
| `cloud.sync` | cloud design sync |
| `cloud.designBrowser` | cloud design browser |
Expand Down
4 changes: 4 additions & 0 deletions DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ database at `/tmp/openpcb-e2e.sqlite*` through `OPENPCB_DB_PATH`.
| `OPENPCB_ALLOWED_ORIGINS` | localhost:1420, :3000 | Comma-separated CORS allowlist |
| `OPENPCB_DEBUG_DIAGNOSTICS` | `false` | Enables `/api/diagnostics/debug/modules` |
| `OPENPCB_MCP_TOKEN` | generated per launch | Bearer token for the MCP endpoint |
| `OPENPCB_MCP_PORTFILE` | `<APP_DATA_DIR>/mcp.json` | stdio bridge: read this discovery file instead of the per-OS default (tests, multiple installs) |
| `OPENPCB_MCP_CLIENT` | client's `clientInfo.name` | stdio bridge: override the client key (half of the MCP actor); must be stable |
| `OPENPCB_MCP_INSTANCE` | hash of `CLAUDE_CODE_SESSION_ID`, else random | stdio bridge: override the session id (the other half of the actor: chats, proposal ownership, undo rights, idempotency) |
| `OPENPCB_MCP_POLL_MS` | `3000` | stdio bridge: how often it re-checks the app's MCP state for `list_changed` |
| `OPENPCB_E2E_NO_WEBSERVER` | unset | Set to `1` to stop Playwright starting its own servers |
| `AUTO_LAYOUT_URL` | dev `http://localhost:3002`, packaged `https://autolayout.cloud.openpcb.app` | Cloud Auto Layout / Route Board service base URL. Legacy `AUTO_ROUTER_URL` / `AUTO_PLACE_URL` still honoured (same merged service). Point it at `cloud-infra/devstack` for local work |
| `NODE_ENV` | — | `development` / `test`; any non-prod value turns feature flags on |
Expand Down
11 changes: 8 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,14 @@ renders and exports to Gerber region primitives today. Bounded zones exist only
from KiCad projects — there is no zone drawing tool, and keepout regions are not implemented. The
work is a zone authoring tool plus keepout support, not a fill engine.

**MCP server.** Exposing OpenPCB's assistant tool registry over the Model Context Protocol so an
external agent — Claude Code, Claude Desktop, Codex — can drive the design you have open, with
writes off by default and destructive operations held for approval. In development.
**MCP server and Claude Code.** OpenPCB's tools over the Model Context Protocol, so an external
agent — above all Claude Code on the user's own Claude subscription, also Claude Desktop and Codex —
can do everything the in-app Assistant does, plus PCB placement, routing, board outline, rules,
zones, keepouts, waivers, design management and undo. Setup is one click in Settings (a local
Claude Code plugin with workflow skills, or the MCP server alone); the server and writes are both
off until the user enables them, and deletions and rule changes wait for approval in the app.
Implemented; awaiting a packaged-build smoke test on each desktop platform before release (see
`TODO.md` §3).

**Drill slot authoring**, project export and import for backup and portability, and mapping
manufacturer part numbers from KiCad symbol fields on import.
Expand Down
Loading
Loading