0ff44f7b78
Nouveaux documents :
- README.md : porte d'entrée humaine, absente jusqu'ici. Objet du projet,
installation, npm test, branchement Claude Desktop, ajout d'un profil WMS,
compilation de l'exécutable.
- DECISIONS.md : 20 décisions et pièges vérifiés sur un WMS réel (D1..D20),
chacun avec son pourquoi. Extrait ce qui était noyé dans CLAUDE.md :
100 % API, tenant_code OAuth, réponses {entities}, casse des propriétés,
dotenv sur stderr, dates relatives LINQ non traduisibles, absence de
CommandParameterData, etc.
- MONITORING.md : supervision du serveur MCP. Préfixes de logs, séquence
d'un démarrage sain, cycle de vie du token OAuth et ses trois filets,
état des caches, table symptôme -> cause. Une section dit explicitement
ce qui n'est pas instrumenté (ni healthcheck, ni métriques, ni alerte).
- docs/logs.md : accès aux logs du WMS. Chemins, placeholder {host},
blocage volontaire sur les profils SaaS, les trois outils, format des
lignes, limites connues.
Mises à jour :
- CLAUDE.md réécrit et aligné sur le code. Correction de l'écart le plus
gênant : le code utilise QueryType 0 (Reading), la doc annonçait 1, soit
l'inverse de ce qui fonctionne pour les comparaisons de statut par
chaîne. Corrigés également : 6 resources et non 7 (workflows://categories
n'existe pas), section .env mono-profil obsolète, références à des
fichiers de test absents, README annoncé mais inexistant. Le suivi de
projet et les checklists de phases sont retirés.
- docs/README.md : index réel du dossier. L'ancien promettait une resource
docs:// qui n'a jamais existé.
- docs/getting_started.md : avertissement en tête, c'est une capture
partielle du portail Mecalux dont les liens internes ne résolvent pas.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
345 lines
14 KiB
Markdown
345 lines
14 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` sélectionne le modèle de données interrogé :
|
|
|
|
| Valeur | Modèle | Champs de statut |
|
|
|---|---|---|
|
|
| `0` | **Reading** | chaînes de caractères (`"Release"`) |
|
|
| `1` | Writing | énumérations |
|
|
|
|
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()`.
|
|
|
|
**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.
|