Skip to content

MCP server for coding agents, and findings that weigh what a net carries - #13

Merged
wardviaene merged 15 commits into
mainfrom
emi-mcp
Oct 11, 2026
Merged

wardviaene merged 15 commits into
mainfrom
emi-mcp

Conversation

@wardviaene

Copy link
Copy Markdown
Member

What

emi-local mcp: an MCP server in the app, so Claude Code (or any MCP client) can analyze a board from disk, read what the checks found, run ESD and cable budgets, and compare versions, against the app on this computer. 16 tools; see "Using it from Claude Code" in the README.

  • server/compare: the version diff in Go, sharing its cases with compare.ts (testdata/compare_fixtures.json, checked by Go and vitest). It cannot live in the server: emi.Blob has no Get.
  • server/localapp: finds or starts the app, the Go twin of the KiCad plugin's desktop.py.
  • server/emiclient: packs a board from disk (.kicad_pcb + .kicad_pro + emi.rules.yaml, Gerber folders, zips) and does the two-step upload; one project per board file, no duplicate queueing.
  • server/mcpserver: the tools, shaped and paginated for an agent.

Analyzer: findings weigh what a net carries and what a pin is (0c5635a). Running the MCP server on the BenchPod boards opened the critical list with I2C, reset and LED nets as radiators, a current monitor's sense pin as an undecoupled supply, and a SPI chip select length-matched to an RMII clock. Net names now set fast/slow/unknown, pads keep KiCad's pin types and names, and a long net is critical only where its return path is also broken. BenchPod digital: 27 to 4 criticals; analog: 20 to 1, every remaining one real. Details in the CHANGELOG.

American English in comments, docs and every string people read (be05f83), changed with each language's tokenizer so identifiers and stored artifact keys ("modelled", "centre") are untouched.

Testing

  • Go, 1479 worker tests, 423 webapp tests and tsc pass.
  • make e2e-mcp (new, opt-in, needs Docker): emi-local with a worker container in a temp folder, emi-local mcp over stdio: analyze, findings, an unchanged board not resent, a rules file turning a check off, compare. Passes in about 6 s.
  • By hand on the two BenchPod boards: analysis, ESD (33 lines), cable suggestions, compare across two revisions.

