Clone
1
mcp_server FR
ispyisail edited this page 2026-09-24 12:55:40 +12:00

Serveur MCP

Permet à un assistant IA (Claude, ou tout autre client MCP) de lire et de vérifier des projets QElectroTech directement, plutôt que de raisonner à partir d'une capture d'écran : ce que contient un projet, ce qu'une modification a réellement changé, et ce que contient tout un corpus de projets.

Il se trouve dans misc/qet-mcp/qet_mcp.py dans l'arborescence des sources — un petit serveur Model Context Protocol en stdio, Python 3.9+ et la bibliothèque standard uniquement, sans dépendance au SDK MCP. Ajouté dans la PR #969 ; sa fondation d'API de script a été ajoutée en même temps dans la PR #970. Les deux ont été fusionnées le 2026-09-21.


Pourquoi ce serveur existe

Vérifier une modification par capture d'écran n'est pas fiable, et cet outil existe parce que cette non-fiabilité a produit deux conclusions erronées dans une seule session de relecture :

  • Un glisser-déposer d'une sélection multi-éléments semblait avoir laissé les symboles en place et détaché leurs étiquettes. La comparaison du fichier enregistré a montré que les quatre éléments avaient tous bougé d'un même delta (0, -80) et qu'aucune étiquette n'avait bougé du tout. Un rapport de bogue est passé à un cheveu d'être déposé.
  • Un bouton « Appliquer » semblait ne rien faire. Il était désactivé, car un champ obligatoire était vide.

Les deux fois, les pixels ont induit en erreur et le modèle a dit la vérité. Les outils présentés ici lisent donc le modèle — le XML du projet et, quand elle existe, la base de données du projet — plutôt que la scène rendue.

La plupart des outils analysent directement le fichier .qet/.elmt : rapide, sans affichage nécessaire, insensible à une boîte de dialogue égarée. Deux d'entre eux (qet_export, qet_edit) lancent QElectroTech lui-même, dans un bac à sable isolé, parce qu'exporter et modifier via l'application réelle est le seul moyen d'obtenir le comportement réel — voir Scripts JavaScript pour le moteur que qet_edit et qet_query pilotent en dessous.


Installation

# depuis l'arborescence des sources de QElectroTech
python3 misc/qet-mcp/qet_mcp.py --list   # liste les outils puis quitte
python3 misc/qet-mcp/qet_mcp.py          # parle MCP sur stdin/stdout

Enregistrez-le auprès d'un client MCP — pour Claude Code ou Claude Desktop, un bloc mcpServers :

{
  "mcpServers": {
    "qet": {
      "command": "python3",
      "args": ["/path/to/qelectrotech/misc/qet-mcp/qet_mcp.py"],
      "env": {
        "QET_MCP_WORKSPACE": "/home/you/drawings",
        "QET_ENABLE_SCRIPTING": "1"
      }
    }
  }
}

Rien à compiler, rien à installer via pip — les deux variables d'environnement ci-dessus sont la seule configuration qui compte, et les deux sont détaillées plus bas.


Variables d'environnement

Variable Effet
QET_MCP_WORKSPACE Répertoires que les appels d'outils peuvent lire et écrire, séparés par : (; sous Windows). Non définie : le répertoire depuis lequel le serveur a été démarré.
QET_MCP_ALLOW_ANY_PATH=1 Désactive entièrement la vérification de l'espace de travail — équivalent à donner au client un accès au système de fichiers local avec les privilèges de ce processus.
QET_ENABLE_SCRIPTING=1 Requis par cinq outils (voir plus bas) ; QElectroTech refuse --run sans cette variable, désactivée par défaut depuis la PR #984.

Confinement de l'espace de travail

Chaque chemin passé dans un appel d'outil est choisi par le modèle. Sans politique de contrôle, cela ferait du serveur une primitive de lecture/écriture pour tout ce que le système d'exploitation permet au processus d'atteindre : lire n'importe quel projet sur le disque, exporter ailleurs, écraser un fichier sans rapport, intégrer une image ou un PDF local arbitraire. C'est pourquoi les chemins de données sont confinés à QET_MCP_WORKSPACE, vérifiés au point où les arguments entrent dans le serveur. Un chemin en dehors de celui-ci est refusé avec une erreur nommant ce qui était autorisé ; les liens symboliques sont résolus au préalable, si bien qu'un lien planté à l'intérieur de l'espace de travail est jugé d'après ce vers quoi il pointe.

