Files
mcp-wms-api/CLAUDE.md
T
Arthur Ria a1acb781b0 L5.3 : champ tool dans toutes les enveloppes d'erreur locales
Convention 3 impose { success: false, error, tool }, mais le champ tool n'etait
ajoute que par le wrapper de src/index.js. Les enveloppes construites
localement dans src/tools/ ne le portaient pas : call_query_api avec
query_type: 7 repondait { success: false, error: "query_type invalide : ..." },
sans tool. Le contrat etait donc respecte ou non selon le chemin d'erreur --
alors qu'il est lu par Claude, pas par un humain.

Balayage des 8 modules : 15 enveloppes success:false au total, 11 y ont gagne
le champ tool (ad-tools, config-tools, wms-query-tools et workflow-tools
l'avaient deja via le catch de leur executeTool). Les champs additionnels sont
conserves et passent apres tool : warning de resolution d'api-tools,
availableCount + hint de get_entity_metadata, listes de profils de
profile-tools.

Grep de controle -- 15 enveloppes, 0 sans tool :

  src/tools/ad-tools.js:140            tool: name
  src/tools/api-tools.js:170           tool: 'call_query_api'
  src/tools/api-tools.js:217           tool: 'execute_command'
  src/tools/config-tools.js:179        tool: 'get_system_parameters'
  src/tools/log-tools.js:116           tool: 'list_log_files'
  src/tools/log-tools.js:157           tool: 'read_recent_logs'
  src/tools/log-tools.js:227           tool: 'search_logs'
  src/tools/metadata-tools.js:110      tool: 'get_entity_metadata'
  src/tools/metadata-tools.js:154      tool: 'get_entity_metadata'
  src/tools/metadata-tools.js:197      tool: 'generic_search'
  src/tools/profile-tools.js:111       tool: 'get_current_wms_profile'
  src/tools/profile-tools.js:129       tool: 'switch_wms_profile'
  src/tools/profile-tools.js:159       tool: 'switch_wms_profile'
  src/tools/wms-query-tools.js:169     tool: name
  src/tools/workflow-tools.js:117      tool: name

Verifie en execution (protocole, LIMAGRAIN sauf mention) -- 11 enveloppes
declenchees, toutes avec tool :

  call_query_api query_type: 7          -> tool: call_query_api
  get_entity_metadata entite inconnue   -> tool + availableCount + hint
  query_wms_entities entite inconnue    -> tool: query_wms_entities
  get_workflow_details id inconnu       -> tool: get_workflow_details
  get_ad_elements type inconnu          -> tool: get_ad_elements
  switch_wms_profile profil inconnu     -> tool + profiles
  read_recent_logs fichier inexistant   -> tool: read_recent_logs
  list_log_files / search_logs /
    read_recent_logs sur EUROTRAFIC     -> tool, garde SaaS D9 (sans reseau)

Trois catch restent couverts statiquement, faute de declencheur : celui
d'execute_command (interdit d'appel, il ecrit dans le WMS), celui de
generic_search (l'API tolere categorie inexistante comme limit negative :
elle repond success), et celui de get_current_wms_profile (inatteignable tant
qu'un profil par defaut se charge au demarrage).

Baseline preservee : 23 outils, 6 resources.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 15:16:28 +02:00

15 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

{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
│   ├── 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

  1. console.error() uniquement. stdout est réservé au JSON MCP ; toute écriture y casse la session Claude Desktop (D6).
  2. Préfixer les logs par composant : [Server], [API], [Profile], [Workflow], [AD], [Logs], [<X>Tools]. Le tableau complet est dans MONITORING.md §2.
  3. Un outil ne plante jamais le serveur. Toute erreur revient en réponse structurée { success: false, error, tool } avec isError: true — le wrapper est dans le handler tools/call de src/index.js. Les enveloppes construites localement dans src/tools/ portent le champ tool elles aussi : le wrapper ne les voit pas, et une erreur sans tool sort du contrat. Les champs supplémentaires utiles (warning de résolution, profiles, hint…) viennent après.
  4. Messages d'erreur actionnables. Ils sont lus par Claude, pas par un humain : dire quoi faire ensuite (« appelez switch_wms_profile », « profils disponibles : … »).
  5. Aucun accès base de données (D1).
  6. 1000 lignes maximum par requête, timeout 30 s (MAX_QUERY_ROWS, QUERY_TIMEOUT).
  7. 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).

Les tailles de page par type viennent de l'observation des timeouts serveur — ne les augmentez pas à l'aveugle.

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
Where dans l'Expression
OrderBy dans l'Expressionobligatoire 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ètre query_type des outils de requête — un opt-in documenté (D25), jamais un défaut : ne recopiez aucun exemple en QueryType: 1.
  • Pas de date relative. DateTime.Now, DateTime.Today, AddDays() ne sont pas traduisibles : écrire new DateTime(2026, 8, 1) (D12).
  • select_expression est instable : les projections via le paramètre Select provoquent des erreurs de compilation. Interroger les lignes complètes (D13).
  • Pour compter, utiliser count_wms_entities (QueryScalarExecute), pas un query suivi d'un .length.
  • executeCommand prend le nom de commande tel quel : ajouter le suffixe d'assembly provoque une FileLoadException (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

  1. Déclarer le schéma dans listTools() du module src/tools/ concerné.
  2. Traiter le cas dans son executeTool().
  3. Rien à faire dans src/index.js pour un module existant : la table de routage est construite depuis listTools() (D22). Un nouveau module doit être ajouté à TOOL_MODULES.
  4. Logger avec le préfixe du module.
  5. Renvoyer les erreurs, ne pas les lever hors du wrapper.
  6. 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.