660c62c32c
Cas reel du 25/08/2026 : une session Cowork cherchant des workflows CST_ sans
passer application="CustomApp" a conclu que l'AD n'en contenait aucun -- alors
que CustomApp en porte 153. Le parametre application existait bien (D26) et
etait documente ; ce qui manquait, c'est que RIEN dans la reponse ne disait
qu'on n'avait regarde qu'une application sur neuf. Un defaut silencieux se lit
comme une exhaustivite.
search_workflows et search_ad_elements rappellent desormais TOUJOURS
l'application effectivement interrogee (plus seulement quand le parametre a ete
passe), et ajoutent un hint quand la recherche revient vide.
Seuil a 0 resultat, pas "peu" : toute valeur non nulle produirait un hint
parasite sur une recherche legitimement etroite, et le mode d'echec observe est
bien le zero pris pour une absence.
Le hint nomme les autres applications depuis la liste allegee DEJA en cache ;
sans elle il reste generique et renvoie vers list_workflow_categories. Aucun
appel reseau n'est fait pour construire un hint -- ce serait exactement le
prechargement que D26 interdit.
Verifie en execution (protocole, LIMAGRAIN) :
search_workflows {"query":"CST_"} 404 chars
application: "EasyWMS", count: 0, hint nommant CustomApp et renvoyant
vers list_workflow_categories
search_workflows {"query":"CST_"} apres
list_workflow_categories 461 chars
meme hint, enrichi de la liste en cache : Common, Notifications, SmartUI,
WarehouseWebDesigner, GalileoFaults, CustomApp, User, AGV
search_workflows {"query":"CST_","application":"CustomApp"}
count: 44, application: "CustomApp" -- dont CST_SendRejectContainersToPK
search_workflows {"query":"stacker"} 14 192 -> 14 220 chars
count: 50 inchange, application: "EasyWMS" presente, aucun hint parasite
search_ad_elements Command "CST_" 457 chars
application: "EasyWMS", count: 0, meme hint
stderr : aucun fetch d'une application non demandee. Seules EasyWMS et
CustomApp sont chargees, chacune sur demande explicite.
Rebouclage complet apres modification des schemas : 23 outils, 6 resources,
les 23 noms de tools/list atteignent leur module (aucun "Unknown tool" ;
execute_command verifie statiquement, non appele car il ecrit dans le WMS),
D23 rejette toujours un parametre inconnu sur les quatre schemas modifies,
npm test 4/4 en code 0.
Lot 5 retire de la ROADMAP.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
345 lines
15 KiB
Markdown
345 lines
15 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` |
|
|
|
|
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](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](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.
|
|
|
|
`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.
|