Add MCP validation tooling and scope specs

This commit is contained in:
Clément SAILLANT
2026-03-07 18:56:31 +01:00
parent 0c175510ac
commit 0ff4a747c3
14 changed files with 808 additions and 12 deletions
+1
View File
@@ -83,6 +83,7 @@ node_modules/
# Docker
*.pid
docker-compose.override.yml
.cad-home/
# Autres outils CI/CD
coverage/
+1 -1
View File
@@ -55,7 +55,7 @@ Kill_LIFE, cest 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.
+8 -9
View File
@@ -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 lautomatisation 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) citeturn0search9
- `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`) citeturn0search9
## 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`
+1
View File
@@ -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.
+2 -2
View File
@@ -44,7 +44,7 @@ Checklist :
Exemple :
```bash
python tools/validate_specs.py || true
python3 tools/validate_specs.py || true
```
## Gates
@@ -57,4 +57,4 @@ python tools/validate_specs.py || true
## Références
- `docs/AI_WORKFLOWS.md`
- `docs/templates/ValidationPlan.md`
- `docs/templates/ValidationPlan.md`
@@ -0,0 +1,42 @@
# 15) Plan dalignement 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 nannonce plus `kicad-sch-mcp` comme chemin principal
+33
View File
@@ -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 nest 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 quil 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.
+21
View File
@@ -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 dexé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`.
+1
View File
@@ -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.
+136
View File
@@ -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.
+40
View File
@@ -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 nest 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é nexpose pas `pcbnew`, donc le runtime KiCad ne peut pas démarrer.
+102
View File
@@ -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()
+284
View File
@@ -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())