# Suivi projet - Migration MCP WMS Wiki (stdio -> HTTP) Derniere mise a jour : 2026-05-31 ## 1. Objectif Rendre le serveur MCP `mcp-wms-wiki` "HTTP-compliant" (transport Streamable HTTP) pour l'heberger en remote sur le serveur TrueNAS Scale "Fangtooth" en app Docker custom, expose derriere Nginx Proxy Manager + Cloudflare sur le domaine cible `mcp-wms.arthur-ria.fr`, consommable comme connecteur MCP distant depuis Claude Desktop. ## 2. Etat actuel (source : analyse Claude Code) - Serveur MCP Node.js ESM (`"type": "module"`), repo : `git.arthur-ria.fr/Mecalux/mcp-wms-wiki`. - Local sur disque : `/home/arthur/dev/mcp-wms-wiki`. - Transport actuel : **stdio** (`StdioServerTransport`, `src/index.js:168-171`). Lance par `node src/index.js`, forke par le client. - SDK : `@modelcontextprotocol/sdk` declare en `"latest"` mais lockfile fige **1.29.0** (= la version qui compte). - Serveur instancie en haut niveau : `new McpServer({ name: 'wms-wiki', version: '1.0.0' })` (`src/index.js:63-66`). - 5 tools (`src/index.js:70-126`) : `search_wiki`, `get_wiki_page`, `list_wiki_sections`, `get_glossary_term`, `get_related_pages`. - 2 resources : `wiki://overview`, `wiki://entities-map` (`src/index.js:130-164`). - Handlers stateless : recoivent un `ctx` immuable construit au demarrage (`{ pages, invertedIndex, glossary, wikiPath }`). - Chargement wiki 100% local au demarrage (`buildWikiIndex`, `buildGlossary`, `loadSpecialPage`), tout en memoire. ~40 MB de markdown. - Une seule var d'env reellement consommee : `WIKI_PATH`. `.env` lu par un parseur maison (dotenv declare mais non utilise). - Pas de `.gitignore` -> `.env` est commite (aujourd'hui sans secret : `WIKI_PATH=./wiki`, `DEBUG=false`). - Contenu potentiellement confidentiel (sous-dossier `limagrain/`, refs Mecalux). ## 3. Architecture cible - Transport : **Streamable HTTP**, mode **stateless**, **`enableJsonResponse: true`** (reponse JSON one-shot, pas de SSE). - App **Express** : `POST /mcp` (+ `GET /mcp` route sur le meme handler), `GET /healthz`. - Un **McpServer neuf par requete** via factory `createServer(ctx)`, le `ctx` etant construit une seule fois et partage (immuable). - Containerise (node:20-alpine), wiki embarque dans l'image, deploye en app Docker custom TrueNAS. - Reverse proxy NPM + Cloudflare. Build de l'image directement sur le NAS (pas de registry externe). ## 4. Decisions actees - Stateless : **oui**, avec factory `createServer(ctx)` par requete (correction du partage de McpServer). - `enableJsonResponse` : **oui (true)** -> simplifie tout le reverse proxy, supprime le risque SSE/timeout Cloudflare. - Wiki : **embarque dans l'image** en V1 (immuable, rollback par tag). Volume monte seulement si la cadence de maj devient genante. - Registry : **build sur le TrueNAS** depuis le repo (coherent avec le workflow Whisper). - Transport legacy `/sse` : **non** (YAGNI). - `wiki_old_13-05-2026/` : **a supprimer** (40 MB inutiles). - SDK : **epingler en 1.29.0**, retirer `"latest"`. - Deps : ajouter `express ^4` + `zod ^3` (alignee SDK), retirer `dotenv`. ### Roadmap auth (point cle) - **V1 : authless.** Ajout direct dans Claude Desktop via Settings > Connectors (connecteur distant authless, aucun MCP local / pas de `mcp-remote`). - **V2 : OAuth cote serveur.** Le connecteur natif Claude Desktop gere OAuth + Dynamic Client Registration. Objectif explicite : eviter de faire tourner un MCP local juste pour l'auth. Le SDK fournit l'infra (`server/auth/`). - Le code V1 doit etre structure pour accueillir l'OAuth en V2 : middleware `authMiddleware` (pass-through en V1) monte uniquement devant `/mcp`, avec commentaire indiquant ou brancher l'OAuth. ## 5. Amendements techniques au plan initial - **Bug evite** : ne PAS partager un seul McpServer entre transports per-requete en stateless (cross-talk de reponses possible en concurrence). -> factory `createServer(ctx)` par requete, ctx partage immuable. - **`enableJsonResponse: true`** rend inutile toute la conf NPM "Advanced" SSE (proxy_buffering off, read_timeout 3600, websockets) et supprime le risque Cloudflare "coupe SSE a 100s". - **DNS rebinding** : `enableDnsRebindingProtection: true` + `allowedHosts` pilote par `MCP_ALLOWED_HOSTS`. Verifier que NPM forwarde bien `Host: $host` (l'app doit voir `mcp-wms.arthur-ria.fr`, pas `127.0.0.1:3000`). Tester au curl post-deploy. - **Config par env** : `PORT` (3000), `HOST` (0.0.0.0), `WIKI_PATH` (/app/wiki), `MCP_ALLOWED_HOSTS`. ## 6. Securite - **BLOQUANT (et critique car V1 authless) : audit traversee de repertoire** sur `get_wiki_page` (`src/services/page.js`). Resoudre par rapport a `wikiPath`, rejeter `..` et chemins absolus, confiner au sous-arbre. Corriger avant toute exposition. - **Authless + contenu confidentiel** : tant qu'OAuth (V2) n'est pas la, ne pas exposer publiquement (LAN / Tailscale / VPN) OU mettre une Access List NPM / Cloudflare Access devant. Si passage en Cloudflare orange-cloud, desactiver le cache sur `/mcp`. - **Secrets** : `.gitignore` + `git rm --cached .env` + `.env.example`. En prod TrueNAS, variables injectees par la definition de l'app, pas de `.env`. Rien a purger dans l'historique aujourd'hui (pas de secret commite), mais discipline a partir de maintenant. ## 7. Deploiement (cible) - **TrueNAS app Docker custom** : service unique `mcp-wms-wiki`, image buildee sur le NAS, `restart: unless-stopped`, env (`PORT`, `HOST`, `WIKI_PATH`, `MCP_ALLOWED_HOSTS`), pas de volume (wiki embarque). - **NPM** : proxy host `mcp-wms.arthur-ria.fr` -> `http://:3000`, HTTPS force + redirect. Avec JSON (pas de SSE), pas de directives avancees particulieres a prevoir. - **Cloudflare** : DNS-only (gris) pour valider, puis orange possible (le risque SSE ayant disparu avec JSON). Si orange + contenu confidentiel : pas de cache sur `/mcp`. ## 8. Checklist d'implementation (V1) - [ ] Factory `createServer(ctx)` + ctx construit une seule fois. - [ ] App Express : `POST /mcp`, `GET /mcp` (meme handler), `GET /healthz`. - [ ] Transport per-requete : stateless + `enableJsonResponse: true` + `enableDnsRebindingProtection: true` + `allowedHosts`. - [ ] Middleware `authMiddleware` pass-through devant `/mcp` (placeholder V2 OAuth). - [ ] package.json : SDK epingle 1.29.0, +express, +zod, -dotenv, engines node >=20. - [ ] Audit + correction traversee de repertoire sur `page.js`. - [ ] Dockerfile (node:20-alpine, npm ci --omit=dev, wiki embarque, USER node, HEALTHCHECK). - [ ] `.dockerignore`, `.gitignore`, `.env.example`, `git rm --cached .env`. - [ ] Suppression `wiki_old_13-05-2026/`. - [ ] Snippet docker-compose TrueNAS + script smoke-test curl. - [ ] Deploy TrueNAS + conf NPM + DNS Cloudflare. - [ ] Test : ajout connecteur authless dans Claude Desktop (Settings > Connectors). ## 9. Questions ouvertes / a trancher plus tard - V2 : choix precis du fournisseur OAuth cote serveur (l'utilisateur a l'habitude de FortiToken). - Le MCP "API" separe doit-il aussi migrer en HTTP et partager le domaine (`/api` + `/wiki`) ou un sous-domaine distinct ? A cadrer car influence la conf NPM. - Multi-utilisateurs prevus ? (sinon pas de rate-limiting / logs d'acces necessaires). ## 10. References - Repo : `https://git.arthur-ria.fr/Mecalux/mcp-wms-wiki` - Local : `/home/arthur/dev/mcp-wms-wiki` - Domaine cible : `mcp-wms.arthur-ria.fr` (endpoint `/mcp`) - SDK : `@modelcontextprotocol/sdk` 1.29.0 ; transport cible `StreamableHTTPServerTransport` (`dist/esm/server/streamableHttp.js`) - Fichiers cles : `src/index.js` (transport l.168-171, McpServer l.63, tools l.70-126, resources l.130-164), `src/services/page.js` (a auditer), `src/config`, `src/resources`, `src/services`, `src/tools` - Infra : TrueNAS Scale 25 Fangtooth, Nginx Proxy Manager, Cloudflare ## 11. Etat d'avancement - [x] Analyse du repo et du transport actuel (Claude Code) - [x] Plan de migration + critique + amendements - [x] Decisions actees (stateless, JSON, wiki embarque, authless V1 / OAuth V2) - [ ] Implementation V1 (en cours / a lancer cote Claude Code) - [ ] Deploiement TrueNAS - [ ] V2 OAuth