A small stdio MCP server that lets an assistant read a project, ask what an edit actually changed, and sweep a corpus. Standard library only -- Python 3.9+, no third-party dependencies, and the MCP SDK is not required. Nothing in the build or the application refers to it; it sits in misc/ beside make_icon_themes.py and is inert unless run. It exists because verifying a change by screenshot is unreliable, and that unreliability produced two wrong conclusions in a single 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. An Apply button looked like it did nothing; it was disabled because a required field was empty. Both times the pixels misled and the file told the truth, so these tools read the file. Seven tools: qet_project_info, qet_elements, qet_conductors, qet_diff, qet_scan, qet_element_info and qet_export. Only qet_export launches QElectroTech; everything else parses the .qet or .elmt directly, which needs no display and cannot be confused by a dialog. Two behaviours of QElectroTech are carried inside the tool rather than left for the caller to rediscover. SingleApplication keys its socket on applicationFilePath(), so a second launch of the same path forwards its request to a running instance and returns that process's answer with no error; qet_export therefore copies the binary to a unique temporary path, gives it a private HOME and runs it offscreen. A symlink would not do, because applicationFilePath() resolves it back. And the CLI matches its export flags by exact string (cli_export.cpp:828) with the project and output as positional arguments (:862, :882), so --export-bom=out.csv is not recognised as an export at all and the run starts the interface and hangs headless; the tool uses the positional form. Worth recording for anyone extending this: the project database would be a better query surface than the XML, but it is not reachable from outside the application. projectDataBase::newQuery() and isReadOnlySelect() are C++-internal and the JavaScript scripting API exposes no SQL binding. A --query CLI verb, or a scripting binding, would let this expose the guarded read-only SELECT surface instead. Verified against the shipped examples: qet_scan reports 3190 conductors across the 24 example projects with no cable value, and qet_diff reproduces the four-element move above from the two saved files. 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. - Conductors in older files have no uuid, so
qet_diffkeys them by their terminal pair. 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.