Deux arguments ne sont volontairement pas confinés : binary (l'exécutable qelectrotech) et elements_dir (la collection d'éléments). Ce sont des réglages de configuration, choisis une fois par la personne qui exécute le serveur, et tous deux vivent normalement dans /usr ou dans une arborescence de compilation — en dehors de tout espace de travail raisonnable. Les confiner rejetterait le cas ordinaire sans rien empêcher.

Rien n'est écrasé sans autorisation. qet_export, qet_edit, qet_project_new et qet_element_build refusent une output déjà existante à moins que l'appel ne passe "overwrite": true — la seule étape que ce serveur ne peut pas annuler est la seule qu'il ne franchira pas de lui-même.

Verrou des scripts

Nécessite QET_ENABLE_SCRIPTING=1 qet_query, qet_continuity, qet_check, qet_project_new, qet_edit
Non concernés Tous les autres — ils lisent le .qet/.elmt directement, ou, pour qet_export, utilisent un simple indicateur de ligne de commande

La variable se règle dans l'environnement où le serveur est démarré (le bloc env ci-dessus), et le serveur la transmet telle quelle à QElectroTech — il ne la définit pas lui-même. Un interrupteur qu'un programme active pour lui-même n'est pas un interrupteur : c'est la personne qui a configuré le serveur et l'a pointé vers un binaire QElectroTech qui a fait ce choix, et son propre QElectroTech interactif conserve ce que dit son propre réglage. Sans la variable, les cinq outils ci-dessus renvoient "ok": false avec un hint nommant la variable. Les versions antérieures à l'existence de ce réglage n'ont besoin de rien.


Outils

Outil À quoi il répond Lance QET ?
qet_project_info Titre, version du format, folios, décompte d'éléments/conducteurs par folio Non
qet_elements Éléments placés : uuid, type, position, étiquette, sac d'informations ; filtre par folio ou par nom Non
qet_conductors Conducteurs et leurs champs documentaires (num, formula, cable, bus, function, colour, section) ; filtre par attribut Non
qet_diff Ce qu'une modification a réellement changé — déplacements/ajouts/suppressions/réétiquetages d'éléments, changements des champs de conducteur, champs/textes/formes/images/champs de texte de symboles/borniers de folio Non
qet_scan Parcourt un répertoire de projets, comptant les nœuds portant un attribut, avec les valeurs distinctes trouvées Non
qet_element_info Introspection d'un .elmt : noms traduits, bornes, champs de texte dynamique, décompte des parties Non
qet_export Export sans interface : pdf, png, svg, bom, cables, wires, wiring, nets, links, info Oui
qet_edit Modifier un projet — placer, déplacer, faire pivoter, étiqueter, câbler, numéroter, faire des renvois, ajouter texte/formes/images, restyler les champs de texte d'un symbole, supprimer ; renvoie un qet_diff du résultat Oui
qet_query Requête SQL en lecture seule (SELECT/WITH) sur la base SQLite du projet ; omettre sql liste les vues/tables interrogeables Oui*
qet_continuity Vérifications de type ERC sur le graphe Borne/Conducteur en direct : bornes non connectées, incohérences de potentiel, incohérences de renvoi de folio Oui*
qet_project_new Démarrer de zéro : un projet vide avec un titre et des folios, écrit et relu par QElectroTech lui-même Oui*
qet_element_search Trouver un symbole dans une collection par nom (toute langue), type de liaison, genre ou nombre de bornes ; les résultats portent le chemin common:// et l'ordre d'index des bornes dont qet_edit a besoin Non
qet_check Vérifications de règles de conception : étiquettes en double, bornes non connectées, conducteurs sans numéro, folios vides, maîtres sans référence fabricant Oui*
qet_element_build Créer un nouveau .elmt : dessiné à partir de lignes/rectangles/ellipses/cercles/arcs/polygones/texte, avec des bornes pour le câbler ; calcule et vérifie l'en-tête de dimensions Non

* Nécessite QET_ENABLE_SCRIPTING=1.


Exemples commentés

Qu'est-ce que cette modification a changé ?

{"name": "qet_diff", "arguments": {"before": "a.qet", "after": "b.qet"}}
"elements": { "moved_count": 4,
              "distinct_move_deltas": [[0.0, -80.0]],
              "relabelled": [], "info_changed": [] }

Quatre éléments ont bougé d'un même delta ; rien n'a été réétiqueté. C'est la réponse qu'une capture d'écran donnait à tort.

