MCP: Claude Code as a full OpenPCB agent — hardening, parity, PCB tools, one-click setup - #7
Draft
andrejvysny wants to merge 18 commits into
Draft
andrejvysny wants to merge 18 commits into
andrejvysny wants to merge 18 commits into
Conversation
Records the review of the MCP integration against master (blockers B1-B6, hardening H1-H8, parity gaps P1-P5), the SDK/Claude Code facts that shape the design, and the target architecture the following commits implement. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
The MCP endpoint lost information and state in ways that made it unusable
from Claude Code. This rebuilds the projection around what the SDK and the
client actually do:
- Result envelope: structuredContent is now {ok, status, summary, warnings,
error, proposal, data} and the text block repeats it. Claude Code forwards
only structuredContent when both halves exist and Claude Desktop reads only
content, so the readable parts must live in both. Failing reads return their
reason instead of "null".
- Per-connection identity from headers (X-OpenPCB-MCP-Client/-Client-Name/
-Instance) passed to the server factory as authInfo. The SDK serves 2025-era
clients statelessly, so nothing else survives between requests.
- Chats: one unbound home chat per client plus one chat per design, bound once
and never rebound (replaces the single chat re-bound on every call). This
fixes designer_create_design after the first design call, removes the race
between concurrent sessions, and puts Claude Code's work in the design dock.
- Every call is recorded as a tool event on a visible activity message, so
pending proposals get their approval card and the proposal id reaches the
client.
- Heartbeat progress while a tool runs (idle-timeout safety), abort signal
passed through, per-tool annotation policy, 2 KB-safe descriptions, server
instructions, schema conversion cached per tool (server is built per POST).
- Digest-based token comparison; MCP chats no longer require an in-app LLM
provider; build prompt offered only with writes; /mcp-state probe and
/mcp/clients for the shim and Settings; list_changed on settings toggles.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
The old shim was a dumb pipe: it exited when OpenPCB was not running (the client marked the server failed), went dead after an app restart (stale port and token), and turned upstream failures into stderr lines so the client hung. The bridge is now the MCP server the client talks to: - answers initialize/ping itself and advertises listChanged, so Claude Code registers the server even while OpenPCB is closed; tool calls then return a readable "start OpenPCB" result, and tools/list serves the last known list - forwards every request as a stateless POST; on a connection failure or a 401 it re-reads the portfile and retries once against the new port/token - maps 503/404/401 to actionable tool results instead of hanging - polls the backend state probe and emits list_changed when OpenPCB starts, stops, or the user toggles MCP/writes (the stateless endpoint cannot push) - aborts the in-flight request on notifications/cancelled - sends an instance id per process so concurrent sessions keep separate pins, and a client key derived from clientInfo.name instead of a constant - answers server/discover with method-not-found so 2026-era clients fall back to the 2025 handshake the bridge speaks Logic lives in Electron-free modules (portfile/upstream/bridge) tested under Bun against a real backend on a loopback port, plus a stdio test with the official MCP client. The portfile is now written atomically and never overwrites a live instance's file. Verified with Claude Code 2.1.282: `claude mcp list` reports Connected both with the app running and with it closed. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
…ates Deletions an external agent proposes still wait for the user in OpenPCB's assistant panel — one approval surface — but the agent could not learn the outcome and the panel did not show the card until reloaded. - AssistantEventBus + GET /api/modules/assistant/events (SSE): chat.activity and proposal.updated, ids only. ConversationStore reports every proposal create/status change (panel apply/reject, auto-apply, cloud) to the bus. - MCP-only tools assistant_get_proposal, assistant_list_pending_proposals and assistant_await_proposal (bounded long-poll, max 240 s, heartbeat progress, honours cancellation), limited to the calling client's own chats. Pending proposal hints and server instructions now point the model at the await tool instead of letting it re-send the write. - Frontend useAssistantEvents (React-free controller + hook, backoff reconnect, burst coalescing): the design dock and the Assistant screen refetch on MCP activity, and the dock shows a "waiting for your approval" banner when a proposal is pending in a chat the user is not looking at. - Connections are keyed by client AND instance id. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
The designer canvas only refreshed after its own commands and after in-app assistant runs, so edits made by Claude Code over MCP left it stale. And the undo stack was quietly broken for every non-UI writer: the designer builds two store instances (routes for the UI, SDK for the assistant/MCP) that share the "designer-ui-session" undo session, but each cached its own copy of the history, so after an assistant edit the UI's Ctrl+Z skipped it and undid the user's older command instead. Verified with a regression test before fixing. - Undo/redo histories are shared per database across store instances. - DesignEventBus (per database) published from the store on every committed command (with actor/session), undo, redo, create, rename and delete, plus a design.focus request; GET /api/modules/designer/events serves it as SSE, optionally filtered by designId. - DesignerSDK gains deleteDesign and requestFocus (pure interface additions). - Frontend useDesignerEvents: a pure planner (tested) decides what a burst of events means — refresh the open design when a revision it has not seen arrives from anyone but this UI's own commands, refresh the list, open a focused design's tab — and the workspace applies it. - The SSE subscription logic is shared (src/shared/frontend/live-events) by the assistant and designer hooks. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
Two things the in-app assistant does that an MCP client could not:
- designer_verify_build runs the same Definition-of-Done verifier the in-app
loop runs after a build (BOM placed, required nets wired, no dangling power,
ERC clean). The projection captures the expected BOM from this session's
library_resolve_bom (held on the connection until a design exists) and
compile_circuit (stored on the design chat). The capture logic moved out of
run-service into verification/build-intent-capture.ts so both paths share it.
- knowledge_search_pages / knowledge_get_page and openpcb://knowledge/{id}
resources read OpenPCB Docs through the same core MentionRegistry path as
in-app @mentions (Tiptap → markdown), with no new SDK.
Server instructions now tell the agent to verify after building and where the
user's notes live.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
…r MCP The in-app assistant is schematic-only; none of the ~40 pcb_* commands had a tool, so "any action" stopped at the schematic. MCP-only tools (the in-app registry stays at its tuned 15): - designer_get_pcb_layout: outline, rules, net classes, footprints with world pad positions and nets, per-net copper, unrouted connections as REF.PAD pairs — everything needed to place and route, in mm. - pcb_place_footprints (move/rotate/flip by refdes), pcb_route (named nets, REF.PAD endpoints, mm waypoints joined with the router's 45° elbows, widths and vias from the net class, one atomic pcb_commit_route with legality "refuse"), pcb_delete_routing, pcb_set_board_outline, pcb_set_design_rules (merges, never guesses a manufacturing value), pcb_manage_zone, pcb_manage_keepout, pcb_set_drc_waivers. - designer_rename_design, designer_delete_design, designer_focus_design, designer_get_history, designer_undo, designer_redo. Every write goes through the proposal pipeline (persisted, rendered as a card, action_id dedupe) and reports the DRC count after it applies. Undoable edits auto-apply; deletions, non-undoable rule changes and whole-design deletion wait for the user's approval in the panel (APPROVAL_REQUIRED_KINDS). Undo/redo share the UI's session, so they only act when the entry on top of the stack is a command this client landed: proposal applies now record every commandId they dispatch, and the history snapshot exposes nextUndo/nextRedo. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
…ocal plugin Setup was the last thing standing between an installed OpenPCB and Claude Code: the advertised command pointed into the app bundle (which moves for AppImage, the portable Windows build and translocated macOS apps), spawned a .cmd without cmd /c on Windows, and registered in `local` scope (one project). - Stable launcher: on every launch Electron main copies the bridge to <userData>/mcp/shim.js and writes <userData>/mcp/openpcb-mcp(.cmd), which execs the current app binary as Node (APPIMAGE / PORTABLE_EXECUTABLE_FILE aware; a translocated macOS launch keeps the last good binary and warns). - Local plugin marketplace: <userData>/claude-code/marketplace is generated from a template shipped with the app (six workflow skills: build circuit, review schematic, DRC triage, BOM check, PCB layout, connection help) with .mcp.json pointing at the launcher and version = app version. Verified with Claude Code 2.1.282: `claude plugin validate`, marketplace add, install, `claude mcp list` → Connected, and the update path. - One-click connect in Settings → Assistant → MCP: finds the `claude` CLI (PATH, login shell, known install dirs), installs or updates the plugin or registers the server at user scope, and disconnects — execFile with fixed argv only, never touching an `openpcb` server it did not create. Settings also shows connected clients, the translocation warning, per-OS manual commands, and demotes the per-launch HTTP snippet to "Advanced". - Tests: launcher resolution/scripts, snippets, plugin content, a drift check that every tool a skill names exists, the CLI flow against a fake runner, and the generated POSIX launcher serving a real MCP client end to end. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
…release notes - Flip the `mcp.server` feature flag to availability "all" so installed builds serve MCP. Both user settings (MCP server, allow writes) still default off; the Settings MCP section is gated on the flag. - Endpoint tests pin the release posture: flag "all", and a fresh install has the server and writes off. - CLAUDE.md MCP section rewritten for the hardened design (stateless handler, identity headers, per-design chats, recorder, envelope, tool inventory, approval tiers, undo ownership, live-UI streams, stdio bridge, stable launcher, local plugin). - docs/assistant/mcp-claude-code.md: tool surface and user guide. - docs/assistant/architecture.md §3.2, DEVELOPER.md (bridge env vars), electron/AGENTS.md (launcher, plugin, RunAsNode fuse, AppImage/portable), TODO.md §3 (only the desktop smoke matrix remains), ROADMAP.md. - Draft release-notes section in .github/release-notes/ for the flag flip. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
…er-session chats Review round 1 (PR #7), finding 1 + 3: ownership collapsed to the client key, so two Claude Code sessions of one client could undo each other's changes, see and await each other's proposals, share "allow this tool" decisions, and race on one shared home chat's binding. - Proposals record their MCP actor (migration 0015: actor_client_key, actor_instance_id). Undo/redo ownership (commandIdsAppliedByActor) and the proposal tools (get/list/await) compare the actor, never the chat. - Chats are per session (clientKey|instanceId): one home chat plus one chat per design, so chat-keyed session allowances can no longer authorise another session. Pre-existing shared MCP chats are left alone. - Writes and designer_resolve_design are serialized per session; design chats created by parallel calls are deduped in flight. - ContextResolver.bindDesignIfUnbound: synchronous check+insert used by every auto-bind path, so a chat can never get two primary designs. - The bridge keys its instance id on Claude Code's CLAUDE_CODE_SESSION_ID (hashed; verified that Claude Code sets it for stdio MCP servers), so a session keeps its ownership across /mcp reconnects; OPENPCB_MCP_INSTANCE overrides, a random id otherwise. - Tests: cross-session undo/proposal/allowance isolation, concurrent create/resolve binding invariants, instance id resolution. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
… a key conflict Review round 1 (PR #7), finding 9, plus a latent bug the review pointed at: the in-memory action_id dedup looked inside one chat and let rejected actions through, while the 0010 UNIQUE index covered (design, action_id) across every chat. When they disagreed — the same deterministic id from another chat or MCP session, or a retry after a rejection — createWriteProposal returned the existing row, the caller ignored it, auto-applied the NEW envelope, then threw "Write proposal not found" after the designer mutations had landed. In-app and MCP alike. - Migration 0016: idempotency_scope ("mcp:<client>:<instance>" or "chat:<chatId>", backfilled), UNIQUE (design_id, idempotency_scope, action_id) replaces the design-wide index. - Lookup and index use the same key (getWriteProposalByActionKey), so they cannot disagree. An MCP session dedupes across its chats; another session or chat with the same deterministic id is a different action. - finalizeAndMaybeApply and the placement tool detect an existing proposal returned by the store and report it — they never apply on a conflict. - A rejected or failed action_id is blocked on re-send (new id required), instead of silently acting as a retry. - MCP PCB tools validate action_id (letters, digits, . _ : -, ≤ 200). - Tests: the store over the REAL migrations (no hand-copied schema); retry replay, parallel duplicates land once, cross-session same id, rejected re-send blocked. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
…ision Review round 1 (PR #7), finding 2: applyDesignDeleteProposal called deleteDesign without looking at the proposal's baseRevision, so approving an old "delete design" card deleted everything the user had done since. - Design deletion re-reads the design and refuses unless its head is still the proposal's baseRevision. No apply-anyway path: a fresh proposal is required. - ProposalStaleError (code STALE_PROPOSAL, expected/current revision) replaces the plain Error in the schematic/PCB and placement apply paths. Approvals and auto-apply persist it as the failed apply result, so the reason survives a reload. - assistant_get_proposal / assistant_await_proposal explain a stale or failed outcome (with revisions) and ask for a new action_id. - Proposal card: shows the persisted failure reason (it never rendered applyResult), marks a refused apply as failed, shows the revision a proposal was made against, and does not offer "allow for this session" on kinds the backend never auto-applies. - Tests: old delete proposal after an edit → refused, design kept; old rules proposal → refused; card helper (Vitest). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
… counts show suppression Review round 1 (PR #7), finding 4: pcb_set_drc_waivers auto-applied (it is not even undoable — view-state commands skip history) and could ignore whole rule classes, letting an agent make a failing board look clean. Its post-apply count also included waived violations while ignored ones simply vanished. - Replaced by two tools: - pcb_waive_drc_violations: ids must be in the current report, never a NON_OVERRIDABLE code, a reason is required; waiving is approval-tier (designer_pcb_drc_waivers). Un-waiving applies at once. - pcb_set_drc_rule_class_ignores: enum-validated classes, a reason, and the card says how many current violations it would hide. Ignoring is approval-tier AND never covered by a session allowance (NEVER_SESSION_ALLOWED_KINDS, with design delete). Un-ignoring applies. - DRC engine: DrcReport.suppressed { byRuleClass, bySeverityOverride }, counted by violation id, present only when something was hidden (reports without suppression keep their exact bytes). - drcCounts / drcCountsLine: designer_run_drc and every post-apply summary report active, waived, hidden and raw counts, and say the board is not clean while anything is suppressed. designer_get_pcb_state exposes the waiver/ignore configuration. - The DRC triage skill no longer invites waiving; it reports suppression. - Tests: engine counts; waive/ignore approval, reason and id checks, session allowance refused for ignores, counts before/after. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
…per tool Review round 1 (PR #7), findings 7, 14, 15, 16. MCP writes bypass the HTTP route parsers, and the store parses leniently, so the tools now validate against the actual design before proposing anything: - pcb_set_design_rules (rules-validation.ts): sizes > 0, via drill < via pad, non-empty and unique names, unique ids (new ids via uniqueSlug; an explicit colliding id is refused), one patch per class, clearances and board thickness > 0. Matching is by id when given, else by name. New classes copy only presentation fields from the first class — never its voltage / current / pair-gap metadata. All problems are reported at once. - pcb_set_board_outline: roundrect requires cornerRadiusMm (0 < r ≤ half the shorter side; the `?? 1` default is gone), circle takes diameterMm, oval is explicit, polygons must be simple (shared ringSelfIntersects). - Layers on pcb_route, zones and keepouts are checked against the board's real copper stack (copperLayersForCount); pcb_route widthMm must be > 0. - pcb_delete_routing reports unknown nets and stale ids (they were dropped silently); failed operations carry the executor's detail, not a bare code. - pcb_manage_zone / pcb_manage_keepout split into pcb_{add,update,delete}_ {zone,keepout}: delete is its own destructive tool, add/update are not. - designer_focus_design is a UI side effect (policy uiSideEffect): available with writes off, never annotated read-only. - Tests: stack-layer refusals, outline cases, rule sanity, the split tools' annotations, plus unit tests for the rules validator. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
…pec SSE framing Review round 1 (PR #7), findings 8 and 11. - /mcp-state's toolset fingerprint hashes the full tool contracts (name, version, description, input schema, annotations, _meta — canonical JSON, SHA-256) instead of the names only, and the state carries a per-boot generation id. An update that keeps every name but changes a schema now reaches clients as list_changed. - The bridge compares enabled / writes / toolset / appVersion / generation, and also re-lists whenever the endpoint changed (url, token or pid) — tracked as an epoch on Upstream so a restart first seen by a call's retry is not missed by the poll. - SSE: the hand-written splitter (per-chunk CRLF replace, no lone-CR support, trailing bytes dropped) is replaced by eventsource-parser (already in the tree via the MCP SDK; now a declared root dependency). The decoder is flushed, a final event without its blank line is still dispatched, a malformed event is logged and skipped, and a non-JSON plain response is a protocol error instead of an unhandled SyntaxError. - Tests: seeded fuzz over random and per-byte chunking with LF / CRLF / CR, multi-byte UTF-8, multi-line data, comments and a malformed event; re-list after a retry-discovered restart; re-list on a same-count contract change; the fingerprint shape. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
… layout Review round 1 (PR #7), finding 10: every call could carry the full data twice to the model (structuredContent + up to 90k chars of text) and a third time into SQLite (the recorder stored the whole result), so long agent loops over big reads grew the conversation database without bound. - Text half capped at 24k chars (was 90k), cut on a code-point boundary, with a pointer to structuredContent, which stays complete. Summary, warnings and proposal lines are always kept. - designer_get_pcb_layout pages footprints (offset / limit, default 100, max 500, page.nextOffset) and caps traces at 2,000 with a hint to filter by nets; the result is marked truncated when either applies. - The recorder stores results whole only where the panel renders them (library search, BOM, placement proposals) or when small (≤ 8k chars); proposal results keep just {id, kind, designId, baseRevision} (the envelope already lives in the proposal record); everything else becomes a digest {bytes, sha256, preview}. Arguments are bounded the same way. - Tests: digest rules, proposal slimming, argument bound, a real layout read stored small while a library search stays renderable, surrogate-safe cut, layout paging. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
…up ownership Review round 1 (PR #7), findings 6 and 12. Windows: - MCP clients are registered with the app binary on the copied bridge plus ELECTRON_RUN_AS_NODE in the client's env (plugin .mcp.json, snippets, Claude Desktop JSON, one-click) — cmd.exe is no longer between any path and the transport. The exe path is recorded; when the app moves, Settings offers "Update connection" / "Update plugin". - An npm-installed claude.cmd is resolved to the node + cli.js (or .exe) it wraps and spawned directly. Only an unreadable .cmd falls back to cmd.exe /d /s /c with verbatim arguments, escaped with a port of cross-spawn's escape.js (tested against cross-spawn itself). - The CLI registers with `claude mcp add --scope user openpcb -e K=V -- …` (no JSON argument), verified against Claude Code 2.1.282. - The fallback openpcb-mcp.cmd doubles `%`, quotes its error echo and reads itself as UTF-8 (chcp 65001). Snippets are PowerShell-quoted. Ownership: - <userData>/claude-code/registration.json records exactly what this installation registered (mode, server entry, plugin, marketplace, stable installation id). A registration is "ours" only if its scope is user and its command + args (+ env) match the record or what the app would register now — the /openpcb-mcp/ regex heuristic is gone. A same-named marketplace at another path is not ours either. Disconnect/update never touch anything else. - `mcp get` output is parsed field by field (fixtures captured from the real CLI); drift between the record and the current app is reported as an update. - Messages point at /reload-plugins (plugins) or /mcp reconnect (server). - Tests: Windows config/snippets, .cmd escaping, cross-spawn parity vectors, cmd-shim resolution, mcp get parsing, look-alike/project-scope servers not owned, moved-app detection, the registration record. Also exercised with the real claude CLI in an isolated HOME (connect → owned → disconnect; a look-alike /usr/local/bin/openpcb-mcp left untouched). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
…d docs Review round 1 (PR #7), finding 13, and documentation for the round. - Build skill and MCP build prompt: ask before assuming supply / logic voltage, currents and ratings, packages the assembly depends on, connector pinouts, isolation or fab limits; choose only reversible layout details and say so. The "assume 5 V, ~1 Hz, 0603" default and the "finish in one go" pressure are gone. New action_id after a rejection. - Review skill and prompt: ERC findings (facts) separate from engineering observations (heuristics with evidence, saying when data is insufficient). BOM check likewise. DRC triage reports waived / hidden counts and never invites waiving. - Server instructions (1,806 chars): ask-before-assuming, DRC suppression, waivers are approval-tier, new action_id after rejection. - Connection-help skill: per-session ownership, Update connection/plugin, /reload-plugins. - CLAUDE.md MCP section, docs/assistant/mcp-claude-code.md (tool tables, guide, new §5 finding → fix), electron/AGENTS.md (Windows transport, registration record, instance id), DEVELOPER.md (OPENPCB_MCP_INSTANCE), TODO.md §3 (the release-gate matrix incl. Windows paths, concurrent sessions, CI key mismatch), release notes. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes Claude Code — on the user's own Claude subscription — a full agent for OpenPCB over MCP. It can do everything the in-app Assistant does, plus PCB placement and routing, board and rules, and design management and undo. Setup is one click from Settings. The original review (6 blockers, 8 hardening items, 5 parity gaps) and the user guide are in
docs/assistant/mcp-claude-code.md. Review round 1 is addressed in nine further commits;docs/assistant/mcp-claude-code.md§5 maps each finding to its fix.Why: on
masteran installed-app user could not use Claude Code at all:structuredContent, which dropped errors and warnings..cmdfiles.What changed — implementation (one commit per phase)
2d502d83ee05ebcontentandstructuredContent; header identity viaauthInfo; home + per-design chats; call recorder so proposal cards render; annotation policy; heartbeat progress and cancellation; serverinstructions;list_changedon toggle; hash-compared token.e432d01b289ca7assistant_{get,list_pending,await}_proposal; assistant SSE so cards appear live.6afbfab08acdf3designer_verify_build; Docs pages via the coreMentionRegistry.bdae8102c66ada91dc437mcp.server→"all"; both user settings still default off.What changed — review round 1
c74e228bindDesignIfUnboundeverywhere. The bridge keys the session on Claude Code'sCLAUDE_CODE_SESSION_ID(hashed; verified that Claude Code sets it for stdio servers).fa4b635UNIQUE(design, scope, action_id)(migration 0016), lookup uses the same key. Fixes a pre-existing path, in-app too: on a key conflict the write path applied the new envelope anyway and then threw. Rejected/failed ids are spent.c7ce438STALE_PROPOSALpersisted and shown on the card and to the agent; no apply-anyway.87b739cDrcReport.suppressed; every summary reports active / waived / hidden / raw.bf92ce82393299eventsource-parserwith seeded fuzz tests.7d3c4f5347c727ELECTRON_RUN_AS_NODE(no cmd.exe in the transport); npmclaude.cmdresolved to node + cli.js; cross-spawn escaping only as fallback;mcp add -e. Registration record + exact command/args ownership (no path regex).7e13544Finding 5 (flag graduation): kept at
"all"by maintainer decision; this PR stays draft until the platform matrix passes. CI is left as is by maintainer decision (see Notes).In-app Assistant behaviour and its 15-tool registry are unchanged apart from the shared fixes above (idempotency conflict, stale error type). The 33 MCP-only tools are never added to it.
Type of change
Test plan
npm run typecheck— no new errors (baseline 26, now 26; the one diff is the pre-existingproposal-apply-service.tsTS2322 from@openpcb/contractsdrift, message variant only).electron/tsc: baseline 16, now 16.npm run gen:check— module registry and SDK stubs clean. The chainedgen:copilot-schemas:checkfails locally because the pinned@openpcb/[email protected]has noschemas/copilot; untouched here.gen:contracts -- --check: 13 files up to date.npm run test:backend— 3199 pass, 2 skip, 6 fail; the 6 are exactly the pre-existingmasterbaseline set (listed in Notes). Nothing new fails.npm run test:react— 70 files, 608 passed, 1 todo.npm run test:e2e— not run (Settings section and SSE hooks covered by Vitest).TODO.md§3): macOS installed / moved / DMG, Windows installer + portable with native and npmclaude, awkward profile paths, Linux AppImage +.deb, two concurrent Claude Code sessions, plugin install → update →/reload-plugins→ uninstall.New automated coverage (review round 1): multi-session isolation (undo, proposals, allowances, concurrent create/resolve), idempotency (retry, parallel duplicates, cross-session ids, rejected re-send; the store over the real migrations), stale delete/rules approvals, DRC waive/ignore flows and engine counts, rules/outline/layer validation, SSE fuzz (LF/CRLF/CR, UTF-8 splits, per-byte chunks), contract-change and endpoint-change re-listing, result/audit size and layout paging, Windows config + cross-spawn escaping parity + cmd-shim resolution,
mcp getparsing and exact ownership.Real Claude Code CLI (2.1.282), isolated HOME: verified that Claude Code passes
CLAUDE_CODE_SESSION_IDto stdio servers;mcp add … -e+mcp getoutput captured for fixtures; one-click server connect → owned → disconnect, with a look-alike/usr/local/bin/openpcb-mcpleft untouched; the regenerated plugin (Windows-style exe + env entry) passesplugin validateand installs; the bundled shim answersinitializewith the app down.Notes for reviewers
ELECTRON_RUN_AS_NODEthrough is still unverified.corelib:fetchfails onmasterand here because the CoreLibrary v0.1.0-beta.1 release'sopenpcb-core.pubdoes not matchresources/keys/openpcb-core-2026.pub, so CI runs no tests. Left alone here by maintainer decision (a trust-store change); tracked inTODO.md§3.action_idindex with the scoped one (a strictly weaker constraint, so existing rows cannot violate it).DrcReport.suppressedis added only when something was hidden, so unsuppressed reports keep their exact bytes (golden and parity suites pass).eventsource-parseris now a declared root dependency (already in the tree via the MCP SDK; lock bumped 3.1.0 → 3.1.1).RunAsNodefuse: required enabled (default; no fuse config today). Documented inelectron/AGENTS.md.TODO.md§3.CLAUDE.mdlists skills that don't exist in.claude/skills/.🤖 Generated with Claude Code
https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV