# Passation — lot 5 (clore la famille D24) 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) — en particulier **D24**, le contrat de troncature que ce lot généralise — avant de toucher au code. **Mission** : éliminer les derniers cas connus de réponses d'outils dépassant le seuil de rejet des clients MCP (~70 000 caractères, D24) — lot 5 de [../ROADMAP.md](../ROADMAP.md) : pagination du blob `data` de `get_workflow_details` (L5.1), garde de taille sur les trois outils de requête (L5.2), champ `tool` manquant dans les enveloppes d'erreur locales (L5.3). **Hors périmètre** : tout le reste de la roadmap (L4.3, L4.4, L4.5, exploration Metrics). **`QueryExecuteStream` est explicitement interdit** — piste long terme consignée en L4.5, pas ce lot. Aucun nouvel outil : le compte reste à 23. 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` (par défaut), host `10.255.255.2`, tenant `LIMAGRAI2512`. - **Le profil `AD` est cassé et c'est diagnostiqué — ne le réinvestigue pas** (tenant introuvable côté STS, point ouvert de la ROADMAP). 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);}});" ``` Mesurer la taille d'une réponse d'outil (la mesure qui fait foi est la longueur de `content[0].text` via le protocole) : ```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 | 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('chars:',m.result.content[0].text.length);}});" ``` ## Contraintes non négociables 1. `console.error()` uniquement (D6). 2. Contrat d'erreur : `{ success: false, error, tool }`, `isError: true` — c'est précisément l'objet de L5.3. 3. Messages et hints actionnables. 4. Aucun accès base de données (D1). 5. **D23** : le wrapper valide les **noms** de paramètres et les requis contre les schémas — tout paramètre ajouté (fenêtre de L5.1…) doit être déclaré dans l'`inputSchema`. Le wrapper ne valide pas les **valeurs** : les gardes de valeur vivent dans le code de l'outil. 6. **D24** : signal commun — `truncated: true` **uniquement** quand la réponse est coupée, `hint` actionnable, `returned` vs total avant coupe. `omitted` en plus quand des éléments entiers sont écartés. Réutilise ce vocabulaire exactement ; ne crée pas un second dialecte. 7. **Pas de nouveau numéro de décision attendu** : ce lot **étend D24** (complète son texte : les outils de requête et `get_workflow_details` entrent dans son périmètre). Si une décision réellement nouvelle s'impose, vérifie le dernier numéro (D26 à ce jour) et réserve D27 dans le commit qui l'acte. --- ## Phase 0 — Confirmer les mesures Toutes rejouées le 25/08/2026 (protocole, LIMAGRAIN) avec la commande de mesure ci-dessus. À **confirmer**, pas à réinvestiguer : | Appel | Taille constatée | |---|---:| | `query_wms_entities` `{"entity_type":"Products","limit":200}` (Reading) | **957 234** | | `search_wms_data` `{"keyword":"PAL"}` | **847 543** | | `call_query_api` `{"entity_type":"Products","query_type":1,"limit":1}` — une seule ligne Writing | **95 288** | | `get_workflow_details` sur `CST_SendRejectContainersToPK` (`application: "CustomApp"`) | ~101 800 | | `get_workflow_details` `{"workflow_id":"2a320000-0642-47df-aa52-3b89c27c016f"}` (StackerCrane, EasyWMS) | 79 092 (dont blob `data` : 71 512) | Attention aux noms de paramètres (D23 les fait respecter) : `search_workflows` prend `query`, `search_wms_data` et `search_logs` prennent `keyword`. Pour trouver l'id du workflow CST : `search_workflows {"query":"CST_SendRejectContainersToPK","application":"CustomApp"}`. --- ## L5.1 — Paginer le blob `data` de `get_workflow_details` **Problème.** La réponse embarque la définition complète du workflow (blob `data`, XML/JSON EasyBuilder) : 71 512 caractères sur le StackerCrane mesuré, davantage sur les gros `CST_*` — la réponse dépasse le seuil client. **À faire.** - Deux paramètres de fenêtre sur le blob `data` (au schéma, D23) : une taille max de tranche (défaut de l'ordre de **20 000** caractères, cohérent avec D24) et un offset (défaut 0). Nommage à ta main (`max_data_chars` / `data_offset` ou équivalent), documenté dans les descriptions. - La réponse porte toujours la taille **totale** du blob ; quand la fenêtre tronque : `truncated: true` + `hint` donnant l'offset suivant. - La tranche est **verbatim** : découpe de chaîne, rien d'autre. - Les métadonnées du workflow (`id`, `name`, `commonInfo`…) restent complètes dans chaque réponse ; seule `data` est fenêtrée. **Pente naturelle interdite** : ne résume pas, ne reformule pas, ne « parse » pas le blob pour n'en renvoyer que des morceaux jugés utiles — la définition EasyBuilder doit rester reconstituable à l'octet près en concaténant les tranches. **Vérification attendue** (protocole, LIMAGRAIN) : - `get_workflow_details` sur le StackerCrane (`2a320000-0642-47df-aa52-3b89c27c016f`) sans paramètre de fenêtre → réponse < ~30 000 caractères, `truncated: true`, taille totale annoncée = 71 512, hint avec l'offset suivant. - En enchaînant les tranches (offset 0, 20 000, 40 000, 60 000) : la somme des longueurs des tranches = 71 512, et la concaténation est identique au blob d'origine (compare au moins les longueurs et les 100 premiers/derniers caractères). - Un workflow à petit blob (< défaut) → réponse strictement inchangée, pas de `truncated`. ## L5.2 — Garde de taille sur les outils de requête **Problème.** Aucune borne de volume sur `query_wms_entities`, `call_query_api` et `search_wms_data` : 957 234 caractères pour 200 lignes Reading, 847 543 pour une recherche, et **95 288 pour une seule ligne Writing** (le modèle Writing sérialise l'agrégat complet — navigations, `$id`… — là où la même ligne Reading fait ~4 500). **À faire.** - Garde de taille commune aux trois outils : plafond en variable d'environnement avec défaut (style `MAX_LOG_SEARCH_CHARS`, 25 000 — même ordre de grandeur, nom à ta main, documenté dans CLAUDE.md). - Au-delà du plafond : écarter des **lignes entières** (pour `search_wms_data` : des résultats entiers, par entité), et signaler D24 : `truncated: true`, `returned`, `omitted`, `hint` (réduire `limit`, ajouter un `filter` ; et pour `query_type != 0` : rappeler que les lignes Writing sont des agrégats complets et suggérer Reading si l'usage le permet). - **Cas limite à traiter explicitement** : une seule ligne dépasse le plafond (réel en Writing). La réponse est alors `returned: 0`, `omitted: `, `truncated: true`, avec un hint qui explique pourquoi et quoi faire — c'est moins bon qu'un résultat, mais c'est mieux qu'un rejet client opaque. - **Ne change ni `MAX_QUERY_ROWS`, ni les limites par défaut des outils, ni le comportement sous le plafond** : une requête qui tient aujourd'hui doit renvoyer exactement la même réponse. - L'avertissement de volume mérite une phrase dans la description du paramètre `query_type` (D25). **Vérification attendue** (protocole, LIMAGRAIN) : - `query_wms_entities` `{"entity_type":"Products","limit":200}` → réponse sous le plafond (+ marge d'enveloppe), `truncated: true`, `returned` < 200, `omitted` cohérent, hint présent. - `search_wms_data` `{"keyword":"PAL"}` → borné et signalé de même. - `call_query_api` `{"entity_type":"Products","query_type":1,"limit":1}` → `returned: 0`, `omitted: 1`, `truncated: true`, hint expliquant le volume Writing. - `query_wms_entities` `{"entity_type":"Container","limit":1}` → réponse **strictement identique** à aujourd'hui (~4 500 caractères, pas de `truncated`). - `count_wms_entities` → non concerné, inchangé. ## L5.3 — Champ `tool` dans les enveloppes d'erreur locales **Problème.** Les `catch` locaux d'`api-tools.js` (deux blocs, vers les lignes 145-162 et 191-208) renvoient `{ success: false, error }` **sans** le champ `tool` du contrat. Preuve : `call_query_api` `{"entity_type":"Products","query_type":7}` répond aujourd'hui `{"success": false, "error": "query_type invalide : …"}` — pas de `tool`. **À faire.** Balayer **tous** les modules de `src/tools/` à la recherche d'enveloppes `success: false` construites localement : soit y ajouter `tool`, soit laisser l'erreur remonter au wrapper de `src/index.js` (qui l'ajoute) — au choix selon le cas, mais le résultat observable est uniforme. Attention à ne pas perdre les champs additionnels utiles des enveloppes locales (le `warning` de résolution d'`api-tools`, les listes de profils de `profile-tools`…). **Vérification attendue** : `call_query_api` avec `query_type: 7` → l'enveloppe contient `"tool": "call_query_api"` ; un `grep` sur `src/tools/` ne montre plus d'enveloppe `success: false` sans `tool` (colle le résultat du grep dans le compte-rendu). --- ## Méthode 1. Phase 0 d'abord (cinq mesures, rejouées telles quelles). 2. L5.1, puis L5.2, puis L5.3 — chaque bloc vérifié **en exécution via le protocole** avant de passer au suivant. 3. Rappel : le serveur traite les `tools/call` en concurrence (point ouvert de la ROADMAP) — pour les vérifications qui comparent des réponses successives, envoie les requêtes séquentiellement. 4. Les schémas changent : reboucler sur les 23 noms de `tools/list` (aucun `Unknown tool` ; `execute_command` vérifié statiquement, non appelé) et vérifier qu'un paramètre inconnu est toujours rejeté (D23). 5. Baseline avant/après : handshake (23/6) + `npm test` (4/4, exit 0). 6. « 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 bloc (L5.1, L5.2, L5.3), messages expliquant le pourquoi, **mesures avant/après dans le corps du message** (tailles en caractères). - Documentation dans les mêmes commits : **compléter D24** (périmètre étendu aux outils de requête et à `get_workflow_details`) ; CLAUDE.md — la nouvelle variable d'environnement dans les réglages partagés, la fenêtre de `get_workflow_details` si elle change l'usage documenté ; ROADMAP.md — retirer le lot 5. - 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 bloc, la vérification attendue rejouée et son résultat **mesuré** (colle les tailles et les sorties), plus la baseline finale. Laisse `handoff-lot5.md` en place pour la révision.