EDA backends
August 18, 2026 ยท View on GitHub
Which EDA the server drives, how each one is reached, and what differs between them. The Altium backend is the default and the most complete; KiCad and EasyEDA Pro are optional.
Back to the README.
The server exposes one tool surface, chosen at startup by the EDA_AGENT_BACKEND environment variable (or the --backend flag):
altium(default) - the full Altium suite. Existing installs are unaffected.kicad- the KiCad-native tools.easyeda- EasyEDA Pro, through its extension API.both- Altium and KiCad together, for one server driving either.
both deliberately excludes easyeda. It exists for the two desktop tools a user is likely to run side by side, and widening it would change what an existing setting means.
Selection happens before any tool registers, so an altium user never sees KiCad tools and vice versa. Because MCP clients set environment per server, a user of several tools registers several servers pointing at the same binary:
claude mcp add -s user altium eda-agent
claude mcp add -s user kicad -e EDA_AGENT_BACKEND=kicad eda-agent
claude mcp add -s user easyeda -e EDA_AGENT_BACKEND=easyeda eda-agent
EasyEDA connects the other way round
Altium polls a directory for request files, so the server writes and waits. EasyEDA dials out instead. Its extension API reaches a WebSocket server (SYS_WebSocket.register), so this process listens and the editor connects to it. Nothing here can start EasyEDA or make it connect; until the extension does, every call reports the source as unreachable and says how to start it.
That means two halves, and both ship here:
- the Python side, which binds the first free port in 49620-49629 and answers
GET /healthwith a service identifier (EDA_AGENT_EASYEDA_HOST/EDA_AGENT_EASYEDA_PORToverride it) extensions/easyeda/main.js, loaded in EasyEDA Pro, which answers the commands
The WebSocket server is written in-house against RFC 6455 rather than pulled in as a dependency, for the same reason the s-expression reader and the EasyEDA part converter were: the framing rules end up verified instead of trusted. The handshake is tested against the specification's own worked example, so the expected value is fixed by the standard rather than by this code. It binds to loopback and is not hardened for a hostile network.
Neither side needs a port configured. The server takes the first free port in 49620-49629, the range EasyEDA's own bridge uses, and the extension scans that range, reads /health, and checks the service identifier before connecting so it never hands a WebSocket handshake to an unrelated service. It then retries every few seconds, which is the part that matters: SYS_WebSocket.register() fails silently when nothing is listening at that instant and never tries again, so without a retry a correct extension and a correct server can sit side by side and never meet. Starting order no longer matters.
Failures are three-way on purpose, because they need different responses: unavailable means start the extension, reason means the editor refused the command, ok means it ran.
Part of this has run inside EasyEDA Pro, and the rest has not. Every API name comes from EasyEDA's published reference rather than recollection, including the instance naming, where class PCB_Drc is reached as eda.pcb_Drc and getting the case wrong yields undefined rather than an error. A published name is not a verified one, so the bridge reports verification per command: verified_live_for(command) answers whether that particular command has round-tripped against a live editor, and every tool result carries it.
Both figures are read from a record the smoke script writes, never hardcoded. A constant there could only ever be an opinion, and published metadata that was derived rather than measured has already misled this project once. On a fresh checkout the answer is that nothing is verified, which is the correct answer.
The global flag is the weaker of the two: after the first successful session it is true forever and says nothing about the tool in hand. A command built on a call that has been seen working and one built on a call that has been seen hanging deserve different answers, which is why the per-command form exists and why tools quote it.
Tools that rewrite or discard work (easyeda_clear_routing, easyeda_auto_route, easyeda_delete_primitives, easyeda_delete_schematic_primitives, easyeda_import_schematic_changes, easyeda_set_copper_layer_count) refuse unless confirm=True, and both halves check independently since the extension is reachable by anything speaking the protocol.
There is a second axis, and it was half open until recently. A named tool is not the only way to reach a method: easyeda_invoke and easyeda_invoke_batch call the API reflectively, so a guard written into one tool protects one route and not the other. pcb_Document.autoRouting was guarded by easyeda_auto_route and reachable unguarded through the shim, which made the sentence above true of the tool and false of the method. What protects the shim is the destructive NAME list, carried in both halves, which now covers the automatic engines (autoRouting, autoLayout), the wholesale imports (setNetlist, importAutoRouteSesFile, importAutoRouteJsonFile, importAutoLayoutJsonFile), the five DRC constraint edits that re-scope or orphan a rule, and restoreDefault. None of those names contains a destructive verb, which is exactly why a prefix test alone was not enough. The last two are guarded for reasons that are not obvious from their names: applying the schematic to the board removes components the schematic no longer has, along with their routing, and reducing the copper layer count discards whatever was on the layers that go away.
Every coordinate is in mils, as everywhere else here. That matters more on this backend than it looks: EasyEDA's PCB canvas counts in mils but its schematic canvas counts in units of 0.01 inch, ten mils, and their own guidance calls mixing the two the most common mistake made against this API. It lasts because nothing errors, a schematic sent unconverted just lands ten times too far out. The conversion happens once, in MILS_PER_SCHEMATIC_UNIT, rather than at each call site.
Layers cross the wire as names, never numbers. EasyEDA's layer ids are a numeric enum and their own guidance is to use the members rather than the values, so easyeda_add_line(layer="TOP_SILKSCREEN") sends the name and the extension resolves it against the runtime's enum. A number chosen on the Python side would be a second copy of their numbering, and it would fail quietly: the primitive would land on a different layer rather than be rejected.
What EasyEDA can and cannot do here
It can read, check and export. Components, nets, pads, vias, layers, attributes, dimensions, schematic pages and pins, the editor's own DRC and ERC, library and LCSC lookups, a rendered image, and every fabrication export including IPC-2581 and an Altium file. The EDA-agnostic analysis (review_design, run_drc, run_erc, the calculators, design_validate_plan, design_review_plan) works on it too, because those need a snapshot rather than a particular editor.
It can author library parts. Symbols, footprints and the device that binds them, which is the object anything gets placed from. Geometry comes from opening the item (easyeda_open_symbol / easyeda_open_footprint) and then using the ordinary drawing tools, which now act on it. easyeda_add_pad and easyeda_add_pin are what make the two halves usable rather than merely drawn: lines and arcs give a footprint an outline, and only pads give it something to solder, just as only pins make a symbol connectable. SMD or through-hole is decided by the drill diameter, and a drill as wide as the pad is refused, since it leaves no annular ring while still rendering as a pad. A pin's x, y is its electrical end, the point a wire attaches to, not the end at the symbol body. Devices, symbols and footprints can each be copied into another library, which is how a vendor part gets adopted: copy it into your own library and edit the copy, so an update to the vendor library cannot silently change your board. Copying only the symbol and footprint leaves nothing placeable, since the device is the object that gets placed. There is no separate library-drawing API on this backend, and inventing one would be a second way to draw the same shapes. Creating the two drawings and stopping leaves a library nobody can place from, so a device bound to neither is refused rather than accepted with a uuid. This is where the atomic-parts standard lands on this backend: symbol, footprint and 3D model bound at the part level. For the common shapes there is a shorter road: easyeda_create_ic_symbol lays out a whole IC symbol from left and right pin groups, easyeda_create_passive_symbol draws a resistor or an inductor, and easyeda_create_standard_footprint computes a chip, sip, dual, header, tab, quad or bga land pattern. All three share the geometry modules the Altium backend uses, so a part generated on either side comes out the same. The passive generator refuses the capacitor, diode, LED, crystal and fuse glyphs: they are drawn from open line segments and EasyEDA's schematic API has no line primitive, so the only way to draw them would be as wires, which would give the symbol electrical connections it should not have. A generated land pattern is a starting point computed from the numbers given, not one read from a datasheet, and should be checked against the manufacturer's recommendation before it reaches a board.
It can draw. On the board: lines, arcs, polylines, vias, pads, text, poured copper zones, solid fills and keepout regions, plus deletion by id and selection. A fill and a zone are not the same thing: a fill is solid and shorts whatever is inside it on another net, while a zone pours around what it meets. A region takes its rules with no default, because a region with no rule constrains nothing and looks exactly like one that works. A copper line on TOP, BOTTOM or an inner layer is a routed segment, since EasyEDA has no separate track primitive. On the schematic: wires, text, rectangles, circles and polygons, plus net labels and power/ground rail glyphs, and selection. Library parts are placed on either by the uuid pair a search returns, never by name, because two libraries can hold the same name and choosing one silently is how a board gets the wrong footprint under a BOM line that reads correctly.
It can start from nothing. Create a project, a schematic, extra schematic pages and a board, then fill in the title block, and organize the result: easyeda_get_team, easyeda_list_folders, easyeda_create_folder and easyeda_move_project_to_folder manage the workspace's project folders, with signatures taken from the installed api-types.d.ts rather than guessed. Without those, the backend can only work on something a human made first, which is the difference between editing a design and authoring one.
It is verified against a live editor, per command. The first real sessions ran against a board of 111 components: 20 of 65 editor commands round-tripped with usable data, and the record keeps each reply's field names plus a truncated example value. easyeda_get_measured_shapes reads that record back as a tool, which is where to look before writing anything against a reply field, because a guessed field name does not fail loudly; it reads nothing and reports a clean empty. Every tool reply carries verified_live for the command it used, from that record: a tool built on pcb.components reports true, one built on pcb.attributes, which hung live, reports false, and one good session is not allowed to launder the rest. The same sessions established that sch.* reads fail inside the editor unless the schematic tab is active, so the smoke run sets wrong-tab probes aside by name rather than reporting them as breakage.
Exports produce real files. The editor's manufacture exports return file data, a Blob, and JSON.stringify(blob) is {}, which is why every export once arrived empty. The extension now packs the bytes as chunked base64, and every export tool (easyeda_export_gerber and its eighteen siblings) takes save_to and writes them to disk, reporting the path, size and suggested name. Refusals are specific: no destination names the file's size, and a null file names the usual cause, the wrong document tab. easyeda_export_bom_html is the exception and is named separately here because it behaves differently: it asks the editor for nothing, rendering a self-contained page from the design snapshot, so it takes output_path and defaults to bom.html in the workspace rather than refusing without one.
It has a safety net. easyeda_checkpoint saves the open document and easyeda_restore_checkpoint puts it back, which is worth doing before easyeda_run_plan or any confirm-guarded tool. The Altium side snapshots a whole project directory; this backend has no directory the server can reach, so what is saved is the one open document, and the tools say so rather than implying a project-wide net. A restore onto a different document is refused: replacing board B with a snapshot of board A destroys B and reports success, which is the worst shape a safety net can fail in.
Bulk edits go in one call. easyeda_modify_schematic_components and easyeda_modify_pcb_components take a list, and the loop runs inside the editor: renumbering forty parts through the single-component tool would be forty round trips over a socket. A malformed entry is rejected before any of the batch is applied, since finding it halfway leaves the design part-edited, and each result is reported individually because knowing that something failed is no use without knowing which.
It can cross-check itself. easyeda_get_unconnected_pins names the pins sitting on no net rather than counting them, easyeda_compare_schematic_pcb compares the two component lists by designator in both directions, easyeda_audit_track_widths reports nets routed at more than one width, per net per layer, since a net legitimately changes width moving between an inner layer and an outer one and folding the layers together would flag every multi-layer board. It reports rather than judges: a deliberate taper into a fine-pitch pad looks exactly like a segment drawn before a rule was set. easyeda_audit_mirrored_text finds bottom-side text that will read backwards on the real board, which is the defect nothing on screen shows: the editor draws the board from the top, so unmirrored bottom silkscreen looks correct there and comes back from the fab reversed. easyeda_audit_components_outside_outline casts a ray against the outline segments rather than comparing against an extent, because a bounding box calls the missing corner of an L-shaped board part of the board, and it works on segments in any order since an edited outline has them in whatever order they were touched. easyeda_audit_off_grid_components reports how far each part sits from the nearest grid line (the offset to the nearer line, not past the last one, or a part a hair short of the next reads as nearly a full pitch out), and easyeda_audit_pads_near_board_edge and easyeda_audit_vias_near_board_edge measure to the outline segments rather than to a bounding box. That last distinction matters in the dangerous direction: a box overstates a rounded or routed-out board, so a pad close to a real curved edge reads as comfortably inside it and the check misses exactly what it exists to find. Pad size is a separate limit, since EasyEDA carries it in an unpublished shape object, so each result says whether it was measured copper-to-edge or centre-to-edge and the summary counts them. Those two compute rather than forward, which the rest of the backend avoids: the line is whether the editor has an answer of its own. DRC and ERC do, so those are always the editor's. The editor's own comparison calls do exist, and they answer a different question: sys_Tool.netlistComparison(netlist1, netlist2) is documented as beta with an unspecified return, and a live capture shows pcbComparison and schematicComparison, which the published reference does not describe at all. All three compare two artefacts the caller supplies rather than reading the open design. The other candidate, import_schematic_changes, applies the schematic rather than reporting on it, so running it to find out what differs would change the board to answer a question about it.
Connecting has one prerequisite that looks like a broken bridge. EasyEDA refuses network access to extensions until external interaction for extensions and standalone scripts is permitted, and until that setting is on the editor never attempts a socket at all. Nothing on this side can tell that apart from an editor that is simply closed, because there is nothing to observe: the server listens, no connection arrives, and every tool correctly reports that no editor is connected. The error naming the permission is raised inside EasyEDA and is the only place it appears. So the order that works is: enable the permission, import the .eext at a bumped version, then click onto a PCB or schematic tab and connect. That last step is not pedantry either. Re-importing an extension leaves the editor on its settings page, and a first connect from there reports the document as unknown, as do library, symbol, footprint and project-home tabs, because EasyEDA only injects the pcb_* and sch_* API into a design document.
It tells you when the editor is running old code. An extension that is installed, enabled and months out of date is indistinguishable from a current one in EasyEDA's Extensions Manager: same name, same uuid, and a size nobody thinks to check. So the build is a hash of the extension source, stamped at build time, reported on every easyeda_ping, and compared against what the server's own tree would build. A mismatch names both builds and the remedy, including the part that wastes the most time: re-importing the same version number is a silent no-op, so the version in extension.json has to be bumped first. This is not housekeeping. A whole live session was spent reading "the export fix is broken" off an editor that was simply running a build from before the fix, and the only clue was one unrelated command being refused. Not knowing is reported as not knowing: an older extension that predates the stamp reports no build, and is never accused of being stale on that basis. A test also refuses to let the built package fall behind its own source, because editing the source and shipping yesterday's .eext produces exactly the same confusion one step earlier.
One audit checks the part rather than the board. easyeda_audit_footprint_vs_datasheet compares the open footprint's real pads against a land pattern transcribed from the manufacturer datasheet: pad count, each pad's position and size, the numbering, the implied pitch. Every other audit here checks the design; this checks what the design is built on, and it catches the defect none of the others can see, since a board can be perfectly placed and perfectly routed onto a land pattern that will not solder. The comparison is the same code the Altium side runs, so the two backends cannot drift into disagreeing about whether a given footprint matches a given datasheet; only the reading differs, and the pads are converted from the mils EasyEDA reports to the millimetres a datasheet gives. The spec must cite the datasheet it came from, and there are no built-in package tables, on purpose.
A review reads the design before it judges it. easyeda_review_snapshot returns what the design IS in one call: the netlist, the parts on both sides, the nets with their classes and rules, the layer stack, what is unrouted, which pins sit on no net, and whether board and schematic agree. Connectivity is judged from that and never from a render, because a picture can show a wire that shares no net and hide a net that is electrically correct. DRC and ERC are opt-in, since each can take a minute and a reviewer wanting the cheap picture should not pay for them. A section that could not be read is listed as failed rather than returned empty: those look identical in a summary and mean opposite things, and a reviewer acting on the first when it was really the second concludes the board is clean.
One call runs every check. easyeda_review_board runs every audit over a single snapshot of the board and ranks what they found, worst first. Two things about it are load-bearing. The audit list is read from the registry rather than written down, because a written list goes stale the first time an audit is added and nothing says so, and silently reviewing all-but-one of the checks is the exact failure a review tool must not have. (This paragraph counted them until the count went stale within a day, which is the same lesson one level up.) And the reply keeps four outcomes apart: found something, ran and found nothing, refused (no board open), and unreadable (answered in a shape the summary cannot read). Only the second is good news. An audit whose count cannot be read is never folded into the clean tally, since a count nobody read is not a count of zero. Making that safe meant first getting the audits to agree on a name for their result: they had grown twelve different ones, and several of the near-misses (segments_counted, vias_counted) are how much was inspected, so a summary matching loosely on _count would have called a clean 813-segment board 813 problems. The reads are shared across the review, because four audits read the board's lines and several read its vias, and the cache is dropped when the review ends rather than living on to answer a later audit with an older board.
It checks decoupling with the Altium engine, not a copy of it. easyeda_audit_missing_decoupling feeds measured EasyEDA components into the same function the Altium audit calls, so an IC lands in the same bucket on either backend. A missing local bypass passes ERC, passes DRC, and bites at first power-on. One limitation is stated rather than hidden: a PCB pad carries a number but no pin name, so power pins are recognised by net name alone, and an IC whose rails are named unusually is skipped rather than judged. easyeda_audit_signal_vias_without_return is the same kind of port, using the Altium side's power-net vocabulary character for character so the two backends flag the same boards.
It can set design rules and manage the stackup. Net classes, differential pairs and equal-length groups, plus reading the per-net rules and which rule configuration is active. Layers can be renamed, restyled, shown, hidden, locked and selected, and the copper layer count changed, which is guarded: reducing it discards whatever was on the layers that go away. That last one matters for reading a DRC result: a board can hold several rule sets, so a violation count means nothing without knowing which was in force. Where a colour is optional it is left to the editor rather than defaulted here, so creating a group cannot silently restyle a board.
The routing tools are not registered here, and that is measured rather than assumed. route_plan_repairs is pure Python and looks portable, but it reads each violation's nets from primitive1 / primitive2, the paired-primitive structure Altium's DRC reports. EasyEDA's DRC returns a flat {description, net, designator, layer} with no primitives, so fed a real EasyEDA payload the planner classifies part of it and escalates the rest for want of net names that are present but in a different place. The output would be honest and useless. route_plan has a second problem: its fetch_geometry option calls the Altium bridge, so on this backend that parameter talks to the wrong tool entirely.
The plan-building design tools work here now. Building, editing, laying out, validating and costing a plan are all pure computation, and on this backend they have somewhere to go: easyeda_emit_plan turns a plan into EasyEDA calls and easyeda_run_plan runs them. Which tools qualify is measured, not judged by name: each was called with the Altium bridge replaced by a tripwire, and a test re-runs that measurement so the list cannot go stale. design_preview_plan is the reason it is measured, since it reads like pure computation, reaches the bridge, and catches the failure, so on a machine with no Altium it answers with less than it appears to and says nothing.
The Altium executor stays Altium-only. design_execute_plan emits Altium bridge commands (generic.place_sch_components_from_library and friends), not an abstract vocabulary, so it would fail here at the first step. Exposing it would raise the tool count and hand you a dead end, which is the same trap the part providers' usable_in check exists to prevent. On this backend a plan is run through easyeda_emit_plan and easyeda_run_plan instead.
easyeda_emit_plan covers the placement half. It takes a validated DesignPlan, runs the same layout engine the Altium path uses, and returns the ordered list of easyeda_* calls as data rather than running them, so the sequence can be read and checked first.
Two things it refuses to do, both because the failure would be invisible:
- It never picks a library part. Altium resolves a symbol by name; EasyEDA needs the
{library_uuid, uuid}pair a search returns, and one MPN can match several parts. A wrong pick leaves the designator, value and BOM line all reading correctly while the footprint is somebody else's. Unresolved parts come back as search steps withrunnable: false. - It emits placement only. A wire or net label is drawn at a pin, and pin positions are not known until the symbols are placed. The result says so, rather than letting a placed design be mistaken for a wired one.
easyeda_emit_connections is the second pass. Give it the pin coordinates read back from the editor and it emits the wires, labels and rail glyphs. How each net is drawn (wired pin to pin, labelled at every pin, or a power/ground glyph) comes from the same rule the Altium path uses, imported rather than restated, so the two backends cannot drift into drawing one plan two ways.
A net missing any pin position is refused outright rather than drawn between the pins that are known: a partly drawn net reads as a working one, and on the label path it genuinely connects the pins it reached, so nothing downstream flags it.
easyeda_run_plan executes an emitted sequence. It takes the calls list rather than a plan, so what runs is exactly what was reviewed, and it stops at the first failure: carrying on would place the remaining parts around the hole where the failed one belongs, and the result would read as a finished schematic with a mistake in it rather than as a run that stopped. A step naming a tool it cannot call is refused before anything runs, since finding that halfway leaves the design half-changed.
So: use EasyEDA here to inspect, check and get data out. Altium remains the backend that builds.