# 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:///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:///ApplicationService/api` et `{ad}` = `https:///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]`, `[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://`. **`_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. `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.