diff --git a/docs/handoff-lot2.md b/docs/handoff-lot2.md deleted file mode 100644 index 9d5f5e7..0000000 --- a/docs/handoff-lot2.md +++ /dev/null @@ -1,213 +0,0 @@ -# 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.