Cas réel du 25/08/2026 : une session Cowork cherchant des workflows CST_ sans passer application=CustomApp a conclu à tort à leur absence de l'AD. Vérifié en révision : CST_PickingTasksSequencing_PR et CST_ChooseDestinationFromPS existent bien dans CustomApp sur LIMAGRAI2512. Le correctif (rappel de l'application interrogée + hint sur résultat maigre, depuis la liste Application/GetAll déjà en cache) rejoint le lot 5, pas encore lancé. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
14 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),
découvrabilité des applications dans les réponses de recherche (L5.4).
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).
L5.4 — Découvrabilité des applications dans les réponses de recherche
Problème (cas réel du 25/08/2026). Une session Cowork cherchant des
workflows CST_* sans passer application: "CustomApp" a conclu à tort que
l'AD n'en contenait aucun — alors que CST_PickingTasksSequencing_PR et
CST_ChooseDestinationFromPS existent (vérifié :
search_workflows {"query":"CST_PickingTasksSequencing","application":"CustomApp"}
→ 1 résultat). Le paramètre application (D26) existe, mais rien dans la
réponse ne dit qu'on n'a interrogé qu'une application sur neuf.
À faire. Dans les réponses de search_workflows et search_ad_elements :
- toujours rappeler l'application interrogée (champ
application) ; - quand la recherche renvoie peu ou pas de résultats (seuil à ta main, 0
au minimum), ajouter un hint nommant les autres applications déclarées et le
paramètre
application. La liste vient de la liste allégée d'Application/GetAlldéjà en cache (D26) — n'ajoute aucun appel réseau ni préchargement pour construire ce hint ; si la liste n'est pas encore en cache, le hint générique (« d'autres applications existent —list_workflow_categoriespour les voir ») suffit.
Vérification attendue (protocole, LIMAGRAIN) :
search_workflows {"query":"CST_"}(sansapplication) → 0 résultat avec un hint nommantCustomApp(ou renvoyant verslist_workflow_categories).search_workflows {"query":"CST_","application":"CustomApp"}→ résultats peuplés, champapplication: "CustomApp"dans la réponse.search_workflows {"query":"stacker"}→ résultats inchangés par ailleurs, champapplication: "EasyWMS"présent, pas de hint parasite.- stderr : aucun fetch d'une application non demandée.
Méthode
- Phase 0 d'abord (cinq mesures, rejouées telles quelles).
- L5.1, puis L5.2, puis L5.3, puis L5.4 — 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, L5.4), 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.