8c5792da52
Lot 1 révisé selon la grille : vérifications L1.1/L1.2/L1.3 rejouées en
protocole (22 outils routés + execute_command en statique), diffs lus,
baseline 23/6 et npm test 4/4 confirmés. Deux anomalies préexistantes
découvertes en bouclant sur les outils avec arguments vides :
get_workflow_details({}) renvoie le premier workflow du cache (clés
mortes Id/Code/Name dans la comparaison), search_logs({}) échoue en
TypeError non actionnable — consignées en L2.3.
handoff-lot1.md supprimé (livré), handoff-lot2.md rédigé : D21 et D23
réservés, profil AD signalé cassé (ne pas réinvestiguer), vérifications
attendues par correctif.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
214 lines
11 KiB
Markdown
214 lines
11 KiB
Markdown
# Passation — lot 2
|
|
|
|
Tu travailles sur `mcp-wms-api` : un serveur MCP (Node.js, CommonJS, stdio) qui
|
|
donne à Claude un accès en lecture à un WMS EasyWMS (Mecalux) via ses API REST.
|
|
Lis [../CLAUDE.md](../CLAUDE.md) et [../DECISIONS.md](../DECISIONS.md) avant de
|
|
toucher au code.
|
|
|
|
**Mission** : livrer le lot 2 de [../ROADMAP.md](../ROADMAP.md) — résolution
|
|
des noms d'entités via l'API Metadata (L2.1), rejet des paramètres inconnus
|
|
(L2.2), deux garde-fous d'arguments manquants (L2.3) — plus un petit correctif
|
|
du lot 3 (L3.2, erreurs d'authentification muettes).
|
|
|
|
**Hors périmètre** : tout le reste de la roadmap (L3.1, lot 4), le build pkg,
|
|
le déploiement. Ne pousse rien (`git push` interdit), ne touche pas au `.env`,
|
|
n'appelle jamais `execute_command` (il écrit dans le WMS).
|
|
|
|
---
|
|
|
|
## Contexte matériel
|
|
|
|
- Profil de travail : `LIMAGRAIN` (profil par défaut), host `10.255.255.2`,
|
|
tenant `LIMAGRAI2512`. `EUROTRAFIC` fonctionne aussi (SaaS).
|
|
- **Le profil `AD` est cassé et c'est déjà diagnostiqué — ne le réinvestigue
|
|
pas.** Son tenant n'existe plus côté STS (`400 Tenant not found`). Conséquence
|
|
pratique : `npm test -- --all` échouera toujours sur AD ; la baseline se
|
|
mesure avec `npm test` (profil par défaut), attendu **4/4, code de sortie 0**.
|
|
- Baseline protocolaire à préserver : **23 outils**, **6 resources**, aucune
|
|
écriture sur stdout hors JSON-RPC.
|
|
|
|
Handshake + comptages :
|
|
|
|
```bash
|
|
printf '%s\n%s\n%s\n%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' '{"jsonrpc":"2.0","id":3,"method":"resources/list"}' | node src/index.js 2>/dev/null | node -e "let b='';process.stdin.on('data',d=>b+=d).on('end',()=>{for(const l of b.split('\n').filter(Boolean)){const m=JSON.parse(l);if(m.id===2)console.log('tools:',m.result.tools.length);if(m.id===3)console.log('resources:',m.result.resources.length);}});"
|
|
```
|
|
|
|
Appeler un outil via le protocole (la seule preuve qu'un outil marche) :
|
|
|
|
```bash
|
|
printf '%s\n%s\n%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"NOM","arguments":{}}}' | node src/index.js 2>/dev/null
|
|
```
|
|
|
|
## Contraintes non négociables
|
|
|
|
1. `console.error()` uniquement — une écriture sur stdout casse Claude Desktop
|
|
(D6).
|
|
2. Un outil ne plante jamais le serveur : erreurs en réponse structurée
|
|
`{ success: false, error, tool }`, `isError: true` (wrapper de
|
|
`src/index.js`).
|
|
3. Messages d'erreur actionnables : dire quoi faire ensuite (convention 4 de
|
|
CLAUDE.md).
|
|
4. Aucun accès base de données (D1).
|
|
5. Tout nouvel état lié au tenant s'invalide par abonnement
|
|
`profileManager.onSwitch()`, jamais à la main depuis un autre module (D8).
|
|
6. `QueryType: 0` (D3), pas de date relative (D12), 1000 lignes max.
|
|
|
|
**Numéros de décision réservés** : **D21** = règle `TableName` (L2.1),
|
|
**D23** = validation des paramètres (L2.2). N'en attribue pas d'autres sans les
|
|
réserver dans DECISIONS.md.
|
|
|
|
---
|
|
|
|
## Phase 0 — Vérifications avant de coder
|
|
|
|
Mesures déjà faites (24/08/2026, `LIMAGRAI2512`) à **confirmer**, pas à
|
|
réinvestiguer :
|
|
|
|
- `GET /Metadata/Entities` renvoie **232 entités** pour `EasyWMS`, chacune avec
|
|
`Name` et `TableName` ; `TableName` est **unique** sur les 232.
|
|
- Le mapping n'est **pas** une pluralisation : `Container` -> `Containers`,
|
|
mais `Alias` -> `Alias` (invariant), et `Item` n'existe pas.
|
|
- Le contexte de lecture est **commun au tenant** : la table de résolution doit
|
|
agréger le Metadata de **toutes** les applications
|
|
(`GET /Metadata/Entities?applicationName=…` — 9 applications, listées par
|
|
`POST /AD/api/Application/GetAll`, payload `null`). `CustomApp` renvoie
|
|
**0 entité** : c'est mesuré et normal, ne cherche pas pourquoi.
|
|
|
|
**V1 — le SDK MCP valide-t-il les schémas ?** Mesure indispensable avant L2.2 :
|
|
ajoute `additionalProperties: false` à **un** schéma, appelle l'outil via le
|
|
protocole avec un paramètre inconnu, et observe. Si le SDK rejette : L2.2 =
|
|
ajout sur les 23 schémas. Si le SDK laisse passer (probable) : la validation
|
|
doit se faire dans le wrapper `tools/call` de `src/index.js` (à la main ou via
|
|
une lib déjà présente dans l'arbre des dépendances — n'ajoute pas de dépendance
|
|
lourde sans nécessité). Time-box : 30 min ; si le comportement du SDK est
|
|
ambigu, note-le dans le commit et implémente la validation côté wrapper.
|
|
|
|
---
|
|
|
|
## L2.1 — Résolution des entités via l'API Metadata
|
|
|
|
**Problème.** `entity_type` est interpolé sans validation dans
|
|
`Context.{entity_type}` à deux endroits : `src/tools/api-tools.js:89` et
|
|
`src/services/wms-query-service.js:29` (et `:149`). Le nom attendu est le
|
|
`TableName` du Metadata, pas le nom d'entité de l'AD. Un mauvais nom part en
|
|
HTTP 500 côté WMS.
|
|
|
|
**Preuve.** `query_wms_entities("Container")` répond aujourd'hui :
|
|
|
|
```
|
|
Query failed for Container: POST …/QueryExecute failed (HTTP 500): …
|
|
Response body: … error CS1061: 'ApplicationReadingContext' ne contient pas de
|
|
définition pour 'Container' …
|
|
```
|
|
|
|
**À faire.**
|
|
- Une table de résolution `Name|TableName (insensible à la casse) -> TableName`,
|
|
construite depuis le Metadata **agrégé sur les 9 applications**, dans un
|
|
service (pas dupliquée dans les deux points d'interpolation).
|
|
- Cache : TTL partagé (`WORKFLOW_CACHE_TTL`), chargement paresseux,
|
|
**abonnement `onSwitch()`** pour l'invalidation (D8).
|
|
- Nom inconnu → échec **avant tout appel réseau**, message actionnable avec
|
|
suggestions proches et renvoi vers `get_entity_metadata`, par exemple :
|
|
« "Item" n'existe pas dans le modèle Reading. Proches : ItemGroup, StockItem.
|
|
232 entités disponibles — utilisez `get_entity_metadata` pour la liste. »
|
|
- Si le Metadata est injoignable au moment de résoudre : laisse passer le nom
|
|
tel quel (comportement actuel) plutôt que de bloquer tout — et dis-le dans la
|
|
réponse.
|
|
|
|
**Pente naturelle interdite** : ne réinvente pas une règle de pluralisation
|
|
(« ajouter un s sauf si… »). Seul `TableName` fait foi ; c'est un mapping mesuré,
|
|
pas une grammaire.
|
|
|
|
**Vérification attendue** (via le protocole, profil LIMAGRAIN) :
|
|
- `query_wms_entities("Container", limit 1)` → **succès**, 1 ligne (résolu en
|
|
`Containers`).
|
|
- `query_wms_entities("Alias")` → succès (invariant, pas de pluriel inventé).
|
|
- `query_wms_entities("Item")` → échec **sans appel réseau** (aucune ligne
|
|
`[API] POST` dans les logs stderr), message avec suggestions.
|
|
- `count_wms_entities("Product")` → succès, ~51 160.
|
|
- `get_entity_schema` et `call_query_api` bénéficient de la même résolution.
|
|
|
|
## L2.2 — Rejeter les paramètres inconnus
|
|
|
|
**Problème.** Le SDK ignore silencieusement les paramètres non déclarés :
|
|
`read_recent_logs(lines: 60)` retombe sur `count = 100` sans signal.
|
|
|
|
**À faire.** Selon le résultat de V1 : `additionalProperties: false` sur les
|
|
23 schémas, et/ou validation dans le wrapper `tools/call`. Le message de rejet
|
|
nomme le paramètre inconnu **et** les paramètres valides de l'outil.
|
|
|
|
**Décision déjà tranchée, ne pas rouvrir** : on ne renomme aucun paramètre
|
|
(cf. ROADMAP « Écarté »).
|
|
|
|
**Vérification attendue** : `read_recent_logs` avec `{"lines": 5}` → erreur
|
|
structurée nommant `lines` comme inconnu et `count` comme valide. Un appel
|
|
valide (`{"count": 5}`) → succès, 5 lignes.
|
|
|
|
## L2.3 — Garde-fous d'arguments manquants
|
|
|
|
**Preuves** (mesurées le 24/08/2026, préexistantes au lot 1) :
|
|
- `get_workflow_details` avec `{}` → `success: true` avec **le premier workflow
|
|
du cache**. Cause dans `workflow-service.js` `getWorkflowDetails()` : la
|
|
recherche compare `w.Id`, `w.Code`, `w.Name` — clés qui n'existent pas sur
|
|
les objets réels (minuscules, D5) — donc `undefined === undefined` matche.
|
|
- `search_logs` avec `{}` →
|
|
`Search failed: Cannot read properties of undefined (reading 'toLowerCase')`.
|
|
|
|
**À faire.** Supprimer les clés mortes (`Id`, `Code`, `Name` majuscules) de la
|
|
comparaison de `getWorkflowDetails()` ; rejeter `workflow_id` absent avec un
|
|
message actionnable. Garde d'entrée sur `search_logs` nommant le paramètre
|
|
attendu. (Si L2.2 rend certains cas impossibles via `required`, garde quand
|
|
même la garde côté code : la validation SDK n'est pas garantie.)
|
|
|
|
**Vérification attendue** : les deux appels avec `{}` renvoient une erreur
|
|
structurée actionnable ; `get_workflow_details` avec un id réel renvoie
|
|
toujours l'objet brut complet.
|
|
|
|
## L3.2 — Erreurs d'authentification muettes
|
|
|
|
**Preuve.** `authenticate()` dans `src/services/api-service.js` (le `catch`
|
|
vers la ligne 79) ré-enveloppe l'erreur axios en
|
|
`Authentication failed: Request failed with status code 400` et **jette le
|
|
corps de la réponse du STS**, qui contenait le diagnostic exact :
|
|
`{"error":"invalid_request","error_description":"Tenant not found"}` (mesuré
|
|
sur le profil `AD`).
|
|
|
|
**À faire.** Faire remonter statut + corps de réponse du STS dans le message,
|
|
sur le modèle de ce que L1.1 a fait pour `post()`/`get()` (réutilise le même
|
|
helper d'enrichissement si possible). **Jamais** les credentials ni les headers
|
|
dans le message. Même traitement pour `refreshOAuthToken()`.
|
|
|
|
**Vérification attendue** : `switch_wms_profile("AD")` puis un
|
|
`count_wms_entities("Products")` → la réponse d'outil contient
|
|
`Tenant not found` (et aucun mot de passe). Ne « répare » pas le profil AD :
|
|
son échec est précisément ce qui rend le correctif vérifiable.
|
|
|
|
---
|
|
|
|
## Méthode
|
|
|
|
1. Phase 0 (V1) d'abord — L2.2 en dépend.
|
|
2. Ordre : L2.1, puis L2.2, puis L2.3, puis L3.2. Chaque correctif vérifié **en
|
|
exécution via le protocole** avant de passer au suivant.
|
|
3. Après tout changement de `src/index.js` ou des schémas : reboucler sur les
|
|
23 noms de `tools/list` et vérifier qu'aucun ne répond `Unknown tool`
|
|
(n'appelle pas `execute_command` — vérifie sa branche statiquement).
|
|
4. Baseline avant/après : handshake (23/6) + `npm test` (4/4, exit 0).
|
|
5. « Non résolu » est une réponse acceptable pour une investigation time-boxée ;
|
|
une hypothèse présentée comme solution ne l'est pas.
|
|
|
|
## Livraison
|
|
|
|
- Un commit par correctif (L2.1, L2.2, L2.3, L3.2), messages expliquant le
|
|
pourquoi, mesures dans le corps du message.
|
|
- Documentation dans les mêmes commits : **D21** et **D23** dans DECISIONS.md ;
|
|
CLAUDE.md — retirer le piège `entity_type` des « Points ouverts » et corriger
|
|
la liste d'entités (`Aliases` -> `Alias`, renvoi vers `get_entity_metadata`) ;
|
|
ROADMAP.md — retirer L2.1/L2.2/L2.3/L3.2 livrés (L3.3 documentation est
|
|
partiellement couverte par ces mises à jour : ajuste-la, ne la supprime que si
|
|
tout est fait, l'exemple `wms://query-examples` compris).
|
|
- Ne pousse pas. `.env` intact. Toute anomalie hors périmètre découverte en
|
|
route : dans ROADMAP.md, pas dans le code.
|
|
- Compte-rendu final : pour chaque correctif, la vérification attendue rejouée
|
|
et son résultat **mesuré** (colle la sortie), plus la baseline finale.
|