54a6849563
Aucune borne de VOLUME n'existait sur query_wms_entities, call_query_api et
search_wms_data -- seulement une borne de LIGNES (MAX_QUERY_ROWS). Le modele
Writing serialise l'agregat complet (navigations, $id) : une seule ligne
Products y pese 95 288 caracteres, la ou la meme ligne Reading en fait ~4 500.
Toutes ces reponses depassaient le seuil de rejet du client MCP (D24), qui
renvoie un echec opaque plutot qu'un resultat partiel.
Plafond commun MAX_QUERY_RESPONSE_CHARS (defaut 25 000, meme ordre de grandeur
que MAX_LOG_SEARCH_CHARS), applique par src/services/response-limit.js : on
ecarte des LIGNES ENTIERES, jamais coupees au milieu, et on signale avec le
vocabulaire D24 (truncated / returned / omitted / hint). Le helper est partage
parce que les trois outils partagent le meme mecanisme -- ce que les mecanismes
de get_system_parameters et search_logs, eux, ne font pas. La recherche
dichotomique evite 200 reconstructions d'une charge utile de ~1 Mo.
Mesures avant/apres (protocole, LIMAGRAIN, longueur de content[0].text) :
query_wms_entities Products limit 200 957 234 -> 24 432
count 200, returned 5, omitted 195, truncated
search_wms_data "PAL" 847 543 -> 22 992
totalFound 150, returned 4, omitted 146 ; reparti en tourniquet :
Products 2, Containers 1, Tasks 1 -- sans quoi Products, en tete,
consommerait tout le budget et les deux autres reviendraient a zero
resultat sans que rien ne le dise
call_query_api Products query_type 1 95 288 -> 738
cas limite : returned 0, omitted 1, truncated, hint expliquant le
volume Writing et renvoyant vers Reading
Sous le plafond, rien ne change -- verifie identique OCTET POUR OCTET contre
la version precedente :
query_wms_entities Container limit 1 4 476 -> 4 476
call_query_api Products limit 2 9 671 -> 9 671
get_entity_schema Container 11 172 -> 11 172
search_wms_data sous plafond 11 767 -> 11 767
count_wms_entities Products 120 -> 120 (non concerne)
MAX_QUERY_ROWS et les limites par defaut des outils sont inchanges : le
correctif est le bornage signale, pas une reduction silencieuse. L'avertissement
de volume rejoint la description du parametre query_type (D25) de
query_wms_entities et call_query_api, pas celle de count_wms_entities dont la
reponse est un scalaire.
Baseline preservee : 23 outils, 6 resources.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
338 lines
15 KiB
Markdown
338 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`.
|
|
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).
|
|
|
|
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.
|