After merge

  • Release 0.4.0 (the plugin's launcher requires it), then merge the plugin: embeddedci-com/benchpod-mcp branch emi-analyzer-plugin.
  • The worker image must be rebuilt: the analyzer changes are in the worker.

🤖 Generated with Claude Code

wardviaene and others added 15 commits October 11, 2026 09:54
The MCP server needs the version diff the compare page shows. The server
cannot do it (Blob has no Get, by design), so server/compare ports
diffFindings, groupByRule, the headline and diffNets from compare.ts to
run in the client on artifacts it downloads.

testdata/compare_fixtures.json holds the cases from compare.test.ts with
explicit answers, and both the Go and the vitest suites check them, so
the two implementations cannot drift apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
server/localapp is the Go twin of the KiCad plugin's desktop.py: read
endpoint.json (loopback URLs only, never believed without a probe of
/api/local/status), honour EMI_ANALYZER_URL and EMI_ANALYZER_APP, and
start the installed app one way at a time so there is never a second
copy with a second data folder.

One addition: a client that is emi-local itself can start it headless
(-open=false) when no app could be started at all, never after one did.
Started processes are detached and log to the user cache folder
(emi-analyzer/launch.log). `open -b` is waited for, since it exits 1 at
once when the app is not registered.

Tests port desktop.py's cases; checked live against a real emi-local
started headless with a scratch endpoint file and data folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
server/emiclient is the Go twin of the KiCad plugin's client.py and
project.py, reading from disk instead of the editor:

- Pack takes a .kicad_pcb (with its .kicad_pro and the first committed
  rules file), a .kicad_pro, a folder with one board or a Gerber set
  (the files the worker reads, by its own rules), or a .zip sent as is.
  Archives are sorted with a fixed timestamp so the same files give the
  same bytes.
- Client covers projects, boards, runs, artifacts (through the URL the
  app mints) and the two-step upload.
- Open files every version of a board in one project, remembered per
  board path, skips the upload when the app has these bytes analysed,
  and returns the still-pending ingest when the same bytes were just
  sent from here. That last check has to be client side: the app only
  records a board's hash once its ingest has read it.

Checked live against emi-local (scratch data, no worker): upload, the
pending-ingest reuse and a forced re-run. The analysed-board lookup is
covered by the fake-app tests only; the local worker could not ingest
(Docker disk full).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`emi-local mcp` serves the EMI Analyzer over MCP on stdio, so Claude Code
or Codex can analyse a board from disk. It is a client of the running
app (found or started through localapp), so its boards show up in the
app's window and nothing leaves the computer.

First tools, in server/mcpserver:
- status: the app, its data folder and the Docker worker, with what to
  do when Docker is missing or stopped
- analyze_board: pack and upload a board (emiclient.Open), optionally
  waiting for the analysis
- run_status: state, stage and progress, the summary once done; wait_s
  (max 600) waits and sends progress notifications when asked
- list_projects, list_versions: versions numbered oldest first as on
  the compare page, each with its newest finished analysis

The instructions tell the agent the results compare versions of one
board and are not a pre-compliance test.

Adds github.com/modelcontextprotocol/go-sdk v1.8.0. No existing
dependency moves; embeddedci-server builds and vendors against this
branch unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven tools that read an analysis, shaped for an agent rather than
passed through: a whole rules.json is hundreds of kilobytes.

- findings: counts by severity and check, then one page worst first
  (no long explanations unless asked); filters for severity, check,
  net, layer and a point on the board
- finding: one in full, what its check looks for, and the suppress
  entry to accept it on purpose
- decoupling: one line per rail, or per IC its capacitors, gaps and
  only the worker's recommendations that help
- nets: lengths, delay and skew, filtered by name, group or tolerance
- compare: two analysed versions (default the newest two) through
  server/compare, grouped by check, plus the nets that changed
- checks: the rule catalogue and the emi.rules.yaml format
- open_in_app: the project or compare page, optionally in the browser

An analysis is named by run_id or by project (newest finished version,
saying when a newer one is still running). Artifacts of finished runs
are cached in memory. The rule catalogue is embedded from a copy the
worker's exporter now also writes; a test fails if it drifts.

Checked live on the BenchPod digital board (156 findings, 11 rails)
and on two of its revisions for compare.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run_esd and run_cables start the two runs that go beyond the ingest on
an analysed version (CreateRun in emiclient), with the same wait and
progress as analyze_board. esd and cables read the newest finished one:

- esd: per connector line the clamp and its model quality, the peak as
  laid out (at the IC pin when there is one) and with the clamp at the
  connector, worst first by how far a pin goes over its supply; with
  the worker's assumptions and what the numbers are not.
- cables: each declared cable's tightest common-mode budget, and the
  connectors with no cable declared with the worker's suggestion; with
  no project it lists the cable types run_cables takes.

The cable types are the worker's library, embedded from a copy a test
keeps equal.

Checked live on the BenchPod digital board: 33 lines simulated in 20 s,
10 connectors recognised with suggestions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Running the MCP server on the BenchPod boards opened the critical list
with I2C, reset and LED nets as radiators, a current monitor's sense
pin and a power mux's status line as undecoupled supplies, a USB
charging strap as a crossed plane gap, and a SPI chip select length-
matched to an RMII clock. Every one is decided by evidence already in
the board file:

- net_activity (rules/model.py): fast, slow or unknown by name (FAST_NET
  moves here from stubs.py and also reads unnamed nets). Radiator,
  return-via and plane-gap make slow lines advisory and cap unknown
  long nets at warning; the radiator list fills with fast nets first.
- On a board with planes a long fast net stays critical only when the
  same net has a critical plane-gap or return-via finding.
- Pads keep KiCad's pinfunction and pintype; is_supply_pin leaves a pin
  out of decoupling only on positive evidence (signal pin type, or a
  GPIO, sense, enable, op-amp or mux pin name).
- Nets that only strap pins of one part are not signals; chassis and
  shield names are ground.
- Length groups: address/command needs DQS or a byte lane; CAN, RS-485,
  RS-422 and LIN pairs get no skew budget.
- dedupe keeps the most severe findings of a rule under its cap.

BenchPod digital: 27 -> 4 criticals; analog: 20 -> 1 (a real one: the
THS4551's VS- pin has no capacitor within 25 mm). 1479 worker tests
pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two BenchPod boards, benchpod/vbench-pod.kicad_pcb and
benchpod-digital/vbench-pod.kicad_pcb, both became "vbench-pod" in
list_projects. A project folder named after its board keeps the plain
name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments, docs, multi-word strings and JSX text across the server, the
worker, the webapp and the KiCad plugin: analyzed, modeled, center,
meter and millimeter, recognized, organization, normalized, labeled,
canceled, behavior, color, toward, and the rest (about 720 changes).

Changed with each language's own tokenizer (go/scanner, Python's
tokenize, the TypeScript parser), so identifiers, keys and one-word
strings were never touched: the stored artifact keys "modelled" (cable
summary) and "centre" (compliance) stay, since saved results and the
webapp read them. The uncertainty label "cable idealisation" became
"cable idealization"; compliance_fixtures.json is regenerated, and the
component and driver document fixtures match the new message. The
rule catalogue copies are re-exported.

Go, 1479 worker tests, 423 webapp tests and tsc pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
make e2e-mcp builds emi-local, starts it headless in a temp folder with
a worker container, and drives `emi-local mcp` over stdio the way a
plugin does: status, analyze_board (waiting for the run), findings,
finding, decoupling, an unchanged board not sent again, then an
emi.rules.yaml beside the board turning the busiest check off, and
compare saying those findings are fixed and nothing is new. Opt-in
(build tag e2e: it needs Docker and the worker image); about 6 s.

EMI_ANALYZER_MCP_STATE moves the server's board-to-project memory out
of the user's config folder, which the test (and a second, separate
setup) needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README: a "Using it from Claude Code" section (the plugin, running
`emi-local mcp` from any MCP client, its two environment variables, and
the tools). CHANGELOG: the MCP server under Unreleased. CONTRIBUTING:
make e2e-mcp. The em dashes in the README and CONTRIBUTING became
colons, commas or sentences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finding titles and details, notes and error messages read with a colon,
a comma, parentheses or a second sentence instead. The rule catalogue
and component fixtures are regenerated by their scripts; the webapp's
copy of the component messages matches. Docstrings keep theirs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UI copy and JSX text. The six left are chart legend line samples, drawn
next to the - - - and – – samples beside them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Left: table cells holding only a placeholder dash, and code blocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wardviaene
wardviaene merged commit 8d2b51b into main Oct 11, 2026
6 checks passed
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.

1 participant