Files
mcp-wms-api/docs/handoff-lot2.md
T
Arthur Ria 8c5792da52 Révision lot 1 : validé ; passation lot 2 (L2.1-L2.3 + L3.2), L2.3 consigné
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>
2026-08-24 17:13:19 +02:00

11 KiB

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 et ../DECISIONS.md avant de toucher au code.

Mission : livrer le lot 2 de ../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 :

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) :

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.