Files
Arthur Ria 242b0c0f1c L6.3 : une reponse hors enveloppe leve, au lieu de se faire passer pour vide
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>
2026-08-25 15:54:54 +02:00

359 lines
16 KiB
Markdown

# CLAUDE.md
Guide pour Claude Code (claude.ai/code) sur ce dépôt.
**Avant de modifier quoi que ce soit, lisez [DECISIONS.md](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](README.md) |
| Pourquoi le code est ainsi, pièges terrain | [DECISIONS.md](DECISIONS.md) |
| Le serveur ne répond pas, lire ses logs | [MONITORING.md](MONITORING.md) |
| Ce qui reste à faire | [ROADMAP.md](ROADMAP.md) |
| Superviser le projet, réviser une livraison | [docs/supervision.md](docs/supervision.md) |
| Accéder aux logs du WMS | [docs/logs.md](docs/logs.md) |
| Références EasyWMS (API, entités) | [docs/](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](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
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](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` :
```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
```js
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è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](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
```bash
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) :
```bash
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 :
```bash
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](ROADMAP.md) : lots de correction planifiés et propositions
explicitement écartées.