# 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.