Les services lisaient `response?.entities || []` sur les reponses de l'API AD
(enveloppe { entities: [...] }, D4). Toute reponse d'une AUTRE forme — corps
vide, objet d'erreur, champ absent — devenait donc un tableau vide,
indistinguable d'une page finale legitime, et etait mise en cache avec un
timestamp valide : un cache vide empoisonne pour tout le TTL, sans le moindre
message. C'est la cause probable du `count: 0` mesure sous rafale, et le mode
d'echec le plus couteux du lot, parce qu'il se lit comme une reponse.
Le contrat est porte par src/services/ad-envelope.js pour les trois sites
(Workflow/GetByApplication, Application/GetAll, <Type>/GetByApplication) :
`{ entities: [...] }`, `[]` reel compris, est rendu tel quel ; toute autre
forme leve. entity-resolver etait deja conforme — il leve deja si
/configuration/applications ou le Metadata ne rendent aucune entite.
Volontairement sans retry ni logique de resilience : le but est de rendre
l'anomalie visible et non persistante. La rattraper la rendrait invisible,
c'est-a-dire exactement le defaut corrige.
--- Verifications (LIMAGRAIN) ---
Vide LEGITIME — search_workflows sur SmartUI (0 workflow, D26) :
search_workflows(SmartUI) : success=true application=SmartUI count=0
isError=false
error : (aucune)
hint : Aucun workflow trouve dans l'application "SmartUI" — c'est la
SEULE interrogee, les autres ne le sont jamais implicitement.
Le specifique client (prefixe CST_) vit dans "CustomApp" : [...]
cache pose ? workflowCachesByApplication =
{"SmartUI":{"cached":true,"count":0,"timestamp":1787665868988,
"age":0,"valid":true}}
stderr : No more workflows to fetch / Successfully cached 0 workflows
Forme SANS `entities` — non declenchable a la demande contre le vrai WMS,
couverte par un test direct (apiService.post substitue, renvoie {}) :
--- workflow-service fetchAllWorkflows("EasyWMS") avec une reponse {} ---
erreur levee : Failed to fetch workflows for application "EasyWMS":
Reponse inattendue de l'API AD sur Workflow/GetByApplication
(application "EasyWMS", offset 0) : un objet vide, au lieu de
l'enveloppe attendue { entities: [...] }. Rien n'a ete mis en cache —
relancez l'appel. Si l'erreur persiste, l'API AD est en defaut [...]
cache : {} (attendu {})
--- workflow-service fetchApplications() avec une reponse {} ---
erreur levee : Reponse inattendue de l'API AD sur Application/GetAll : [...]
cache : {} (attendu {})
--- ad-service getElements("Command") avec une reponse {} ---
erreur levee : Failed to fetch Command for application "EasyWMS": [...]
cache : {} (attendu {})
--- puis une reponse normale : le refetch repart (rien de coince) ---
1 workflow(s), cache : {"EasyWMS":{"cached":true,"count":1,...}}
Cas nominaux du helper (unitaire) : { entities: [] } et { entities: [1,2] }
passent ; {}, null, undefined, [], { error }, "texte" levent tous.
--- Non-regression, rafales rejouees 3 fois ---
L6.1 rafale workflow RUN 1/2/3 : 6/6 a count=44 | fetch=1 joins=5
L6.1 rafale resolver RUN 1/2/3 : 6/6 succes | chargements=1 joins=5 GET=5
L6.1 rafale mixte RUN 1/2/3 : CustomApp@44=3 EasyWMS@50=3 |
fetch=2 joins=4
L6.2 bascule RUN 1/2/3 : caches peuples : 0 (attendu 0)
sequentiel nominal count=50 puis count=50 | fetch=1 cached=1 joins=0
Baseline finale : tools/list 23, resources/list 6 ; npm test 4/4, exit 0.
ROADMAP : lot 6 retire. Le point ouvert « bascule de profil concurrente aux
appels en vol » reste — D27 borne les chargements paresseux, pas le routage
d'une requete deja partie.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
16 KiB
CLAUDE.md
Guide pour Claude Code (claude.ai/code) sur ce dépôt.
Avant de modifier quoi que ce soit, lisez DECISIONS.md. Il
consigne les choix d'architecture et les pièges vérifiés sur un WMS réel. La
plupart des comportements qui semblent bizarres y sont expliqués et sont
volontaires ; les références D1, D2… de ce fichier y renvoient.
| Besoin | Fichier |
|---|---|
| Installer, lancer, brancher Claude Desktop | README.md |
| Pourquoi le code est ainsi, pièges terrain | DECISIONS.md |
| Le serveur ne répond pas, lire ses logs | MONITORING.md |
| Ce qui reste à faire | ROADMAP.md |
| Superviser le projet, réviser une livraison | docs/supervision.md |
| Accéder aux logs du WMS | docs/logs.md |
| Références EasyWMS (API, entités) | docs/ |
Objet
Serveur MCP donnant à Claude un accès en lecture à un WMS EasyWMS (Mecalux) pour le debug et l'analyse. Fonctionnel et en service.
Architecture : 100 % API REST, aucun accès base de données (D1).
| API | Endpoint | Usage |
|---|---|---|
| Query | POST {api}/QueryExecute |
requêtes LINQ, lignes |
| Référence complète | https://<host>/ApplicationService/Help |
page d'aide générée du service — la source de vérité sur les champs et les endpoints |
| Query scalaire | POST {api}/QueryScalarExecute |
Count(), Sum() — à préférer pour « combien » |
| Command | POST {api}/CommandExecute |
exécution de commandes WMS |
| Metadata | GET {api}/Metadata/Entities, GET {api}/Metadata/EntityProperties |
entités interrogeables et leurs champs |
| GenericSearch | GET {api}/GenericSearch/Categories, POST {api}/GenericSearch/Search |
recherche plein texte indexée |
| Application Dictionary | POST {ad}/{Type}/GetByApplication |
20 types d'éléments, dont Workflow |
où {api} = https://<host>/ApplicationService/api et {ad} =
https://<host>/AD/api, construits depuis le host du profil actif.
Authentification : OAuth 2.0 avec refresh automatique (D2, et MONITORING.md §4).
Structure
src/
├── index.js Point d'entrée MCP : handlers list/read/call, routage
├── config/
│ └── profile-manager.js Registre multi-profils + bascule runtime
├── resources/ Contexte en lecture seule (6 resources)
│ ├── wms-entities.js wms://entities
│ ├── entity-schemas.js wms://entity-schemas
│ ├── query-examples.js wms://query-examples — exemples LINQ + recettes de diagnostic
│ ├── workflows.js workflows://overview
│ ├── apis.js api://catalog
│ └── logs.js logs://guide — patterns d'erreur et scénarios de debug
├── services/ Logique métier
│ ├── api-service.js OAuth + client HTTP + helpers de requête (singleton)
│ ├── entity-resolver.js Résolution Name|TableName -> TableName (D21)
│ ├── workflow-service.js Workflows, lazy loading + cache
│ ├── ad-service.js Application Dictionary, 20 types, cache par type
│ ├── wms-query-service.js Construction d'expressions LINQ
│ ├── single-flight.js Déduplication des chargements + génération (D27)
│ ├── ad-envelope.js Enveloppe { entities } des API AD : vide anormal = erreur (D27)
│ ├── log-service.js Lecture et recherche dans les fichiers de logs
│ └── response-limit.js Plafond de taille commun aux outils de requête (D24)
└── tools/ 23 outils MCP
├── wms-query-tools.js query_wms_entities, count_wms_entities,
│ get_entity_schema, search_wms_data
├── api-tools.js call_query_api, execute_command
├── workflow-tools.js search_workflows, get_workflow_details,
│ list_workflow_categories
├── ad-tools.js get_application_summary, get_ad_elements,
│ search_ad_elements, get_ad_element_details, list_ad_types
├── metadata-tools.js get_entity_metadata, generic_search
├── config-tools.js get_system_parameters
├── profile-tools.js list_wms_profiles, get_current_wms_profile,
│ switch_wms_profile
└── log-tools.js read_recent_logs, list_log_files, search_logs
scripts/
├── test-connection.js Smoke test de connectivité (npm test)
└── test-ad-api.ps1 Validation curl des endpoints AD (credentials en paramètres)
docs/
└── reference-queries-api.php Client PHP d'origine — source des patterns d'API.
⚠️ utilise QueryType 1 : ne pas recopier (D3)
Routage. src/index.js construit au démarrage une table nom d'outil →
module depuis les listTools() des 8 modules de src/tools/ ; tools/list
et le dispatch sont servis par cette même table, donc un outil listé est routé
par construction (D22). Deux modules déclarant le même nom font échouer le
serveur au démarrage.
Conventions non négociables
console.error()uniquement. stdout est réservé au JSON MCP ; toute écriture y casse la session Claude Desktop (D6).- Préfixer les logs par composant :
[Server],[API],[Profile],[Workflow],[AD],[Logs],[<X>Tools]. Le tableau complet est dans MONITORING.md §2. - Un outil ne plante jamais le serveur. Toute erreur revient en réponse
structurée
{ success: false, error, tool }avecisError: true— le wrapper est dans le handlertools/calldesrc/index.js. Les enveloppes construites localement danssrc/tools/portent le champtoolelles aussi : le wrapper ne les voit pas, et une erreur sanstoolsort du contrat. Les champs supplémentaires utiles (warningde résolution,profiles,hint…) viennent après. - Messages d'erreur actionnables. Ils sont lus par Claude, pas par un
humain : dire quoi faire ensuite (« appelez
switch_wms_profile», « profils disponibles : … »). - Aucun accès base de données (D1).
- 1000 lignes maximum par requête, timeout 30 s (
MAX_QUERY_ROWS,QUERY_TIMEOUT). - Pas de concaténation LINQ à partir d'entrées utilisateur quand un filtre côté JS suffit (D11).
Multi-profils
Un même serveur dessert plusieurs backends WMS. Un profil = un host + des credentials + un tenant (D8).
Déclaration dans .env :
WMS_PROFILES=AD,LIMAGRAIN,EUROTRAFIC
DEFAULT_WMS_PROFILE=LIMAGRAIN
AD_HOST=10.255.255.2
AD_USERNAME=…
AD_PASSWORD=…
AD_TENANT=AD
AD_SAAS=false
Réglages partagés par tous les profils : WMS_API_AUTH,
WMS_APPLICATION, WMS_API_PATH, WMS_TOKEN_PATH, WORKFLOW_API_PATH,
LOGS_PATH, et les variables de cache / token / requêtes. Seul le host varie :
les URL sont assemblées en https://<HOST><PATH>.
<NOM>_SAAS=true → WMS cloud : les outils de log échouent avec un message
explicite (D9). Par défaut false.
LOGS_PATH accepte le placeholder {host}, substitué par le host du profil
actif à chaque appel.
MAX_LOG_SEARCH_CHARS (défaut 25 000) : plafond en caractères de la réponse
de search_logs — au-delà, des résultats entiers sont écartés et signalés
(truncated, D24).
MAX_QUERY_RESPONSE_CHARS (défaut 25 000) : même plafond pour
query_wms_entities, call_query_api et search_wms_data — au-delà, des
lignes entières sont écartées et signalées (D24). count_wms_entities n'est
pas concerné.
Au runtime. profile-manager est un singleton d'état global. Les services
s'abonnent via onSwitch() pour invalider ce qui dépend du tenant :
| Service | Réaction |
|---|---|
api-service |
resetToken() |
workflow-service |
clearCache() |
ad-service |
invalidateCache() |
N'invalidez jamais ces caches à la main depuis un autre module — l'abonnement suffit (D8). Tout nouveau service portant un état lié au tenant doit s'abonner.
Sans profil actif, getCurrent() lève une erreur qui énumère les profils
disponibles : c'est ainsi que Claude sait appeler switch_wms_profile.
Caches
TTL commun WORKFLOW_CACHE_TTL (3 600 000 ms), chargement paresseux, vidés à
chaque bascule de profil (D10). Les outils AD et workflow acceptent un
paramètre application (défaut : l'application du profil) — les clés de
cache incluent l'application pour éviter toute pollution croisée (D26).
| Cache | Granularité | Pagination |
|---|---|---|
workflow-service |
un par application (~4 000 EasyWMS, 153 CustomApp) + liste allégée d'Application/GetAll |
WORKFLOW_PAGE_SIZE, 5000 |
ad-service |
un par (application, type) (20 types) | AD_ELEMENT_TYPES : View 200, Workflow 5000, Resource 15000, autres 100000 |
Ne préchargez jamais les 9 applications : seule l'application demandée est
chargée (D26). search_workflows et search_ad_elements rappellent toujours
l'application interrogée et, sur résultat vide, ajoutent un hint nommant
les autres — construit depuis la liste d'applications déjà en cache, jamais
par un appel réseau (D26).
Les tailles de page par type viennent de l'observation des timeouts serveur — ne les augmentez pas à l'aveugle.
Les chargements sont dédupliqués par clé de cache : sous appels concurrents,
une seule chaîne de fetch part par clé et les autres appelants la rejoignent
(D27) — deux applications différentes se chargent toujours en parallèle. Un
fetch parti avant une invalidation ne repeuple plus le cache après elle : la
publication passe par un commit gardé par un compteur de génération. Ne
remettez jamais d'écriture de cache dans une fonction de chargement.
Une réponse d'API AD hors enveloppe { entities: [...] } lève au lieu de
passer pour un tableau vide : sinon un cache vide s'installe pour tout le TTL
(D27). Un entities: [] réel reste cachable — des applications sont
légitimement vides.
get_application_summary expose l'état des caches sans redémarrage.
Sorties bornées (D24)
Une réponse d'outil de plus de ~70 000 caractères est rejetée par le client
MCP. Les outils qui peuvent dépasser ce seuil bornent et signalent :
truncated: true (jamais false), hint actionnable, returned, et le total
avant la coupe. Réutilisez ce vocabulaire, n'en inventez pas un second.
get_workflow_details fenêtre le blob data (la définition EasyBuilder :
71 512 caractères sur un StackerCrane, 92 362 sur un gros CST_*) :
max_data_chars (défaut 20 000) et data_offset (défaut 0). Les métadonnées
restent complètes, dataTotalChars est porté par toute réponse, et la tranche
est verbatim — concaténer les tranches dans l'ordre des offsets reconstitue
la définition à l'octet près. Ne la résumez pas, ne la « parsez » pas.
Les trois outils de requête plafonnent leur volume via
src/services/response-limit.js (MAX_QUERY_RESPONSE_CHARS) : au-delà, des
lignes entières sont écartées, jamais coupées au milieu. Sous le plafond, la
réponse est inchangée octet pour octet — c'est la contrainte à préserver si
vous y touchez.
| Outil | Unité écartée | Total porté |
|---|---|---|
query_wms_entities |
une ligne | count (déjà présent) |
call_query_api |
une ligne | totalRows (ajouté à la coupe) |
search_wms_data |
un résultat, réparti en tourniquet entre les entités | totalFound (déjà présent) |
Cas limite réel : une seule ligne Writing dépasse le plafond (95 288
caractères mesurés) — la réponse est alors returned: 0, omitted: 1,
truncated: true, avec un hint qui renvoie vers Reading.
Écrire une requête WMS
await apiService.executeQuery(
'Context.OutboundOrders.Where(z => z.OutboundOrderStatus == "Release").OrderBy(z => z.Id)',
{ take: 100 }
);
Répartition entre l'expression et les options — c'est la source d'erreur la plus fréquente :
| Élément | Où |
|---|---|
Where |
dans l'Expression |
OrderBy |
dans l'Expression — obligatoire dès qu'on utilise take |
Take / Skip / Select |
paramètres d'API, pas dans l'expression |
Autres règles :
QueryType: 0(Reading) par défaut : les statuts sont alors des chaînes (D3). La bascule vers Writing/Metrics passe par le paramètrequery_typedes outils de requête — un opt-in documenté (D25), jamais un défaut : ne recopiez aucun exemple enQueryType: 1.- Pas de date relative.
DateTime.Now,DateTime.Today,AddDays()ne sont pas traduisibles : écrirenew DateTime(2026, 8, 1)(D12). select_expressionest instable : les projections via le paramètreSelectprovoquent des erreurs de compilation. Interroger les lignes complètes (D13).- Pour compter, utiliser
count_wms_entities(QueryScalarExecute), pas unquerysuivi d'un.length. executeCommandprend le nom de commande tel quel : ajouter le suffixe d'assembly provoque uneFileLoadException(D14).
_parseQueryResponse() gère les deux formes de réponse (tableau plat, ou
{ Table: { Columns, Rows } }) — ne réimplémentez pas ce décodage ailleurs.
Entités et éléments AD
Entités interrogeables (Query API) : entity_type accepte le nom d'entité
AD (Container) ou le TableName (Containers), insensible à la casse — la
résolution passe par entity-resolver.js (D21). Courantes : Products,
Containers, Accounts, Suppliers, Kits, Alias (invariant, pas de
pluriel), Tasks, Stocks, ProductLocations, InboundOrders, Receptions,
OutboundOrders. La liste faisant foi (288 entités, toutes applications
confondues) s'obtient par get_entity_metadata (API Metadata) — le catalogue
de la resource wms://entities est un raccourci de confort, pas la référence.
Application Dictionary : 20 types, ~38 800 éléments (sur EasyWMS).
Resource (29 374) est de loin le plus lourd ; 3 types sont valides mais vides
(Dashboard, TimelineTemplate, Toggle). WorkflowAction et WritingModel
ont été retirés — 404 (D17). Détail :
docs/ad-api-validation.md. 9 applications AD sont
déclarées ; CustomApp porte le spécifique client (workflows CST_*) et
s'interroge via le paramètre application des outils AD et workflow (D26).
Paramètres système : pas d'entité CommandParameterData. La configuration
se lit dans Parameter (+ DefaultValue) et ParamValue (surcharges par
entrepôt, jointure sur ParameterId), fusionnées côté JS par
get_system_parameters (D11).
Commandes
npm start # lancer le serveur (stdio)
npm test # smoke test du profil actif — lecture seule
npm test -- LIMAGRAIN # smoke test d'un profil précis
npm test -- --all # tous les profils
npm run build # dist/wms-mcp-server.exe (node22-win-x64)
Le smoke test vérifie OAuth, QueryExecute, QueryScalarExecute et l'API AD,
et sort en code 1 au moindre échec.
Validation des endpoints AD (PowerShell, credentials en paramètres) :
powershell -ExecutionPolicy Bypass -File scripts/test-ad-api.ps1 -WmsHost 10.255.255.2 -Username user -Password '***' -Tenant AD
Ajouter un outil
- Déclarer le schéma dans
listTools()du modulesrc/tools/concerné. - Traiter le cas dans son
executeTool(). - Rien à faire dans
src/index.jspour un module existant : la table de routage est construite depuislistTools()(D22). Un nouveau module doit être ajouté àTOOL_MODULES. - Logger avec le préfixe du module.
- Renvoyer les erreurs, ne pas les lever hors du wrapper.
- Tester le handshake complet :
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/list"}' | node src/index.js
Points ouverts
Voir ROADMAP.md : lots de correction planifiés et propositions explicitement écartées.