L'application venait de WMS_APPLICATION (partagée par tous les profils) : le
MCP n'interrogeait que EasyWMS, alors que CustomApp porte le spécifique
client (153 workflows CST_ sur ce tenant) et que 9 applications sont
déclarées par Application/GetAll.
Paramètre application (défaut : l'application du profil, comportement
inchangé sans lui) sur get_ad_elements, search_ad_elements,
get_ad_element_details, search_workflows, get_workflow_details,
list_workflow_categories. Clés de cache : ad-service passe par
(application, type), workflow-service par application — sans quoi un appel
CustomApp polluerait le cache EasyWMS. L'invalidation reste l'abonnement
onSwitch (D8), le chargement reste paresseux (aucun préchargement des 9
applications, D10). list_workflow_categories s'adosse à Application/GetAll
(liste allégée en cache : le blob data de chaque application pèse ~100 Ko) ;
get_application_summary regroupe par application et ne détaille que les
entrées en cache (D24), workflows compris. Acté en D26 ; CLAUDE.md mis à
jour (Caches, AD), L4.2 retiré de la ROADMAP.
Vérifications rejouées via le protocole (LIMAGRAIN / LIMAGRAI2512),
requêtes séquentielles :
- search_workflows(CST_, application:CustomApp) -> 5 objets peuplés dont
CST_SendRejectContainersToPK ([Workflow] Successfully cached 153 workflows
for "CustomApp").
- get_ad_elements(Workflow, application:CustomApp) -> count 153, éléments
CST_* ([AD] Successfully cached 153 CustomApp::Workflow).
- Séquence EasyWMS -> CustomApp -> EasyWMS sur search_workflows : 4012 vs
153, retour en cache hit ([Workflow] Using cached data for "EasyWMS"),
aucune pollution ; get_application_summary montre les deux caches
(workflowCachesByApplication EasyWMS 4012 / CustomApp 153).
- Sans paramètre application -> comportement inchangé (W1 = W3).
- switch_wms_profile EUROTRAFIC puis retour -> [Workflow] Cache cleared,
[AD] All caches invalidated, [EntityResolver] Cache cleared ; summary vide.
- list_workflow_categories -> 9 applications, comptes réels des applications
chargées.
Baseline : tools/list 23 (les 23 noms répondent), resources 6, rejet D23
d'un paramètre inconnu OK (search_workflows/applikation), npm test 4/4
exit 0.
Anomalie hors périmètre consignée dans ROADMAP.md : get_workflow_details
peut dépasser le seuil de rejet client (~101 800 caractères mesurés sur
CST_SendRejectContainersToPK), comportement antérieur au lot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QueryType était figé à 0 en dur dans executeQuery/executeScalarQuery : le
modèle Writing, opérationnel et mesuré, était inatteignable. Paramètre
query_type (défaut 0) sur call_query_api, query_wms_entities,
count_wms_entities — get_entity_schema et search_wms_data restent des
raccourcis Reading.
D3 reste la règle par défaut : la bascule est un opt-in, avertie dans les
descriptions d'outils (statuts en énumérations en Writing). Garde de valeur
assertValidQueryType dans le code (le wrapper D23 ne valide pas les valeurs),
avant tout réseau. En query_type != 0, un nom inconnu du Metadata Reading
passe tel quel avec warning (allowUnknown du resolver) — conservé aussi dans
la réponse d'erreur si le WMS échoue ensuite. Acté en D25 ; CLAUDE.md nuancé,
L4.1 retiré de la ROADMAP (reliquat : exploration Metrics, rapport à part).
Vérifications rejouées via le protocole (LIMAGRAIN / LIMAGRAI2512) :
- call_query_api(Products, query_type:1, limit:1) -> success, 1 ligne,
queryType:1 dans la réponse.
- call_query_api(Products, query_type:3) -> erreur structurée contenant
'ApplicationMetricDataContext' ne contient pas de définition pour 'Products'.
- call_query_api(Products, query_type:7) -> erreur locale nommant les 4
contextes, aucun [API] POST/GET dans stderr.
- query_wms_entities(Container, limit:1) sans query_type -> comportement
inchangé (resolvedTableName Containers, Reading, pas de champ queryType).
- call_query_api(FooBar123, query_type:1) -> transmis tel quel (payload
QueryType:1 Expression Context.FooBar123...), erreur WMS
ApplicationWritingRepository + warning de résolution dans la réponse.
- count_wms_entities(Product, query_type:1) -> count 51145 (~51160).
Baseline : tools/list 23, resources 6, rejet D23 d'un paramètre inconnu OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
La resource api://catalog, lue par les sessions Claude, contredisait les
décisions actées :
- exemple QueryExecute en QueryType 1 (D3 interdit de le recopier) ;
- Select/Take dans l'expression, contraire à la répartition
expression/options et à D13 (projections instables) ;
- entité fantôme Aliases (corrigée partout ailleurs au lot 2) ;
- exemple Command avec suffixe d'assembly, cause de FileLoadException (D14) ;
- réponse Workflow décrite avec des champs inexistants (Category, Status,
Definition) au lieu des clés réelles minuscules id/name/version/
applicationName/commonInfo (D4, D5) ;
- section Configuration décrivant l'ancien .env mono-profil (D8).
L'exemple passe en QueryType 0 avec les règles de répartition, la liste
d'entités renvoie vers get_entity_metadata comme source de vérité, la
réponse AD documente l'enveloppe { entities: [...] }.
Vérifié via le protocole (resources/read api://catalog) :
contient "QueryType": 1 : false
contient Aliases : false
renvoie vers get_entity_metadata : true
tools/list : 23 outils, resources : inchangées.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
max_results (50) borne le nombre de résultats mais pas le volume : les
context_lines multiplient la taille, et la réponse aux seuls défauts
atteignait 52-56 000 caractères — rejetée par le client MCP. Le tool
écarte désormais des résultats ENTIERS (jamais coupés au milieu de leur
contexte) jusqu'à passer sous le plafond, et le signale : truncated,
returned/omitted, hint actionnable (affiner keyword, réduire
context_lines, baisser max_results).
Plafond : MAX_LOG_SEARCH_CHARS, variable d'environnement avec défaut
25 000 — même style de lecture que MAX_QUERY_ROWS/QUERY_TIMEOUT
(parseInt(process.env.X) || défaut), documentée dans CLAUDE.md
(réglages partagés). 25 000 correspond à l'ordre de grandeur cible du
lot (~20-25 000 chars) et se mesure sur content[0].text, la taille qui
fait foi côté protocole. Les défauts max_results=50 et context_lines=2
sont inchangés : le correctif est le bornage signalé, pas un changement
de comportement par défaut.
Mesures via le protocole (LIMAGRAIN, content[0].text) :
- {"keyword":"Error"} : avant 52 162 chars / 50 résultats ; après
24 164 chars, returned 16, omitted 34, truncated true, hint présent.
- {"keyword":"Error","max_results":3} : 4 855 chars, 3/3, pas de
troncature signalée.
- mot-clé sans occurrence : 112 chars, 0 résultat, pas de truncated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
La sortie sans filtre (168 paramètres fusionnés) atteignait 70 141
caractères et se faisait rejeter par le client MCP. Ajout de limit
(défaut 50) et offset (défaut 0), déclarés au schéma (D23), appliqués
après filtres et tri. Quand la pagination coupe, la réponse porte
truncated: true et un hint donnant l'offset suivant et les filtres
pour réduire.
totalParameters change de sens : c'était le nombre brut d'entités
Parameter chargées (toujours 168), c'est désormais le total
correspondant aux filtres AVANT pagination — le signal truncated se
vérifie ainsi depuis la réponse (offset + returned < totalParameters).
Sans filtre les deux définitions coïncident.
Mesures via le protocole (LIMAGRAIN, content[0].text) :
- {} : avant 70 141 chars / 168 renvoyés ; après 21 404 chars,
returned 50, totalParameters 168, truncated true, hint présent.
- {"limit":200} : 70 156 chars, les 168, pas de truncated (opt-in
explicite au volume complet).
- {"offset":160} : 3 503 chars, 8 renvoyés, pas de truncated.
- {"search":"PICK"} : 10 041 chars, 23/23, pas de truncated —
comportement inchangé sur petit résultat.
- {"lines":5} : toujours rejeté par le wrapper (D23 intact).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Les vérifications attendues des quatre correctifs ont été rejouées via le
protocole et passent toutes : résolution Container/Alias/Item (échec avant
tout POST, 6 GET seulement), rejet des paramètres inconnus et requis
manquants, garde-fous get_workflow_details/search_logs, corps STS dans
les erreurs d'auth sans fuite de credential. Contrôle latéral séquentiel :
la bascule EUROTRAFIC reconstruit bien la table par tenant (280 entités,
7 applications, contre 288/5 sur LIMAGRAIN).
Trivialité corrigée : tableNames.size (undefined sur un tableau) ->
.length dans le log de chargement du resolver.
Découverte de révision consignée en point ouvert : le serveur traite les
tools/call en concurrence, une bascule de profil pendant des appels en
vol les fait partir sur le nouveau profil.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
authenticate() ré-enveloppait l'erreur axios en "Authentication failed:
Request failed with status code 400" et jetait le corps de la réponse du
STS, qui contenait le diagnostic exact ({"error":"invalid_request",
"error_description":"Tenant not found"} sur le profil AD).
Réutilise _enrichHttpError (L1.1) dans authenticate() et
refreshOAuthToken(), avec payload volontairement omis : il contient les
credentials (password grant) ou le refresh_token ; l'helper n'inclut
jamais les headers. Le chemin "token trop vieux -> password grant" de
refreshOAuthToken() sort du try : un échec d'authenticate() y était
rattrapé pour... rappeler authenticate() à l'identique ; il propage
désormais son erreur enrichie directement.
Mesure (via le protocole) : switch_wms_profile("AD") puis
count_wms_entities("Products") ->
Count failed for Products: Authentication failed: POST
https://10.255.255.2/EasySTS/OAuth/Token failed (HTTP 400): Request
failed with status code 400
Response body: {"error":"invalid_request","error_description":"Tenant
not found"}
Aucun credential ni header dans la réponse (vérifié : pas de
password/Basic/Bearer/payload).
ROADMAP : L3.2 retirée, point ouvert "profil AD" mis à jour.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deux anomalies préexistantes au lot 1, mesurées le 24/08/2026 :
- get_workflow_details({}) renvoyait success: true avec le premier
workflow du cache : getWorkflowDetails() comparait w.Id/w.Code/w.Name,
clés qui n'existent pas sur les objets AD réels (minuscules, D5) —
undefined === undefined matchait. Clés mortes supprimées (id et name
seuls existent, les parseInt sur des GUID étaient morts aussi), garde
d'entrée rejetant workflow_id absent avec renvoi vers search_workflows.
- search_logs({}) plantait en "Cannot read properties of undefined
(reading 'toLowerCase')" : garde d'entrée nommant "keyword" avec un
exemple d'appel.
Le wrapper D23 rejette déjà ces appels via required — les gardes côté
code restent, la validation SDK n'étant pas garantie pour les appelants
directs des services.
Mesures :
- via le protocole, les deux appels {} -> "Paramètre(s) requis
manquant(s) pour ... " (wrapper D23)
- gardes appelées en direct (sans wrapper) :
getWorkflowDetails(undefined) jette "workflow_id est requis (id ou nom
exact du workflow)..." ; search_logs({}) répond success: false avec le
message nommant keyword
- get_workflow_details avec un id réel (2e workflow du cache, pas le
premier) -> objet brut complet ($id, validFrom, ..., data), id/name
conformes à la recherche
ROADMAP : L2.3 retirée, lot 2 soldé.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mesure V1 (24/08/2026) : le SDK MCP ignore additionalProperties: false —
read_recent_logs({lines: 5}) avec la clause sur le schéma répondait
success: true, returnedLines: 100 (retombée silencieuse sur le défaut).
La validation vit donc dans le wrapper tools/call de src/index.js,
pilotée par les schémas de la table de routage (D22) : paramètre inconnu
ou requis manquant -> erreur structurée nommant le fautif et les
paramètres valides, avant tout dispatch.
Les 23 schémas portent additionalProperties: false — inerte côté SDK,
mais c'est le contrat que lisent les clients. Pas de renommage de
paramètres (écarté, cf. ROADMAP).
Mesures (via le protocole) :
- read_recent_logs({"lines": 5}) -> "Paramètre(s) inconnu(s) pour
read_recent_logs : lines. Paramètres valides : count, log_file."
- read_recent_logs({"count": 5}) -> succès, returnedLines: 5
- boucle sur 22 outils avec {} (execute_command vérifié statiquement) :
22/22 répondent, aucun Unknown tool, les 11 outils à paramètres requis
échouent avec le message actionnable
- handshake : 23 outils, 6 resources
Docs : D23 dans DECISIONS.md, L2.2 retirée de la ROADMAP.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Context.{entity_type} attend le TableName du Metadata, pas le nom
d'entité de l'AD (Container -> Containers, mais Alias -> Alias) : un nom
faux partait en HTTP 500 de compilation LINQ. Nouveau service
entity-resolver.js : table Name|TableName (insensible à la casse) ->
TableName, agrégée sur les applications déployées (via
GET /configuration/applications — les applications sans contexte
requêtable n'y figurent pas et n'apportent 0 entité Metadata), cache TTL
partagé, invalidation par onSwitch (D8). Branché dans wms-query-service
(query/count/schema/search) et call_query_api.
Nom inconnu -> échec avant tout appel réseau de requête, suggestions
proches + renvoi vers get_entity_metadata. Metadata injoignable -> le
nom passe tel quel avec un warning dans la réponse.
Mesures (LIMAGRAIN, via le protocole) :
- query_wms_entities("Container", limit 1) -> succès, 1 ligne, résolu
Containers
- query_wms_entities("Alias") -> succès, invariant (pas de pluriel)
- query_wms_entities("Item") -> "Item" n'existe pas dans le modèle
Reading. Proches : RFMenuItems, Sites. 288 entités disponibles —
aucune ligne [API] POST dans stderr
- count_wms_entities("Product") -> 51160
- get_entity_schema("Container") et call_query_api("Container") : mêmes
résolutions
- 288 TableName distincts sur 5 applications, aucun conflit
Name -> TableName (mesuré le 24/08/2026)
Docs : D21 dans DECISIONS.md ; CLAUDE.md (piège retiré des points
ouverts, liste d'entités corrigée Aliases -> Alias, entity-resolver dans
la structure) ; exemple singulier/pluriel dans wms://query-examples ;
ROADMAP allégée (cause racine + L2.1 + L3.3 livrés).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Les clés réelles d'un workflow AD (relevées en direct, minuscules,
cf. D5) sont : id, name, version, applicationName, commonInfo, data…
search_workflows projetait w.Id, w.Code, w.Name, w.Category,
w.Description, w.Created, w.Modified — toutes undefined, supprimées par
JSON.stringify : 50 objets vides pour un count pourtant correct.
La projection porte désormais id, name, applicationName, version, et
les équivalents réels de created/modified trouvés dans commonInfo
(createdBy, createDate, updateDate). Code et Description n'existent
dans aucune casse : non projetés.
La notion de catégorie n'a aucun support dans les données : elle est
mappée explicitement sur applicationName, seul regroupement fourni par
l'API AD — assumé dans les descriptions d'outils et par une note dans
la réponse de list_workflow_categories, qui renvoyait 0 catégorie et
classait les 4012 workflows en « Uncategorized ». Le paramètre category
de search_workflows filtre sur applicationName. Le filtre de recherche
ne teste plus description/code, clés inexistantes.
Vérifié contre le WMS réel : search_workflows("stacker") renvoie des
objets peuplés (StackerCrane_…), list_workflow_categories renvoie
EasyWMS avec 4012 workflows, get_workflow_details renvoie toujours
l'objet brut complet (data 71 Ko).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le routage par préfixe de nom laissait deux outils listés dans
tools/list mais injoignables : get_entity_metadata (capté par
startsWith('get_entity_') avant sa propre branche) et list_log_files
(aucune branche : le nom contient _log_files, pas _logs).
Une table nom d'outil -> module est construite au démarrage depuis les
listTools() des 8 modules de src/tools/. tools/list est servi depuis
cette même table et le dispatch devient un lookup : un outil listé est
un outil routé, par construction. Deux modules déclarant le même nom
font échouer le serveur au démarrage avec un message nommant les deux
modules. Le wrapper d'erreur du handler tools/call est inchangé, les
23 outils gardent leurs noms.
Vérifié contre le WMS réel : get_entity_metadata renvoie 232 entités,
list_log_files renvoie 19 fichiers, tools/list expose toujours 23
outils et chaque nom listé est traité par le executeTool() de son
module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Toute erreur d'API se résumait à « Request failed with status code 500 »
alors que le WMS renvoie le diagnostic complet (erreurs de compilation
LINQ, entité inconnue…) dans le corps de la réponse, jusqu'ici jeté par
les catch de post() et get().
L'erreur propagée porte désormais : verbe, URL complète, statut HTTP,
payload envoyé (dont Application et QueryType), et corps de réponse
tronqué à 2000 caractères. Pour les corps structurés, Message et
InnerException.Message sont extraits plutôt qu'un JSON.stringify
intégral qui noierait le diagnostic dans le bruit WatsonBuckets.
Le rejeu après refresh de token 401 est conservé, et une erreur pendant
le rejeu est enrichie de la même façon. Aucun credential ni token dans
le message (les headers ne sont jamais inclus).
Vérifié contre le WMS réel : query_wms_entities("Container") fait
apparaître « 'ApplicationReadingContext' ne contient pas de définition
pour 'Container' » dans la réponse de l'outil. npm test : 4/4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fichiers hors périmètre ou dupliqués :
- suppression des 5 .md dupliqués à la racine (copies md5-identiques de
docs/api/ et docs/entities/)
- suppression de JANITOR_main.js / JANITOR_entities.json (application
Electron sans lien avec le serveur MCP)
- suppression de temp/*.json (dumps de workflows versionnés par accident)
et ajout de temp/ au .gitignore
- suppression de claude_desktop_config_ssh.json : mots de passe en clair et
variables ORACLE_* d'une architecture abandonnée
- AD_API_TEST_RESULTS.md -> docs/ad-api-validation.md (credentials du
snippet remplacés par des variables d'environnement)
- suppression d'IMPLEMENTATION_SUMMARY.md, doublon du précédent
- queries api.php -> docs/reference-queries-api.php (renommage seul)
Code mort :
- suppression de src/resources/documentation.js : la resource docs:// n'a
jamais été branchée dans src/index.js
- suppression de src/config/constants.js : module entièrement inutilisé,
requis par wms-query-service.js mais dont aucune constante n'était lue.
Emporte RESOURCE_URIS.WORKFLOWS_CATEGORIES, URI déclarée jamais servie.
- log-service.js : suppression de findRecentErrors, readFullLog et
getLogStats, exportées mais exposées par aucun outil MCP
- suppression de LOG_FILE_PATTERN (lue depuis .env, jamais appliquée : le
scan filtre sur .log en dur), y compris dans .env.example
- log-service.js : préfixe [Logs] sur les messages, comme les autres modules
Secrets :
- test-ad-api.ps1 -> scripts/test-ad-api.ps1, credentials passés en
paramètres ou par WMS_USERNAME / WMS_PASSWORD au lieu d'être en dur
Build et test :
- @yao-pkg/pkg en devDependency, cible node22-win-x64 : npm run build
échouait faute de pkg, et node20 n'a pas de binaire prébuilt (bascule sur
une compilation de Node qui échoue sans toolchain MSVC)
- index.js : le .env est lu à côté de l'exécutable quand le serveur est
packagé. Avec un chemin statique, pkg embarquait le .env dans le snapshot,
figeant les credentials dans le binaire.
- scripts/test-connection.js : npm test pointait sur un fichier absent.
Smoke test en lecture seule (OAuth, QueryExecute, QueryScalarExecute,
API AD), par profil ou sur tous.
Vérifié après nettoyage : 23 outils et 6 resources répondent au handshake
MCP, npm test passe 4/4 contre le WMS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>