Deux mesures nouvelles du 25/08/2026 aggravent le dossier : une requête Reading ordinaire query_wms_entities(Products, limit 200) pèse 957 234 caractères et search_wms_data(PAL) 847 543 — le dépassement du seuil de rejet client (~70 000, D24) ne se limite donc pas au Writing ni aux workflows. Les points ouverts correspondants deviennent un lot 5 planifié dans la ROADMAP (L5.1 fenêtre verbatim sur le blob data, L5.2 garde de taille commune aux trois outils de requête avec le cas limite d'une ligne seule au-dessus du plafond, L5.3 balayage du champ tool). Pas de nouveau numéro de décision : le lot étend D24. D27 réservable si besoin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 KiB
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 et ../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 : 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), host10.255.255.2, tenantLIMAGRAI2512. - Le profil
ADest cassé et c'est diagnostiqué — ne le réinvestigue pas (tenant introuvable côté STS, point ouvert de la ROADMAP). 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);}});"
Mesurer la taille d'une réponse d'outil (la mesure qui fait foi est la
longueur de content[0].text via le protocole) :
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
console.error()uniquement (D6).- Contrat d'erreur :
{ success: false, error, tool },isError: true— c'est précisément l'objet de L5.3. - Messages et hints actionnables.
- Aucun accès base de données (D1).
- 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. - D24 : signal commun —
truncated: trueuniquement quand la réponse est coupée,hintactionnable,returnedvs total avant coupe.omitteden plus quand des éléments entiers sont écartés. Réutilise ce vocabulaire exactement ; ne crée pas un second dialecte. - 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_detailsentrent 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_offsetou équivalent), documenté dans les descriptions. - La réponse porte toujours la taille totale du blob ; quand la fenêtre
tronque :
truncated: true+hintdonnant 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 ; seuledataest 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_detailssur 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éduirelimit, ajouter unfilter; et pourquery_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: <n>,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,omittedcohé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 detruncated).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
- Phase 0 d'abord (cinq mesures, rejouées telles quelles).
- L5.1, puis L5.2, puis L5.3 — chaque bloc vérifié en exécution via le protocole avant de passer au suivant.
- Rappel : le serveur traite les
tools/callen concurrence (point ouvert de la ROADMAP) — pour les vérifications qui comparent des réponses successives, envoie les requêtes séquentiellement. - Les schémas changent : reboucler sur les 23 noms de
tools/list(aucunUnknown tool;execute_commandvérifié statiquement, non appelé) et vérifier qu'un paramètre inconnu est toujours rejeté (D23). - 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 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 deget_workflow_detailssi elle change l'usage documenté ; ROADMAP.md — retirer le lot 5. - 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 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.mden place pour la révision.