Add MCP validation tooling and scope specs
This commit is contained in:
@@ -83,6 +83,7 @@ node_modules/
|
||||
# Docker
|
||||
*.pid
|
||||
docker-compose.override.yml
|
||||
.cad-home/
|
||||
|
||||
# Autres outils CI/CD
|
||||
coverage/
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ Kill_LIFE, c’est un gros dossier pour faire des projets électroniques avec de
|
||||
|
||||
Dans le terminal, tape :
|
||||
```bash
|
||||
python tools/validate_specs.py
|
||||
python3 tools/validate_specs.py
|
||||
```
|
||||
Ça vérifie que les règles du projet sont OK.
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ Repo “de base” qui combine le meilleur de :
|
||||
|
||||
## TL;DR
|
||||
- Écris/itère la spec dans `specs/`
|
||||
- (optionnel) Génère un squelette de spec depuis `.specify/` : `python tools/ai/specify_init.py --name <feature>`
|
||||
- (optionnel) Génère un squelette de spec depuis `.specify/` : `python3 tools/ai/specify_init.py --name <feature>`
|
||||
- Applique les standards dans `standards/`
|
||||
- Exécute le cockpit : `python tools/cockpit/cockpit.py menu`
|
||||
- Exécute le cockpit : `python3 tools/cockpit/cockpit.py menu`
|
||||
- Pour l’automatisation GitHub : label `ai:codex` → Issue → PR
|
||||
|
||||
## Structure
|
||||
@@ -37,7 +37,7 @@ pio run -e esp32s3_arduino
|
||||
pio test -e native
|
||||
|
||||
# cockpit
|
||||
python tools/cockpit/cockpit.py menu
|
||||
python3 tools/cockpit/cockpit.py menu
|
||||
```
|
||||
|
||||
## Conventions de sortie
|
||||
@@ -46,8 +46,8 @@ Tous les scripts écrivent sous `artifacts/<domain>/<timestamp>/` (logs + export
|
||||
|
||||
## V4 — KiCad agentique (bulk + previews + MCP)
|
||||
- `bash tools/hw/hw_gate.sh hardware/kicad` : exports SVG + ERC/DRC JSON + BOM/netlist
|
||||
- `python tools/watch/watch_hw.py` : watch mode (re-run gate on save)
|
||||
- `docs/MCP_SETUP.md` : configuration MCP (kicad-sch-mcp) citeturn0search9
|
||||
- `python3 tools/watch/watch_hw.py` : watch mode (re-run gate on save)
|
||||
- `docs/MCP_SETUP.md` : configuration MCP (`kicad-sch-api` / `kicad-sch-mcp`) citeturn0search9
|
||||
|
||||
## Compliance (2 options)
|
||||
- **Prototype interne** : profil `prototype` (pas de CE/RED)
|
||||
@@ -55,10 +55,9 @@ Tous les scripts écrivent sous `artifacts/<domain>/<timestamp>/` (logs + export
|
||||
|
||||
Changer de profil :
|
||||
```bash
|
||||
python tools/compliance/use_profile.py prototype
|
||||
python tools/compliance/use_profile.py iot_wifi_eu
|
||||
python tools/compliance/validate.py
|
||||
python3 tools/compliance/use_profile.py prototype
|
||||
python3 tools/compliance/use_profile.py iot_wifi_eu
|
||||
python3 tools/compliance/validate.py
|
||||
```
|
||||
|
||||
Docs : `docs/COMPLIANCE.md`
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ Le fichier `constraints.yaml` est la **source de vérité** des contraintes non-
|
||||
|
||||
Specs complémentaires:
|
||||
|
||||
- `kicad_mcp_scope_spec.md`: perimetre fonctionnel, hors scope et criteres d'acceptation du MCP KiCad supporte.
|
||||
- `zeroclaw_dual_hw_orchestration_spec.md`: architecture d'orchestration ZeroClaw multi-repo + double matériel.
|
||||
- `zeroclaw_dual_hw_todo.md`: backlog opérationnel court terme pour autonomie contrôlée.
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# Spec perimetre MCP KiCad
|
||||
|
||||
Last updated: 2026-03-07
|
||||
|
||||
## Objectifs
|
||||
|
||||
- O1. Definir le perimetre fonctionnel du serveur MCP KiCad supporte par `Kill_LIFE`.
|
||||
- O2. Eviter la derive entre implementation serveur, launcher local, docs et attentes des agents.
|
||||
- O3. Fixer une frontiere claire entre ce que le MCP KiCad MUST couvrir en v1, ce qui SHOULD venir en v2, et ce qui reste hors scope.
|
||||
|
||||
## Non-objectifs
|
||||
|
||||
- N1. Faire du serveur MCP KiCad un assistant generaliste ou un agent conversationnel.
|
||||
- N2. Exposer un transport reseau ou multi-tenant en v1.
|
||||
- N3. Couvrir Git, CI, docs repo, orchestration infra, ou des actions hors CAD.
|
||||
- N4. Ajouter des effets de bord caches hors des chemins explicitement fournis par l'utilisateur ou le runtime.
|
||||
|
||||
## Ownership
|
||||
|
||||
- Implementation serveur de reference: `mascarade/finetune/kicad_mcp_server`
|
||||
- Contrat local, launcher, smoke test et doc d'usage: `Kill_LIFE`
|
||||
- Consommateurs externes eventuels: autres repos, sans ownership serveur
|
||||
|
||||
## User stories
|
||||
|
||||
- US1. En tant qu'ingenieur hardware, je veux creer, ouvrir, modifier et exporter un projet KiCad depuis un client MCP local.
|
||||
- US2. En tant qu'agent outille, je veux inspecter schema, board, librairies et regles sans corrompre le projet.
|
||||
- US3. En tant qu'operateur de fabrication, je veux lancer les validations et exports minimaux avant production.
|
||||
- US4. En tant qu'agent de sourcing, je veux relier composants, footprints, datasheets et references JLCPCB/LCSC.
|
||||
|
||||
## Exigences fonctionnelles v1
|
||||
|
||||
- F1. Transport
|
||||
- Le serveur MUST exposer `stdio` local uniquement.
|
||||
- Le serveur MUST parler JSON-RPC compatible MCP avec framing ligne par ligne, conforme au SDK utilise.
|
||||
- Le serveur MUST fonctionner via `tools/hw/run_kicad_mcp.sh`.
|
||||
|
||||
- F2. Cycle projet
|
||||
- Le serveur MUST permettre de creer, ouvrir, sauvegarder et inspecter un projet KiCad.
|
||||
- Le serveur MUST distinguer les operations de lecture des operations de mutation.
|
||||
|
||||
- F3. Schema
|
||||
- Le serveur MUST permettre le placement de symboles, la connexion de pins, la pose de labels/nets et la generation de netlist.
|
||||
- Le serveur SHOULD permettre l'enrichissement des champs de composant utiles a la suite du flux CAD.
|
||||
|
||||
- F4. PCB / layout
|
||||
- Le serveur MUST permettre l'inspection du board, des couches, des extents et de la geometrie.
|
||||
- Le serveur MUST couvrir outline, trous de fixation, textes, zones et operations de placement/deplacement/rotation de composants.
|
||||
|
||||
- F5. Routage
|
||||
- Le serveur MUST permettre la creation ou l'inspection de pistes, vias, pads, nets et couches utiles au routage.
|
||||
- Le serveur SHOULD exposer des aides de recherche/outillage de routage.
|
||||
|
||||
- F6. Librairies
|
||||
- Le serveur MUST lister et rechercher des symboles et footprints.
|
||||
- Le serveur MUST permettre l'enregistrement de librairies projet/globales quand l'operation est explicitement demandee.
|
||||
- Le serveur SHOULD permettre la creation de symboles et footprints custom en local.
|
||||
|
||||
- F7. Validation
|
||||
- Le serveur MUST permettre l'execution de checks de validation de type DRC ou equivalent.
|
||||
- Le serveur MUST renvoyer des erreurs structurees et exploitables quand une validation echoue.
|
||||
|
||||
- F8. Export
|
||||
- Le serveur MUST couvrir les exports minimaux de fabrication et de revue: Gerber, PDF, 3D ou sorties equivalentes deja supportees par l'implementation.
|
||||
- Le serveur SHOULD rendre le chemin de sortie explicite.
|
||||
|
||||
- F9. Sourcing
|
||||
- Le serveur SHOULD permettre la recherche de composants JLCPCB/LCSC, les alternatives proches et l'acces aux datasheets.
|
||||
- Les integrations de sourcing MAY utiliser un backend local ou une API externe si les credentials sont presents.
|
||||
|
||||
- F10. Runtime
|
||||
- Le serveur MUST preferer un backend KiCad reel.
|
||||
- Le backend SHOULD preferer IPC quand disponible.
|
||||
- Le backend MUST pouvoir retomber sur SWIG si IPC n'est pas disponible.
|
||||
- Le runtime MUST fonctionner soit sur host avec `pcbnew`, soit via le conteneur KiCad supporte.
|
||||
|
||||
## Exigences non-fonctionnelles
|
||||
|
||||
- Securite
|
||||
- Le serveur MUST rester local par defaut.
|
||||
- Le serveur MUST eviter les ecritures hors `projectPath`, `libraryPath`, runtime home et `KICAD_MCP_DATA_DIR`.
|
||||
- Le serveur MUST NOT exiger de secrets pour les usages locaux de base.
|
||||
|
||||
- Fiabilite
|
||||
- `initialize` puis `tools/list` MUST passer sur une machine supportee.
|
||||
- Le launcher MUST gerer le fallback host -> container quand l'hote n'expose pas `pcbnew`.
|
||||
- Les erreurs MUST etre actionnables et non silencieuses.
|
||||
|
||||
- Determinisme
|
||||
- Les outils de lecture MUST NOT muter l'etat du projet.
|
||||
- Les outils de mutation SHOULD exiger des parametres explicites et des chemins non ambigus.
|
||||
|
||||
- Observabilite
|
||||
- Le serveur MUST garder les erreurs et warnings exploitables.
|
||||
- Le niveau de log par defaut SHOULD etre discret.
|
||||
- En succes nominal, le runtime SHOULD eviter de polluer `stderr` avec du bruit de demarrage non essentiel.
|
||||
|
||||
- Compatibilite
|
||||
- Le contrat de transport MUST rester stable pour les consommateurs locaux de `Kill_LIFE`.
|
||||
- Le runtime conteneur supporte SHOULD rester aligne avec la version KiCad cible du projet, y compris la trajectoire v10.
|
||||
|
||||
## V2 / extensions legitimes
|
||||
|
||||
- V2.1. Session IPC plus riche, synchronisee avec une UI KiCad vivante.
|
||||
- V2.2. Coverage schematic plus avancee: edition structurelle plus large, contraintes electriques plus fines, automation de patterns repetitifs.
|
||||
- V2.3. Workflows de revue plus riches: snapshots, diff de board/schema, previews avant mutation.
|
||||
- V2.4. Sourcing plus fort: BOM, substitutions, scoring cout/disponibilite.
|
||||
- V2.5. Dry-run systematique pour les mutations a fort impact.
|
||||
|
||||
## Hors scope
|
||||
|
||||
- H1. Chat libre, planification generale, ou raisonnement non CAD.
|
||||
- H2. Serveur MCP HTTP/SSE expose sur le reseau par defaut.
|
||||
- H3. Git operations, PR management, CI/CD, ou orchestration repo.
|
||||
- H4. Gestion de secrets globale de la machine.
|
||||
- H5. Autonomie d'achat ou de commande fournisseur.
|
||||
- H6. Couverture d'autres outils CAD hors KiCad dans ce serveur de reference.
|
||||
|
||||
## Contrats / interfaces
|
||||
|
||||
- I1. Point d'entree supporte: `tools/hw/run_kicad_mcp.sh`
|
||||
- I2. Config consommatrice supportee: `mcp.json` cote `Kill_LIFE`
|
||||
- I3. Implementation serveur de reference: `mascarade/finetune/kicad_mcp_server`
|
||||
- I4. Smoke minimal supporte:
|
||||
- `initialize`
|
||||
- `notifications/initialized`
|
||||
- `tools/list`
|
||||
|
||||
## Critieres d'acceptation
|
||||
|
||||
- AC1. Le repo documente un seul serveur KiCad MCP supporte en v1.
|
||||
- AC2. Le serveur expose au moins une famille d'outils fonctionnelle pour: projet, schema, PCB, librairies, validation, export.
|
||||
- AC3. `python3 tools/hw/mcp_smoke.py` passe sur un environnement supporte.
|
||||
- AC4. En absence de `pcbnew` host, le launcher supporte bascule vers le runtime conteneur.
|
||||
- AC5. La doc d'usage locale renvoie explicitement vers cette spec de perimetre.
|
||||
- AC6. Les sujets hors scope sont documentes au lieu d'etre supposes implicitement.
|
||||
@@ -44,7 +44,7 @@ Checklist :
|
||||
|
||||
Exemple :
|
||||
```bash
|
||||
python tools/validate_specs.py || true
|
||||
python3 tools/validate_specs.py || true
|
||||
```
|
||||
|
||||
## Gates
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# 15) Plan d’alignement MCP local
|
||||
|
||||
Last updated: 2026-03-07
|
||||
|
||||
## Objectif
|
||||
|
||||
Faire de `Kill_LIFE` le repo de consommation et de gouvernance MCP, sans maintenir un second serveur MCP concurrent.
|
||||
|
||||
## Cible supportée
|
||||
|
||||
- serveur: `../mascarade/finetune/kicad_mcp_server`
|
||||
- launcher local: `tools/hw/run_kicad_mcp.sh`
|
||||
- config MCP versionnée: `mcp.json`
|
||||
- transport: `stdio`
|
||||
- serveur auxiliaire repo/specs: `tools/validate_specs.py --mcp`
|
||||
|
||||
## Actions
|
||||
|
||||
### 1. Source de vérité
|
||||
|
||||
1. Retirer toute promesse de serveur fantôme.
|
||||
2. Pointer `mcp.json` vers le launcher supporté et les MCP auxiliaires réels.
|
||||
3. Basculer la doc MCP canonique sur `docs/MCP_SETUP.md`.
|
||||
|
||||
### 2. Runtime opérable
|
||||
|
||||
1. Faire de `tools/hw/cad_stack.sh mcp` un simple wrapper vers le launcher supporté.
|
||||
2. Préparer un runtime writable sous `.cad-home/kicad-mcp`.
|
||||
3. Garder `MASCARADE_DIR` comme override explicite, pas comme dépendance cachée.
|
||||
|
||||
### 3. Documentation
|
||||
|
||||
1. Transformer les duplications `ai-agentic-embedded-base/docs/*` en pointeurs vers les docs canoniques.
|
||||
2. Corriger les références à `validate_specs.py`.
|
||||
3. Garder `stdio only` comme politique MCP locale.
|
||||
|
||||
## Critères de sortie
|
||||
|
||||
- `mcp.json` ne référence plus de fichier absent
|
||||
- `tools/hw/run_kicad_mcp.sh --doctor` résout un serveur réel
|
||||
- `tools/hw/cad_stack.sh mcp` utilise le même chemin que `mcp.json`
|
||||
- la doc root n’annonce plus `kicad-sch-mcp` comme chemin principal
|
||||
@@ -0,0 +1,33 @@
|
||||
# 15) Plan MCP stack
|
||||
|
||||
Last updated: 2026-03-07
|
||||
|
||||
## Objectif
|
||||
|
||||
Rendre la couche MCP de `Kill_LIFE` cohérente côté config, validation locale et usage opérateur, sans embarquer une promesse cassée.
|
||||
|
||||
## Décisions figées
|
||||
|
||||
- `mcp.json` publie le launcher KiCad supporté `tools/hw/run_kicad_mcp.sh`.
|
||||
- `Kill_LIFE` expose aussi `validate-specs` comme serveur/CLI auxiliaire de validation repo.
|
||||
- Le runtime KiCad côté `Kill_LIFE` est piloté par `mcp.json` et `tools/hw/run_kicad_mcp.sh`.
|
||||
- `Kill_LIFE` ne publie pas de second serveur KiCad host-side concurrent tant que la convergence n’est pas terminée.
|
||||
|
||||
## Implémenté
|
||||
|
||||
- [x] Ajouter `tools/validate_specs.py` comme script CLI réel.
|
||||
- [x] Exposer le même script en serveur MCP `stdio` minimal.
|
||||
- [x] Garder `mcp.json` sur le launcher KiCad versionné.
|
||||
- [x] Ajouter des tests sur le mode CLI et le handshake MCP minimal.
|
||||
|
||||
## Reste à faire
|
||||
|
||||
- [ ] Documenter précisément ce que couvre `validate-specs` et ce qu’il ne couvre pas.
|
||||
- [ ] Réaligner `tools/hw/cad_stack.sh mcp` sur le launcher supporté ou le déclasser explicitement.
|
||||
- [ ] Réconcilier `docs/MCP_SETUP.md`, `docs/KICAD_AI_LOCAL.md` et `deploy/cad/README.md`.
|
||||
|
||||
## Critères de sortie
|
||||
|
||||
- `python3 tools/validate_specs.py` fonctionne sans ambigüité.
|
||||
- `mcp.json` démarre un serveur MCP KiCad réel.
|
||||
- La doc `Kill_LIFE` ne présente plus plusieurs vérités contradictoires pour le lancement MCP.
|
||||
@@ -0,0 +1,21 @@
|
||||
# TODO MCP stack — `Kill_LIFE`
|
||||
|
||||
Last updated: 2026-03-07
|
||||
|
||||
## Sprint actuel
|
||||
|
||||
- [x] Implémenter `tools/validate_specs.py`.
|
||||
- [x] Rendre `mcp.json` lançable.
|
||||
- [x] Ajouter un test de handshake MCP minimal.
|
||||
- [ ] Documenter le périmètre exact de `validate-specs`.
|
||||
|
||||
## J7
|
||||
|
||||
- [ ] Aligner la doc KiCad MCP sur un seul chemin opérateur supporté.
|
||||
- [ ] Décrire la dépendance éventuelle à `mascarade` pour le runtime KiCad avancé.
|
||||
- [ ] Ajouter une preuve d’exécution `tools/hw/cad_stack.sh mcp`.
|
||||
|
||||
## J30
|
||||
|
||||
- [ ] Transformer `docs/MCP_SETUP.md` en doc canonique unique ou en simple pointeur.
|
||||
- [ ] Ajouter une matrice `supporté / expérimental / démo`.
|
||||
@@ -12,6 +12,7 @@ Le fichier `constraints.yaml` est la **source de vérité** des contraintes non-
|
||||
|
||||
Specs complémentaires:
|
||||
|
||||
- `kicad_mcp_scope_spec.md`: perimetre fonctionnel, hors scope et criteres d'acceptation du MCP KiCad supporte.
|
||||
- `zeroclaw_dual_hw_orchestration_spec.md`: architecture d'orchestration ZeroClaw multi-repo + double matériel.
|
||||
- `zeroclaw_dual_hw_todo.md`: backlog opérationnel court terme pour autonomie contrôlée.
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# Spec perimetre MCP KiCad
|
||||
|
||||
Last updated: 2026-03-07
|
||||
|
||||
## Objectifs
|
||||
|
||||
- O1. Definir le perimetre fonctionnel du serveur MCP KiCad supporte par `Kill_LIFE`.
|
||||
- O2. Eviter la derive entre implementation serveur, launcher local, docs et attentes des agents.
|
||||
- O3. Fixer une frontiere claire entre ce que le MCP KiCad MUST couvrir en v1, ce qui SHOULD venir en v2, et ce qui reste hors scope.
|
||||
|
||||
## Non-objectifs
|
||||
|
||||
- N1. Faire du serveur MCP KiCad un assistant generaliste ou un agent conversationnel.
|
||||
- N2. Exposer un transport reseau ou multi-tenant en v1.
|
||||
- N3. Couvrir Git, CI, docs repo, orchestration infra, ou des actions hors CAD.
|
||||
- N4. Ajouter des effets de bord caches hors des chemins explicitement fournis par l'utilisateur ou le runtime.
|
||||
|
||||
## Ownership
|
||||
|
||||
- Implementation serveur de reference: `mascarade/finetune/kicad_mcp_server`
|
||||
- Contrat local, launcher, smoke test et doc d'usage: `Kill_LIFE`
|
||||
- Consommateurs externes eventuels: autres repos, sans ownership serveur
|
||||
|
||||
## User stories
|
||||
|
||||
- US1. En tant qu'ingenieur hardware, je veux creer, ouvrir, modifier et exporter un projet KiCad depuis un client MCP local.
|
||||
- US2. En tant qu'agent outille, je veux inspecter schema, board, librairies et regles sans corrompre le projet.
|
||||
- US3. En tant qu'operateur de fabrication, je veux lancer les validations et exports minimaux avant production.
|
||||
- US4. En tant qu'agent de sourcing, je veux relier composants, footprints, datasheets et references JLCPCB/LCSC.
|
||||
|
||||
## Exigences fonctionnelles v1
|
||||
|
||||
- F1. Transport
|
||||
- Le serveur MUST exposer `stdio` local uniquement.
|
||||
- Le serveur MUST parler JSON-RPC compatible MCP avec framing ligne par ligne, conforme au SDK utilise.
|
||||
- Le serveur MUST fonctionner via `tools/hw/run_kicad_mcp.sh`.
|
||||
|
||||
- F2. Cycle projet
|
||||
- Le serveur MUST permettre de creer, ouvrir, sauvegarder et inspecter un projet KiCad.
|
||||
- Le serveur MUST distinguer les operations de lecture des operations de mutation.
|
||||
|
||||
- F3. Schema
|
||||
- Le serveur MUST permettre le placement de symboles, la connexion de pins, la pose de labels/nets et la generation de netlist.
|
||||
- Le serveur SHOULD permettre l'enrichissement des champs de composant utiles a la suite du flux CAD.
|
||||
|
||||
- F4. PCB / layout
|
||||
- Le serveur MUST permettre l'inspection du board, des couches, des extents et de la geometrie.
|
||||
- Le serveur MUST couvrir outline, trous de fixation, textes, zones et operations de placement/deplacement/rotation de composants.
|
||||
|
||||
- F5. Routage
|
||||
- Le serveur MUST permettre la creation ou l'inspection de pistes, vias, pads, nets et couches utiles au routage.
|
||||
- Le serveur SHOULD exposer des aides de recherche/outillage de routage.
|
||||
|
||||
- F6. Librairies
|
||||
- Le serveur MUST lister et rechercher des symboles et footprints.
|
||||
- Le serveur MUST permettre l'enregistrement de librairies projet/globales quand l'operation est explicitement demandee.
|
||||
- Le serveur SHOULD permettre la creation de symboles et footprints custom en local.
|
||||
|
||||
- F7. Validation
|
||||
- Le serveur MUST permettre l'execution de checks de validation de type DRC ou equivalent.
|
||||
- Le serveur MUST renvoyer des erreurs structurees et exploitables quand une validation echoue.
|
||||
|
||||
- F8. Export
|
||||
- Le serveur MUST couvrir les exports minimaux de fabrication et de revue: Gerber, PDF, 3D ou sorties equivalentes deja supportees par l'implementation.
|
||||
- Le serveur SHOULD rendre le chemin de sortie explicite.
|
||||
|
||||
- F9. Sourcing
|
||||
- Le serveur SHOULD permettre la recherche de composants JLCPCB/LCSC, les alternatives proches et l'acces aux datasheets.
|
||||
- Les integrations de sourcing MAY utiliser un backend local ou une API externe si les credentials sont presents.
|
||||
|
||||
- F10. Runtime
|
||||
- Le serveur MUST preferer un backend KiCad reel.
|
||||
- Le backend SHOULD preferer IPC quand disponible.
|
||||
- Le backend MUST pouvoir retomber sur SWIG si IPC n'est pas disponible.
|
||||
- Le runtime MUST fonctionner soit sur host avec `pcbnew`, soit via le conteneur KiCad supporte.
|
||||
|
||||
## Exigences non-fonctionnelles
|
||||
|
||||
- Securite
|
||||
- Le serveur MUST rester local par defaut.
|
||||
- Le serveur MUST eviter les ecritures hors `projectPath`, `libraryPath`, runtime home et `KICAD_MCP_DATA_DIR`.
|
||||
- Le serveur MUST NOT exiger de secrets pour les usages locaux de base.
|
||||
|
||||
- Fiabilite
|
||||
- `initialize` puis `tools/list` MUST passer sur une machine supportee.
|
||||
- Le launcher MUST gerer le fallback host -> container quand l'hote n'expose pas `pcbnew`.
|
||||
- Les erreurs MUST etre actionnables et non silencieuses.
|
||||
|
||||
- Determinisme
|
||||
- Les outils de lecture MUST NOT muter l'etat du projet.
|
||||
- Les outils de mutation SHOULD exiger des parametres explicites et des chemins non ambigus.
|
||||
|
||||
- Observabilite
|
||||
- Le serveur MUST garder les erreurs et warnings exploitables.
|
||||
- Le niveau de log par defaut SHOULD etre discret.
|
||||
- En succes nominal, le runtime SHOULD eviter de polluer `stderr` avec du bruit de demarrage non essentiel.
|
||||
|
||||
- Compatibilite
|
||||
- Le contrat de transport MUST rester stable pour les consommateurs locaux de `Kill_LIFE`.
|
||||
- Le runtime conteneur supporte SHOULD rester aligne avec la version KiCad cible du projet, y compris la trajectoire v10.
|
||||
|
||||
## V2 / extensions legitimes
|
||||
|
||||
- V2.1. Session IPC plus riche, synchronisee avec une UI KiCad vivante.
|
||||
- V2.2. Coverage schematic plus avancee: edition structurelle plus large, contraintes electriques plus fines, automation de patterns repetitifs.
|
||||
- V2.3. Workflows de revue plus riches: snapshots, diff de board/schema, previews avant mutation.
|
||||
- V2.4. Sourcing plus fort: BOM, substitutions, scoring cout/disponibilite.
|
||||
- V2.5. Dry-run systematique pour les mutations a fort impact.
|
||||
|
||||
## Hors scope
|
||||
|
||||
- H1. Chat libre, planification generale, ou raisonnement non CAD.
|
||||
- H2. Serveur MCP HTTP/SSE expose sur le reseau par defaut.
|
||||
- H3. Git operations, PR management, CI/CD, ou orchestration repo.
|
||||
- H4. Gestion de secrets globale de la machine.
|
||||
- H5. Autonomie d'achat ou de commande fournisseur.
|
||||
- H6. Couverture d'autres outils CAD hors KiCad dans ce serveur de reference.
|
||||
|
||||
## Contrats / interfaces
|
||||
|
||||
- I1. Point d'entree supporte: `tools/hw/run_kicad_mcp.sh`
|
||||
- I2. Config consommatrice supportee: `mcp.json` cote `Kill_LIFE`
|
||||
- I3. Implementation serveur de reference: `mascarade/finetune/kicad_mcp_server`
|
||||
- I4. Smoke minimal supporte:
|
||||
- `initialize`
|
||||
- `notifications/initialized`
|
||||
- `tools/list`
|
||||
|
||||
## Critieres d'acceptation
|
||||
|
||||
- AC1. Le repo documente un seul serveur KiCad MCP supporte en v1.
|
||||
- AC2. Le serveur expose au moins une famille d'outils fonctionnelle pour: projet, schema, PCB, librairies, validation, export.
|
||||
- AC3. `python3 tools/hw/mcp_smoke.py` passe sur un environnement supporte.
|
||||
- AC4. En absence de `pcbnew` host, le launcher supporte bascule vers le runtime conteneur.
|
||||
- AC5. La doc d'usage locale renvoie explicitement vers cette spec de perimetre.
|
||||
- AC6. Les sujets hors scope sont documentes au lieu d'etre supposes implicitement.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Tasks MCP local
|
||||
|
||||
Last updated: 2026-03-07
|
||||
|
||||
Format:
|
||||
|
||||
- `[ ]` non fait
|
||||
- `[x]` fait
|
||||
|
||||
## Sprint actif
|
||||
|
||||
- [x] K-001 — Rendre `validate-specs` réel ou retirer la promesse
|
||||
- AC: aucun chemin absent n’est versionné comme serveur MCP.
|
||||
|
||||
- [x] K-002 — Ajouter un launcher supporté `tools/hw/run_kicad_mcp.sh`
|
||||
- AC: le launcher résout `mascarade`, prépare un runtime writable et exécute le serveur.
|
||||
|
||||
- [x] K-003 — Aligner `tools/hw/cad_stack.sh mcp`
|
||||
- AC: la commande `mcp` appelle le même launcher que `mcp.json`.
|
||||
|
||||
- [x] K-004 — Désigner `docs/MCP_SETUP.md` comme doc canonique
|
||||
- AC: la version miroir renvoie vers la doc racine.
|
||||
|
||||
- [x] K-005 — Retirer les références à `tools/validate_specs.py`
|
||||
- AC: les docs visibles pointent vers un validateur réellement présent.
|
||||
|
||||
## À faire ensuite
|
||||
|
||||
- [x] K-006 — Ajouter un smoke test MCP consommateur côté `Kill_LIFE`
|
||||
- AC: un script versionné existe pour tester `initialize` et `tools/list`.
|
||||
|
||||
- [ ] K-007 — Documenter les prérequis machine minimum pour le runtime MCP
|
||||
- AC: Node, KiCad et repo compagnon sont explicitement listés.
|
||||
|
||||
- [ ] K-008 — Décider du sort final du service Docker `kicad-mcp` historique
|
||||
- AC: soit supprimé, soit rebranché sur le runtime supporté sans drift.
|
||||
|
||||
- [ ] K-009 — Faire passer le smoke réel `initialize -> tools/list`
|
||||
- AC: `python3 tools/hw/mcp_smoke.py` passe sur une machine avec `pcbnew` disponible.
|
||||
- Blocage actuel: le host audité n’expose pas `pcbnew`, donc le runtime KiCad ne peut pas démarrer.
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = REPO_ROOT / "tools" / "validate_specs.py"
|
||||
|
||||
|
||||
def write_message(proc: subprocess.Popen[str], payload: dict) -> None:
|
||||
body = json.dumps(payload)
|
||||
proc.stdin.write(f"Content-Length: {len(body.encode('utf-8'))}\r\n\r\n{body}")
|
||||
proc.stdin.flush()
|
||||
|
||||
|
||||
def read_message(proc: subprocess.Popen[str]) -> dict:
|
||||
headers = {}
|
||||
while True:
|
||||
line = proc.stdout.readline()
|
||||
if not line:
|
||||
raise AssertionError("unexpected EOF while reading MCP headers")
|
||||
if line in ("\r\n", "\n"):
|
||||
break
|
||||
key, _, value = line.partition(":")
|
||||
headers[key.strip().lower()] = value.strip()
|
||||
|
||||
length = int(headers["content-length"])
|
||||
body = proc.stdout.read(length)
|
||||
return json.loads(body)
|
||||
|
||||
|
||||
class ValidateSpecsTests(unittest.TestCase):
|
||||
def test_cli_json_mode_emits_machine_readable_summary(self):
|
||||
proc = subprocess.run(
|
||||
["python3", str(SCRIPT), "--json"],
|
||||
cwd=REPO_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
payload = json.loads(proc.stdout)
|
||||
self.assertIn("ok", payload)
|
||||
self.assertIn("compliance", payload)
|
||||
self.assertIn("rfc2119", payload)
|
||||
|
||||
def test_mcp_server_supports_initialize_and_tools_list(self):
|
||||
proc = subprocess.Popen(
|
||||
["python3", str(SCRIPT), "--mcp"],
|
||||
cwd=REPO_ROOT,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
try:
|
||||
write_message(
|
||||
proc,
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": {},
|
||||
},
|
||||
)
|
||||
initialize = read_message(proc)
|
||||
self.assertEqual(initialize["id"], 1)
|
||||
self.assertEqual(
|
||||
initialize["result"]["serverInfo"]["name"], "validate-specs"
|
||||
)
|
||||
|
||||
write_message(
|
||||
proc,
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/list",
|
||||
"params": {},
|
||||
},
|
||||
)
|
||||
tools_list = read_message(proc)
|
||||
tool_names = {tool["name"] for tool in tools_list["result"]["tools"]}
|
||||
self.assertIn("validate_specs", tool_names)
|
||||
self.assertIn("scan_rfc2119", tool_names)
|
||||
finally:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=5)
|
||||
if proc.stdin:
|
||||
proc.stdin.close()
|
||||
if proc.stdout:
|
||||
proc.stdout.close()
|
||||
if proc.stderr:
|
||||
proc.stderr.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate repo specs as a CLI and as a minimal MCP stdio server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RFC2119_TERMS = ("MUST", "SHOULD", "MAY")
|
||||
RFC2119_FORBIDDEN = ("must", "should", "may", "Must", "Should", "May")
|
||||
|
||||
|
||||
def run_repo_command(args: list[str]) -> Dict[str, Any]:
|
||||
proc = subprocess.run(
|
||||
args,
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return {
|
||||
"ok": proc.returncode == 0,
|
||||
"returncode": proc.returncode,
|
||||
"stdout": proc.stdout.strip(),
|
||||
"stderr": proc.stderr.strip(),
|
||||
}
|
||||
|
||||
|
||||
def scan_rfc2119() -> Dict[str, Any]:
|
||||
files = sorted((ROOT / "specs").rglob("*.md"))
|
||||
counts = {term: 0 for term in RFC2119_TERMS}
|
||||
forbidden_matches: list[str] = []
|
||||
file_summary: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
for file_path in files:
|
||||
text = file_path.read_text(encoding="utf-8")
|
||||
relative = file_path.relative_to(ROOT).as_posix()
|
||||
file_summary[relative] = {
|
||||
term: len(re.findall(rf"\b{term}\b", text)) for term in RFC2119_TERMS
|
||||
}
|
||||
file_summary[relative]["forbidden"] = []
|
||||
|
||||
for term in RFC2119_TERMS:
|
||||
counts[term] += file_summary[relative][term]
|
||||
|
||||
for forbidden in RFC2119_FORBIDDEN:
|
||||
matches = re.findall(rf"\b{forbidden}\b", text)
|
||||
if matches:
|
||||
forbidden_matches.extend(matches)
|
||||
file_summary[relative]["forbidden"].extend(matches)
|
||||
|
||||
return {
|
||||
"spec_file_count": len(files),
|
||||
"counts": counts,
|
||||
"forbidden": forbidden_matches,
|
||||
"files": file_summary,
|
||||
"ok": len(files) > 0 and not forbidden_matches,
|
||||
}
|
||||
|
||||
|
||||
def validate_specs(strict: bool = False) -> Dict[str, Any]:
|
||||
required_files = [
|
||||
"specs/03_plan.md",
|
||||
"specs/04_tasks.md",
|
||||
"compliance/plan.yaml",
|
||||
]
|
||||
missing_files = [path for path in required_files if not (ROOT / path).exists()]
|
||||
|
||||
compliance_cmd = [sys.executable, str(ROOT / "tools/compliance/validate.py")]
|
||||
if strict:
|
||||
compliance_cmd.append("--strict")
|
||||
compliance = run_repo_command(compliance_cmd)
|
||||
|
||||
rfc2119 = scan_rfc2119()
|
||||
|
||||
ok = not missing_files and compliance["ok"] and rfc2119["ok"]
|
||||
return {
|
||||
"ok": ok,
|
||||
"missing_files": missing_files,
|
||||
"strict": strict,
|
||||
"compliance": compliance,
|
||||
"rfc2119": rfc2119,
|
||||
}
|
||||
|
||||
|
||||
def format_cli_summary(result: Dict[str, Any]) -> str:
|
||||
status = "OK" if result["ok"] else "FAIL"
|
||||
compliance = result["compliance"]
|
||||
rfc2119 = result["rfc2119"]
|
||||
lines = [
|
||||
f"{status}: spec validation",
|
||||
f"- missing files: {len(result['missing_files'])}",
|
||||
f"- compliance ok: {compliance['ok']}",
|
||||
(
|
||||
"- RFC2119 counts: "
|
||||
f"MUST={rfc2119['counts']['MUST']} "
|
||||
f"SHOULD={rfc2119['counts']['SHOULD']} "
|
||||
f"MAY={rfc2119['counts']['MAY']}"
|
||||
),
|
||||
f"- RFC2119 forbidden terms: {len(rfc2119['forbidden'])}",
|
||||
]
|
||||
|
||||
if result["missing_files"]:
|
||||
lines.append("- missing: " + ", ".join(result["missing_files"]))
|
||||
if compliance["stdout"]:
|
||||
lines.append("- compliance stdout: " + compliance["stdout"])
|
||||
if compliance["stderr"]:
|
||||
lines.append("- compliance stderr: " + compliance["stderr"])
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def tool_validate_specs(arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
strict = bool(arguments.get("strict", False))
|
||||
result = validate_specs(strict=strict)
|
||||
return {
|
||||
"content": [{"type": "text", "text": format_cli_summary(result)}],
|
||||
"structuredContent": result,
|
||||
"isError": not result["ok"],
|
||||
}
|
||||
|
||||
|
||||
def tool_scan_rfc2119(_: Dict[str, Any]) -> Dict[str, Any]:
|
||||
result = scan_rfc2119()
|
||||
summary = (
|
||||
"RFC2119 summary: "
|
||||
f"MUST={result['counts']['MUST']} "
|
||||
f"SHOULD={result['counts']['SHOULD']} "
|
||||
f"MAY={result['counts']['MAY']} "
|
||||
f"forbidden={len(result['forbidden'])}"
|
||||
)
|
||||
return {
|
||||
"content": [{"type": "text", "text": summary}],
|
||||
"structuredContent": result,
|
||||
"isError": not result["ok"],
|
||||
}
|
||||
|
||||
|
||||
TOOLS = [
|
||||
{
|
||||
"name": "validate_specs",
|
||||
"description": "Validate Kill_LIFE spec structure, compliance profile and RFC2119 usage.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"strict": {
|
||||
"type": "boolean",
|
||||
"description": "Enable strict evidence checks from tools/compliance/validate.py.",
|
||||
"default": False,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "scan_rfc2119",
|
||||
"description": "Summarize RFC2119 keywords across specs/*.md without writing files.",
|
||||
"inputSchema": {"type": "object", "properties": {}},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def make_response(request_id: Any, result: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
||||
|
||||
|
||||
def make_error(request_id: Any, code: int, message: str) -> Dict[str, Any]:
|
||||
return {"jsonrpc": "2.0", "id": request_id, "error": {"code": code, "message": message}}
|
||||
|
||||
|
||||
def read_message() -> Dict[str, Any] | None:
|
||||
headers: Dict[str, str] = {}
|
||||
while True:
|
||||
line = sys.stdin.buffer.readline()
|
||||
if not line:
|
||||
return None
|
||||
if line in (b"\r\n", b"\n"):
|
||||
break
|
||||
key, _, value = line.decode("utf-8").partition(":")
|
||||
headers[key.strip().lower()] = value.strip()
|
||||
|
||||
content_length = int(headers.get("content-length", "0"))
|
||||
if content_length <= 0:
|
||||
return None
|
||||
|
||||
body = sys.stdin.buffer.read(content_length)
|
||||
if not body:
|
||||
return None
|
||||
|
||||
return json.loads(body.decode("utf-8"))
|
||||
|
||||
|
||||
def write_message(message: Dict[str, Any]) -> None:
|
||||
payload = json.dumps(message).encode("utf-8")
|
||||
sys.stdout.buffer.write(f"Content-Length: {len(payload)}\r\n\r\n".encode("utf-8"))
|
||||
sys.stdout.buffer.write(payload)
|
||||
sys.stdout.buffer.flush()
|
||||
|
||||
|
||||
def serve_mcp() -> int:
|
||||
while True:
|
||||
request = read_message()
|
||||
if request is None:
|
||||
return 0
|
||||
|
||||
method = request.get("method")
|
||||
request_id = request.get("id")
|
||||
params = request.get("params") or {}
|
||||
|
||||
if method == "initialize":
|
||||
write_message(
|
||||
make_response(
|
||||
request_id,
|
||||
{
|
||||
"protocolVersion": "2025-06-18",
|
||||
"capabilities": {"tools": {"listChanged": False}},
|
||||
"serverInfo": {
|
||||
"name": "validate-specs",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if method == "notifications/initialized":
|
||||
continue
|
||||
|
||||
if method == "ping":
|
||||
write_message(make_response(request_id, {}))
|
||||
continue
|
||||
|
||||
if method == "tools/list":
|
||||
write_message(make_response(request_id, {"tools": TOOLS}))
|
||||
continue
|
||||
|
||||
if method == "tools/call":
|
||||
tool_name = params.get("name")
|
||||
arguments = params.get("arguments") or {}
|
||||
|
||||
if tool_name == "validate_specs":
|
||||
write_message(make_response(request_id, tool_validate_specs(arguments)))
|
||||
continue
|
||||
|
||||
if tool_name == "scan_rfc2119":
|
||||
write_message(make_response(request_id, tool_scan_rfc2119(arguments)))
|
||||
continue
|
||||
|
||||
write_message(make_error(request_id, -32602, f"Unknown tool: {tool_name}"))
|
||||
continue
|
||||
|
||||
if request_id is not None:
|
||||
write_message(make_error(request_id, -32601, f"Method not found: {method}"))
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Validate Kill_LIFE specs")
|
||||
parser.add_argument("--strict", action="store_true", help="Enable strict compliance evidence validation.")
|
||||
parser.add_argument("--json", action="store_true", help="Print JSON output in CLI mode.")
|
||||
parser.add_argument("--mcp", action="store_true", help="Run as an MCP stdio server.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if args.mcp:
|
||||
return serve_mcp()
|
||||
|
||||
result = validate_specs(strict=args.strict)
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(format_cli_summary(result))
|
||||
return 0 if result["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user