From 8c5792da52a5997b82b6391ea92b5ec61ec87087 Mon Sep 17 00:00:00 2001 From: Arthur Ria Date: Mon, 24 Aug 2026 17:13:19 +0200 Subject: [PATCH] =?UTF-8?q?R=C3=A9vision=20lot=201=20:=20valid=C3=A9=20;?= =?UTF-8?q?=20passation=20lot=202=20(L2.1-L2.3=20+=20L3.2),=20L2.3=20consi?= =?UTF-8?q?gn=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ROADMAP.md | 16 +++ docs/handoff-lot1.md | 268 ------------------------------------------- docs/handoff-lot2.md | 213 ++++++++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+), 268 deletions(-) delete mode 100644 docs/handoff-lot1.md create mode 100644 docs/handoff-lot2.md diff --git a/ROADMAP.md b/ROADMAP.md index 364a8c8..abfb7fe 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -70,6 +70,22 @@ C'est le correctif retenu **à la place** d'une uniformisation des noms de paramètres : renommer casse les usages existants pour un gain cosmétique, alors que la cause réelle est l'absence de signal. +### L2.3 — Arguments manquants : deux garde-fous + +Découverts lors de la révision du lot 1 (24/08/2026), en bouclant sur les 23 +outils avec des arguments vides. Préexistants au lot 1 (vérifié sur le diff) : + +- `get_workflow_details` sans `workflow_id` renvoie `success: true` avec **le + premier workflow du cache**. Cause : `getWorkflowDetails()` + (`workflow-service.js`) compare `w.Code === workflowId` — or `Code`, `Id`, + `Name` n'existent pas sur les objets réels (clés minuscules, D5), donc + `undefined === undefined` matche. Supprimer les clés mortes de la + comparaison et rejeter un `workflow_id` absent avec un message actionnable. +- `search_logs` sans terme de recherche renvoie + `Search failed: Cannot read properties of undefined (reading 'toLowerCase')` + — le contrat d'erreur tient, mais le message viole la convention 4 + (actionnable). Garde d'entrée avec le nom du paramètre attendu. + --- ## Lot 3 — Ergonomie et documentation diff --git a/docs/handoff-lot1.md b/docs/handoff-lot1.md deleted file mode 100644 index 74cc72e..0000000 --- a/docs/handoff-lot1.md +++ /dev/null @@ -1,268 +0,0 @@ - - -# Passation — lot 1 - -> **Comment s'en servir.** Ouvrir une nouvelle session Claude Code dans -> `D:\GIT\_PERSO\mcp-wms-api` et lui donner tout ce qui suit, ou simplement : -> « Lis `docs/handoff-lot1.md` et exécute-le. » -> -> Rédigé le 24/08/2026. Toutes les mesures citées ont été prises contre le WMS -> réel (profil `LIMAGRAIN`, tenant `LIMAGRAI2512`). - ---- - -Tu interviens sur le dépôt `D:\GIT\_PERSO\mcp-wms-api` : un serveur MCP (Node.js, CommonJS) qui donne à Claude un accès en lecture à un WMS EasyWMS de Mecalux, via ses API REST uniquement. Lis `CLAUDE.md`, `DECISIONS.md` et `ROADMAP.md` avant de coder — ils contiennent les conventions et les pièges déjà constatés en production. - -Ta mission : implémenter le **lot 1** décrit dans `ROADMAP.md`. Trois correctifs indépendants, tous reproduits contre le WMS réel le 24/08/2026. Ne fais rien du lot 2 ni du lot 3. - -## Contexte matériel - -Le profil `LIMAGRAIN` du `.env` local est fonctionnel et pointe sur un WMS joignable. `npm test` passe 4/4 aujourd'hui — c'est ta baseline, il doit continuer à passer. - -Deux commandes de vérification que tu réutiliseras : - -```bash -npm test -``` - -```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/list"}' | node src/index.js -``` - -Le serveur parle JSON-RPC sur stdin/stdout. Pour appeler un outil, ajoute une ligne `{"jsonrpc":"2.0","id":N,"method":"tools/call","params":{"name":"...","arguments":{...}}}` après la notification `initialized`. - -## Contraintes non négociables - -1. **`console.error()` uniquement, jamais `console.log()`.** stdout est réservé au JSON du protocole MCP ; une seule écriture parasite casse la session Claude Desktop. Voir D6. -2. **Un outil ne lève jamais d'exception hors du wrapper.** Toute erreur revient en `{ success: false, error, tool }` avec `isError: true`. -3. **Les messages d'erreur sont lus par un agent, pas par un humain** : ils doivent dire quoi faire ensuite. -4. **Aucun accès base de données** (D1). Tout passe par les API REST. -5. Préfixer les logs par composant : `[Server]`, `[API]`, `[Workflow]`, `[WorkflowTools]`… -6. Ne touche pas à `.env` (ignoré par git, contient des credentials réels). - ---- - -## Phase 0 — Vérifications avant de coder - -**À faire en premier, avant toute modification de code.** Ces deux points -conditionnent des évolutions du lot 4 ; ils ne sont pas à implémenter -maintenant, mais tu dois les vérifier pour ne pas coder le lot 1 d'une manière -qui les bloquerait, et pour confirmer ou infirmer les mesures ci-dessous. - -Aucune modification de code dans cette phase. Uniquement des appels en lecture. - -### V1 — Modèle de données : Reading ou Writing ? - -`QueryType` est figé à `0` (Reading) en dur dans `src/services/api-service.js` -(`executeQuery` ~ligne 249, `executeScalarQuery` ~ligne 286). Aucun outil -n'expose de bascule. - -Or `QueryContextType` a **quatre** valeurs, pas deux. Mesures déjà faites sur -`LIMAGRAI2512` — confirme-les : - -| Valeur | Contexte | Résultat mesuré | -|---|---|---| -| `0` | Reading (`ApplicationReadingContext`) | opérationnel, seul utilisé | -| `1` | Writing (`ApplicationWritingRepository`) | **opérationnel** — `Context.Products` répond | -| `2` | DataWarehouse | non configuré : `Could not resolve serviceType 'IDataWarehouse…'` | -| `3` | Metrics (`ApplicationMetricDataContext`) | contexte présent, modèle non exploré | - -Astuce de diagnostic à retenir : le message d'erreur **nomme le contexte** -utilisé. C'est le moyen le plus rapide de savoir quel `QueryType` a réellement -servi. - -Rappel D3 : en `QueryType: 1` les champs de statut sont des énumérations, donc -`== "Release"` échoue. Le défaut devra rester `0`. - -### V2 — Applications : le MCP ne voit-il que `EasyWMS` ? - -Constat à vérifier : `Application` vient de `WMS_APPLICATION` dans `.env`, -partagé par tous les profils, sans surcharge par appel ni paramètre d'outil. - -`POST /AD/api/Application/GetAll` déclare **9 applications** sur le tenant -`LIMAGRAI2512`. Mesures à confirmer : - -| Application | Workflows | Queries | Entities | -|---|---:|---:|---:| -| EasyWMS | 4012 | 2239 | 338 | -| **CustomApp** | **153** | **54** | **11** | -| AGV | 71 | 14 | 5 | -| Notifications | 26 | 35 | 24 | -| GalileoFaults | 9 | 20 | 24 | -| Common | 1 | 7 | 25 | - -`CustomApp` porte le spécifique client (workflows et entités préfixés `CST_`) — -exactement ce qu'on cherche en debug, et invisible aujourd'hui. - -Deux sous-questions, de difficulté très différente : - -**a) API AD.** L'application est un simple champ du payload -`[application, tenant, pageSize, offset]`. Mesuré : `["CustomApp", tenant, 5, 0]` -sur `/Workflow/GetByApplication` renvoie bien des workflows `CST_`. Confirme-le, -et note que les caches de `ad-service` et `workflow-service` sont aujourd'hui -indexés **sans** l'application : les rendre multi-applications imposera une clé -de cache incluant l'application, sinon les résultats se mélangeront. - -**b) QueryExecute — déjà tranché, ne le réinvestigue pas.** Le champ -`Application` **ne partitionne pas** le contexte de lecture : `Context.AgvTasks` -(entité de l'application AGV) répond aussi bien avec `Application: "AGV"` qu'avec -`Application: "EasyWMS"`. Le contexte est commun au tenant. - -Et les entités `CustomApp` ne sont interrogeables **dans aucun contexte** : -les 11 entités `CST_` testées sous les quatre `QueryType`, au singulier et au -pluriel, échouent toutes ; `Metadata/Entities` et `Metadata/EntitiesAll` -renvoient 0 entité pour `CustomApp` ; aucune n'est marquée `isDataWarehouse`. -Ce sont des définitions EasyBuilder sans projection requêtable. - -Deux conséquences à retenir, elles portent sur le **lot 2** : - -- la table de résolution de L2.1 devra agréger le Metadata de **toutes** les - applications (`?applicationName=` par application : 232 + 20 + 6 + …), pas du - seul `EasyWMS` ; -- inutile d'ajouter un paramètre `application` à `QueryExecute`, il ne changerait - rien. Le paramètre n'a de sens que sur les outils **AD et workflow**. - -Contente-toi de confirmer ces mesures si tu veux, mais **ne relance pas -d'investigation dessus** : c'est du temps perdu. - -### V3 — Champs de `QueryExecute` que le MCP n'envoie pas - -La référence officielle de l'API (`https:///ApplicationService/Help`) -documente des champs inexploités. Un seul te concerne pour le lot 1 : - -**`ClientModule`.** Le MCP ne l'envoie pas, donc ses requêtes apparaissent dans -les logs du WMS sous `Execute error. Client: GNA` — le client OAuth partagé, -indistinguable du vrai client GNA. Le champ est accepté (vérifié). - -**Tu peux le renseigner dans le cadre de L1.1**, c'est cohérent avec l'objectif -de traçabilité et ça tient en une ligne : envoyer `ClientModule: "MCP-WMS"` dans -les corps de `QueryExecute` et `QueryScalarExecute`. Vérifie ensuite dans les -logs du WMS (`search_logs`) que les requêtes du MCP apparaissent bien sous ce -nom. Si la vérification n'est pas concluante, retire le changement et signale-le -plutôt que de le laisser non vérifié. - -Les autres champs (`Parameters`, `CommandTimeout`, `QueryId`, -`QueryExecuteStream`) sont consignés en L4.5 : **n'y touche pas**. - -### Ce que tu fais de ces vérifications - -- Rapporte les résultats dans ton compte-rendu final, en distinguant ce que tu - as mesuré de ce que tu supposes. -- Mets à jour la section **Lot 4** de `ROADMAP.md` si tes mesures diffèrent, ou - si tu as tranché la question (b). -- **N'implémente ni V1 ni V2.** C'est le lot 4. Seul `ClientModule` (V3) entre - dans ton périmètre, et seulement s'il est vérifiable. -- Deux incidences concrètes sur ta façon de coder le lot 1, et elles seules : - - **L1.1** : inclure `Application` et `QueryType` dans le payload rapporté par - l'erreur enrichie. Sans ça, un futur bug multi-applications sera aussi - opaque que les 500 d'aujourd'hui. - - **L1.2** : la table de routage ne doit rien présumer de la signature des - outils — ajouter demain un paramètre `application` ou `query_type` ne doit - pas la toucher. - ---- - -## L1.1 — Remonter le détail des erreurs HTTP - -**Problème.** Toute erreur d'API se résume aujourd'hui à `Request failed with status code 500`. Or le WMS renvoie déjà le diagnostic complet dans le corps de la réponse. Vérifié : - -``` -POST /QueryExecute Expression: "Context.Container.OrderBy(z => z.Id)" -→ 500, content-type: application/json -→ {"ClassName":"System.AggregateException","Message":"Compile Error: ... - 'ApplicationReadingContext' ne contient pas de définition pour 'Container' ..."} -``` - -Ce corps est jeté par le `catch`. Une session Cowork a passé une heure sur des 500 dont la cause était écrite noir sur blanc dans la réponse HTTP. - -**À faire.** Dans `src/services/api-service.js`, aux blocs `catch` de `post()` (ligne ~164) et `get()` (ligne ~212), enrichir l'erreur propagée avec : - -- le statut HTTP, -- l'URL complète et le verbe, -- le payload envoyé, -- le corps de la réponse, **tronqué à ~2000 caractères**, -- le message d'origine. - -Points d'attention : - -- Le corps peut être un objet ou une chaîne. S'il est structuré, `Message` (et `InnerException.Message`) portent l'essentiel — privilégie-les au `JSON.stringify` intégral, qui noie l'information dans du bruit `WatsonBuckets` / `HResult`. -- **Ne casse pas le retry 401 existant** : le rejeu après refresh de token doit rester intact, et une erreur pendant le rejeu doit elle aussi être enrichie. -- Le point de passage est unique : ne duplique pas cette logique dans les modules `tools/`. -- N'inclus jamais le token Bearer ni un mot de passe dans le message enrichi. - -**Vérification attendue.** Appeler `query_wms_entities` avec `entity_type: "Container"` (singulier — volontairement faux) doit faire apparaître `ApplicationReadingContext ne contient pas de définition pour 'Container'` dans la réponse de l'outil. - ---- - -## L1.2 — Fiabiliser le routage des outils - -**Problème.** `src/index.js` route les appels d'outils par **préfixe de nom** dans une cascade de `else if` (handler `tools/call`, ~ligne 167). Deux outils sont listés dans `tools/list` mais n'atteignent jamais leur module : - -| Outil | Cause | Erreur reproduite | -|---|---|---| -| `get_entity_metadata` | `startsWith('get_entity_')` le capte pour `wmsQueryTools`, dont la branche vient **avant** la sienne | `Unknown WMS query tool: get_entity_metadata` | -| `list_log_files` | la branche logs teste `includes('_logs')`, or le nom contient `_log_files` | `Unknown tool: list_log_files` | - -**À faire.** Remplacer la cascade par une **table explicite `nom d'outil → module`**, construite au démarrage en parcourant les `listTools()` des 8 modules de `src/tools/`. Le dispatch devient un simple lookup. - -Exigences : - -- Si deux modules déclarent le même nom d'outil, **échouer au démarrage** avec un message nommant les deux modules — c'est un bug de développement, pas un cas d'exécution. -- Un outil listé est un outil routé, par construction : c'est tout l'intérêt du changement. -- Le wrapper d'erreur existant du handler `tools/call` doit être conservé tel quel. -- Les 23 outils doivent rester exposés, aux mêmes noms. Ce n'est pas une refonte de l'API : aucun renommage. - -**Vérification attendue.** `get_entity_metadata` et `list_log_files` répondent tous les deux, et un `tools/list` renvoie toujours 23 outils. Vérifie aussi que rien n'a régressé sur les 21 autres : chaque nom listé doit résoudre vers un module. - ---- - -## L1.3 — Corriger les projections de champs des workflows - -**Problème.** L'API Application Dictionary renvoie ses champs **en minuscules**. Les clés réelles d'un objet workflow, relevées en direct : - -``` -$id, validFrom, validTo, versionId, version, lockInfo, id, name, -commonInfo, applicationName, dataType, data -``` - -Deux endroits supposent autre chose : - -1. `src/tools/workflow-tools.js`, fonction `searchWorkflows` (~ligne 110) : projette `w.Id`, `w.Code`, `w.Name`, `w.Category`, `w.Description`, `w.Version`, `w.Created`, `w.Modified`. Tous valent `undefined`, `JSON.stringify` les supprime — d'où **50 objets vides `{}`** alors que `count` est correct. -2. `src/services/workflow-service.js` (~lignes 123, 167, 187) : lit `w.category || w.Category`. Ces deux clés n'existent pas. Conséquence mesurée : `list_workflow_categories` renvoie **0 catégorie** et `getWorkflowStats()` classe les **4012** workflows en `Uncategorized`. - -C'est une application incomplète du piège D5 : le service gère bien la double casse pour `name`/`id` (d'où un filtrage correct), mais la projection du tool et la notion de catégorie ont été écrites sur des champs imaginaires. - -**À faire.** - -- Aligner la projection de `searchWorkflows` sur les clés réelles, en conservant le repli `w.id || w.Id` de D5. -- `Code`, `Description`, `Created`, `Modified` n'existent dans **aucune** casse : ne les projette pas plutôt que d'exposer des `null` permanents. Si tu juges qu'un équivalent existe dans `commonInfo`, inspecte-le d'abord et décide sur pièces. -- Pour la catégorie : le seul champ approchant est `applicationName`, **mais il vaut `EasyWMS` pour tous les workflows**. La notion de catégorie n'a donc aucun support dans les données. Traite-la honnêtement — mappe sur `applicationName` en l'assumant, ou retire la notion — mais **n'invente pas une taxonomie** en dérivant des catégories d'un préfixe de nom. Le paramètre `category` de `search_workflows` doit rester cohérent avec ce que tu décides, et `list_workflow_categories` ne doit pas mentir sur ce qu'il renvoie. -- Vérifie au passage `get_workflow_details` : il renvoie l'objet brut, il ne devrait pas être affecté — confirme-le plutôt que de le supposer. - -**Vérification attendue.** `search_workflows({query: "stacker"})` renvoie des objets peuplés avec au minimum `id` et `name` (attendu : des noms comme `StackerCrane_SortPickingTasksBalancingOutputs_PR`). `list_workflow_categories` renvoie un résultat cohérent avec la réalité des données, et non `0`. - ---- - -## Méthode - -Fais la **phase 0** d'abord, sans toucher au code. Puis les trois correctifs dans l'ordre : L1.1 en premier, parce qu'il rend les deux autres plus faciles à diagnostiquer. - -**Vérifie contre le WMS réel, pas seulement par lecture de code.** Chaque correctif a une vérification attendue décrite ci-dessus ; exécute-la. Les appels concernés sont en lecture seule, aucun risque d'écriture WMS. Si un point ne peut pas être vérifié, dis-le explicitement plutôt que de le déclarer fait. - -Avant de conclure, relance la baseline complète : - -- `npm test` → 4/4, -- handshake MCP → 23 outils, 6 resources, -- les trois vérifications attendues ci-dessus. - -## Livraison - -- Un commit par correctif (L1.1, L1.2, L1.3), messages en français, pour que chacun soit revertable seul. Stage chemin par chemin, **pas de `git add -A`**. -- Termine les messages de commit par `Co-Authored-By: Claude Opus 5 `. -- **Ne pousse pas** — la branche `main` a un remote, le push est une décision du propriétaire du dépôt. -- Mets à jour `ROADMAP.md` : retire le lot 1 une fois livré. -- Ajoute à `DECISIONS.md` l'entrée **D22** (routage par table explicite : pourquoi le routage par préfixe a été abandonné, avec les deux collisions comme preuve). Suis le format des entrées existantes. **D21 est réservée au lot 2, ne l'utilise pas.** -- Si tu découvres une anomalie hors périmètre, **ne la corrige pas** : ajoute-la à `ROADMAP.md` et signale-la dans ton compte-rendu. - -Dans ton compte-rendu final, indique ce que tu as vérifié en exécution et ce que tu n'as pas pu vérifier. diff --git a/docs/handoff-lot2.md b/docs/handoff-lot2.md new file mode 100644 index 0000000..9d5f5e7 --- /dev/null +++ b/docs/handoff-lot2.md @@ -0,0 +1,213 @@ +# 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.