Files
mcp-wms-api/docs/handoff-lot5.md
T
Arthur Ria 5ec2990347 L5.4 : découvrabilité des applications dans les réponses de recherche
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>
2026-08-25 12:50:17 +02:00

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), 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 :

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

  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: <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, 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).

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/GetAll dé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_categories pour les voir ») suffit.

Vérification attendue (protocole, LIMAGRAIN) :

  • search_workflows {"query":"CST_"} (sans application) → 0 résultat avec un hint nommant CustomApp (ou renvoyant vers list_workflow_categories).
  • search_workflows {"query":"CST_","application":"CustomApp"} → résultats peuplés, champ application: "CustomApp" dans la réponse.
  • search_workflows {"query":"stacker"} → résultats inchangés par ailleurs, champ application: "EasyWMS" présent, pas de hint parasite.
  • stderr : aucun fetch d'une application non demandée.

Méthode

  1. Phase 0 d'abord (cinq mesures, rejouées telles quelles).
  2. 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.
  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, 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 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.