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>
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), host10.255.255.2, tenantLIMAGRAI2512.EUROTRAFICfonctionne aussi (SaaS). - Le profil
ADest 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 avecnpm 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
console.error()uniquement — une écriture sur stdout casse Claude Desktop (D6).- Un outil ne plante jamais le serveur : erreurs en réponse structurée
{ success: false, error, tool },isError: true(wrapper desrc/index.js). - Messages d'erreur actionnables : dire quoi faire ensuite (convention 4 de CLAUDE.md).
- Aucun accès base de données (D1).
- Tout nouvel état lié au tenant s'invalide par abonnement
profileManager.onSwitch(), jamais à la main depuis un autre module (D8). 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/Entitiesrenvoie 232 entités pourEasyWMS, chacune avecNameetTableName;TableNameest unique sur les 232.- Le mapping n'est pas une pluralisation :
Container->Containers, maisAlias->Alias(invariant), etItemn'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 parPOST /AD/api/Application/GetAll, payloadnull).CustomApprenvoie 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, abonnementonSwitch()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 — utilisezget_entity_metadatapour 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 enContainers).query_wms_entities("Alias")→ succès (invariant, pas de pluriel inventé).query_wms_entities("Item")→ échec sans appel réseau (aucune ligne[API] POSTdans les logs stderr), message avec suggestions.count_wms_entities("Product")→ succès, ~51 160.get_entity_schemaetcall_query_apibé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_detailsavec{}→success: trueavec le premier workflow du cache. Cause dansworkflow-service.jsgetWorkflowDetails(): la recherche comparew.Id,w.Code,w.Name— clés qui n'existent pas sur les objets réels (minuscules, D5) — doncundefined === undefinedmatche.search_logsavec{}→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
- Phase 0 (V1) d'abord — L2.2 en dépend.
- 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.
- Après tout changement de
src/index.jsou des schémas : reboucler sur les 23 noms detools/listet vérifier qu'aucun ne répondUnknown tool(n'appelle pasexecute_command— vérifie sa branche statiquement). - Baseline avant/après : handshake (23/6) +
npm test(4/4, exit 0). - « 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_typedes « Points ouverts » et corriger la liste d'entités (Aliases->Alias, renvoi versget_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'exemplewms://query-examplescompris). - Ne pousse pas.
.envintact. 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.