Dessiner quelque chose, et vérifier que c'est bien arrivé

{"name": "qet_edit", "arguments": {
  "binary": "/path/to/qelectrotech",
  "project": "in.qet", "output": "out.qet",
  "elements_dir": "/path/to/qelectrotech/elements",
  "operations": [
    {"op": "add_folio", "id": "f"},
    {"op": "set_folio_title", "folio": "$f", "title": "Starter"},
    {"op": "add_element", "id": "k1", "folio": "$f", "path": "common://.../coil.elmt", "x": 100, "y": 100},
    {"op": "add_element", "id": "k2", "folio": "$f", "path": "common://.../coil.elmt", "x": 320, "y": 100},
    {"op": "add_conductor", "folio": "$f", "from": "$k1", "from_terminal": 0, "to": "$k2", "to_terminal": 0},
    {"op": "set_conductor", "folio": "$f", "element": "$k1", "terminal": 0, "property": "num", "value": "W7"},
    {"op": "set_label", "folio": "$f", "element": "$k1", "label": "KM1"}
  ]}}

Une opération qui crée quelque chose porte un "id" ; les opérations suivantes le désignent comme "$id". Les bornes sont adressées par index — de haut en bas puis de gauche à droite, pas l'ordre dans lequel le .elmt les liste ; qet_element_info et qet_element_search rapportent tous deux cet ordre d'index. Le résultat porte à la fois un résultat par opération et un qet_diff, car "addConductor → true" dit que l'appel a été accepté, pas que le fichier obtenu est correct :

"diff": {"elements":   {"before": 11, "after": 13, "added": ["{0aa3…}", "{6f63…}"]},
         "conductors": {"before": 47, "after": 48, "added": ["4:{0aa3…}/{2904…}--{6f63…}/{2904…}"],
                        "removed": []}}

Dessiner un symbole qui n'existe pas encore

{"name": "qet_element_build", "arguments": {
  "output": "/path/to/collection/99_custom/my_resistor.elmt",
  "names": {"en": "Test resistor", "fr": "Résistance de test"},
  "parts": [
    {"type": "rect", "x": -10, "y": -20, "width": 20, "height": 40},
    {"type": "line", "x1": 0, "y1": -30, "x2": 0, "y2": -20},
    {"type": "line", "x1": 0, "y1": 20,  "x2": 0, "y2": 30},
    {"type": "text", "x": 14, "y": -4, "text": "R"}
  ],
  "terminals": [{"x": 0, "y": -30, "orientation": "n", "name": "1"},
                {"x": 0, "y": 30,  "orientation": "s", "name": "2"}]}}

Placez-le ensuite avec qet_edit comme n'importe quel élément de catalogue. Contrairement à un projet, un .elmt n'est pas réécrit par QElectroTech lors d'un aller-retour, donc en générer un ici est sûr d'une façon qui ne le serait pas pour un .qet — il n'y a pas de toXml() en embuscade pour perdre ce que cet écrivain ne savait pas produire.

Poser une question à laquelle le XML ne peut pas répondre

{"name": "qet_query", "arguments": {
  "binary": "/path/to/qelectrotech", "project": "industrial.qet",
  "sql": "SELECT label, COUNT(*) AS n FROM element_nomenclature_view WHERE label <> '' GROUP BY label HAVING n > 1 ORDER BY n DESC"}}
"rows": [{"label": "V6", "n": 7}, {"label": "V5", "n": 6}, {"label": "V4", "n": 6}]

Des étiquettes d'éléments en double dans un exemple fourni — une question de règle de conception, à laquelle la base de données savait déjà répondre. Voir La base de données du projet pour ce que couvrent element_nomenclature_view, project_summary_view et wiring_list_view.

Quelle part d'un corpus utilise un champ ?

{"name": "qet_scan",
 "arguments": {"directory": "examples", "tag": "conductor", "attribute": "cable"}}
{ "files": 24, "total": 3190, "non_empty": 0, "distinct_values": [] }

Sur les exemples fournis : 3190 conducteurs, aucun n'a de valeur de câble.


