mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-07 04:04:14 +02:00
Install the MCP server with QElectroTech
The MCP server (misc/qet-mcp) was only reachable from a source checkout. It now ships with every package, next to the program, and finds that QElectroTech and its element collection from where it sits: - make install (Linux distributions, snap, flatpak): <prefix>/share/qelectrotech/mcp/qet_mcp.py, executable, with its README. - Windows installer: a new "AI assistant (MCP)" component (on by default, ~200 KB), installed to <folder>\mcp. The workflow stages files/mcp, so the portable folder and the MSI, which packs all of files/, carry it too. The server learns the Windows layout (<root>/mcp beside <root>/bin, whose program is QElectroTech.exe, and <root>/elements), in addition to <prefix>/share/qelectrotech/mcp. A copy saved anywhere else, even beside some bin/ folder, is not taken for an installation. The installer strings are given in all 29 installer languages: French translated, the others in English until translated, which is what NSIS would fall back to anyway but without its warning 6040 per language. Checked: make install into a scratch prefix, then from the installed script with nothing configured and no qelectrotech on PATH, an SVG export and a qet_edit placing a common:// element both succeeded. makensis 3.10 compiles the installer with 0 warnings before and after (removing the French description gives warning 6040), and the installer contains mcp/qet_mcp.py. Suite 274/274 none skipped; each layout check was removed in turn and a test failed. Snap/flatpak: installed by the same CMake rule; launching their QElectroTech from outside is untested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,28 @@ Register it with an MCP client, for example:
|
||||
QElectroTech (as `<prefix>/share/qelectrotech/mcp/qet_mcp.py`, which also
|
||||
finds the installed element collection).
|
||||
|
||||
## Installed with QElectroTech
|
||||
|
||||
QElectroTech's packages install the server next to the program, and from
|
||||
there it finds that QElectroTech and its element collection by itself:
|
||||
|
||||
| Package | Server | Finds |
|
||||
|---|---|---|
|
||||
| `make install`, Linux distributions | `<prefix>/share/qelectrotech/mcp/qet_mcp.py` | `<prefix>/bin/qelectrotech`, `<prefix>/share/qelectrotech/elements` |
|
||||
| Windows installer, MSI, portable folder | `<folder>\mcp\qet_mcp.py` (the "AI assistant (MCP)" component) | `<folder>\bin\QElectroTech.exe`, `<folder>\elements` |
|
||||
|
||||
So a client configuration needs only the path to the server and the
|
||||
workspace, for example on Windows:
|
||||
|
||||
```json
|
||||
"args": ["C:\\Program Files\\QElectroTech\\mcp\\qet_mcp.py"]
|
||||
```
|
||||
|
||||
Snap and flatpak install it too. Their QElectroTech is built to run inside
|
||||
the package's sandbox, and starting it from the server has not been tested:
|
||||
the tools that read a file work, exports and edits may not. If they fail,
|
||||
point `QET_BINARY` at a QElectroTech installed another way.
|
||||
|
||||
## Using it from the Claude app
|
||||
|
||||
A web chat in a browser cannot start a program on your computer, so it
|
||||
|
||||
+26
-14
@@ -3189,12 +3189,24 @@ def _env_paths(name: str) -> list:
|
||||
return out
|
||||
|
||||
|
||||
def _installed_prefix() -> Path | None:
|
||||
"""The install prefix when this script is <prefix>/share/qelectrotech/mcp/."""
|
||||
def _installation() -> tuple | None:
|
||||
"""(program directory, element collection) of the QElectroTech this
|
||||
script was installed with, or None when it runs from anywhere else.
|
||||
|
||||
Two layouts, both put there by QElectroTech's own packaging:
|
||||
<prefix>/share/qelectrotech/mcp/ -> <prefix>/bin, <prefix>/share/qelectrotech/elements
|
||||
(make install: Linux, snap, flatpak, macOS)
|
||||
<root>/mcp/ -> <root>/bin, <root>/elements
|
||||
(the Windows installers and portable folder)
|
||||
"""
|
||||
here = Path(__file__).resolve().parent
|
||||
if here.name == "mcp" and here.parent.name == "qelectrotech" \
|
||||
and here.parent.parent.name == "share":
|
||||
return here.parent.parent.parent
|
||||
if here.name != "mcp":
|
||||
return None
|
||||
if here.parent.name == "qelectrotech" and here.parent.parent.name == "share":
|
||||
prefix = here.parent.parent.parent
|
||||
return prefix / "bin", here.parent / "elements"
|
||||
if (here.parent / "bin").is_dir():
|
||||
return here.parent / "bin", here.parent / "elements"
|
||||
return None
|
||||
|
||||
|
||||
@@ -3208,10 +3220,12 @@ def resolve_binary() -> Path | None:
|
||||
env = os.environ.get("QET_BINARY", "").strip()
|
||||
if env:
|
||||
return Path(env).expanduser().resolve()
|
||||
prefix = _installed_prefix()
|
||||
if prefix is not None:
|
||||
for name in ("qelectrotech", "qelectrotech.exe"):
|
||||
cand = prefix / "bin" / name
|
||||
install = _installation()
|
||||
if install is not None:
|
||||
# The Windows build names it QElectroTech.exe; only a case-sensitive
|
||||
# file system tells the spellings apart.
|
||||
for name in ("qelectrotech", "qelectrotech.exe", "QElectroTech.exe"):
|
||||
cand = install[0] / name
|
||||
if cand.is_file():
|
||||
return cand.resolve()
|
||||
found = shutil.which("qelectrotech")
|
||||
@@ -3220,11 +3234,9 @@ def resolve_binary() -> Path | None:
|
||||
|
||||
def default_elements_dir() -> Path | None:
|
||||
"""The element collection of the install this script ships in, if any."""
|
||||
prefix = _installed_prefix()
|
||||
if prefix is not None:
|
||||
coll = prefix / "share" / "qelectrotech" / "elements"
|
||||
if coll.is_dir():
|
||||
return coll.resolve()
|
||||
install = _installation()
|
||||
if install is not None and install[1].is_dir():
|
||||
return install[1].resolve()
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -2021,6 +2021,43 @@ class BinaryPolicy(unittest.TestCase):
|
||||
self.assertEqual(args["elements_dir"],
|
||||
str((prefix / "share" / "qelectrotech" / "elements").resolve()))
|
||||
|
||||
def installed_at(self, root: Path, mcp: Path):
|
||||
"""Import a copy of the server placed at mcp/qet_mcp.py."""
|
||||
import importlib.util
|
||||
mcp.mkdir(parents=True)
|
||||
shutil.copy2(HERE / "qet_mcp.py", mcp / "qet_mcp.py")
|
||||
spec = importlib.util.spec_from_file_location(f"qet_mcp_{root.name}", mcp / "qet_mcp.py")
|
||||
inst = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(inst)
|
||||
return inst
|
||||
|
||||
def test_the_windows_layout_is_found_too(self):
|
||||
"""The Windows installers and the portable folder put the server in
|
||||
<root>/mcp, the program in <root>/bin as QElectroTech.exe, and the
|
||||
collection in <root>/elements."""
|
||||
root = Path(self.tmp.name) / "QElectroTech"
|
||||
(root / "elements").mkdir(parents=True)
|
||||
exe = fake_qet(root / "bin", "QElectroTech.exe")
|
||||
inst = self.installed_at(root, root / "mcp")
|
||||
del os.environ["QET_BINARY"]
|
||||
args = {"project": str(self.root / "ok.qet")}
|
||||
inst.enforce_path_policy("qet_query", args)
|
||||
self.assertEqual(args["binary"], str(exe.resolve()))
|
||||
self.assertEqual(args["elements_dir"], str((root / "elements").resolve()))
|
||||
|
||||
def test_a_copy_saved_anywhere_is_not_an_install(self):
|
||||
"""A downloaded copy sitting beside some bin/ folder must not go
|
||||
looking for QElectroTech there."""
|
||||
home = Path(self.tmp.name) / "home"
|
||||
fake_qet(home / "bin")
|
||||
inst = self.installed_at(home, home / "Downloads")
|
||||
self.assertIsNone(inst._installation())
|
||||
|
||||
def test_a_folder_named_mcp_is_not_an_install(self):
|
||||
"""Without a bin/ beside it, an mcp/ folder is just a folder."""
|
||||
inst = self.installed_at(Path(self.tmp.name) / "x", Path(self.tmp.name) / "x" / "mcp")
|
||||
self.assertIsNone(inst._installation())
|
||||
|
||||
|
||||
class ScriptingDisabledHint(unittest.TestCase):
|
||||
"""QElectroTech may refuse to run scripts at all, and says so in French.
|
||||
|
||||
Reference in New Issue
Block a user