660c62c32c
Cas reel du 25/08/2026 : une session Cowork cherchant des workflows CST_ sans
passer application="CustomApp" a conclu que l'AD n'en contenait aucun -- alors
que CustomApp en porte 153. Le parametre application existait bien (D26) et
etait documente ; ce qui manquait, c'est que RIEN dans la reponse ne disait
qu'on n'avait regarde qu'une application sur neuf. Un defaut silencieux se lit
comme une exhaustivite.
search_workflows et search_ad_elements rappellent desormais TOUJOURS
l'application effectivement interrogee (plus seulement quand le parametre a ete
passe), et ajoutent un hint quand la recherche revient vide.
Seuil a 0 resultat, pas "peu" : toute valeur non nulle produirait un hint
parasite sur une recherche legitimement etroite, et le mode d'echec observe est
bien le zero pris pour une absence.
Le hint nomme les autres applications depuis la liste allegee DEJA en cache ;
sans elle il reste generique et renvoie vers list_workflow_categories. Aucun
appel reseau n'est fait pour construire un hint -- ce serait exactement le
prechargement que D26 interdit.
Verifie en execution (protocole, LIMAGRAIN) :
search_workflows {"query":"CST_"} 404 chars
application: "EasyWMS", count: 0, hint nommant CustomApp et renvoyant
vers list_workflow_categories
search_workflows {"query":"CST_"} apres
list_workflow_categories 461 chars
meme hint, enrichi de la liste en cache : Common, Notifications, SmartUI,
WarehouseWebDesigner, GalileoFaults, CustomApp, User, AGV
search_workflows {"query":"CST_","application":"CustomApp"}
count: 44, application: "CustomApp" -- dont CST_SendRejectContainersToPK
search_workflows {"query":"stacker"} 14 192 -> 14 220 chars
count: 50 inchange, application: "EasyWMS" presente, aucun hint parasite
search_ad_elements Command "CST_" 457 chars
application: "EasyWMS", count: 0, meme hint
stderr : aucun fetch d'une application non demandee. Seules EasyWMS et
CustomApp sont chargees, chacune sur demande explicite.
Rebouclage complet apres modification des schemas : 23 outils, 6 resources,
les 23 noms de tools/list atteignent leur module (aucun "Unknown tool" ;
execute_command verifie statiquement, non appele car il ecrit dans le WMS),
D23 rejette toujours un parametre inconnu sur les quatre schemas modifies,
npm test 4/4 en code 0.
Lot 5 retire de la ROADMAP.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
648 lines
30 KiB
Markdown
648 lines
30 KiB
Markdown
# Décisions d'architecture et pièges vérifiés
|
|
|
|
Ce fichier consigne **pourquoi** le code est écrit comme il l'est. Chaque entrée
|
|
décrit une décision prise ou un piège constaté **sur un WMS réel** — pas une
|
|
supposition. Avant de « corriger » un comportement qui paraît étrange, cherchez-le
|
|
ici : il est probablement volontaire.
|
|
|
|
Convention : une décision reste dans le fichier même si elle est révisée ; on
|
|
ajoute alors une entrée `Révisée le …` plutôt que de réécrire l'histoire.
|
|
|
|
---
|
|
|
|
## D1 — 100 % API, aucun accès Oracle direct
|
|
|
|
**Décision.** Toutes les données transitent par les API REST du WMS. Aucune
|
|
connexion base de données.
|
|
|
|
**Pourquoi.** Le serveur MCP doit fonctionner depuis un poste ou une VM sans
|
|
credentials Oracle, sans client Oracle installé, et sans risque d'écriture
|
|
directe en base. L'API impose en prime les règles métier et les droits du
|
|
compte utilisé.
|
|
|
|
**Conséquence.** Les fichiers `src/services/oracle-service.js`,
|
|
`src/resources/database.js` et `src/tools/database-tools.js` ont été supprimés
|
|
(ils étaient de toute façon morts : non branchés, `oracledb` n'était même pas
|
|
une dépendance). **Ne pas les réintroduire.** Si une donnée n'est pas
|
|
atteignable par API, elle est hors périmètre — voir D16.
|
|
|
|
---
|
|
|
|
## D2 — OAuth : `tenant_code` est obligatoire
|
|
|
|
**Piège.** L'endpoint `/EasySTS/OAuth/Token` répond `400 Bad Request` si le
|
|
paramètre `tenant_code` est absent, sans message explicite.
|
|
|
|
**Solution.** Le corps du grant `password` contient toujours les quatre
|
|
paramètres :
|
|
|
|
```
|
|
grant_type=password&tenant_code=<TENANT>&username=<USER>&password=<PASS>
|
|
```
|
|
|
|
Voir `src/services/api-service.js`, méthode `authenticate()`.
|
|
|
|
---
|
|
|
|
## D3 — `QueryType: 0` (Reading), pas 1
|
|
|
|
**Piège.** `QueryType` (type `QueryContextType`) sélectionne le contexte de
|
|
données interrogé. Il a **quatre** valeurs, pas deux — vérifiées une à une sur
|
|
le tenant `LIMAGRAI2512` :
|
|
|
|
| Valeur | Contexte | Statut sur ce tenant |
|
|
|---|---|---|
|
|
| `0` | **Reading** — `ApplicationReadingContext` | opérationnel, champs de statut en **chaînes** (`"Release"`) |
|
|
| `1` | Writing — `ApplicationWritingRepository` | opérationnel, champs de statut en **énumérations** |
|
|
| `2` | DataWarehouse | **non configuré** : `Could not resolve serviceType 'IDataWarehouse…'` |
|
|
| `3` | Metrics — `ApplicationMetricDataContext` | contexte présent, modèle de données non exploré |
|
|
|
|
Les comparaisons de statut par chaîne — de loin le cas le plus courant en
|
|
debug — **échouent** en `QueryType: 1`. Le code force donc `0` dans
|
|
`executeQuery()` et `executeScalarQuery()`.
|
|
|
|
Le message d'erreur nomme le contexte (`ApplicationReadingContext`,
|
|
`ApplicationWritingRepository`, …) : c'est le moyen le plus rapide de savoir
|
|
quel `QueryType` a réellement été utilisé.
|
|
|
|
**Attention.** D'anciens exemples (dont le PHP de référence) utilisent `1`. Ne
|
|
les recopiez pas.
|
|
|
|
---
|
|
|
|
## D4 — Les API AD renvoient `{ entities: [...] }`, pas un tableau
|
|
|
|
**Piège.** `POST /AD/api/{Type}/GetByApplication` renvoie un objet enveloppe. Un
|
|
code qui traite la réponse comme un tableau obtient silencieusement
|
|
`0 élément` — le symptôme historique était « Successfully cached 0 workflows ».
|
|
|
|
**Solution.** Toujours extraire : `response?.entities || []`.
|
|
|
|
---
|
|
|
|
## D5 — Les propriétés arrivent en minuscules *ou* en majuscules
|
|
|
|
**Piège.** Selon le type d'élément et la version du WMS, l'API renvoie `name`
|
|
ou `Name`, `id` ou `Id`.
|
|
|
|
**Solution.** Systématiquement `const name = w.name || w.Name || ''` avant tout
|
|
filtrage ou tri. Une recherche qui « ne trouve pas » un élément qui existe est
|
|
presque toujours ce bug.
|
|
|
|
---
|
|
|
|
## D6 — dotenv doit écrire sur stderr
|
|
|
|
**Piège.** dotenv affiche une bannière de version sur **stdout**. Or le
|
|
protocole MCP réserve stdout au JSON : Claude Desktop échoue alors avec
|
|
`Unexpected token 'd', "[dotenv@17."... is not valid JSON`.
|
|
|
|
**Solution.** `src/index.js` détourne `process.stdout.write` vers stderr le
|
|
temps du chargement de dotenv, puis le restaure.
|
|
|
|
**Règle générale.** Dans tout le projet, on log avec `console.error()`.
|
|
**Jamais** `console.log()`.
|
|
|
|
---
|
|
|
|
## D7 — Le `.env` est lu à côté de l'exécutable quand le serveur est packagé
|
|
|
|
**Décision.** `src/index.js` résout le chemin du `.env` selon le contexte :
|
|
|
|
| Contexte | Chemin du `.env` |
|
|
|---|---|
|
|
| Sources (`npm start`) | racine du projet |
|
|
| Exécutable pkg (`process.pkg`) | dossier de `process.execPath` |
|
|
|
|
**Pourquoi.** Avec un chemin statique, pkg **embarque le `.env` dans le
|
|
snapshot** de l'exe : les credentials sont figés dans le binaire et
|
|
reconfigurer un déploiement impose un rebuild. Le chemin dynamique via
|
|
`process.execPath` empêche pkg de le détecter, donc rien n'est embarqué, et
|
|
`dist/.env` devient le fichier de configuration du déploiement.
|
|
|
|
**Vérification.** Sans `.env` à côté de l'exe, le serveur démarre en
|
|
avertissant `WMS_PROFILES is empty` — preuve qu'aucune valeur n'est embarquée.
|
|
|
|
---
|
|
|
|
## D8 — Multi-profils au runtime plutôt qu'un serveur MCP par WMS
|
|
|
|
**Décision.** Un seul serveur MCP dessert plusieurs backends WMS ; Claude bascule
|
|
avec `switch_wms_profile`.
|
|
|
|
**Pourquoi.** L'alternative — une entrée par client dans
|
|
`claude_desktop_config.json` — multiplie les processus, les jeux de credentials
|
|
et les caches, pour un usage où l'on ne consulte qu'un WMS à la fois.
|
|
|
|
**Conséquence.** L'état actif est **global au processus**. Un changement de
|
|
profil doit invalider tout ce qui dépend du tenant. Les services s'abonnent via
|
|
`profileManager.onSwitch()` :
|
|
|
|
| Service | Réaction au switch |
|
|
|---|---|
|
|
| `api-service` | `resetToken()` — le token OAuth appartient au tenant précédent |
|
|
| `workflow-service` | `clearCache()` |
|
|
| `ad-service` | `invalidateCache()` — tous les types |
|
|
|
|
**Ne jamais** appeler ces invalidations à la main depuis un autre module :
|
|
l'abonnement suffit, et le doublon masquerait un oubli d'abonnement.
|
|
|
|
**Sans profil actif** (`DEFAULT_WMS_PROFILE` absent ou invalide), `getCurrent()`
|
|
lève une erreur qui **énumère les profils disponibles**. C'est intentionnel :
|
|
Claude lit ce message et enchaîne sur `switch_wms_profile` au lieu d'échouer.
|
|
|
|
---
|
|
|
|
## D9 — Profils SaaS : accès aux logs refusé, pas silencieux
|
|
|
|
**Décision.** Quand `<PROFIL>_SAAS=true`, `read_recent_logs`, `search_logs` et
|
|
`list_log_files` **lèvent une erreur explicite** renvoyant vers les outils API.
|
|
|
|
**Pourquoi.** Le WMS est hébergé dans le cloud Mecalux : le partage
|
|
`\\<host>\inetpub\logs\...` n'est pas joignable. Retourner « 0 fichier » ferait
|
|
croire à une absence d'erreurs dans les logs, ce qui est un faux négatif
|
|
dangereux en diagnostic. Voir [docs/logs.md](docs/logs.md).
|
|
|
|
---
|
|
|
|
## D10 — Chargement paresseux + cache 1 h
|
|
|
|
**Décision.** Workflows et éléments AD ne sont **pas** chargés au démarrage,
|
|
mais à la première requête qui les concerne, puis mis en cache
|
|
(`WORKFLOW_CACHE_TTL`, 3 600 000 ms par défaut).
|
|
|
|
**Pourquoi.** L'ensemble représente ~38 800 éléments dont 29 374 `Resource` :
|
|
tout charger au boot ferait échouer le handshake MCP par timeout, pour des
|
|
données souvent inutiles à la session.
|
|
|
|
**Pagination.** La taille de page est réglée **par type** dans
|
|
`AD_ELEMENT_TYPES` (`src/services/ad-service.js`) : `View: 200`,
|
|
`Workflow: 5000`, `Resource: 15000`, tout le reste `100000` (soit une seule
|
|
page). Ces valeurs viennent de l'observation des timeouts serveur — les
|
|
augmenter à l'aveugle fait échouer les types lourds.
|
|
|
|
---
|
|
|
|
## D11 — Les paramètres système : `Parameter` + `ParamValue`, fusionnés en JS
|
|
|
|
**Piège.** Le modèle Reading ne contient **pas** d'entité
|
|
`CommandParameterData`. La configuration se lit dans deux entités :
|
|
|
|
| Entité | Contenu |
|
|
|---|---|
|
|
| `Parameter` | définition + `DefaultValue` |
|
|
| `ParamValue` | surcharges par entrepôt, liées par `ParameterId` |
|
|
|
|
**Décision.** `get_system_parameters` charge les deux intégralement (~200 et
|
|
~50 lignes) et fait la fusion **côté JavaScript**, en exposant la *valeur
|
|
effective* par entrepôt (surcharge si présente, défaut sinon).
|
|
|
|
**Pourquoi côté JS.** Les filtres (`warehouse`, `param_class`, `search`,
|
|
`only_overridden`) sont appliqués en JS pour éviter toute concaténation de
|
|
chaîne LINQ à partir d'entrées utilisateur — pas d'injection possible, et pas
|
|
de dépendance aux limites du traducteur LINQ (D12).
|
|
|
|
Fichier : `src/tools/config-tools.js`.
|
|
|
|
---
|
|
|
|
## D12 — Les dates relatives ne sont pas traduisibles en LINQ
|
|
|
|
**Piège vérifié en production.** `DateTime.Now`, `DateTime.Today` et
|
|
`AddDays()` ne sont **pas** traduits par le moteur de requêtes : la requête
|
|
échoue à la compilation.
|
|
|
|
**Solution.** Toujours une date littérale :
|
|
|
|
```csharp
|
|
Context.OutboundOrders.Where(z => z.CreationDate > new DateTime(2026, 8, 1))
|
|
```
|
|
|
|
C'est à l'appelant (donc à Claude) de calculer la date avant d'écrire la
|
|
requête.
|
|
|
|
---
|
|
|
|
## D13 — `select_expression` reste instable
|
|
|
|
**État.** Les projections passées via le paramètre API `Select` déclenchent des
|
|
erreurs de compilation côté serveur.
|
|
|
|
**Contournement actuel.** Interroger les lignes complètes et filtrer les
|
|
colonnes côté client.
|
|
|
|
**Non résolu.** C'est le principal point ouvert du projet. Toute tentative de
|
|
correction doit être validée sur un vrai WMS avant d'être documentée ici.
|
|
|
|
---
|
|
|
|
## D14 — `executeCommand` : pas de suffixe d'assembly
|
|
|
|
**Piège.** Ajouter `, Mecalux.ITSW.EasyWMS.Modules.Contracts` au nom de commande
|
|
provoque une `FileLoadException`.
|
|
|
|
**Solution.** Utiliser le `command_name` **tel quel** :
|
|
l'`InternalCommandName` fourni par l'AD contient déjà le nom pleinement
|
|
qualifié correct.
|
|
|
|
---
|
|
|
|
## D15 — Validation TLS désactivée
|
|
|
|
**Décision.** `httpsAgent: new https.Agent({ rejectUnauthorized: false })`.
|
|
|
|
**Pourquoi.** Les WMS on-premise sont exposés en HTTPS avec un certificat
|
|
auto-signé sur une IP privée.
|
|
|
|
**Limite assumée.** Acceptable sur réseau interne ou via VPN. Sur un profil
|
|
SaaS joint par Internet, cela supprime la protection contre l'interception —
|
|
à revoir si l'outil sort du cadre du diagnostic interne.
|
|
|
|
---
|
|
|
|
## D16 — Historique des shipment templates : hors périmètre
|
|
|
|
**Constat.** L'entité Reading `ShipmentTemplate` n'expose que la **dernière**
|
|
exécution (`LastExecuteDate`, `Status`, `IsEnabled`).
|
|
|
|
L'historique complet n'existe que dans les logs texte
|
|
`ApplyShipmentTemplates`, **absents de l'hôte joignable** (`10.255.255.2`) :
|
|
ils résident sur les serveurs de production / ETL des clients.
|
|
|
|
**Décision.** Aucun outil d'analyse de ces logs n'a été construit — il n'aurait
|
|
rien à lire. Les recettes purement API sont dans la resource
|
|
`wms://query-examples`.
|
|
|
|
---
|
|
|
|
## D17 — `WorkflowAction` et `WritingModel` retirés de la liste AD
|
|
|
|
**Constat.** Les endpoints `/AD/api/WorkflowAction/GetByApplication` et
|
|
`/AD/api/WritingModel/GetByApplication` répondent `404 Not Found`.
|
|
|
|
**Décision.** Ces deux types sont sortis de `AD_ELEMENT_TYPES` : il en reste
|
|
**20**, dont 3 valides mais vides (`Dashboard`, `TimelineTemplate`, `Toggle`).
|
|
Détail de la campagne de validation :
|
|
[docs/ad-api-validation.md](docs/ad-api-validation.md).
|
|
|
|
---
|
|
|
|
## D18 — Build : `@yao-pkg/pkg` ciblant node22
|
|
|
|
**Décision.** Le build utilise `@yao-pkg/pkg` (fork maintenu de `pkg`, archivé
|
|
depuis) avec la cible **`node22-win-x64`**.
|
|
|
|
**Pourquoi cette cible.** `node20-win-x64` n'a pas de binaire prébuilt
|
|
disponible : pkg bascule alors sur une compilation de Node depuis les sources,
|
|
qui échoue faute de `vcbuild.bat` (toolchain MSVC absente).
|
|
|
|
**Avertissements normaux au build.** `Cannot find module
|
|
'@modelcontextprotocol/sdk/server/index.js'` et `Entry 'main' not found` :
|
|
pkg ne sait pas résoudre statiquement la table `exports` du SDK. L'exécutable
|
|
produit **fonctionne** — vérifié en démarrant l'exe. Ne pas chercher à
|
|
« corriger » ces avertissements.
|
|
|
|
---
|
|
|
|
## D19 — La resource `docs://` a été supprimée
|
|
|
|
**Constat.** `src/resources/documentation.js` (193 lignes) exposait un index des
|
|
`.md` de `docs/`, mais n'a **jamais été branché** dans `src/index.js` : le
|
|
handler `resources/list` ne l'incluait pas et `resources/read` ne routait aucune
|
|
URI `docs://`. `docs/README.md` promettait pourtant la fonctionnalité aux
|
|
utilisateurs.
|
|
|
|
**Décision.** Fichier supprimé, `docs/README.md` corrigé. `docs/` reste un
|
|
dossier de référence pour les humains et pour un agent qui lit le dépôt — pas
|
|
une resource MCP.
|
|
|
|
**Si on veut la fonctionnalité un jour**, il faut la brancher réellement (2
|
|
lignes dans `src/index.js`) *et* décider de son sort dans l'exécutable pkg, qui
|
|
n'embarque pas `docs/`.
|
|
|
|
---
|
|
|
|
## D20 — Purge du dépôt (2026-08-24)
|
|
|
|
Supprimés lors du nettoyage :
|
|
|
|
| Élément | Raison |
|
|
|---|---|
|
|
| 5 `.md` dupliqués à la racine | copies md5-identiques de `docs/api/` et `docs/entities/` |
|
|
| `JANITOR_main.js`, `JANITOR_entities.json` | application Electron sans lien avec le MCP |
|
|
| `temp/*.json` | dumps de workflows versionnés par accident (`temp/` désormais ignoré) |
|
|
| `claude_desktop_config_ssh.json` | **mots de passe en clair** + variables `ORACLE_*` de l'architecture supprimée (D1) |
|
|
| `IMPLEMENTATION_SUMMARY.md` | doublon d'`AD_API_TEST_RESULTS.md`, déplacé en `docs/ad-api-validation.md` |
|
|
| `src/resources/documentation.js` | code mort (D19) |
|
|
| `src/config/constants.js` (83 l.) | module entier inutilisé : `require` présent dans `wms-query-service.js`, mais **aucune** de ses constantes n'était lue |
|
|
| `log-service.js` : `findRecentErrors`, `readFullLog`, `getLogStats` (~100 l.) | exportées, jamais appelées — aucun outil MCP ne les exposait |
|
|
| `RESOURCE_URIS.WORKFLOWS_CATEGORIES` | URI déclarée, jamais servie |
|
|
| `LOG_FILE_PATTERN` | lue depuis `.env`, jamais utilisée (le scan filtre sur `.log` en dur) |
|
|
|
|
Les trois fonctions de `log-service.js` étaient fonctionnelles ; si l'une d'elles
|
|
redevient utile (`findRecentErrors` en particulier), la reprendre depuis le
|
|
commit `b59cbb3` et **l'exposer réellement** comme outil MCP plutôt que de la
|
|
laisser inatteignable.
|
|
|
|
⚠️ **Credentials à faire tourner.** `claude_desktop_config_ssh.json` et
|
|
l'ancienne version de `test-ad-api.ps1` contenaient des mots de passe en clair.
|
|
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.
|
|
|
|
---
|
|
|
|
## D21 — `Context.{...}` attend le `TableName` du Metadata, résolu par service
|
|
|
|
**Piège.** Les expressions LINQ de `QueryExecute` référencent les entités par
|
|
le `TableName` de l'API Metadata, **pas** par le nom d'entité de l'Application
|
|
Dictionary. Ce n'est pas une pluralisation : `Container` -> `Containers`, mais
|
|
`Alias` -> `Alias` (invariant), et `Item` n'existe pas. Un nom faux part en
|
|
HTTP 500 (erreur de compilation `'ApplicationReadingContext' ne contient pas
|
|
de définition pour '...'`). Seul `TableName` fait foi — **ne réinventez pas de
|
|
règle grammaticale**.
|
|
|
|
**Décision.** `src/services/entity-resolver.js` construit une table
|
|
`Name | TableName (insensible à la casse) -> TableName` et tous les points
|
|
d'interpolation (`wms-query-service`, `call_query_api`) passent par elle.
|
|
Mesures du 24/08/2026 (`LIMAGRAI2512`) :
|
|
|
|
- Le contexte de lecture est **commun au tenant** : la table agrège le
|
|
Metadata de toutes les applications. La liste vient de
|
|
`GET /configuration/applications` (5 applications déployées avec version) —
|
|
les applications EasyBuilder sans contexte requêtable (`CustomApp`…) n'y
|
|
figurent pas et ne fournissent de toute façon **0 entité** Metadata.
|
|
- 288 `TableName` distincts, aucun conflit `Name -> TableName` entre
|
|
applications.
|
|
|
|
Comportements :
|
|
|
|
- **Nom inconnu** : échec avant tout appel réseau de requête, message avec
|
|
suggestions proches et renvoi vers `get_entity_metadata`.
|
|
- **Metadata injoignable** : le nom passe tel quel (comportement historique)
|
|
et la réponse porte un `warning` — on ne bloque pas tout le serveur pour un
|
|
cache irrécupérable.
|
|
- Cache : TTL partagé (`WORKFLOW_CACHE_TTL`), chargement paresseux,
|
|
invalidation par abonnement `onSwitch()` (D8, D10).
|
|
|
|
---
|
|
|
|
## 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.
|
|
|
|
---
|
|
|
|
## D23 — Le SDK ne valide pas les arguments : validation dans le wrapper
|
|
|
|
**Piège mesuré (24/08/2026).** Le SDK MCP (`@modelcontextprotocol/sdk` 1.x)
|
|
ne valide **pas** les arguments d'appel contre l'`inputSchema` déclaré :
|
|
`additionalProperties: false` est ignoré, et un paramètre inconnu
|
|
(`read_recent_logs(lines: 60)`) retombe silencieusement sur les défauts
|
|
(`count = 100`) sans le moindre signal.
|
|
|
|
**Décision.** Le wrapper `tools/call` de `src/index.js` valide chaque appel
|
|
contre le schéma de la table de routage (D22) avant le dispatch — schéma
|
|
déclaré = contrat appliqué, pour les 23 outils d'un coup :
|
|
|
|
- **paramètre inconnu** → erreur structurée nommant le paramètre fautif **et**
|
|
les paramètres valides de l'outil ;
|
|
- **paramètre `required` manquant** → même forme d'erreur.
|
|
|
|
Les 23 schémas portent aussi `additionalProperties: false` : inerte côté SDK,
|
|
mais c'est le contrat que lisent les clients. La validation reste volontairement
|
|
superficielle (noms et présence, pas les types) : le but est de supprimer le
|
|
silence, pas de réimplémenter JSON Schema.
|
|
|
|
Le renommage des paramètres (`entity_type`/`query` uniformisés) a été **écarté**
|
|
au profit de cette validation — voir ROADMAP « Écarté ».
|
|
|
|
---
|
|
|
|
## D24 — Contrat de troncature : borné + signalé, jamais un rejet silencieux
|
|
|
|
**Piège mesuré (24-25/08/2026).** Une réponse d'outil de ~70 000 caractères
|
|
(`get_system_parameters` sans filtre ; `search_logs` atteignait 52-56 000 avec
|
|
les seuls défauts) est **rejetée par le client MCP** — l'utilisateur voit un
|
|
échec opaque au lieu d'un résultat partiel.
|
|
|
|
**Décision.** Tout outil susceptible de produire une sortie volumineuse borne
|
|
sa réponse et **signale** la coupe. Le signal est commun :
|
|
|
|
| Champ | Sémantique |
|
|
|---|---|
|
|
| `truncated: true` | présent **uniquement** quand la réponse a été coupée — jamais `truncated: false` |
|
|
| `hint` | présent ssi `truncated` ; actionnable : dit comment continuer (`offset` suivant) ou réduire (filtres, `context_lines`…) |
|
|
| `returned` | nombre d'éléments effectivement renvoyés |
|
|
| total (`totalParameters`, `totalResults`, `dataTotalChars`) | total **avant** la coupe — `truncated` se vérifie donc depuis la réponse elle-même |
|
|
|
|
Les mécanismes restent **volontairement locaux**, car ils diffèrent :
|
|
`get_system_parameters` pagine (`limit`/`offset` au schéma — rien n'est perdu,
|
|
on continue avec l'offset suivant) ; `search_logs` plafonne le volume
|
|
(`MAX_LOG_SEARCH_CHARS`, défaut 25 000 caractères) en écartant des résultats
|
|
**entiers** — jamais coupés au milieu de leurs lignes de contexte — et annonce
|
|
en plus `omitted`, le compte écarté. Ces deux-là ne partagent pas de helper : le
|
|
factoriser forcerait une abstraction commune à deux mécanismes qui n'en ont pas.
|
|
Les **trois outils de requête**, eux, partagent le même mécanisme — ils
|
|
partagent donc `src/services/response-limit.js` (voir ci-dessous). Le critère
|
|
est le mécanisme, pas le nombre d'appelants.
|
|
|
|
**Périmètre étendu (lot 5, 25/08/2026).** Trois familles d'outils dépassaient
|
|
encore le seuil, toutes mesurées sur `LIMAGRAI2512` :
|
|
|
|
`get_workflow_details` **fenêtre le blob `data`** (`max_data_chars`, défaut
|
|
20 000 ; `data_offset`, défaut 0) — 79 092 caractères pour un StackerCrane
|
|
(dont 71 512 de blob), 101 816 pour `CST_SendRejectContainersToPK` (92 362 de
|
|
blob), ramenés à ~23 000. La tranche est **verbatim** : découpe de chaîne, rien
|
|
d'autre. Ne jamais résumer, reformuler ni « parser » cette définition
|
|
EasyBuilder — la concaténation des tranches dans l'ordre des offsets doit la
|
|
reconstituer à l'octet près (vérifié : 20 000 + 20 000 + 20 000 + 11 512 =
|
|
71 512, concaténation identique au blob d'origine). Les métadonnées du workflow
|
|
restent complètes dans chaque tranche ; seul `data` est fenêtré, et
|
|
`dataTotalChars` est porté par **toute** réponse — y compris non tronquée, où
|
|
la seule différence avec l'ancienne réponse est ces trois champs de fenêtre
|
|
(+65 caractères mesurés).
|
|
|
|
`query_wms_entities`, `call_query_api` et `search_wms_data` **plafonnent leur
|
|
volume** (`MAX_QUERY_RESPONSE_CHARS`, défaut 25 000 — même ordre de grandeur que
|
|
`MAX_LOG_SEARCH_CHARS`) en écartant des **lignes entières**, via le helper
|
|
commun `src/services/response-limit.js` (recherche dichotomique : ~8
|
|
constructions au lieu de 200 retraits ligne à ligne sur des charges utiles de
|
|
~1 Mo) :
|
|
|
|
| Appel | Avant | Après |
|
|
|---|---:|---:|
|
|
| `query_wms_entities("Products", limit: 200)` | 957 234 | 24 432 (5 lignes sur 200) |
|
|
| `search_wms_data("PAL")` | 847 543 | 22 992 (4 résultats sur 150) |
|
|
| `call_query_api("Products", query_type: 1, limit: 1)` | 95 288 | 738 |
|
|
|
|
Trois points de cadrage, tous vérifiés en exécution :
|
|
|
|
- **Sous le plafond, rien ne change.** Aucun champ ajouté, réponse identique
|
|
**octet pour octet** (mesuré sur `query_wms_entities("Container", limit: 1)`,
|
|
`call_query_api`, `get_entity_schema`, `search_wms_data` sous plafond).
|
|
`MAX_QUERY_ROWS` et les limites par défaut des outils sont inchangés :
|
|
le correctif est le bornage signalé, pas une réduction silencieuse.
|
|
- **Cas limite : une seule ligne dépasse le plafond.** Réel en Writing —
|
|
`call_query_api("Products", query_type: 1, limit: 1)` répond `returned: 0`,
|
|
`omitted: 1`, `truncated: true`, avec un hint qui explique le volume Writing
|
|
et renvoie vers Reading. C'est moins bon qu'un résultat, mais c'est mieux
|
|
qu'un rejet client opaque.
|
|
- **`search_wms_data` répartit en tourniquet** les résultats gardés entre les
|
|
entités, et porte `returned`/`omitted` par entité en plus des totaux. Sans
|
|
cela, une entité volumineuse placée en tête consommerait tout le budget et
|
|
les suivantes reviendraient à zéro résultat sans que rien ne le dise —
|
|
exactement le faux négatif que corrige L5.4.
|
|
|
|
`count_wms_entities` n'est pas concerné (`QueryScalarExecute` renvoie un
|
|
scalaire), et son `query_type` ne porte donc pas l'avertissement de volume
|
|
ajouté aux deux autres.
|
|
|
|
Deux garde-fous de cadrage :
|
|
|
|
- **Ne pas réduire les défauts existants** (`max_results` 50, `context_lines` 2)
|
|
pour passer sous le plafond : le correctif est le bornage signalé, pas un
|
|
changement silencieux de comportement.
|
|
- La taille qui fait foi est celle de `content[0].text` **mesurée via le
|
|
protocole**, pas une estimation. Ordre de grandeur cible : ~20-25 000
|
|
caractères par réponse.
|
|
|
|
Au passage, `totalParameters` a changé de sens : c'était le nombre brut
|
|
d'entités `Parameter` chargées, c'est désormais le total correspondant aux
|
|
filtres avant pagination (identique sans filtre).
|
|
|
|
---
|
|
|
|
## D25 — `query_type` : opt-in explicite, D3 reste la règle par défaut
|
|
|
|
**Contexte (mesures des 24-25/08/2026, `LIMAGRAI2512`).** `QueryType` était
|
|
figé à `0` en dur dans `executeQuery()` et `executeScalarQuery()`, rendant le
|
|
modèle Writing — opérationnel et mesuré (`Context.Products` en `QueryType: 1`
|
|
répond) — inatteignable.
|
|
|
|
**Décision.** Un paramètre `query_type` (entier, défaut `0`) est exposé sur
|
|
**trois outils** : `call_query_api`, `query_wms_entities`,
|
|
`count_wms_entities`. `get_entity_schema` et `search_wms_data` restent des
|
|
raccourcis Reading, sans paramètre.
|
|
|
|
**Rapport à D3.** D3 n'est pas révisée : le Reading reste la règle par défaut,
|
|
car en Writing les statuts sont des **énumérations** — les comparaisons de
|
|
chaînes (`== "Release"`), cas le plus courant en debug, y échouent. La bascule
|
|
est un opt-in explicite et les descriptions d'outils portent l'avertissement.
|
|
|
|
Modalités :
|
|
|
|
- **Garde de valeur dans le code de l'outil**, pas dans le wrapper : D23 valide
|
|
les noms de paramètres, pas les valeurs. Hors `0..3` (ou non entier) →
|
|
erreur locale via `assertValidQueryType()` (`wms-query-service.js`), **avant
|
|
tout appel réseau**, nommant les quatre contextes.
|
|
- **`2` et `3` sont transmis tels quels** : le WMS répond et son diagnostic
|
|
remonte entier (L1.1). Sur `LIMAGRAI2512` : `2` = DataWarehouse non configuré
|
|
(`Could not resolve serviceType 'IDataWarehouse…'`), `3` = Metrics, contexte
|
|
présent mais modèle distinct (`'ApplicationMetricDataContext' ne contient pas
|
|
de définition pour 'Products'`).
|
|
- **Interaction avec le resolver (D21)** : la table de résolution est
|
|
construite sur le Metadata **Reading**. Quand `query_type != 0`, un nom qui
|
|
se résout se résout normalement (`Products` marche en Writing, mesuré) ; un
|
|
nom **inconnu** du Reading n'est **pas** bloqué — il passe tel quel avec un
|
|
`warning` dans la réponse (`allowUnknown` du resolver, même mécanique que le
|
|
repli « Metadata injoignable »), car le modèle Writing/Metrics peut contenir
|
|
des entités hors Reading. Le `warning` est conservé aussi dans la réponse
|
|
d'erreur si le WMS échoue ensuite.
|
|
|
|
---
|
|
|
|
## D26 — Paramètre `application` : caches par application, chargement toujours paresseux
|
|
|
|
**Contexte (mesures des 24-25/08/2026, `LIMAGRAI2512`).** L'application
|
|
interrogée venait de `WMS_APPLICATION` (partagée par tous les profils) : le MCP
|
|
ne voyait que `EasyWMS`. Or `POST /AD/api/Application/GetAll` déclare **9
|
|
applications**, et **CustomApp porte le spécifique client** (153 workflows
|
|
`CST_*` sur ce tenant) — précisément ce qu'on cherche en debug. Les 11 entités
|
|
`CustomApp` ne sont requêtables dans aucun contexte : l'API AD est le seul
|
|
accès au spécifique client.
|
|
|
|
**Décision.** Un paramètre `application` (défaut : l'application du profil,
|
|
donc comportement strictement inchangé sans lui) sur six outils :
|
|
`get_ad_elements`, `search_ad_elements`, `get_ad_element_details`,
|
|
`search_workflows`, `get_workflow_details`, `list_workflow_categories`.
|
|
|
|
**Contrat de cache.**
|
|
|
|
| Service | Clé avant | Clé après |
|
|
|---|---|---|
|
|
| `ad-service` | un cache par type | un cache par **(application, type)** (`app::type`) |
|
|
| `workflow-service` | un cache global | un cache par **application** |
|
|
|
|
Sans ces clés, un appel CustomApp polluerait le cache EasyWMS du même type.
|
|
Règles associées :
|
|
|
|
- **L'invalidation reste l'abonnement `onSwitch()`** (D8) : la bascule de
|
|
profil vide **tous** les caches, toutes applications confondues. Aucune
|
|
invalidation manuelle inter-module.
|
|
- **Pas de préchargement des 9 applications** (D10) : seule l'application
|
|
effectivement demandée est chargée — le type `Resource` pèse 29 374 éléments
|
|
sur la seule EasyWMS.
|
|
- `workflow-service` cache aussi la liste de `Application/GetAll`, **allégée**
|
|
(`name`, `id`, `version`) : chaque élément de la réponse brute embarque un
|
|
blob `data` de ~100 Ko (la définition EasyBuilder complète) qu'on ne
|
|
conserve pas.
|
|
- `list_workflow_categories` est adossé à `Application/GetAll` (les 9
|
|
applications) et non plus aux `applicationName` du seul cache actif. La note
|
|
de L1.3 reste vraie — pas de champ catégorie ; les comptes de workflows ne
|
|
sont affichés que pour les applications déjà chargées (paresseux). Le
|
|
paramètre `category` de `search_workflows` (filtre sur `applicationName`)
|
|
subsiste : `application` choisit le jeu chargé, `category` filtre dedans —
|
|
leur articulation est documentée dans les descriptions.
|
|
- `get_application_summary` regroupe l'état par application puis par type et
|
|
ne détaille que les entrées **effectivement en cache** : la sortie reste
|
|
bornée quel que soit le nombre d'applications interrogées (D24). Il expose
|
|
aussi les caches de workflows par application.
|
|
|
|
**Le paramètre ne suffisait pas : il faut que la réponse le dise** (lot 5,
|
|
25/08/2026). Cas réel : une session Cowork cherchant des workflows `CST_*` sans
|
|
passer `application: "CustomApp"` a conclu que l'AD n'en contenait aucun — alors
|
|
que `CST_PickingTasksSequencing_PR` et `CST_ChooseDestinationFromPS` existent.
|
|
Le paramètre était disponible et documenté ; ce qui manquait, c'est que **rien
|
|
dans la réponse ne disait qu'on n'avait regardé qu'une application sur neuf**.
|
|
Un défaut silencieux se lit comme une exhaustivité.
|
|
|
|
`search_workflows` et `search_ad_elements` rappellent donc **toujours**
|
|
l'application effectivement interrogée (plus seulement quand le paramètre a été
|
|
passé), et ajoutent un `hint` quand la recherche revient **vide** :
|
|
|
|
- Seuil à **0 résultat**, pas « peu ». Toute valeur non nulle produirait un hint
|
|
parasite sur une recherche légitimement étroite, et le mode d'échec observé
|
|
est bien le zéro pris pour une absence.
|
|
- Le hint nomme les autres applications depuis la liste allégée **déjà en
|
|
cache** ; sans elle, il reste générique et renvoie vers
|
|
`list_workflow_categories`. **Jamais de fetch pour construire un hint** —
|
|
ce serait précisément le préchargement que cette décision interdit.
|
|
- Il nomme `CustomApp` en clair, sauf quand c'est déjà l'application
|
|
interrogée : c'est une connaissance statique, déjà portée par les
|
|
descriptions d'outils, pas une donnée à aller chercher.
|