Remarques et limites

  • qet_export isole son lancement. SingleApplication indexe son socket sur applicationFilePath(), donc un second lancement du même chemin binaire redirige sa requête vers une instance déjà en cours d'exécution et renvoie sa réponse, sans erreur. L'outil copie le binaire vers un chemin temporaire unique, lui donne un HOME privé, et le lance sur la plateforme hors écran. Un lien symbolique ne fonctionnerait pas — applicationFilePath() le résout jusqu'au chemin réel.
  • La ligne de commande exige la forme exacte des indicateurs. --export-bom out.csv est la forme prise en charge ; --export-bom=out.csv n'est pas reconnu comme un export du tout, donc l'application démarre son interface à la place et un lancement sans interface reste bloqué. L'outil utilise la forme positionnelle.
  • L'identité des conducteurs est la partie difficile de qet_diff. Les conducteurs sont identifiés par l'uuid de l'élément propriétaire plus la borne, ce qui est stable d'un enregistrement à l'autre — les identifiants entiers propres au folio du fichier sont renumérotés à chaque enregistrement et feraient lire chaque conducteur d'un folio non modifié comme supprimé puis rajouté. Quand un élément est antérieur aux uuid persistés, l'extrémité ne peut pas être résolue et conserve une clé instable marquée # ; le diff rapporte alors unstable_keys plutôt que de prétendre être comparable.
  • Les textes, formes et images n'ont pas d'uuid, donc un texte modifié est lu comme l'ancien supprimé et un nouveau ajouté, les deux étant affichés. Les formes et images sont identifiées par position, donc un changement de style ou d'échelle est rapporté comme un changement de cet élément, mais un déplacement se lit comme une suppression plus un ajout.
  • qet_edit nécessite une version dont l'API de script porte les verbes de dessin. Face à une version plus ancienne, il indique précisément quelles méthodes manquent et ne change rien.
  • elements_dir n'est pas optionnel pour les chemins common://. Le lancement en bac à sable a son propre HOME vide, donc QElectroTech se rabat sur le chemin de collection compilé en dur, qui n'existe pas sur une machine où make install n'a jamais été exécuté. Le seul symptôme est que add_element rapporte qu'un fichier pourtant bien présent « ne correspond à aucun élément ». Un chemin .elmt absolu fonctionne sans cela.
  • set_conductor change tout le potentiel, pas un seul segment — c'est ce que fait l'application, puisqu'un numéro de fil décrit un potentiel. Nommez une borne portant exactement un conducteur ; une borne où plusieurs conducteurs se rejoignent n'en désigne aucun et est refusée.
  • link_elements prend un folio pour chaque extrémité, car un maître et son esclave sont normalement sur des folios différents. La possibilité de lier une paire est décidée par le isLinkable() propre à QElectroTech, donc un script ne peut pas créer un lien que l'interface refuserait.
  • Un élément doit se trouver dans une collection pour être plaçable. Un chemin .elmt absolu fonctionne, mais seulement si le fichier se trouve sous un répertoire que QElectroTech connaît comme collection — écrivez-le sous l'arborescence passée comme elements_dir.
  • qet_element_build vérifie son en-tête de dimensions par rapport à une contrainte de contenance, pas une formule : la boîte déclarée va de (-hotspot_x, -hotspot_y) à (width - hotspot_x, height - hotspot_y) et le dessin doit tenir à l'intérieur. Un dessin qui a débordé de sa boîte est la façon classique dont un élément écrit à la main s'affiche tronqué dans le panneau de collection tout en semblant correct dans le XML.
  • QElectroTech interrompt un script au bout de 30 s de sa propre initiative, indépendamment du timeout propre à l'outil. Une très longue liste d'opérations atteint cette limite en premier.
  • qet_edit n'écrit jamais le fichier d'entrée. Il enregistre dans un fichier séparé et compare les deux, donc l'original reste toujours ce contre quoi le diff est fait.

Tests

python3 misc/qet-mcp/test_qet_mcp.py                      # unitaires + protocole, sans QElectroTech

QET_BINARY=/path/to/qelectrotech \
QET_ELEMENTS=/path/to/qelectrotech/elements \
QET_EXAMPLES=/path/to/qelectrotech/examples \
QET_ENABLE_SCRIPTING=1 \
    python3 misc/qet-mcp/test_qet_mcp.py                  # tout, y compris l'intégration

QET_ENABLE_SCRIPTING=1 compte aussi ici : sans cette variable, les tests d'intégration qui pilotent QElectroTech via un script échouent tous, et ils échouent en disant « la modification n'a rien fait » plutôt que « les scripts sont désactivés » — ce qui ressemble à une régression de la chose testée, pas à un interrupteur manquant.


Voir aussi