Compare commits

...

8 Commits

Author SHA1 Message Date
Arthur Ria 8c5792da52 Révision lot 1 : validé ; passation lot 2 (L2.1-L2.3 + L3.2), L2.3 consigné
Lot 1 révisé selon la grille : vérifications L1.1/L1.2/L1.3 rejouées en
protocole (22 outils routés + execute_command en statique), diffs lus,
baseline 23/6 et npm test 4/4 confirmés. Deux anomalies préexistantes
découvertes en bouclant sur les outils avec arguments vides :
get_workflow_details({}) renvoie le premier workflow du cache (clés
mortes Id/Code/Name dans la comparaison), search_logs({}) échoue en
TypeError non actionnable — consignées en L2.3.

handoff-lot1.md supprimé (livré), handoff-lot2.md rédigé : D21 et D23
réservés, profil AD signalé cassé (ne pas réinvestiguer), vérifications
attendues par correctif.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 17:13:19 +02:00
Arthur Ria 52b5f90521 Roadmap : profil AD en échec (tenant introuvable), erreurs d'auth muettes (L3.2)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 17:04:46 +02:00
Arthur Ria 03f561fdf7 Supervision : rôle durable de vérification et de passation
Ajoute docs/supervision.md, référencé depuis CLAUDE.md et docs/README.md.

Décrit le rôle de superviseur du projet, distinct des sessions qui codent :
vérifier l'état réel du MCP contre le WMS, réviser leurs livraisons sans les
croire sur parole, et rédiger la passation suivante.

Contient la baseline chiffrée à préserver (23 outils, 6 resources, npm test
4/4) et les mesures de référence du tenant, la boîte à outils de vérification
(handshake MCP, appel d'outil via le protocole, sonde directe de l'API), une
grille de revue en sept points, les six règles de rédaction d'une passation, et
les garde-fous (lecture seule, pas de push, pas de réécriture d'historique).

Consigne les quatre modes d'échec déjà observés sur ce dépôt : taxonomie
inventée, casse de champ supposée, collision de préfixe de routage, hypothèse
présentée comme solution. Ils sont récurrents et se repèrent vite quand on
sait quoi chercher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 16:51:08 +02:00
Arthur Ria e5614f3b60 Roadmap : retire le lot 1 livré, invalide la piste ClientModule (L4.3)
Le lot 1 (erreurs HTTP détaillées, routage par table, projections des
workflows) est livré et vérifié contre le WMS réel — il sort de la
roadmap. D22 étant écrite, L3.2 est ajusté en conséquence.

L4.3 est corrigé d'après mesure : le champ ClientModule de QueryExecute
est accepté mais sans effet observable dans les logs du WMS — une
requête en échec envoyée avec ClientModule: "MCP-WMS" reste tracée
« Execute error. Client: GNA », et la chaîne n'apparaît dans aucun log.
Le champ n'a donc pas été renseigné.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 16:47:14 +02:00
Arthur Ria 3a89c317e8 L1.3 : aligne les projections des workflows sur les clés réelles de l'AD
Les clés réelles d'un workflow AD (relevées en direct, minuscules,
cf. D5) sont : id, name, version, applicationName, commonInfo, data…
search_workflows projetait w.Id, w.Code, w.Name, w.Category,
w.Description, w.Created, w.Modified — toutes undefined, supprimées par
JSON.stringify : 50 objets vides pour un count pourtant correct.

La projection porte désormais id, name, applicationName, version, et
les équivalents réels de created/modified trouvés dans commonInfo
(createdBy, createDate, updateDate). Code et Description n'existent
dans aucune casse : non projetés.

La notion de catégorie n'a aucun support dans les données : elle est
mappée explicitement sur applicationName, seul regroupement fourni par
l'API AD — assumé dans les descriptions d'outils et par une note dans
la réponse de list_workflow_categories, qui renvoyait 0 catégorie et
classait les 4012 workflows en « Uncategorized ». Le paramètre category
de search_workflows filtre sur applicationName. Le filtre de recherche
ne teste plus description/code, clés inexistantes.

Vérifié contre le WMS réel : search_workflows("stacker") renvoie des
objets peuplés (StackerCrane_…), list_workflow_categories renvoie
EasyWMS avec 4012 workflows, get_workflow_details renvoie toujours
l'objet brut complet (data 71 Ko).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 16:45:24 +02:00
Arthur Ria e0bdc1707d L1.2 : route les outils par table explicite nom -> module (D22)
Le routage par préfixe de nom laissait deux outils listés dans
tools/list mais injoignables : get_entity_metadata (capté par
startsWith('get_entity_') avant sa propre branche) et list_log_files
(aucune branche : le nom contient _log_files, pas _logs).

Une table nom d'outil -> module est construite au démarrage depuis les
listTools() des 8 modules de src/tools/. tools/list est servi depuis
cette même table et le dispatch devient un lookup : un outil listé est
un outil routé, par construction. Deux modules déclarant le même nom
font échouer le serveur au démarrage avec un message nommant les deux
modules. Le wrapper d'erreur du handler tools/call est inchangé, les
23 outils gardent leurs noms.

Vérifié contre le WMS réel : get_entity_metadata renvoie 232 entités,
list_log_files renvoie 19 fichiers, tools/list expose toujours 23
outils et chaque nom listé est traité par le executeTool() de son
module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 16:43:40 +02:00
Arthur Ria 3dad5c6088 L1.1 : remonte le détail des erreurs HTTP dans les réponses d'outils
Toute erreur d'API se résumait à « Request failed with status code 500 »
alors que le WMS renvoie le diagnostic complet (erreurs de compilation
LINQ, entité inconnue…) dans le corps de la réponse, jusqu'ici jeté par
les catch de post() et get().

L'erreur propagée porte désormais : verbe, URL complète, statut HTTP,
payload envoyé (dont Application et QueryType), et corps de réponse
tronqué à 2000 caractères. Pour les corps structurés, Message et
InnerException.Message sont extraits plutôt qu'un JSON.stringify
intégral qui noierait le diagnostic dans le bruit WatsonBuckets.
Le rejeu après refresh de token 401 est conservé, et une erreur pendant
le rejeu est enrichie de la même façon. Aucun credential ni token dans
le message (les headers ne sont jamais inclus).

Vérifié contre le WMS réel : query_wms_entities("Container") fait
apparaître « 'ApplicationReadingContext' ne contient pas de définition
pour 'Container' » dans la réponse de l'outil. npm test : 4/4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 16:41:18 +02:00
Arthur Ria 7c722dae91 Passation : prompt autoportant pour le lot 1
Ajoute docs/handoff-lot1.md, le prompt à donner à une nouvelle session Claude
Code pour implémenter le lot 1 de la roadmap. Il vivait jusqu'ici dans un
dossier temporaire de session, donc perdable.

Contient la phase 0 de vérifications préalables (les quatre contextes de
requête, le périmètre applications, le champ ClientModule), les trois
correctifs avec leurs preuves et leurs vérifications attendues, et les
consignes de livraison.

Document à usage unique : à supprimer une fois le lot 1 livré, la référence
durable restant ROADMAP.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 16:33:11 +02:00
10 changed files with 740 additions and 173 deletions
+12 -11
View File
@@ -13,6 +13,7 @@ volontaires ; les références `D1`, `D2`… de ce fichier y renvoient.
| Pourquoi le code est ainsi, pièges terrain | [DECISIONS.md](DECISIONS.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) | | Le serveur ne répond pas, lire ses logs | [MONITORING.md](MONITORING.md) |
| Ce qui reste à faire | [ROADMAP.md](ROADMAP.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) | | Accéder aux logs du WMS | [docs/logs.md](docs/logs.md) |
| Références EasyWMS (API, entités) | [docs/](docs/) | | Références EasyWMS (API, entités) | [docs/](docs/) |
@@ -86,10 +87,11 @@ docs/
⚠️ utilise QueryType 1 : ne pas recopier (D3) ⚠️ utilise QueryType 1 : ne pas recopier (D3)
``` ```
**Routage.** `src/index.js` route les appels d'outils **par préfixe de nom** **Routage.** `src/index.js` construit au démarrage une **table nom d'outil →
(`name.startsWith('query_wms_')`, `name.includes('_logs')`, …). En ajoutant un module** depuis les `listTools()` des 8 modules de `src/tools/` ; `tools/list`
outil, vérifiez que son nom tombe dans la bonne branche — sinon il apparaîtra et le dispatch sont servis par cette même table, donc un outil listé est routé
dans `tools/list` mais renverra `Unknown tool`. par construction (D22). Deux modules déclarant le même nom font échouer le
serveur au démarrage.
--- ---
@@ -261,8 +263,9 @@ powershell -ExecutionPolicy Bypass -File scripts/test-ad-api.ps1 -WmsHost 10.255
1. Déclarer le schéma dans `listTools()` du module `src/tools/` concerné. 1. Déclarer le schéma dans `listTools()` du module `src/tools/` concerné.
2. Traiter le cas dans son `executeTool()`. 2. Traiter le cas dans son `executeTool()`.
3. **Vérifier le routage par préfixe** dans `src/index.js` — ou ajouter une 3. Rien à faire dans `src/index.js` pour un module existant : la table de
branche. routage est construite depuis `listTools()` (D22). Un **nouveau module**
doit être ajouté à `TOOL_MODULES`.
4. Logger avec le préfixe du module. 4. Logger avec le préfixe du module.
5. Renvoyer les erreurs, ne pas les lever hors du wrapper. 5. Renvoyer les erreurs, ne pas les lever hors du wrapper.
6. Tester le handshake complet : 6. Tester le handshake complet :
@@ -279,12 +282,10 @@ Voir [ROADMAP.md](ROADMAP.md) : lots de correction planifiés, cause racine
commune (résolution `Name` -> `TableName` des entités), et propositions commune (résolution `Name` -> `TableName` des entités), et propositions
explicitement écartées. explicitement écartées.
⚠️ Deux pièges connus et non encore corrigés, à garder en tête en attendant le ⚠️ Piège connu et non encore corrigé, à garder en tête en attendant le lot 2 :
lot 1 :
- `entity_type` est interpolé sans validation dans `Context.{entity_type}`. Le - `entity_type` est interpolé sans validation dans `Context.{entity_type}`. Le
nom attendu est le `TableName` de l'API Metadata, pas le nom d'entité de l'AD nom attendu est le `TableName` de l'API Metadata, pas le nom d'entité de l'AD
(`Container` -> `Containers`, mais `Alias` -> `Alias`). Un mauvais nom donne un (`Container` -> `Containers`, mais `Alias` -> `Alias`). Un mauvais nom donne un
HTTP 500 dont le détail est aujourd'hui perdu. HTTP 500 dont le détail (erreur de compilation LINQ) remonte désormais dans
- `get_entity_metadata` et `list_log_files` sont listés dans `tools/list` mais la réponse de l'outil (L1.1).
non routés dans `src/index.js` : ils renvoient `Unknown tool`.
+29
View File
@@ -350,3 +350,32 @@ Le fichier est retiré du répertoire de travail, **mais il reste dans
l'historique git** (commit `b59cbb3`). Considérez ces mots de passe comme l'historique git** (commit `b59cbb3`). Considérez ces mots de passe comme
compromis et changez-les ; à défaut, réécrivez l'historique avant toute compromis et changez-les ; à défaut, réécrivez l'historique avant toute
publication du dépôt. publication du dépôt.
---
## D22 — Routage des outils par table explicite, plus par préfixe de nom
**Piège.** Le handler `tools/call` de `src/index.js` routait par préfixe de nom
(`startsWith`, `includes`) dans une cascade de `else if`. Deux outils listés
dans `tools/list` n'atteignaient jamais leur module — reproduits le
24/08/2026 :
| Outil | Cause | Erreur renvoyée |
|---|---|---|
| `get_entity_metadata` | capté par `startsWith('get_entity_')` (branche `wms-query-tools`, placée avant la sienne) | `Unknown WMS query tool: get_entity_metadata` |
| `list_log_files` | la branche logs testait `includes('_logs')`, or le nom contient `_log_files` | `Unknown tool: list_log_files` |
Le routage par préfixe fait dépendre la joignabilité d'un outil de l'**ordre
des branches** et de conventions de nommage implicites : chaque ajout d'outil
pouvait en casser un autre silencieusement.
**Décision.** Une table `nom d'outil → module` est construite au démarrage en
parcourant les `listTools()` des 8 modules de `src/tools/`. `tools/list` est
servi depuis cette même table et le dispatch est un lookup : un outil listé
est un outil routé, **par construction**. Deux modules déclarant le même nom
font échouer le serveur au démarrage (message nommant les deux modules) —
c'est un bug de développement, pas un cas d'exécution.
La table ne présume rien de la signature des outils : `(name, args)` est
transmis tel quel au `executeTool()` du module. Ajouter un paramètre à un
outil ne la concerne pas.
+54 -66
View File
@@ -42,64 +42,6 @@ Conséquences déjà constatées :
--- ---
## Lot 1 — Déblocage
Objectif : rendre le MCP auto-diagnosticable et réparer ce qui est cassé. Ce lot
seul aurait suffi à ce qu'une session se débrouille sans intervention.
### L1.1 — Remonter le détail des erreurs HTTP
Aujourd'hui toute erreur d'API se résume à `Request failed with status code 500`.
Or le WMS renvoie déjà le diagnostic complet dans le corps de la réponse :
```json
{"ClassName":"System.AggregateException","Message":"Compile Error: ...
'ApplicationReadingContext' ne contient pas de définition pour 'Container' ..."}
```
Enrichir l'erreur au point de passage unique (`api-service.post` / `.get`) avec :
statut, URL, verbe, payload envoyé, corps de réponse tronqué à ~2000 caractères.
**Fichier :** `src/services/api-service.js` (catch de `post` et `get`).
### L1.2 — Fiabiliser le routage des outils
Deux outils sont listés dans `tools/list` mais ne sont routés vers aucun module,
à cause du routage par préfixe :
| Outil | Cause | Erreur observée |
|---|---|---|
| `get_entity_metadata` | capté par `startsWith('get_entity_')` avant sa propre branche | `Unknown WMS query tool` |
| `list_log_files` | ne contient pas `_logs` mais `_log_files` | `Unknown tool` |
Remplacer le routage par préfixe par une **table explicite nom → module**,
construite depuis les `listTools()` de chaque module. Un outil listé mais non
routé devient alors impossible par construction, au lieu d'être rattrapé au cas
par cas.
**Fichier :** `src/index.js` (handler `tools/call`).
### L1.3 — Corriger les projections de champs des workflows
L'API AD renvoie les champs en minuscules (`id`, `name`, `version`,
`applicationName`). Deux endroits supposent une autre forme :
- `search_workflows` projette `w.Id`, `w.Code`, `w.Name`, `w.Category` → tous
`undefined`, supprimés par `JSON.stringify`**50 objets vides** pour un
`count` pourtant correct ;
- `workflow-service` lit `w.category || w.Category`, deux clés inexistantes →
`list_workflow_categories` renvoie **0 catégorie** et classe les 4012
workflows en `Uncategorized`.
Le champ le plus proche d'une catégorie est `applicationName`, mais il vaut
`EasyWMS` pour tous les workflows : la notion de catégorie n'a **aucun support**
dans les données. Décider en connaissance de cause plutôt que d'inventer une
taxonomie.
**Fichiers :** `src/tools/workflow-tools.js`, `src/services/workflow-service.js`.
---
## Lot 2 — Correctif de fond ## Lot 2 — Correctif de fond
### L2.1 — Résolution des entités via l'API Metadata ### L2.1 — Résolution des entités via l'API Metadata
@@ -128,6 +70,22 @@ C'est le correctif retenu **à la place** d'une uniformisation des noms de
paramètres : renommer casse les usages existants pour un gain cosmétique, alors paramètres : renommer casse les usages existants pour un gain cosmétique, alors
que la cause réelle est l'absence de signal. que la cause réelle est l'absence de signal.
### L2.3 — Arguments manquants : deux garde-fous
Découverts lors de la révision du lot 1 (24/08/2026), en bouclant sur les 23
outils avec des arguments vides. Préexistants au lot 1 (vérifié sur le diff) :
- `get_workflow_details` sans `workflow_id` renvoie `success: true` avec **le
premier workflow du cache**. Cause : `getWorkflowDetails()`
(`workflow-service.js`) compare `w.Code === workflowId` — or `Code`, `Id`,
`Name` n'existent pas sur les objets réels (clés minuscules, D5), donc
`undefined === undefined` matche. Supprimer les clés mortes de la
comparaison et rejeter un `workflow_id` absent avec un message actionnable.
- `search_logs` sans terme de recherche renvoie
`Search failed: Cannot read properties of undefined (reading 'toLowerCase')`
— le contrat d'erreur tient, mais le message viole la convention 4
(actionnable). Garde d'entrée avec le nom du paramètre attendu.
--- ---
## Lot 3 — Ergonomie et documentation ## Lot 3 — Ergonomie et documentation
@@ -141,10 +99,25 @@ que la cause réelle est l'absence de signal.
- Renvoyer `truncated: true` explicitement plutôt que de laisser le client se - Renvoyer `truncated: true` explicitement plutôt que de laisser le client se
faire rejeter. faire rejeter.
### L3.2 — Documentation ### L3.2 — Erreurs d'authentification muettes
- DECISIONS.md : **D21** la règle `TableName`, **D22** le routage par table `authenticate()` (`api-service.js`) ré-enveloppe l'erreur axios en
explicite. `new Error('Authentication failed: ' + error.message)` : le **corps de la
réponse du STS est perdu**, alors qu'il contient le diagnostic complet.
Preuve (24/08/2026, profil `AD`) : le smoke test affiche seulement
`Authentication failed: Request failed with status code 400` ; en rejouant la
même requête à la main, le corps était
`{"error":"invalid_request","error_description":"Tenant not found"}` — le
diagnostic exact, invisible depuis les outils comme depuis le smoke test.
Faire remonter `error.response.data` dans le message, comme L1.1 l'a fait pour
les outils. Même contrainte : ne jamais logguer les credentials.
### L3.3 — Documentation
- DECISIONS.md : **D21** la règle `TableName` (D22, le routage par table
explicite, a été livrée avec le lot 1).
- CLAUDE.md : corriger la liste d'entités (`Aliases``Alias`) et renvoyer vers - CLAUDE.md : corriger la liste d'entités (`Aliases``Alias`) et renvoyer vers
`get_entity_metadata` comme source de vérité. `get_entity_metadata` comme source de vérité.
- `wms://query-examples` : un exemple singulier/pluriel commenté. - `wms://query-examples` : un exemple singulier/pluriel commenté.
@@ -153,7 +126,7 @@ que la cause réelle est l'absence de signal.
## Lot 4 — Modèle de données et applications ## Lot 4 — Modèle de données et applications
Deux angles morts constatés le 24/08/2026, plus larges que les lots 1 à 3. Les Deux angles morts constatés le 24/08/2026, plus larges que les lots 2 et 3. Les
chiffres ci-dessous sont mesurés sur le tenant `LIMAGRAI2512`. chiffres ci-dessous sont mesurés sur le tenant `LIMAGRAI2512`.
### L4.1 — Le modèle Writing est inatteignable ### L4.1 — Le modèle Writing est inatteignable
@@ -231,13 +204,20 @@ paramètre `application` sur les outils AD et workflow d'autant plus utile.
### L4.3 — Identifier le MCP dans les logs du WMS ### L4.3 — Identifier le MCP dans les logs du WMS
`QueryExecute` accepte un champ **`ClientModule`** que le MCP n'envoie pas. Les requêtes du MCP apparaissent dans les logs du WMS sous
Résultat : ses requêtes apparaissent dans les logs du WMS sous
`Execute error. Client: GNA` — le client OAuth partagé — donc indistinguables de `Execute error. Client: GNA` — le client OAuth partagé — donc indistinguables de
celles du vrai client GNA. celles du vrai client GNA.
Renseigner `ClientModule` (`"MCP-WMS"` ou le nom du profil actif) rend chaque **La piste `ClientModule` est invalidée** (mesuré le 24/08/2026, lot 1) : le
requête du MCP traçable côté serveur. Vérifié : le champ est accepté. champ est bien accepté par `QueryExecute` (pas d'erreur), mais il est **sans
effet observable**. Une requête en échec envoyée avec
`ClientModule: "MCP-WMS"` est tracée `Execute error. Client: GNA`, et ni
`MCP-WMS` ni `ClientModule` n'apparaissent nulle part dans
`ApplicationService.log` ni `HttpResponseTime.log`. Le `Client:` des logs vient
du client OAuth, pas du payload — le champ n'a donc **pas** été renseigné.
Piste restante (non vérifiée) : un client OAuth dédié au MCP côté EasySTS
changerait le `Client:` des logs, mais suppose une configuration côté WMS.
### L4.4 — Historique d'exécution des workflows par API ### L4.4 — Historique d'exécution des workflows par API
@@ -293,6 +273,14 @@ plus riche que `/AD/api/Application/GetAll`), `GET /healthcheck?tenantCode=` et
## Points ouverts (hors lots) ## Points ouverts (hors lots)
- **Profil `AD` : tenant introuvable** (mesuré le 24/08/2026). `npm test -- --all`
échoue 0/4 sur ce profil ; le STS de `10.255.255.2` répond
`400 {"error":"invalid_request","error_description":"Tenant not found"}` pour
le tenant `AD`. L'hôte et le STS fonctionnent (LIMAGRAIN, même hôte, passe
4/4) : c'est la valeur `AD_TENANT` du `.env` qui ne correspond plus à un
tenant existant. Correction côté propriétaire du dépôt (mettre à jour ou
retirer le profil) — pas un bug du code. Le message opaque du smoke test est
traité à part (L3.2).
- **`select_expression`** : les projections via le paramètre `Select` provoquent - **`select_expression`** : les projections via le paramètre `Select` provoquent
des erreurs de compilation côté serveur (D13). Irritant principal restant. des erreurs de compilation côté serveur (D13). Irritant principal restant.
- **Déploiement SSH sur la VM** : l'exécutable est validé, la configuration SSH - **Déploiement SSH sur la VM** : l'exécutable est validé, la configuration SSH
+2
View File
@@ -12,6 +12,8 @@ Documents de référence sur EasyWMS et ses API, conservés dans le dépôt pour
| Fichier | Nature | | Fichier | Nature |
|---|---| |---|---|
| [supervision.md](supervision.md) | **Rôle de supervision** — vérifier le MCP, réviser les livraisons des sessions de codage, rédiger les passations |
| [handoff-lot1.md](handoff-lot1.md) | **Passation** — prompt autoportant pour le lot 1 de la [roadmap](../ROADMAP.md). À supprimer une fois le lot livré |
| [logs.md](logs.md) | **Rédigé pour ce projet** — accès aux logs WMS, outils, limites | | [logs.md](logs.md) | **Rédigé pour ce projet** — accès aux logs WMS, outils, limites |
| [ad-api-validation.md](ad-api-validation.md) | **Rédigé pour ce projet** — campagne de validation curl des 19 types AD testés (17 valides) | | [ad-api-validation.md](ad-api-validation.md) | **Rédigé pour ce projet** — campagne de validation curl des 19 types AD testés (17 valides) |
| [api/Application Service API Reference.md](api/Application%20Service%20API%20Reference.md) | Référence de l'API ApplicationService | | [api/Application Service API Reference.md](api/Application%20Service%20API%20Reference.md) | Référence de l'API ApplicationService |
+213
View File
@@ -0,0 +1,213 @@
# Passation — lot 2
Tu travailles sur `mcp-wms-api` : un serveur MCP (Node.js, CommonJS, stdio) qui
donne à Claude un accès en lecture à un WMS EasyWMS (Mecalux) via ses API REST.
Lis [../CLAUDE.md](../CLAUDE.md) et [../DECISIONS.md](../DECISIONS.md) avant de
toucher au code.
**Mission** : livrer le lot 2 de [../ROADMAP.md](../ROADMAP.md) — résolution
des noms d'entités via l'API Metadata (L2.1), rejet des paramètres inconnus
(L2.2), deux garde-fous d'arguments manquants (L2.3) — plus un petit correctif
du lot 3 (L3.2, erreurs d'authentification muettes).
**Hors périmètre** : tout le reste de la roadmap (L3.1, lot 4), le build pkg,
le déploiement. Ne pousse rien (`git push` interdit), ne touche pas au `.env`,
n'appelle jamais `execute_command` (il écrit dans le WMS).
---
## Contexte matériel
- Profil de travail : `LIMAGRAIN` (profil par défaut), host `10.255.255.2`,
tenant `LIMAGRAI2512`. `EUROTRAFIC` fonctionne aussi (SaaS).
- **Le profil `AD` est cassé et c'est déjà diagnostiqué — ne le réinvestigue
pas.** Son tenant n'existe plus côté STS (`400 Tenant not found`). Conséquence
pratique : `npm test -- --all` échouera toujours sur AD ; la baseline se
mesure avec `npm test` (profil par défaut), attendu **4/4, code de sortie 0**.
- Baseline protocolaire à préserver : **23 outils**, **6 resources**, aucune
écriture sur stdout hors JSON-RPC.
Handshake + comptages :
```bash
printf '%s\n%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"}' '{"jsonrpc":"2.0","id":3,"method":"resources/list"}' | node src/index.js 2>/dev/null | node -e "let b='';process.stdin.on('data',d=>b+=d).on('end',()=>{for(const l of b.split('\n').filter(Boolean)){const m=JSON.parse(l);if(m.id===2)console.log('tools:',m.result.tools.length);if(m.id===3)console.log('resources:',m.result.resources.length);}});"
```
Appeler un outil via le protocole (la seule preuve qu'un outil marche) :
```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/call","params":{"name":"NOM","arguments":{}}}' | node src/index.js 2>/dev/null
```
## Contraintes non négociables
1. `console.error()` uniquement — une écriture sur stdout casse Claude Desktop
(D6).
2. Un outil ne plante jamais le serveur : erreurs en réponse structurée
`{ success: false, error, tool }`, `isError: true` (wrapper de
`src/index.js`).
3. Messages d'erreur actionnables : dire quoi faire ensuite (convention 4 de
CLAUDE.md).
4. Aucun accès base de données (D1).
5. Tout nouvel état lié au tenant s'invalide par abonnement
`profileManager.onSwitch()`, jamais à la main depuis un autre module (D8).
6. `QueryType: 0` (D3), pas de date relative (D12), 1000 lignes max.
**Numéros de décision réservés** : **D21** = règle `TableName` (L2.1),
**D23** = validation des paramètres (L2.2). N'en attribue pas d'autres sans les
réserver dans DECISIONS.md.
---
## Phase 0 — Vérifications avant de coder
Mesures déjà faites (24/08/2026, `LIMAGRAI2512`) à **confirmer**, pas à
réinvestiguer :
- `GET /Metadata/Entities` renvoie **232 entités** pour `EasyWMS`, chacune avec
`Name` et `TableName` ; `TableName` est **unique** sur les 232.
- Le mapping n'est **pas** une pluralisation : `Container` -> `Containers`,
mais `Alias` -> `Alias` (invariant), et `Item` n'existe pas.
- Le contexte de lecture est **commun au tenant** : la table de résolution doit
agréger le Metadata de **toutes** les applications
(`GET /Metadata/Entities?applicationName=…` — 9 applications, listées par
`POST /AD/api/Application/GetAll`, payload `null`). `CustomApp` renvoie
**0 entité** : c'est mesuré et normal, ne cherche pas pourquoi.
**V1 — le SDK MCP valide-t-il les schémas ?** Mesure indispensable avant L2.2 :
ajoute `additionalProperties: false` à **un** schéma, appelle l'outil via le
protocole avec un paramètre inconnu, et observe. Si le SDK rejette : L2.2 =
ajout sur les 23 schémas. Si le SDK laisse passer (probable) : la validation
doit se faire dans le wrapper `tools/call` de `src/index.js` (à la main ou via
une lib déjà présente dans l'arbre des dépendances — n'ajoute pas de dépendance
lourde sans nécessité). Time-box : 30 min ; si le comportement du SDK est
ambigu, note-le dans le commit et implémente la validation côté wrapper.
---
## L2.1 — Résolution des entités via l'API Metadata
**Problème.** `entity_type` est interpolé sans validation dans
`Context.{entity_type}` à deux endroits : `src/tools/api-tools.js:89` et
`src/services/wms-query-service.js:29` (et `:149`). Le nom attendu est le
`TableName` du Metadata, pas le nom d'entité de l'AD. Un mauvais nom part en
HTTP 500 côté WMS.
**Preuve.** `query_wms_entities("Container")` répond aujourd'hui :
```
Query failed for Container: POST …/QueryExecute failed (HTTP 500): …
Response body: … error CS1061: 'ApplicationReadingContext' ne contient pas de
définition pour 'Container' …
```
**À faire.**
- Une table de résolution `Name|TableName (insensible à la casse) -> TableName`,
construite depuis le Metadata **agrégé sur les 9 applications**, dans un
service (pas dupliquée dans les deux points d'interpolation).
- Cache : TTL partagé (`WORKFLOW_CACHE_TTL`), chargement paresseux,
**abonnement `onSwitch()`** pour l'invalidation (D8).
- Nom inconnu → échec **avant tout appel réseau**, message actionnable avec
suggestions proches et renvoi vers `get_entity_metadata`, par exemple :
« "Item" n'existe pas dans le modèle Reading. Proches : ItemGroup, StockItem.
232 entités disponibles — utilisez `get_entity_metadata` pour la liste. »
- Si le Metadata est injoignable au moment de résoudre : laisse passer le nom
tel quel (comportement actuel) plutôt que de bloquer tout — et dis-le dans la
réponse.
**Pente naturelle interdite** : ne réinvente pas une règle de pluralisation
(« ajouter un s sauf si… »). Seul `TableName` fait foi ; c'est un mapping mesuré,
pas une grammaire.
**Vérification attendue** (via le protocole, profil LIMAGRAIN) :
- `query_wms_entities("Container", limit 1)`**succès**, 1 ligne (résolu en
`Containers`).
- `query_wms_entities("Alias")` → succès (invariant, pas de pluriel inventé).
- `query_wms_entities("Item")` → échec **sans appel réseau** (aucune ligne
`[API] POST` dans les logs stderr), message avec suggestions.
- `count_wms_entities("Product")` → succès, ~51 160.
- `get_entity_schema` et `call_query_api` bénéficient de la même résolution.
## L2.2 — Rejeter les paramètres inconnus
**Problème.** Le SDK ignore silencieusement les paramètres non déclarés :
`read_recent_logs(lines: 60)` retombe sur `count = 100` sans signal.
**À faire.** Selon le résultat de V1 : `additionalProperties: false` sur les
23 schémas, et/ou validation dans le wrapper `tools/call`. Le message de rejet
nomme le paramètre inconnu **et** les paramètres valides de l'outil.
**Décision déjà tranchée, ne pas rouvrir** : on ne renomme aucun paramètre
(cf. ROADMAP « Écarté »).
**Vérification attendue** : `read_recent_logs` avec `{"lines": 5}` → erreur
structurée nommant `lines` comme inconnu et `count` comme valide. Un appel
valide (`{"count": 5}`) → succès, 5 lignes.
## L2.3 — Garde-fous d'arguments manquants
**Preuves** (mesurées le 24/08/2026, préexistantes au lot 1) :
- `get_workflow_details` avec `{}``success: true` avec **le premier workflow
du cache**. Cause dans `workflow-service.js` `getWorkflowDetails()` : la
recherche compare `w.Id`, `w.Code`, `w.Name` — clés qui n'existent pas sur
les objets réels (minuscules, D5) — donc `undefined === undefined` matche.
- `search_logs` avec `{}`
`Search failed: Cannot read properties of undefined (reading 'toLowerCase')`.
**À faire.** Supprimer les clés mortes (`Id`, `Code`, `Name` majuscules) de la
comparaison de `getWorkflowDetails()` ; rejeter `workflow_id` absent avec un
message actionnable. Garde d'entrée sur `search_logs` nommant le paramètre
attendu. (Si L2.2 rend certains cas impossibles via `required`, garde quand
même la garde côté code : la validation SDK n'est pas garantie.)
**Vérification attendue** : les deux appels avec `{}` renvoient une erreur
structurée actionnable ; `get_workflow_details` avec un id réel renvoie
toujours l'objet brut complet.
## L3.2 — Erreurs d'authentification muettes
**Preuve.** `authenticate()` dans `src/services/api-service.js` (le `catch`
vers la ligne 79) ré-enveloppe l'erreur axios en
`Authentication failed: Request failed with status code 400` et **jette le
corps de la réponse du STS**, qui contenait le diagnostic exact :
`{"error":"invalid_request","error_description":"Tenant not found"}` (mesuré
sur le profil `AD`).
**À faire.** Faire remonter statut + corps de réponse du STS dans le message,
sur le modèle de ce que L1.1 a fait pour `post()`/`get()` (réutilise le même
helper d'enrichissement si possible). **Jamais** les credentials ni les headers
dans le message. Même traitement pour `refreshOAuthToken()`.
**Vérification attendue** : `switch_wms_profile("AD")` puis un
`count_wms_entities("Products")` → la réponse d'outil contient
`Tenant not found` (et aucun mot de passe). Ne « répare » pas le profil AD :
son échec est précisément ce qui rend le correctif vérifiable.
---
## Méthode
1. Phase 0 (V1) d'abord — L2.2 en dépend.
2. Ordre : L2.1, puis L2.2, puis L2.3, puis L3.2. Chaque correctif vérifié **en
exécution via le protocole** avant de passer au suivant.
3. Après tout changement de `src/index.js` ou des schémas : reboucler sur les
23 noms de `tools/list` et vérifier qu'aucun ne répond `Unknown tool`
(n'appelle pas `execute_command` — vérifie sa branche statiquement).
4. Baseline avant/après : handshake (23/6) + `npm test` (4/4, exit 0).
5. « Non résolu » est une réponse acceptable pour une investigation time-boxée ;
une hypothèse présentée comme solution ne l'est pas.
## Livraison
- Un commit par correctif (L2.1, L2.2, L2.3, L3.2), messages expliquant le
pourquoi, mesures dans le corps du message.
- Documentation dans les mêmes commits : **D21** et **D23** dans DECISIONS.md ;
CLAUDE.md — retirer le piège `entity_type` des « Points ouverts » et corriger
la liste d'entités (`Aliases` -> `Alias`, renvoi vers `get_entity_metadata`) ;
ROADMAP.md — retirer L2.1/L2.2/L2.3/L3.2 livrés (L3.3 documentation est
partiellement couverte par ces mises à jour : ajuste-la, ne la supprime que si
tout est fait, l'exemple `wms://query-examples` compris).
- Ne pousse pas. `.env` intact. Toute anomalie hors périmètre découverte en
route : dans ROADMAP.md, pas dans le code.
- Compte-rendu final : pour chaque correctif, la vérification attendue rejouée
et son résultat **mesuré** (colle la sortie), plus la baseline finale.
+253
View File
@@ -0,0 +1,253 @@
# Supervision du projet mcp-wms-api
> **Comment s'en servir.** Ouvrir une session Claude Code dans
> `D:\GIT\_PERSO\mcp-wms-api` et lui dire : « Lis `docs/supervision.md` et
> prends ce rôle. »
>
> Document durable, contrairement aux passations `docs/handoff-*.md` qui sont à
> usage unique.
---
Tu tiens le rôle de **superviseur** du serveur MCP `mcp-wms-api` : un serveur
MCP (Node.js, CommonJS) qui donne à Claude un accès en lecture à un WMS EasyWMS
de Mecalux, via ses API REST uniquement.
Tu n'écris pas les fonctionnalités. D'autres sessions Claude Code le font, à
partir de prompts de passation que **tu** rédiges. Ton travail tient en trois
gestes qui se répètent :
1. **Vérifier** l'état réel du MCP contre le WMS réel.
2. **Réviser** ce que les sessions de codage ont livré, sans les croire sur
parole.
3. **Rédiger** la passation suivante.
Ta valeur tient entièrement à un principe : **tu mesures, tu ne supposes pas.**
Un rapport d'agent, une doc, un commentaire de code sont des indices — la seule
preuve est l'exécution contre le WMS.
---
## 1. Où vit la vérité
| Fichier | Rôle | Qui l'écrit |
|---|---|---|
| [../CLAUDE.md](../CLAUDE.md) | architecture, conventions de code | toi, quand le code change |
| [../DECISIONS.md](../DECISIONS.md) | **pourquoi** le code est ainsi, pièges vérifiés (`D1`…) | toi, ou la session de codage sur consigne |
| [../ROADMAP.md](../ROADMAP.md) | ce qui reste à faire, par lot, et ce qui est écarté | toi |
| [../MONITORING.md](../MONITORING.md) | supervision du serveur MCP en exploitation | toi |
| [logs.md](logs.md) | accès aux logs du WMS | toi |
| `handoff-*.md` | passations à usage unique | toi, supprimées une fois livrées |
Règle de répartition, pour éviter que tout finisse en vrac dans le même
fichier :
- un fait **mesuré et acté**`DECISIONS.md`, avec un numéro `D<n>` ;
- un travail **à faire**`ROADMAP.md` ;
- une **consigne à un agent** → un `handoff-*.md` ;
- une proposition **écartée** → la section « Écarté » de `ROADMAP.md`, avec sa
raison. Sans ça, elle sera reproposée dans trois mois.
**Numérotation des décisions.** `D21` est réservée au lot 2 (règle
`TableName`), `D22` au lot 1 (routage par table explicite). Vérifie le dernier
numéro utilisé avant d'en attribuer un.
---
## 2. Baseline : ce qui doit rester vrai
Toute session de codage doit laisser ces valeurs intactes. Un écart non
expliqué est une régression, pas une amélioration.
| Contrôle | Attendu |
|---|---|
| `tools/list` | **23** outils |
| `resources/list` | **6** resources |
| `npm test` | **4/4**, code de sortie 0 |
| Démarrage | aucune écriture sur stdout hors JSON-RPC |
Mesures de référence sur le tenant `LIMAGRAI2512` (24/08/2026). Elles dépendent
du tenant : les revérifier plutôt que de les citer de mémoire sur un autre
profil.
| Mesure | Valeur |
|---|---|
| Entités du Metadata `EasyWMS` | 232 |
| Applications déclarées | 9 |
| Workflows `EasyWMS` / `CustomApp` | 4012 / 153 |
| Types AD | 20, ~38 800 éléments |
| Contextes de requête utilisables | Reading (0), Writing (1), Metrics (3). DataWarehouse (2) non configuré |
---
## 3. Boîte à outils de vérification
Toutes ces commandes sont **en lecture seule** côté WMS. Elles ont été
exécutées et fonctionnent telles quelles.
### Handshake MCP complet
```bash
printf '%s\n%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"}' '{"jsonrpc":"2.0","id":3,"method":"resources/list"}' | node src/index.js 2>/dev/null | node -e "let b='';process.stdin.on('data',d=>b+=d).on('end',()=>{for(const l of b.split('\n').filter(Boolean)){const m=JSON.parse(l);if(m.id===2)console.log('tools:',m.result.tools.length);if(m.id===3)console.log('resources:',m.result.resources.length);}});"
```
### Appeler un outil réellement, via le protocole
Ajoute une ligne `tools/call` après la notification `initialized`. C'est la
**seule** façon de vérifier qu'un outil est routé — un outil peut apparaître
dans `tools/list` et renvoyer `Unknown tool` (c'est arrivé pour
`get_entity_metadata` et `list_log_files`).
```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/call","params":{"name":"NOM_OUTIL","arguments":{}}}' | node src/index.js 2>/dev/null
```
**Contrôle systématique après toute modification du routage** : chaque nom
renvoyé par `tools/list` doit résoudre. Boucle sur les 23, ne teste pas
seulement ceux qu'on vient de corriger.
### Sonder le WMS directement
Court-circuite les outils pour savoir ce que l'API répond vraiment :
```bash
node -e "
require('dotenv').config();
const pm=require('./src/config/profile-manager'); pm.loadProfiles();
const api=require('./src/services/api-service').getInstance();
(async()=>{
try{ const r=await api.executeQuery('Context.Products.OrderBy(z => z.Id)',{take:1}); console.log('OK', r.length); }
catch(e){ console.log('status', e.response?.status); console.log(JSON.stringify(e.response?.data).slice(0,600)); }
})();"
```
C'est ce qui a révélé que les HTTP 500 portaient déjà le diagnostic complet
dans leur corps. **Quand un outil échoue, descends toujours à ce niveau** avant
de conclure quoi que ce soit sur la cause.
### Référence de l'API
`https://<host>/ApplicationService/Help` — page d'aide générée par le service,
**source de vérité** sur les champs et les endpoints. Elle a déjà démenti deux
de nos affirmations. La consulter avant d'affirmer qu'une capacité n'existe pas.
---
## 4. Réviser une livraison
Quand une session de codage rend son travail, applique cette grille. Ne saute
pas d'étape parce que le compte-rendu a l'air soigné : les comptes-rendus les
plus assurés sont souvent les moins vérifiés.
**a. Reproduis la vérification attendue toi-même.** Chaque passation en définit
une par correctif. Rejoue-la. Si elle passe chez toi, c'est un fait ; si elle
n'est pas rejouable, c'est une affirmation.
**b. Relance la baseline complète** (§2). Une correction qui casse le handshake
ou `npm test` n'est pas une correction.
**c. Cherche la régression latérale.** Le correctif touche un point de passage
partagé ? `api-service.post` sert tous les outils, `index.js` route tout,
`workflow-service` alimente trois outils. Teste au-delà du périmètre annoncé.
**d. Lis le diff, pas seulement le compte-rendu.** `git show --stat` puis le
diff complet. Tu cherches en particulier :
- un `console.log()` ajouté — casse la session Claude Desktop (D6) ;
- un secret introduit dans un fichier suivi ;
- une invalidation de cache faite à la main plutôt que par `onSwitch()` (D8) ;
- un `git add -A` qui a emporté des fichiers hors périmètre ;
- une valeur inventée là où l'agent aurait dû mesurer.
**e. Traque les quatre modes d'échec déjà observés sur ce dépôt.** Ils
reviennent :
| Mode | Signature |
|---|---|
| Taxonomie inventée | l'agent dérive une catégorie d'un préfixe de nom faute de champ réel |
| Casse supposée | `w.Name` alors que l'API renvoie `w.name` — objets vides, comptage correct (D5) |
| Collision de préfixe | un outil listé et non routé, à cause d'un `startsWith` |
| Hypothèse présentée en solution | « il suffit de… » sans exécution derrière |
**f. Vérifie la trace écrite.** Une décision prise pendant l'implémentation
doit atterrir dans `DECISIONS.md` avec son numéro ; le lot livré doit sortir de
`ROADMAP.md` ; une anomalie découverte hors périmètre doit y entrer.
**g. Rends un verdict net.** Ce qui est **mesuré**, ce qui est **déclaré mais
non vérifiable**, ce qui est **à reprendre**. Pas de « globalement bon ».
---
## 5. Rédiger la passation suivante
Un `docs/handoff-<lot>.md`, autoportant : la session qui le lit n'a pas ton
contexte et ne l'aura jamais.
Structure qui a fonctionné :
1. **Cadre** — le dépôt, la mission en une phrase, ce qui est explicitement
**hors** périmètre.
2. **Contexte matériel** — le profil qui marche, la baseline, les commandes de
vérification copiables.
3. **Contraintes non négociables**`console.error` seulement, le contrat
d'erreur des outils, pas d'accès base, ne pas toucher au `.env`.
4. **Phase 0 s'il y a lieu** — vérifications avant de coder, avec les mesures
déjà faites à confirmer.
5. **Un bloc par correctif** — problème, **preuve mesurée**, ce qu'il faut
faire, points d'attention, **vérification attendue**.
6. **Méthode** — ordre des travaux, obligation de vérifier en exécution.
7. **Livraison** — granularité des commits, mises à jour de doc, ne pas pousser.
Les six règles qui font la différence entre un prompt suivi et un prompt
réinterprété :
- **Donne les preuves, pas les symptômes.** Colle la sortie brute, les clés
réelles d'un objet, les numéros de ligne. Sinon l'agent refait le diagnostic
et peut aboutir ailleurs.
- **Marque ce qui est déjà tranché** — « ne le réinvestigue pas ». Économise des
heures et évite les conclusions contradictoires.
- **Time-boxe les investigations ouvertes** et autorise explicitement « non
résolu » comme réponse. Sans ça, l'agent invente plutôt que d'admettre.
- **Nomme la pente naturelle et interdis-la.** Exemple réel : « n'invente pas
une taxonomie en dérivant des catégories d'un préfixe de nom ».
- **Une vérification attendue par correctif**, formulée en résultat observable.
- **Réserve les numéros** de décisions pour éviter les collisions entre lots
menés en parallèle.
---
## 6. Surveillance courante
Entre deux livraisons, ce qui mérite un passage régulier :
- **`npm test` sur tous les profils** — `npm test -- --all`. Détecte une
expiration de credentials ou un WMS injoignable avant que ça ne devienne un
faux diagnostic.
- **Cohérence doc / code.** Le nombre d'outils annoncé, les listes d'entités,
les chemins de fichiers cités. Cette doc a déjà annoncé 7 resources pour 6, un
`README.md` inexistant et une entité `Aliases` qui n'existe pas.
- **Retours d'usage.** Une session Cowork ou Desktop qui bute est la meilleure
source de bugs réels — mais **ses conclusions sont à revérifier**. Sur les
8 anomalies du rapport du 24/08, 3 étaient réelles, 3 partiellement fausses,
2 non fondées, et la cause racine n'y figurait pas.
- **Les logs du WMS**, quand une erreur reste opaque : `search_logs`, ou les
partages décrits dans [logs.md](logs.md). Ce sont eux qui ont livré la cause
racine des HTTP 500.
---
## 7. Garde-fous
- **Lecture seule côté WMS.** `QueryExecute`, `QueryScalarExecute`, Metadata et
l'API AD ne modifient rien. `execute_command` **écrit** : ne l'appelle pas
pour tester.
- **Ne pousse pas.** `main` a un remote (`git.arthur-ria.fr`). Le push est une
décision du propriétaire du dépôt.
- **Ne réécris pas l'historique.** Le dépôt est publié ; un `filter-repo`
imposerait un force-push sur une branche partagée.
- **Le `.env` contient des credentials réels** et est ignoré par git. Ne le
modifie pas, ne le recopie pas ailleurs, n'en cite pas le contenu.
- **`console.error()` uniquement.** stdout appartient au protocole MCP (D6).
- **Ne corrige pas toi-même** ce que tu découvres en révisant, sauf trivialité
évidente : consigne-le dans `ROADMAP.md` et mets-le dans la passation
suivante. Sinon tu deviens l'implémenteur et plus personne ne te révise.
+41 -42
View File
@@ -62,6 +62,38 @@ const metadataTools = require('./tools/metadata-tools.js');
const configTools = require('./tools/config-tools.js'); const configTools = require('./tools/config-tools.js');
const profileTools = require('./tools/profile-tools.js'); const profileTools = require('./tools/profile-tools.js');
const TOOL_MODULES = [
{ moduleName: 'workflow-tools', module: workflowTools },
{ moduleName: 'wms-query-tools', module: wmsQueryTools },
{ moduleName: 'api-tools', module: apiTools },
{ moduleName: 'log-tools', module: logTools },
{ moduleName: 'ad-tools', module: adTools },
{ moduleName: 'metadata-tools', module: metadataTools },
{ moduleName: 'config-tools', module: configTools },
{ moduleName: 'profile-tools', module: profileTools },
];
// Table explicite nom d'outil -> module, construite depuis les listTools() de
// chaque module : un outil listé est un outil routé, par construction. Le
// routage par préfixe de nom laissait des outils listés mais injoignables
// (get_entity_metadata capté par la mauvaise branche, list_log_files capté
// par aucune).
// Un nom déclaré par deux modules est un bug de développement : on échoue au
// démarrage, pas à l'exécution.
const toolRegistry = new Map();
for (const { moduleName, module } of TOOL_MODULES) {
for (const definition of module.listTools()) {
const existing = toolRegistry.get(definition.name);
if (existing) {
throw new Error(
`[Server] Duplicate tool name "${definition.name}" declared by both ` +
`${existing.moduleName} and ${moduleName} — rename one of them`
);
}
toolRegistry.set(definition.name, { moduleName, module, definition });
}
}
// Create MCP Server // Create MCP Server
const server = new Server( const server = new Server(
{ {
@@ -138,19 +170,10 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
* List all available tools * List all available tools
*/ */
server.setRequestHandler(ListToolsRequestSchema, async () => { server.setRequestHandler(ListToolsRequestSchema, async () => {
const allTools = [ // Servi depuis la table de routage : la liste exposée et le dispatch ne
...workflowTools.listTools(), // peuvent pas diverger.
...wmsQueryTools.listTools(),
...apiTools.listTools(),
...logTools.listTools(),
...adTools.listTools(),
...metadataTools.listTools(),
...configTools.listTools(),
...profileTools.listTools(),
];
return { return {
tools: allTools, tools: Array.from(toolRegistry.values(), entry => entry.definition),
}; };
}); });
@@ -164,37 +187,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
try { try {
console.error(`[Server] Executing tool: ${name}`); console.error(`[Server] Executing tool: ${name}`);
// Route to the appropriate handler based on tool name const entry = toolRegistry.get(name);
if (name.startsWith('search_workflows') || if (!entry) {
name.startsWith('get_workflow_') || throw new Error(
name.startsWith('list_workflow_')) { `Unknown tool: ${name}. Available tools: ${Array.from(toolRegistry.keys()).join(', ')}`
return await workflowTools.executeTool(name, args); );
} else if (name.startsWith('query_wms_') ||
name.startsWith('count_wms_') ||
name.startsWith('get_entity_') ||
name.startsWith('search_wms_')) {
return await wmsQueryTools.executeTool(name, args);
} else if (name.startsWith('call_query_api') ||
name.startsWith('execute_command')) {
return await apiTools.executeTool(name, args);
} else if (name.includes('_logs')) {
return await logTools.executeTool(name, args);
} else if (name.startsWith('get_application_') ||
name.startsWith('get_ad_') ||
name.startsWith('search_ad_') ||
name.startsWith('list_ad_')) {
return await adTools.executeTool(name, args);
} else if (name === 'get_entity_metadata' || name === 'generic_search') {
return await metadataTools.executeTool(name, args);
} else if (name === 'get_system_parameters') {
return await configTools.executeTool(name, args);
} else if (name === 'list_wms_profiles' ||
name === 'get_current_wms_profile' ||
name === 'switch_wms_profile') {
return await profileTools.executeTool(name, args);
} else {
throw new Error(`Unknown tool: ${name}`);
} }
return await entry.module.executeTool(name, args);
} catch (error) { } catch (error) {
console.error(`[Server] Error executing tool ${name}:`, error.message); console.error(`[Server] Error executing tool ${name}:`, error.message);
return { return {
+83 -6
View File
@@ -136,6 +136,71 @@ class APIService {
} }
} }
/**
* Extract the useful part of an HTTP error response body.
* Structured WMS errors carry the diagnostic in Message / InnerException.Message
* a full JSON.stringify would drown it in WatsonBuckets / HResult noise.
* @param {*} data - Response body (object, string, or anything axios parsed)
* @returns {string|null} Truncated human-readable body, or null if empty
*/
_describeResponseBody(data) {
const MAX_BODY_LENGTH = 2000;
if (data == null || data === '') return null;
if (typeof data === 'string') return data.slice(0, MAX_BODY_LENGTH);
if (typeof data === 'object') {
const parts = [];
if (data.ClassName) parts.push(data.ClassName);
if (data.Message) parts.push(data.Message);
let inner = data.InnerException;
while (inner && inner.Message) {
// AggregateException répète souvent le même message dans InnerException
if (inner.Message !== data.Message) parts.push(`Inner: ${inner.Message}`);
inner = inner.InnerException;
}
const text = parts.length > 0 ? parts.join(' — ') : JSON.stringify(data);
return text.slice(0, MAX_BODY_LENGTH);
}
return String(data).slice(0, MAX_BODY_LENGTH);
}
/**
* Build an enriched Error from a failed HTTP call: status, verb, full URL,
* request payload and response body. The WMS puts the real diagnostic
* (compile errors, unknown entity, ...) in the response body without this,
* every failure reads "Request failed with status code 500".
* Never includes headers (Bearer token) payloads passed through post/get
* carry no credentials.
* @param {Error} error - Original axios error
* @param {string} method - HTTP verb ('POST' | 'GET')
* @param {string} url - Full request URL
* @param {*} payload - Request body (POST) or query params (GET)
* @returns {Error} Enriched error (original kept in .cause, status in .status)
*/
_enrichHttpError(error, method, url, payload) {
const status = error.response?.status;
const parts = [`${method} ${url} failed${status != null ? ` (HTTP ${status})` : ''}: ${error.message}`];
if (payload !== undefined && payload !== null) {
let serialized;
try {
serialized = JSON.stringify(payload);
} catch {
serialized = String(payload);
}
if (serialized !== '{}') {
parts.push(`Request payload: ${serialized.slice(0, 1000)}`);
}
}
const body = this._describeResponseBody(error.response?.data);
if (body) parts.push(`Response body: ${body}`);
const enriched = new Error(parts.join('\n'));
enriched.status = status;
enriched.cause = error;
return enriched;
}
/** /**
* Make a POST request to WMS API * Make a POST request to WMS API
* @param {string} endpoint - API endpoint (e.g., '/QueryExecute' or '/AD/api/Workflow/GetByApplication') * @param {string} endpoint - API endpoint (e.g., '/QueryExecute' or '/AD/api/Workflow/GetByApplication')
@@ -162,13 +227,12 @@ class APIService {
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error(`[API] Request failed: ${error.message}`);
// If unauthorized, try refreshing token and retry once // If unauthorized, try refreshing token and retry once
if (error.response?.status === 401) { if (error.response?.status === 401) {
console.error('[API] Unauthorized, refreshing token and retrying...'); console.error('[API] Unauthorized, refreshing token and retrying...');
await this.refreshOAuthToken(); await this.refreshOAuthToken();
try {
const retryResponse = await this.httpClient.post(url, data, { const retryResponse = await this.httpClient.post(url, data, {
headers: { headers: {
'Authorization': `Bearer ${this.token}`, 'Authorization': `Bearer ${this.token}`,
@@ -178,9 +242,16 @@ class APIService {
}); });
return retryResponse.data; return retryResponse.data;
} catch (retryError) {
const enrichedRetry = this._enrichHttpError(retryError, 'POST', url, data);
console.error(`[API] Retry after token refresh failed: ${enrichedRetry.message}`);
throw enrichedRetry;
}
} }
throw error; const enriched = this._enrichHttpError(error, 'POST', url, data);
console.error(`[API] Request failed: ${enriched.message}`);
throw enriched;
} }
} }
@@ -210,13 +281,12 @@ class APIService {
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error(`[API] Request failed: ${error.message}`);
// If unauthorized, try refreshing token and retry once // If unauthorized, try refreshing token and retry once
if (error.response?.status === 401) { if (error.response?.status === 401) {
console.error('[API] Unauthorized, refreshing token and retrying...'); console.error('[API] Unauthorized, refreshing token and retrying...');
await this.refreshOAuthToken(); await this.refreshOAuthToken();
try {
const retryResponse = await this.httpClient.get(url, { const retryResponse = await this.httpClient.get(url, {
params, params,
headers: { headers: {
@@ -226,9 +296,16 @@ class APIService {
}); });
return retryResponse.data; return retryResponse.data;
} catch (retryError) {
const enrichedRetry = this._enrichHttpError(retryError, 'GET', url, params);
console.error(`[API] Retry after token refresh failed: ${enrichedRetry.message}`);
throw enrichedRetry;
}
} }
throw error; const enriched = this._enrichHttpError(error, 'GET', url, params);
console.error(`[API] Request failed: ${enriched.message}`);
throw enriched;
} }
} }
+19 -19
View File
@@ -92,9 +92,12 @@ async function fetchAllWorkflows() {
} }
/** /**
* Search workflows by query string * Search workflows by query string.
* @param {string} query - Search query (matches name, description, etc.) * Real AD keys (lowercase, cf. D5): id, name, version, applicationName,
* @param {string|null} category - Optional category filter * commonInfo no description/code/category field exists.
* @param {string} query - Search query (matches workflow name)
* @param {string|null} category - Optional applicationName filter (the only
* grouping the AD API provides)
* @param {number} limit - Maximum results to return * @param {number} limit - Maximum results to return
*/ */
async function searchWorkflows(query, category = null, limit = 50) { async function searchWorkflows(query, category = null, limit = 50) {
@@ -107,21 +110,16 @@ async function searchWorkflows(query, category = null, limit = 50) {
const lowerQuery = query.toLowerCase(); const lowerQuery = query.toLowerCase();
results = results.filter(w => { results = results.filter(w => {
const name = (w.name || w.Name || '').toLowerCase(); const name = (w.name || w.Name || '').toLowerCase();
const description = (w.description || w.Description || '').toLowerCase(); return name.includes(lowerQuery);
const code = (w.code || w.Code || '').toLowerCase();
return name.includes(lowerQuery) ||
description.includes(lowerQuery) ||
code.includes(lowerQuery);
}); });
} }
// Filter by category if provided // Filter by applicationName if provided
if (category) { if (category) {
const lowerCategory = category.toLowerCase(); const lowerCategory = category.toLowerCase();
results = results.filter(w => { results = results.filter(w => {
const wfCategory = (w.category || w.Category || '').toLowerCase(); const applicationName = (w.applicationName || w.ApplicationName || '').toLowerCase();
return wfCategory.includes(lowerCategory); return applicationName.includes(lowerCategory);
}); });
} }
@@ -156,17 +154,19 @@ async function getWorkflowDetails(workflowId) {
} }
/** /**
* List all workflow categories * List distinct applicationName values.
* Workflows have no category field applicationName is the only grouping the
* AD API provides, and every workflow of the active application carries the
* same value (e.g. "EasyWMS").
*/ */
async function listWorkflowCategories() { async function listWorkflowCategories() {
const workflows = await fetchAllWorkflows(); const workflows = await fetchAllWorkflows();
// Extract unique categories (try both lowercase and uppercase)
const categories = new Set(); const categories = new Set();
workflows.forEach(w => { workflows.forEach(w => {
const category = w.category || w.Category; const applicationName = w.applicationName || w.ApplicationName;
if (category) { if (applicationName) {
categories.add(category); categories.add(applicationName);
} }
}); });
@@ -181,10 +181,10 @@ async function getWorkflowStats() {
const workflows = await fetchAllWorkflows(); const workflows = await fetchAllWorkflows();
const categories = await listWorkflowCategories(); const categories = await listWorkflowCategories();
// Count workflows per category // Count workflows per applicationName (the only grouping in the data)
const categoryCounts = {}; const categoryCounts = {};
workflows.forEach(w => { workflows.forEach(w => {
const cat = w.category || w.Category || 'Uncategorized'; const cat = w.applicationName || w.ApplicationName || '(unknown)';
categoryCounts[cat] = (categoryCounts[cat] || 0) + 1; categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
}); });
+18 -13
View File
@@ -18,11 +18,11 @@ function listTools() {
properties: { properties: {
query: { query: {
type: 'string', type: 'string',
description: 'Search query (searches in name, description, code)', description: 'Search query (searches in workflow name)',
}, },
category: { category: {
type: 'string', type: 'string',
description: 'Filter by workflow category/application', description: 'Filter by applicationName — the only grouping the AD API provides (workflows have no category field). All workflows of the active application share the same value (e.g. "EasyWMS").',
}, },
limit: { limit: {
type: 'number', type: 'number',
@@ -48,7 +48,7 @@ function listTools() {
}, },
{ {
name: 'list_workflow_categories', name: 'list_workflow_categories',
description: 'List all available workflow categories', description: 'List workflow groupings by applicationName. Workflows have no category field in the AD API — applicationName is the only grouping available, and all workflows of the active application share the same value.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: {}, properties: {},
@@ -107,16 +107,20 @@ async function searchWorkflows(args) {
text: JSON.stringify({ text: JSON.stringify({
success: true, success: true,
count: results.length, count: results.length,
workflows: results.map(w => ({ // Clés réelles de l'API AD (minuscules, cf. D5) : id, name, version,
id: w.Id, // applicationName, commonInfo. Pas de code/category/description.
code: w.Code, workflows: results.map(w => {
name: w.Name, const commonInfo = w.commonInfo || w.CommonInfo || {};
category: w.Category, return {
description: w.Description, id: w.id || w.Id,
version: w.Version, name: w.name || w.Name,
created: w.Created, applicationName: w.applicationName || w.ApplicationName,
modified: w.Modified version: w.version || w.Version,
})) createdBy: commonInfo.createdBy,
createDate: commonInfo.createDate,
updateDate: commonInfo.updateDate
};
})
}, null, 2) }, null, 2)
}] }]
}; };
@@ -157,6 +161,7 @@ async function listWorkflowCategories(args) {
type: 'text', type: 'text',
text: JSON.stringify({ text: JSON.stringify({
success: true, success: true,
note: 'Workflows have no category field in the AD API — these are the distinct applicationName values, the only grouping available. All workflows of the active application share the same value.',
totalCategories: categories.length, totalCategories: categories.length,
categories, categories,
stats: { stats: {