qet_diff keyed each conductor on its raw terminal1/terminal2 pair, with a comment claiming that pair was "stable within a folio". It is stable within a folio; it is not stable across a save. QElectroTech reassigns those folio-scoped integer ids on every write, in whatever order it serialises the elements, so one untouched conductor of ArduinoLCD.qet goes from terminal1="1" terminal2="16" to terminal1="34" terminal2="15". Diffing a project against a re-saved copy of itself therefore reported 29 of its 47 conductors as removed and 29 as added, with nothing changed. That is the main thing this tool is for, so the conductor half of the answer was noise in exactly the case it was wanted. The format has two addressing schemes and a file can hold both at once. Older conductors use the integer ids with no element1/element2; current ones use terminal uuids from the .elmt definition plus element1/element2 naming the placed instances. A terminal uuid alone is not an identity -- it belongs to the definition, so two coils of one type share it and a conductor between them keys as a self-loop -- so an end is identified by the (instance, terminal) pair, taken from the conductor where it carries one and resolved through the folio's elements where it does not. Where an element predates persisted uuids there is nothing stable to key on. Keying those on terminal geometry alone collapsed nine distinct conductors of schema_indus.qet onto a single key, which is worse than the instability it was meant to fix, so such ends stay unresolved, keep a "#"-marked key, and the diff reports unstable_keys and says in words that added/removed may not mean what they look like. Measured over the 24 shipped example projects, 3190 conductors: 0 colliding keys, against 8 for the geometry-only key. On a re-saved but otherwise untouched project: 0 added, 0 removed, against 29 and 29 before this change. A project with two conductors genuinely added still reports exactly two added and none removed, so the check still discriminates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
qet-mcp — a Model Context Protocol server for QElectroTech projects
A small stdio MCP server that lets an AI assistant read and verify QElectroTech projects: what is in a project, what an edit actually changed, and what a whole corpus of projects contains.
It has no third-party dependencies — Python 3.9+ and the standard library only. The MCP SDK is not required.
Why
Verifying a change by screenshot is unreliable, and this tool exists because that unreliability produced two wrong conclusions in one review session:
- A drag of a multi-element selection looked like it had left the
symbols behind and detached their labels. Diffing the saved file showed
all four elements had moved by an identical
(0, -80)and no label had moved at all. A bug report was one step away from being filed. - An "Apply" button looked like it did nothing. It was disabled, because a required field was empty.
Both times the pixels misled and the model told the truth. So the tools here read the model.
Tools
| Tool | What it answers |
|---|---|
qet_project_info |
title, format version, folios, element and conductor counts |
qet_elements |
placed elements: uuid, type, position, label, information bag |
qet_conductors |
conductors and their documentation fields; filter by attribute |
qet_diff |
what an edit actually changed — moves with deltas, adds, removes, relabels, conductor field changes |
qet_scan |
sweep a directory of projects, counting nodes carrying an attribute |
qet_element_info |
a .elmt: translated names, terminals, info fields, part counts |
qet_export |
run a headless export (pdf, png, svg, bom, cables, wires, wiring, nets, links, info) |
Only qet_export launches QElectroTech. Everything else parses the file
directly, which is faster, needs no display, and cannot be confused by a
dialog.
Running it
# list the tools and exit
misc/qet-mcp/qet_mcp.py --list
# speak MCP on stdin/stdout
misc/qet-mcp/qet_mcp.py
Register it with an MCP client, for example:
{
"mcpServers": {
"qet": {
"command": "python3",
"args": ["/path/to/qelectrotech/misc/qet-mcp/qet_mcp.py"]
}
}
}
Worked examples
What did that edit change?
{"name": "qet_diff", "arguments": {"before": "a.qet", "after": "b.qet"}}
"elements": { "moved_count": 4,
"distinct_move_deltas": [[0.0, -80.0]],
"relabelled": [], "info_changed": [] }
Four elements moved by one uniform delta; nothing was relabelled. That is the answer a screenshot gave wrongly.
How much of a corpus uses a field?
{"name": "qet_scan",
"arguments": {"directory": "examples", "tag": "conductor", "attribute": "cable"}}
{ "files": 24, "total": 3190, "non_empty": 0, "distinct_values": [] }
Across the shipped examples: 3190 conductors, not one with a cable value.
Notes and limits
- The project database is not reachable from outside the application.
projectDataBase::newQuery()andisReadOnlySelect()are C++-internal and the JavaScript scripting API exposes no SQL binding, so structural queries here are done over the XML. A--queryCLI verb, or a scripting binding, would let this server expose the guarded read-only SQL surface instead, and would be a better foundation. qet_exportisolates its launch. SingleApplication keys its socket onapplicationFilePath(), so a second launch of the same binary path forwards its request to an already-running instance and returns that process's answer with no error. The tool copies the binary to a unique temporary path, gives it a privateHOME, and runs it on the offscreen platform. A symlink would not work:applicationFilePath()resolves it back to the real path.- The CLI matches its flags exactly.
--export-bom out.csvis the supported form;--export-bom=out.csvis not recognised as an export at all, so the application starts its interface instead and a headless run hangs. The tool uses the positional form. - Conductor identity is the hard part of
qet_diff. A conductor names its ends withterminal1/terminal2, and the project format has two schemes: folio-scoped integer ids in older files, terminal-definition uuids pluselement1/element2in newer ones. The integer ids are renumbered on every save, so keying on them made all 47 conductors of an untouchedArduinoLCD.qetread as 29 removed and 29 re-added the moment the other side had been through QElectroTech. Ends are now keyed by owning element uuid plus terminal, which is stable across a save: measured at 0 colliding keys over 3190 conductors in the 24 shipped examples, and 0 churn on a re-saved but otherwise untouched project. Where an element predates persisted uuids the end cannot be resolved and keeps a#-marked unstable key; the diff then reportsunstable_keysand says so rather than pretending to be comparable. - Elements written before persisted uuids fall back to a positional key, which makes a move in such a file read as a remove plus an add rather than as a move.
- Read-only by design. Nothing here writes to a project.