Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c5792da52 | |||
| 52b5f90521 | |||
| 03f561fdf7 | |||
| e5614f3b60 | |||
| 3a89c317e8 | |||
| e0bdc1707d | |||
| 3dad5c6088 | |||
| 7c722dae91 |
@@ -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) |
|
||||
| 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/) |
|
||||
|
||||
@@ -86,10 +87,11 @@ docs/
|
||||
⚠️ utilise QueryType 1 : ne pas recopier (D3)
|
||||
```
|
||||
|
||||
**Routage.** `src/index.js` route les appels d'outils **par préfixe de nom**
|
||||
(`name.startsWith('query_wms_')`, `name.includes('_logs')`, …). En ajoutant un
|
||||
outil, vérifiez que son nom tombe dans la bonne branche — sinon il apparaîtra
|
||||
dans `tools/list` mais renverra `Unknown tool`.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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é.
|
||||
2. Traiter le cas dans son `executeTool()`.
|
||||
3. **Vérifier le routage par préfixe** dans `src/index.js` — ou ajouter une
|
||||
branche.
|
||||
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 :
|
||||
@@ -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
|
||||
explicitement écartées.
|
||||
|
||||
⚠️ Deux pièges connus et non encore corrigés, à garder en tête en attendant le
|
||||
lot 1 :
|
||||
⚠️ Piège connu et non encore corrigé, à garder en tête en attendant le lot 2 :
|
||||
|
||||
- `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
|
||||
(`Container` -> `Containers`, mais `Alias` -> `Alias`). Un mauvais nom donne un
|
||||
HTTP 500 dont le détail est aujourd'hui perdu.
|
||||
- `get_entity_metadata` et `list_log_files` sont listés dans `tools/list` mais
|
||||
non routés dans `src/index.js` : ils renvoient `Unknown tool`.
|
||||
HTTP 500 — dont le détail (erreur de compilation LINQ) remonte désormais dans
|
||||
la réponse de l'outil (L1.1).
|
||||
|
||||
@@ -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
|
||||
compromis et changez-les ; à défaut, réécrivez l'historique avant toute
|
||||
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
@@ -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
|
||||
|
||||
### 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
|
||||
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
|
||||
@@ -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
|
||||
faire rejeter.
|
||||
|
||||
### L3.2 — Documentation
|
||||
### L3.2 — Erreurs d'authentification muettes
|
||||
|
||||
- DECISIONS.md : **D21** la règle `TableName`, **D22** le routage par table
|
||||
explicite.
|
||||
`authenticate()` (`api-service.js`) ré-enveloppe l'erreur axios en
|
||||
`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
|
||||
`get_entity_metadata` comme source de vérité.
|
||||
- `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
|
||||
|
||||
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`.
|
||||
|
||||
### 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
|
||||
|
||||
`QueryExecute` accepte un champ **`ClientModule`** que le MCP n'envoie pas.
|
||||
Résultat : ses requêtes apparaissent dans les logs du WMS sous
|
||||
Les requêtes du MCP apparaissent dans les logs du WMS sous
|
||||
`Execute error. Client: GNA` — le client OAuth partagé — donc indistinguables de
|
||||
celles du vrai client GNA.
|
||||
|
||||
Renseigner `ClientModule` (`"MCP-WMS"` ou le nom du profil actif) rend chaque
|
||||
requête du MCP traçable côté serveur. Vérifié : le champ est accepté.
|
||||
**La piste `ClientModule` est invalidée** (mesuré le 24/08/2026, lot 1) : le
|
||||
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
|
||||
|
||||
@@ -293,6 +273,14 @@ plus riche que `/AD/api/Application/GetAll`), `GET /healthcheck?tenantCode=` et
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
@@ -12,6 +12,8 @@ Documents de référence sur EasyWMS et ses API, conservés dans le dépôt pour
|
||||
|
||||
| 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 |
|
||||
| [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 |
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -62,6 +62,38 @@ const metadataTools = require('./tools/metadata-tools.js');
|
||||
const configTools = require('./tools/config-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
|
||||
const server = new Server(
|
||||
{
|
||||
@@ -138,19 +170,10 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
* List all available tools
|
||||
*/
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
const allTools = [
|
||||
...workflowTools.listTools(),
|
||||
...wmsQueryTools.listTools(),
|
||||
...apiTools.listTools(),
|
||||
...logTools.listTools(),
|
||||
...adTools.listTools(),
|
||||
...metadataTools.listTools(),
|
||||
...configTools.listTools(),
|
||||
...profileTools.listTools(),
|
||||
];
|
||||
|
||||
// Servi depuis la table de routage : la liste exposée et le dispatch ne
|
||||
// peuvent pas diverger.
|
||||
return {
|
||||
tools: allTools,
|
||||
tools: Array.from(toolRegistry.values(), entry => entry.definition),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -164,37 +187,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
console.error(`[Server] Executing tool: ${name}`);
|
||||
|
||||
// Route to the appropriate handler based on tool name
|
||||
if (name.startsWith('search_workflows') ||
|
||||
name.startsWith('get_workflow_') ||
|
||||
name.startsWith('list_workflow_')) {
|
||||
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}`);
|
||||
const entry = toolRegistry.get(name);
|
||||
if (!entry) {
|
||||
throw new Error(
|
||||
`Unknown tool: ${name}. Available tools: ${Array.from(toolRegistry.keys()).join(', ')}`
|
||||
);
|
||||
}
|
||||
return await entry.module.executeTool(name, args);
|
||||
} catch (error) {
|
||||
console.error(`[Server] Error executing tool ${name}:`, error.message);
|
||||
return {
|
||||
|
||||
+99
-22
@@ -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
|
||||
* @param {string} endpoint - API endpoint (e.g., '/QueryExecute' or '/AD/api/Workflow/GetByApplication')
|
||||
@@ -162,25 +227,31 @@ class APIService {
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`[API] Request failed: ${error.message}`);
|
||||
|
||||
// If unauthorized, try refreshing token and retry once
|
||||
if (error.response?.status === 401) {
|
||||
console.error('[API] Unauthorized, refreshing token and retrying...');
|
||||
await this.refreshOAuthToken();
|
||||
|
||||
const retryResponse = await this.httpClient.post(url, data, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
try {
|
||||
const retryResponse = await this.httpClient.post(url, data, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
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,25 +281,31 @@ class APIService {
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`[API] Request failed: ${error.message}`);
|
||||
|
||||
// If unauthorized, try refreshing token and retry once
|
||||
if (error.response?.status === 401) {
|
||||
console.error('[API] Unauthorized, refreshing token and retrying...');
|
||||
await this.refreshOAuthToken();
|
||||
|
||||
const retryResponse = await this.httpClient.get(url, {
|
||||
params,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
try {
|
||||
const retryResponse = await this.httpClient.get(url, {
|
||||
params,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,9 +92,12 @@ async function fetchAllWorkflows() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search workflows by query string
|
||||
* @param {string} query - Search query (matches name, description, etc.)
|
||||
* @param {string|null} category - Optional category filter
|
||||
* Search workflows by query string.
|
||||
* Real AD keys (lowercase, cf. D5): id, name, version, applicationName,
|
||||
* 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
|
||||
*/
|
||||
async function searchWorkflows(query, category = null, limit = 50) {
|
||||
@@ -107,21 +110,16 @@ async function searchWorkflows(query, category = null, limit = 50) {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
results = results.filter(w => {
|
||||
const name = (w.name || w.Name || '').toLowerCase();
|
||||
const description = (w.description || w.Description || '').toLowerCase();
|
||||
const code = (w.code || w.Code || '').toLowerCase();
|
||||
|
||||
return name.includes(lowerQuery) ||
|
||||
description.includes(lowerQuery) ||
|
||||
code.includes(lowerQuery);
|
||||
return name.includes(lowerQuery);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by category if provided
|
||||
// Filter by applicationName if provided
|
||||
if (category) {
|
||||
const lowerCategory = category.toLowerCase();
|
||||
results = results.filter(w => {
|
||||
const wfCategory = (w.category || w.Category || '').toLowerCase();
|
||||
return wfCategory.includes(lowerCategory);
|
||||
const applicationName = (w.applicationName || w.ApplicationName || '').toLowerCase();
|
||||
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() {
|
||||
const workflows = await fetchAllWorkflows();
|
||||
|
||||
// Extract unique categories (try both lowercase and uppercase)
|
||||
const categories = new Set();
|
||||
workflows.forEach(w => {
|
||||
const category = w.category || w.Category;
|
||||
if (category) {
|
||||
categories.add(category);
|
||||
const applicationName = w.applicationName || w.ApplicationName;
|
||||
if (applicationName) {
|
||||
categories.add(applicationName);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -181,10 +181,10 @@ async function getWorkflowStats() {
|
||||
const workflows = await fetchAllWorkflows();
|
||||
const categories = await listWorkflowCategories();
|
||||
|
||||
// Count workflows per category
|
||||
// Count workflows per applicationName (the only grouping in the data)
|
||||
const categoryCounts = {};
|
||||
workflows.forEach(w => {
|
||||
const cat = w.category || w.Category || 'Uncategorized';
|
||||
const cat = w.applicationName || w.ApplicationName || '(unknown)';
|
||||
categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
|
||||
});
|
||||
|
||||
|
||||
+18
-13
@@ -18,11 +18,11 @@ function listTools() {
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query (searches in name, description, code)',
|
||||
description: 'Search query (searches in workflow name)',
|
||||
},
|
||||
category: {
|
||||
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: {
|
||||
type: 'number',
|
||||
@@ -48,7 +48,7 @@ function listTools() {
|
||||
},
|
||||
{
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
@@ -107,16 +107,20 @@ async function searchWorkflows(args) {
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: results.length,
|
||||
workflows: results.map(w => ({
|
||||
id: w.Id,
|
||||
code: w.Code,
|
||||
name: w.Name,
|
||||
category: w.Category,
|
||||
description: w.Description,
|
||||
version: w.Version,
|
||||
created: w.Created,
|
||||
modified: w.Modified
|
||||
}))
|
||||
// Clés réelles de l'API AD (minuscules, cf. D5) : id, name, version,
|
||||
// applicationName, commonInfo. Pas de code/category/description.
|
||||
workflows: results.map(w => {
|
||||
const commonInfo = w.commonInfo || w.CommonInfo || {};
|
||||
return {
|
||||
id: w.id || w.Id,
|
||||
name: w.name || w.Name,
|
||||
applicationName: w.applicationName || w.ApplicationName,
|
||||
version: w.version || w.Version,
|
||||
createdBy: commonInfo.createdBy,
|
||||
createDate: commonInfo.createDate,
|
||||
updateDate: commonInfo.updateDate
|
||||
};
|
||||
})
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
@@ -157,6 +161,7 @@ async function listWorkflowCategories(args) {
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
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,
|
||||
categories,
|
||||
stats: {
|
||||
|
||||
Reference in New Issue
Block a user