Skip to content

MCP: Claude Code as a full OpenPCB agent — hardening, parity, PCB tools, one-click setup - #7

Draft
andrejvysny wants to merge 18 commits into
masterfrom
claude/focused-cori-s0xh0c
Draft

andrejvysny wants to merge 18 commits into
masterfrom
claude/focused-cori-s0xh0c

Conversation

@andrejvysny

@andrejvysny andrejvysny commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

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 master an installed-app user could not use Claude Code at all:

  • The MCP route was dev-only, so packaged builds returned a 404 behind a working-looking Settings panel.
  • Claude Code received only structuredContent, which dropped errors and warnings.
  • Deletions it proposed had no approval card anywhere.
  • The canvas never refreshed after its edits.
  • It could not create a second design.
  • The advertised setup broke on AppImage, the portable build, translocated macOS apps and Windows .cmd files.

What changed — implementation (one commit per phase)

Commit Phase
2d502d8 Review + architecture doc
3ee05eb Server correctness. Result envelope readable in content and structuredContent; header identity via authInfo; home + per-design chats; call recorder so proposal cards render; annotation policy; heartbeat progress and cancellation; server instructions; list_changed on toggle; hash-compared token.
e432d01 Resilient stdio bridge. Works while the app is closed, survives restarts, maps failures to actionable tool errors, relays cancellation.
b289ca7 Approval round-trip. assistant_{get,list_pending,await}_proposal; assistant SSE so cards appear live.
6afbfab Live design events + fix for a pre-existing shared-undo bug (Ctrl+Z after an Assistant edit could undo the user's older change).
08acdf3 Parity. designer_verify_build; Docs pages via the core MentionRegistry.
bdae810 Expansion tools (MCP-only). PCB layout, placement, routing, board outline, rules, zones, keepouts, DRC waivers, design management, history.
2c66ada Setup. Stable launcher, one-click Connect / Update / Disconnect, app-generated local plugin marketplace (MCP server + 6 skills).
91dc437 Graduation + docs. mcp.server → "all"; both user settings still default off.

What changed — review round 1

Commit Findings Change
c74e228 1, 3 Session isolation. Actor = client key + session id, persisted on proposals (migration 0015); proposal tools and undo/redo compare it. Chats per session (so "allow for this session" cannot cross sessions); writes and binding tools serialized per session; bindDesignIfUnbound everywhere. The bridge keys the session on Claude Code's CLAUDE_CODE_SESSION_ID (hashed; verified that Claude Code sets it for stdio servers).
fa4b635 9 + a latent bug Idempotency. Scope-aware UNIQUE(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.
c7ce438 2 Stale approvals. Design delete checks its revision; typed STALE_PROPOSAL persisted and shown on the card and to the agent; no apply-anyway.
87b739c 4 DRC suppression. Waive / rule-class-ignore split into two tools, both approval-tier, ignores never on a session allowance; DrcReport.suppressed; every summary reports active / waived / hidden / raw.
bf92ce8 7, 14, 15, 16 Validation + one risk per tool. Rules (sizes > 0, drill < pad, unique ids/names), outlines (no invented radius; circle/oval/simple polygon), layers vs the real stack, zone/keepout add/update/delete split, focus as a UI side effect.
2393299 8, 11 Bridge. Fingerprint = SHA-256 of full tool contracts + per-boot generation; re-list on endpoint change; SSE via eventsource-parser with seeded fuzz tests.
7d3c4f5 10 Size. 24k text cap (code-point safe), layout paging, audit digests for large results.
347c727 6, 12 Windows + ownership. Windows clients run the app exe + shim with ELECTRON_RUN_AS_NODE (no cmd.exe in the transport); npm claude.cmd resolved to node + cli.js; cross-spawn escaping only as fallback; mcp add -e. Registration record + exact command/args ownership (no path regex).
7e13544 13 + docs Guidance. Skills, prompts and server instructions ask before assuming voltages, ratings, packages, pinouts or fab limits; review separates ERC facts from heuristic observations; DRC triage reports suppression. Docs, release notes, TODO.

Finding 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

  • Bug fix (shared undo; idempotency conflict; stale design delete; MCP result/approval/refresh defects)
  • Feature
  • Refactor / cleanup
  • Docs
  • Test / CI

Test plan

  • npm run typecheck — no new errors (baseline 26, now 26; the one diff is the pre-existing proposal-apply-service.ts TS2322 from @openpcb/contracts drift, message variant only). electron/ tsc: baseline 16, now 16.
  • npm run gen:check — module registry and SDK stubs clean. The chained gen:copilot-schemas:check fails locally because the pinned @openpcb/[email protected] has no schemas/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-existing master baseline 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).
  • Manual desktop matrix — still required before merge (TODO.md §3): macOS installed / moved / DMG, Windows installer + portable with native and npm claude, 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 get parsing and exact ownership.

Real Claude Code CLI (2.1.282), isolated HOME: verified that Claude Code passes CLAUDE_CODE_SESSION_ID to stdio servers; mcp add … -e + mcp get output captured for fixtures; one-click server connect → owned → disconnect, with a look-alike /usr/local/bin/openpcb-mcp left untouched; the regenerated plugin (Windows-style exe + env entry) passes plugin validate and installs; the bundled shim answers initialize with the app down.

Notes for reviewers

  • Needs a desktop before merge — see the matrix above. In particular, whether the AppImage runtime and the Windows portable wrapper pass ELECTRON_RUN_AS_NODE through is still unverified.
  • CI: corelib:fetch fails on master and here because the CoreLibrary v0.1.0-beta.1 release's openpcb-core.pub does not match resources/keys/openpcb-core-2026.pub, so CI runs no tests. Left alone here by maintainer decision (a trust-store change); tracked in TODO.md §3.
  • Behaviour change for existing MCP users: chats are now per Claude Code session; earlier shared MCP chats stay as they are but are not reused, and proposals made before this change carry no actor, so no session can await or undo them (the user still can, in OpenPCB).
  • Migrations 0015 and 0016 (assistant module): add actor and idempotency-scope columns; 0016 replaces the design-wide action_id index with the scoped one (a strictly weaker constraint, so existing rows cannot violate it).
  • DRC engine: DrcReport.suppressed is added only when something was hidden, so unsuppressed reports keep their exact bytes (golden and parity suites pass).
  • Dependency: eventsource-parser is now a declared root dependency (already in the tree via the MCP SDK; lock bumped 3.1.0 → 3.1.1).
  • Electron RunAsNode fuse: required enabled (default; no fuse config today). Documented in electron/AGENTS.md.
  • Known limitation carried forward: net-class assignments are keyed by ephemeral net id (as in the in-app editor); tracked in TODO.md §3.
  • Pre-existing and unrelated: the backend baseline failures (Astra run 2 There are no releases available to download. #5, audit B4-10, oracle s17/s18 dense corpus, tick results-neutral, cloud B2 credential test); CLAUDE.md lists skills that don't exist in .claude/skills/.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UZ5mU5aZjvGGoxGV9CsqXV

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants