@imenam/mcp-http-gateway 2.0.14 → 2.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,512 +1,707 @@
1
- # MCP HTTP Gateway
2
-
3
- Serveur HTTP qui expose n'importe quel serveur MCP (Model Context Protocol) en tant qu'API REST, sans modifier les MCPs eux-mêmes. Conçu pour permettre à des agents ou applications ne supportant pas nativement le protocole MCP de communiquer avec l'écosystème MCP via de simples requêtes HTTP.
4
-
5
- ## Fonctionnement
6
-
7
- Chaque MCP tourne en tant que processus enfant (transport stdio). Le gateway s'y connecte via le SDK MCP officiel et expose ses outils sur des routes HTTP configurables.
8
-
9
- ```
10
- Agent / App externe
11
- │
12
- │ HTTP
13
- ▼
14
- MCP HTTP Gateway (port 3500)
15
- │
16
- │ stdio (SDK MCP)
17
- ├──▶ mcp-documentor (/documentor)
18
- ├──▶ mcp-raindrop (/raindrop)
19
- └──▶ ...
20
- ```
21
-
22
- ## Installation
23
-
24
- **En tant que CLI globale :**
25
-
26
- ```bash
27
- npm install -g @imenam/mcp-http-gateway
28
- ```
29
-
30
- **Via npx (sans installation) :**
31
-
32
- ```bash
33
- npx @imenam/mcp-http-gateway
34
- ```
35
-
36
- **En dépendance locale d'un projet :**
37
-
38
- ```bash
39
- npm install @imenam/mcp-http-gateway
40
- ```
41
-
42
- ## Configuration
43
-
44
- ### `.env`
45
-
46
- ```env
47
- GATEWAY_PORT=3500
48
- GATEWAY_CONFIG_DIR=./config
49
- ```
50
-
51
- | Variable | Défaut | Description |
52
- |---|---|---|
53
- | `GATEWAY_PORT` | `3500` | Port HTTP du gateway |
54
- | `GATEWAY_CONFIG_DIR` | `./config` (relatif au cwd) | Dossier contenant `mcp-servers.json` |
55
- | `GATEWAY_BODY_LIMIT` | `32mb` | Taille maximale du corps d'une requête. Le défaut d'express (100 ko) ne suffit pas dès qu'un appel d'outil porte le contenu d'un fichier en base64 ; au-delà de cette limite, la réponse est un `413`. |
56
-
57
- ### `mcp-servers.json`
58
-
59
- À créer dans le dossier de configuration (`./config` par défaut, ou le chemin pointé par `GATEWAY_CONFIG_DIR`). Un fichier `config/mcp-servers.example.json` est fourni comme point de départ.
60
-
61
- Même format que la configuration `mcpServers` de Cursor. Ajouter `MCP_SERVER_ROUTE` dans `env` pour définir la route HTTP du MCP, et `description` pour la documentation.
62
-
63
- ```json
64
- {
65
- "mcpServers": {
66
- "mon-mcp": {
67
- "command": "node",
68
- "args": ["/chemin/vers/dist/index.js"],
69
- "description": "Description courte du MCP",
70
- "env": {
71
- "MA_VARIABLE": "valeur",
72
- "MCP_SERVER_ROUTE": "/ma-route"
73
- }
74
- }
75
- }
76
- }
77
- ```
78
-
79
- > Un MCP sans `MCP_SERVER_ROUTE` est ignoré au démarrage.
80
-
81
- ## Démarrage
82
-
83
- ```bash
84
- npm start
85
- ```
86
-
87
- Le gateway connecte chaque MCP configuré et démarre le serveur HTTP. Les MCPs en échec au démarrage sont ignorés sans bloquer les autres.
88
-
89
- ## Routes
90
-
91
- ### `GET /mcps`
92
-
93
- Retourne la liste des MCPs connectés avec leur route, description et instructions d'utilisation (format Markdown).
94
-
95
- ```bash
96
- curl http://localhost:3500/mcps
97
- ```
98
-
99
- ---
100
-
101
- ### `GET /{route}/tools`
102
-
103
- Liste les outils disponibles d'un MCP avec leur nom, description et schéma d'arguments.
104
-
105
- ```bash
106
- curl http://localhost:3500/documentor/tools
107
- ```
108
-
109
- ---
110
-
111
- ### `POST /{route}/tools/:toolName`
112
-
113
- Appelle un outil du MCP. Les arguments sont passés dans le body JSON.
114
-
115
- ```bash
116
- curl -X POST http://localhost:3500/documentor/tools/list_projects -H "Content-Type: application/json" -d '{}'
117
- ```
118
-
119
- ```bash
120
- curl -X POST http://localhost:3500/documentor/tools/get_doc -H "Content-Type: application/json" -d '{"id":"mon-doc-id"}'
121
- ```
122
-
123
- #### Réponse
124
-
125
- La réponse est le format brut du protocole MCP — un tableau `content` de blocs typés :
126
-
127
- ```json
128
- {
129
- "content": [
130
- { "type": "text", "text": "Résultat de l'outil..." }
131
- ]
132
- }
133
- ```
134
-
135
- Pour extraire le texte directement avec `jq` :
136
-
137
- ```bash
138
- curl -s -X POST http://localhost:3500/documentor/tools/list_projects -H "Content-Type: application/json" -d '{}' | jq -r '.content[0].text'
139
- ```
140
-
141
- ## Mode MCP
142
-
143
- Le gateway peut également démarrer en tant que **serveur MCP** (transport stdio). Dans ce mode, il se connecte à une instance du gateway HTTP et expose ses outils comme des outils MCP natifs.
144
-
145
- ### Cas d'usage : agent dans un conteneur Docker
146
-
147
- Le gateway HTTP tourne **hors du conteneur**, avec accès aux MCPs et leurs clés sensibles. L'agent dans le conteneur reçoit uniquement le gateway en mode MCP — aucune clé ne lui est injectée.
148
-
149
- ```
150
- [Agent dans conteneur]
151
- │ stdio (MCP)
152
- ▼
153
- [mcp-http-gateway --mcp] ←── GATEWAY_URL
154
- │ HTTP
155
- ▼
156
- [mcp-http-gateway hors conteneur]
157
- │ stdio
158
- ▼
159
- [MCPs réels (clés sensibles)]
160
- ```
161
-
162
- ### Démarrage
163
-
164
- ```bash
165
- GATEWAY_URL=http://host.docker.internal:3500 npx @imenam/mcp-http-gateway --mcp
166
- ```
167
-
168
- ### Configuration dans le conteneur
169
-
170
- ```json
171
- {
172
- "mcpServers": {
173
- "gateway": {
174
- "command": "npx",
175
- "args": ["@imenam/mcp-http-gateway", "--mcp"],
176
- "env": {
177
- "GATEWAY_URL": "http://host.docker.internal:3500"
178
- }
179
- }
180
- }
181
- }
182
- ```
183
-
184
- > `host.docker.internal` est l'adresse de l'hôte depuis un conteneur Docker.
185
-
186
- ### Variables d'environnement du mode `--mcp`
187
-
188
- | Variable | Requis | Description |
189
- |---|---|---|
190
- | `GATEWAY_URL` | oui | Base HTTP du gateway à relayer, sans barre finale. Le relais compose lui-même ses routes REST (`/startup-exposition`, `/mcp-definitions`, `/{route}/tools`, `/{route}/tools/{outil}`) : **ne pas** y ajouter `/mcp`. |
191
- | `GATEWAY_TOKEN` | non | Token porteur. S'il est défini, **tous** les appels sortants du relais portent `Authorization: Bearer <token>`. Absent, aucun en-tête n'est ajouté — le comportement est exactement celui des versions antérieures. |
192
- | `GATEWAY_INLINE_ROOTS` | non | Dossiers depuis lesquels le relais peut lire un fichier et en transmettre le contenu (cf. [Fichiers locaux](#fichiers-locaux--lagent-nomme-le-relais-lit)). **Non renseignée, tout appel à un outil qui déclare la paire échoue explicitement.** Séparateur : `;` sous Windows, `:` ailleurs. |
193
- | `GATEWAY_INLINE_EXTENSIONS` | non | Extensions autorisées à la lecture, séparées par des virgules. Défaut : `*` (toutes) — les racines restent le vrai garde-fou. |
194
- | `GATEWAY_INLINE_MAX_BYTES` | non | Taille maximale d'un fichier transmis, en octets, dans les deux sens. Défaut : `8388608` (8 Mio). |
195
- | `GATEWAY_INLINE_WRITE_ROOTS` | non | Dossiers dans lesquels le relais peut écrire un fichier renvoyé par un MCP (cf. [Fichiers reçus](#fichiers-reçus--le-mcp-produit-le-relais-écrit)). **Non renseignée, la fonction est désactivée** et tout outil qui la réclame échoue explicitement. Séparateur : `;` sous Windows, `:` ailleurs. |
196
- | `GATEWAY_INLINE_WRITE_EXTENSIONS` | non | Extensions autorisées à l'écriture, séparées par des virgules. Défaut : `*` (toutes) — les racines restent le vrai garde-fou. |
197
-
198
- > Ces deux variables sont lues dans l'environnement du process. Le relais fait
199
- > `import 'dotenv/config'` : un fichier `.env` présent dans le **répertoire courant de
200
- > l'agent** peut donc les fournir. dotenv n'écrase jamais une variable déjà définie —
201
- > le bloc `env` de la configuration MCP l'emporte toujours.
202
-
203
- ### Derrière un frontal qui authentifie
204
-
205
- Quand le gateway n'est pas joint en direct mais à travers un frontal qui exige un token
206
- porteur (hub [`mcp-gateway-manager`](https://github.com/Matthieu-Pesnot-Pin), reverse-proxy
207
- maison…), `GATEWAY_TOKEN` est ce que le relais présente à chaque requête :
208
-
209
- ```json
210
- {
211
- "mcpServers": {
212
- "gateway": {
213
- "command": "npx",
214
- "args": ["@imenam/mcp-http-gateway", "--mcp"],
215
- "env": {
216
- "GATEWAY_URL": "https://my-gateway.fr/jskdlf-jsfklhj",
217
- "GATEWAY_TOKEN": "…"
218
- }
219
- }
220
- }
221
- }
222
- ```
223
-
224
- Un refus (`401`/`403`) est rapporté à l'agent en clair — token manquant, ou token invalide —
225
- plutôt qu'en `HTTP 401` nu. Le token lui-même n'est **jamais** journalisé, pas même tronqué.
226
-
227
- ### Outils exposés
228
-
229
- Le mode MCP expose 3 outils qui reflètent directement les routes du gateway :
230
-
231
- | Outil | Description |
232
- |---|---|
233
- | `list_mcps` | Liste les MCPs disponibles avec leur route et description |
234
- | `list_tools` | Liste les outils d'un MCP donné (`route` requis) |
235
- | `call_tool` | Appelle un outil (`route`, `tool_name`, `arguments`) |
236
-
237
- L'agent découvre d'abord les MCPs disponibles, explore les outils d'un MCP si besoin, puis appelle l'outil voulu.
238
-
239
- ### Fichiers locaux : l'agent nomme, le relais lit
240
-
241
- Le relais tourne sur la machine de l'**agent** ; les MCP, eux, tournent sur la machine du
242
- **gateway**. Un chemin de fichier transmis tel quel ne veut donc rien dire à l'arrivée : le
243
- MCP le cherche chez lui et répond « fichier introuvable ».
244
-
245
- Le relais sait résoudre ça. Dès qu'un outil distant déclare une paire de paramètres
246
- `<x>_path` / `<x>_content`, le relais **lit le fichier localement, l'encode en base64 et
247
- envoie le contenu** dans `<x>_content` (plus le nom dans `<x>_name`, si le schéma le
248
- prévoit). Le paramètre `file_path` reste la seule chose que l'agent fournit : les
249
- paramètres que le relais remplit sont **retirés du schéma** publié à l'agent, par
250
- `tools/list` comme par `list_tools`. L'agent n'a donc aucun arbitrage à faire, et aucune
251
- occasion d'envoyer un chemin qui ne serait pas lisible à l'arrivée.
252
-
253
- Un outil qui ne déclare pas cette paire n'est jamais concerné : un `output_path` seul reste
254
- un chemin, interprété côté gateway comme avant.
255
-
256
- `<x>_path` est obligatoire pour l'agent si, et seulement si, le MCP le déclare obligatoire.
257
- Un outil qui le laisse optionnel accepte sa donnée autrement — un texte passé en ligne, par
258
- exemple. Sans `<x>_path`, le relais ne lit rien et transmet l'appel tel quel.
259
-
260
- **Le chemin vient de l'agent : il n'est pas digne de confiance.** La fonction est donc
261
- fermée par défaut et s'ouvre dossier par dossier :
262
-
263
- ```json
264
- {
265
- "mcpServers": {
266
- "gateway": {
267
- "command": "npx",
268
- "args": ["@imenam/mcp-http-gateway", "--mcp"],
269
- "env": {
270
- "GATEWAY_URL": "https://my-gateway.fr/jskdlf-jsfklhj",
271
- "GATEWAY_TOKEN": "…",
272
- "GATEWAY_INLINE_ROOTS": "C:\\Users\\moi\\Documents\\tableurs;C:\\travail\\exports"
273
- }
274
- }
275
- }
276
- }
277
- ```
278
-
279
- - **`GATEWAY_INLINE_ROOTS` est l'interrupteur.** Non renseignée, le relais ne lit aucun
280
- fichier et **l'appel échoue** en nommant la variable à définir. Un outil qui déclare la
281
- paire attend le contenu : laisser filer le chemin ferait chercher le fichier au MCP sur
282
- la machine du gateway, où il ne le trouverait pas — ou pire, où il traiterait un homonyme
283
- sans rien signaler. Pas de repli, pas de dégradation silencieuse.
284
-
285
- > **Changement de comportement.** Les versions antérieures laissaient passer le chemin tel
286
- > quel dans ce cas. Si un déploiement existant s'appuyait là-dessus, renseignez
287
- > `GATEWAY_INLINE_ROOTS` — le message d'erreur le dit explicitement.
288
- - Les liens symboliques sont résolus **avant** le contrôle : un lien posé dans un dossier
289
- autorisé ne fait pas sortir un fichier situé ailleurs. Une remontée par `..` est refusée
290
- pour la même raison.
291
- - Seules les extensions de `GATEWAY_INLINE_EXTENSIONS` passent (défaut : toutes).
292
- - Au-delà de `GATEWAY_INLINE_MAX_BYTES` (8 Mio par défaut), refus explicite. Ce défaut
293
- tient sous le plafond de 16 Mio que le hub `mcp-gateway-manager` applique quand il
294
- journalise les corps de requête.
295
- - Tout refus est rapporté à l'agent en clair, et **chaque fichier transmis laisse une ligne
296
- sur `stderr`** : ce qui sort de la machine est lisible dans le journal de l'agent.
297
-
298
- > Côté MCP, cette fonction suppose que l'outil accepte le contenu en base64. C'est le cas
299
- > de [`@imenam/xls-reader`](https://www.npmjs.com/package/@imenam/xls-reader) ≥ 2.2. Un MCP
300
- > qui n'expose que `file_path` ne déclare pas la paire, et rien ne change pour lui.
301
-
302
- > **Le mode MCP distant (`/mcp`, Claude.ai) n'a pas de relais local** : rien ne tourne alors
303
- > sur la machine de l'appelant, donc personne ne peut lire son disque. Par ce chemin, un
304
- > agent qui veut faire lire un fichier doit en envoyer lui-même le contenu.
305
-
306
- ### Fichiers reçus : le MCP produit, le relais écrit
307
-
308
- Le sens retour, symétrique du précédent. Un MCP qui *fabrique* un fichier — un export, une
309
- pièce jointe, un téléchargement — ne peut pas le déposer sur la machine de l'agent : le
310
- protocole n'offre au serveur aucune primitive pour y écrire. Un chemin de destination
311
- transmis tel quel ferait écrire le MCP sur **son** disque, sans que personne s'en aperçoive.
312
-
313
- Dès qu'un outil distant déclare la paire `<x>_path` / `<x>_relay`, le relais :
314
-
315
- 1. retire `<x>_relay` du schéma publié à l'agent, qui ne voit qu'un dossier à nommer ;
316
- 2. retient `<x>_path`, ne le transmet pas, et pose `<x>_relay` à `true` ;
317
- 3. reçoit le contenu du fichier dans la réponse du MCP, l'écrit **ici**, et le retire du
318
- résultat avant de le rendre à l'agent.
319
-
320
- ```json
321
- {
322
- "mcpServers": {
323
- "gateway": {
324
- "command": "npx",
325
- "args": ["@imenam/mcp-http-gateway", "--mcp"],
326
- "env": {
327
- "GATEWAY_URL": "https://my-gateway.fr/jskdlf-jsfklhj",
328
- "GATEWAY_TOKEN": "…",
329
- "GATEWAY_INLINE_WRITE_ROOTS": "C:\\Users\\moi\\Téléchargements"
330
- }
331
- }
332
- }
333
- }
334
- ```
335
-
336
- - **`GATEWAY_INLINE_WRITE_ROOTS` est l'interrupteur**, distinct de celui de la lecture :
337
- écrire est plus dangereux que lire, et n'hérite donc pas des dossiers autorisés en lecture.
338
- - **Non renseignée, l'appel échoue** en nommant la variable à définir. Pas de repli, pas de
339
- dégradation silencieuse : mieux vaut un fichier perdu qu'un fichier écrit au mauvais endroit.
340
- - `<x>_path` est toujours un **dossier**, créé si besoin ; le fichier garde le nom que lui
341
- donne le MCP. Le relais ne renomme jamais.
342
- - Comme au sens aller, `<x>_path` n'est obligatoire que si le MCP le déclare tel.
343
- Un outil qui le déclare optionnel dit par là qu'il sait quoi faire sans destination —
344
- renvoyer une capture inline, par exemple. Sans `<x>_path`, le relais ne retient rien,
345
- ne pose pas `<x>_relay`, et le résultat repart intact.
346
- - Si l'outil déclare aussi `<x>_overwrite`, le relais l'honore ; sinon il refuse d'écraser.
347
- - Les liens symboliques sont résolus avant contrôle, y compris sur le plus proche ancêtre
348
- existant quand la destination n'existe pas encore.
349
- - **Le contenu est retiré du résultat dans tous les cas**, y compris en cas d'échec
350
- d'écriture. Un fichier de 8 Mio encodé en base64 n'a rien à faire dans le contexte d'un
351
- agent : ça coûte cher, et un agent qui le recopierait à la main le corromprait.
352
- - Chaque fichier écrit laisse une ligne sur `stderr`.
353
-
354
- > Côté MCP, cette fonction suppose que l'outil sache renvoyer son fichier en bloc
355
- > `resource` (avec `blob` et `name`) quand `<x>_relay` est posé. C'est le cas de
356
- > [`file-share-mcp`](https://www.npmjs.com/package/file-share-mcp). Un MCP qui ne déclare
357
- > pas la paire n'est pas concerné, et son résultat repart intact.
358
-
359
- ---
360
-
361
- ## Mode MCP distant (HTTPS / Claude.ai)
362
-
363
- Le gateway expose également un **endpoint MCP natif** sur `/mcp` via le transport **Streamable HTTP**, en mode **stateless**. Ce mode permet à Claude.ai — et à tout client MCP distant compatible — de se connecter directement au gateway, sans intermédiaire.
364
-
365
- Chaque `POST /mcp` est traité de façon autonome : aucun `mcp-session-id` n'est émis, aucun état n'est conservé entre deux requêtes. Les trois outils exposés étant du pur requête/réponse (aucune notification serveur→client), aucune session n'est nécessaire. Concrètement :
366
-
367
- - un connecteur laissé inactif ne peut pas « expirer » côté serveur — il n'y a rien à expirer ;
368
- - un redémarrage ou un redéploiement du gateway est transparent pour les clients connectés ;
369
- - l'endpoint peut être scalé horizontalement derrière un load balancer sans affinité de session.
370
-
371
- `GET /mcp` et `DELETE /mcp` répondent `405 Method Not Allowed` : sans session, il n'y a ni flux SSE autonome ni terminaison de session à gérer (la spec Streamable HTTP l'autorise explicitement).
372
-
373
- ### Flow
374
-
375
- ```
376
- Claude.ai
377
- │ HTTPS + Bearer JWT
378
- ▼
379
- [mcp-http-gateway /mcp] ──── 401 + WWW-Authenticate si non authentifié
380
- │
381
- │ (découverte OAuth 2.1)
382
- ▼
383
- [Authorization Server externe] (Auth0, Keycloak, Authentik, Zitadel, Logto…)
384
- ```
385
-
386
- Le gateway agit uniquement comme **Resource Server** : il valide les JWT via JWKS et expose l'URL de l'AS via le Protected Resource Metadata (RFC 9728). Toute la logique OAuth (DCR, PKCE, `/authorize`, `/token`) est déléguée à l'AS externe.
387
-
388
- ### Variables d'environnement
389
-
390
- | Variable | Description |
391
- |---|---|
392
- | `MCP_OAUTH_ENABLED` | `true` pour activer la protection OAuth (recommandé en production) |
393
- | `MCP_PUBLIC_URL` | URL publique canonique du endpoint — ex. `https://gateway.example.com/mcp`. Utilisée comme `resource` (RFC 9728) et `aud` JWT. |
394
- | `OAUTH_JWKS_URI` | URI du JWKS de l'Authorization Server — ex. `https://your-auth-server/.well-known/jwks.json` |
395
- | `OAUTH_ISSUER` | Issuer attendu dans les JWT — ex. `https://your-auth-server` |
396
-
397
- > Sans `MCP_OAUTH_ENABLED=true`, `/mcp` est ouvert (utile pour les tests locaux). Claude.ai refusera cependant de s'y connecter sans 401 + `WWW-Authenticate`.
398
-
399
- ### Configuration Claude.ai
400
-
401
- Dans Claude.ai → **Paramètres → Connecteurs** → Ajouter un connecteur MCP :
402
-
403
- ```
404
- URL : https://gateway.example.com/mcp
405
- Authentification : OAuth (auto-détectée via /.well-known/oauth-protected-resource)
406
- ```
407
-
408
- ### Prérequis côté Authorization Server
409
-
410
- L'AS doit supporter :
411
- - **DCR** (RFC 7591) — `registration_endpoint` exposé
412
- - **PKCE S256** — `code_challenge_methods_supported: ["S256"]`
413
- - `redirect_uri` autorisé : `https://claude.ai/api/mcp/auth_callback`
414
- - JWT signés (RS256 ou ES256) avec claims `iss`, `aud=MCP_PUBLIC_URL`, `exp`
415
-
416
- ### Exposition HTTPS publique
417
-
418
- Le endpoint `/mcp` doit être accessible depuis l'IP range `160.79.104.0/21` (Anthropic) en HTTPS. Options :
419
-
420
- | Option | Commande / Notes |
421
- |---|---|
422
- | **Cloudflare Tunnel** | `cloudflared tunnel --url http://localhost:3500` |
423
- | **ngrok** | `ngrok http 3500` — idéal pour les tests |
424
- | **Reverse-proxy** | Caddy / Traefik en frontal avec cert Let's Encrypt |
425
-
426
- ### Outils exposés
427
-
428
- Les mêmes 3 outils que le mode stdio — avec accès direct aux clients MCP internes (sans repasser par HTTP) :
429
-
430
- | Outil | Description |
431
- |---|---|
432
- | `list_mcps` | Liste les MCPs disponibles avec leur route et description |
433
- | `list_tools` | Liste les outils d'un MCP donné (`route` requis) |
434
- | `call_tool` | Appelle un outil (`route`, `tool_name`, `arguments`) |
435
-
436
- ### Pré-exposition de MCP au démarrage (`EXPOSE_AT_AGENT_STARTUP`)
437
-
438
- Par défaut, l'agent ne reçoit que les 3 outils de découverte et doit appeler `list_mcps` /
439
- `list_tools` pour découvrir les MCP. Pour les MCP fréquemment utilisés, la clé
440
- `EXPOSE_AT_AGENT_STARTUP` publie leurs outils **directement dans la réponse `tools/list`**,
441
- nommés `<serveur>__<outil>`, avec description et `inputSchema` complets. L'agent les appelle
442
- directement, sans passer par `call_tool` et sans aucun aller-retour de découverte.
443
-
444
- L'option se déclare **par MCP**, dans son bloc `env` de `config/mcp-servers.json` — au même
445
- endroit que `MCP_SERVER_ROUTE`, qui est également une directive lue par le gateway :
446
-
447
- ```json
448
- {
449
- "mcpServers": {
450
- "documentor": {
451
- "command": "node",
452
- "args": ["/chemin/vers/documentor/dist/index.js"],
453
- "description": "Documentation manager MCP",
454
- "env": {
455
- "MCP_SERVER_ROUTE": "/documentor",
456
- "EXPOSE_AT_AGENT_STARTUP": "true"
457
- }
458
- },
459
- "scraper": {
460
- "command": "node",
461
- "args": ["/chemin/vers/scraper/dist/index.js"],
462
- "env": {
463
- "MCP_SERVER_ROUTE": "/scraper"
464
- }
465
- }
466
- }
467
- }
468
- ```
469
-
470
- Ici `documentor` est pré-exposé ; `scraper`, sans la clé, ne l'est pas et reste accessible
471
- via `list_mcps` / `list_tools` / `call_tool`. Le `.env` du gateway ne contient **aucune**
472
- indication relative aux MCP.
473
-
474
- La valeur est un booléen : `"true"` (également `"1"`, `"yes"`, `"on"`) ou `"false"`. Toute
475
- autre valeur est refusée avec un avertissement au démarrage et le MCP n'est pas pré-exposé.
476
-
477
- **Pourquoi ce canal ?** Les définitions d'outils passent par `tools/list`, seul canal sans
478
- limite de taille. Le champ `instructions` du protocole, lui, est plafonné par les clients
479
- autour de **2 048 caractères** et tronqué silencieusement au-delà — un MCP de 30 outils
480
- avec ses schémas pèse ~33 000 caractères, il ne peut donc pas y transiter.
481
-
482
- **Quand l'activer ?** Le compromis, pour un MCP réellement utilisé :
483
-
484
- | | Découverte (défaut) | Pré-exposé |
485
- |---|---|---|
486
- | Allers-retours | 3 (`list_mcps`, `list_tools`, `call_tool`) | 1 (appel direct) |
487
- | Contexte | `list_tools` déverse tous les schémas dans la conversation dès le 1ᵉʳ usage, définitivement | les noms d'outils, puis le schéma des seuls outils utilisés |
488
-
489
- À activer donc pour les MCP utilisés dans la majorité des sessions. À laisser en découverte
490
- pour les MCP occasionnels : un MCP pré-exposé mais inutilisé coûte ses noms d'outils à
491
- chaque requête, et multiplier les outils similaires dégrade la sélection par l'agent.
492
-
493
- Dans tous les cas, le MCP enfant reste hébergé côté gateway : seules ses **définitions**
494
- sont relayées, les appels sont proxifiés. L'isolation est préservée.
495
-
496
- Cette config vit **uniquement côté serveur** et s'applique de façon transparente aux deux
497
- points d'entrée par lesquels un agent atteint le gateway :
498
-
499
- - **endpoint `/mcp` distant** (cas Claude.ai) — les outils proxy sont publiés directement ;
500
- - **bridge stdio** (`server.js --mcp`, cas local) — au démarrage, le bridge interroge le
501
- serveur (`GET /startup-exposition`), publie les outils reçus et route leurs appels vers
502
- la bonne route. Le bridge ne connaît pas la config : il ne fait que transmettre ce que le
503
- serveur décide.
504
-
505
- ---
506
-
507
- ## Ajouter un MCP
508
-
509
- 1. Ajouter l'entrée dans `config/mcp-servers.json` avec `MCP_SERVER_ROUTE` dans `env`
510
- 2. Redémarrer le gateway
511
-
512
- Aucune modification du MCP n'est nécessaire.
1
+ # MCP HTTP Gateway
2
+
3
+ Serveur HTTP qui expose n'importe quel serveur MCP (Model Context Protocol) en tant qu'API REST, sans modifier les MCPs eux-mêmes. Conçu pour permettre à des agents ou applications ne supportant pas nativement le protocole MCP de communiquer avec l'écosystème MCP via de simples requêtes HTTP.
4
+
5
+ ## Fonctionnement
6
+
7
+ Chaque MCP tourne en tant que processus enfant (transport stdio). Le gateway s'y connecte via le SDK MCP officiel et expose ses outils sur des routes HTTP configurables.
8
+
9
+ ```
10
+ Agent / App externe
11
+ │
12
+ │ HTTP
13
+ ▼
14
+ MCP HTTP Gateway (port 3500)
15
+ │
16
+ │ stdio (SDK MCP)
17
+ ├──▶ mcp-documentor (/documentor)
18
+ ├──▶ mcp-raindrop (/raindrop)
19
+ └──▶ ...
20
+ ```
21
+
22
+ ## Installation
23
+
24
+ **En tant que CLI globale :**
25
+
26
+ ```bash
27
+ npm install -g @imenam/mcp-http-gateway
28
+ ```
29
+
30
+ **Via npx (sans installation) :**
31
+
32
+ ```bash
33
+ npx @imenam/mcp-http-gateway
34
+ ```
35
+
36
+ **En dépendance locale d'un projet :**
37
+
38
+ ```bash
39
+ npm install @imenam/mcp-http-gateway
40
+ ```
41
+
42
+ ## Configuration
43
+
44
+ ### `.env`
45
+
46
+ ```env
47
+ GATEWAY_PORT=3500
48
+ GATEWAY_CONFIG_DIR=./config
49
+ ```
50
+
51
+ | Variable | Défaut | Description |
52
+ |---|---|---|
53
+ | `GATEWAY_PORT` | `3500` | Port HTTP du gateway |
54
+ | `GATEWAY_CONFIG_DIR` | `./config` (relatif au cwd) | Dossier contenant `mcp-servers.json` |
55
+ | `GATEWAY_ADMIN_TOKEN` | — | Active `POST /admin/reload` et en est le jeton. Absent, la route n'existe pas. Voir [Rechargement à chaud](#rechargement-à-chaud). |
56
+ | `GATEWAY_BODY_LIMIT` | `32mb` | Taille maximale du corps d'une requête. Le défaut d'express (100 ko) ne suffit pas dès qu'un appel d'outil porte le contenu d'un fichier en base64 ; au-delà de cette limite, la réponse est un `413`. Ne s'applique pas aux routes de [relais HTTP](#relais-http-mcp_http_relay_port). |
57
+
58
+ ### `mcp-servers.json`
59
+
60
+ À créer dans le dossier de configuration (`./config` par défaut, ou le chemin pointé par `GATEWAY_CONFIG_DIR`). Un fichier `config/mcp-servers.example.json` est fourni comme point de départ.
61
+
62
+ Même format que la configuration `mcpServers` de Cursor. Ajouter `MCP_SERVER_ROUTE` dans `env` pour définir la route HTTP du MCP, et `description` pour la documentation.
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "mon-mcp": {
68
+ "command": "node",
69
+ "args": ["/chemin/vers/dist/index.js"],
70
+ "description": "Description courte du MCP",
71
+ "env": {
72
+ "MA_VARIABLE": "valeur",
73
+ "MCP_SERVER_ROUTE": "/ma-route"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ > Un MCP sans `MCP_SERVER_ROUTE` est ignoré au démarrage.
81
+
82
+ #### Relais HTTP (`MCP_HTTP_RELAY_PORT`)
83
+
84
+ Un MCP peut avoir besoin de recevoir du trafic HTTP brut en plus de ses appels d'outils : le protocole HTTP de git, un flux binaire, etc. Il déclare alors `MCP_HTTP_RELAY_PORT` dans son bloc `env` :
85
+
86
+ ```json
87
+ "env": {
88
+ "MCP_SERVER_ROUTE": "/github",
89
+ "MCP_HTTP_RELAY_PORT": "4610"
90
+ }
91
+ ```
92
+
93
+ - Le MCP écoute lui-même en HTTP sur `127.0.0.1:<port>`. Il reçoit la valeur par son environnement, comme toute variable du bloc `env`.
94
+ - Le gateway relaie tel quel `/{route}/http/*` vers `http://127.0.0.1:<port>/*` : méthode, chemin, requête, en-têtes et corps à l'aller, statut, en-têtes et corps au retour.
95
+ - Le gateway ne connaît rien du protocole relayé. Il authentifie la requête et applique le rate limiting comme sur toutes ses routes, puis retire l'en-tête `Authorization` avant de transmettre : son jeton ne parvient jamais au MCP.
96
+ - Le corps circule en flux, sans être lu ni décodé. `GATEWAY_BODY_LIMIT` ne s'y applique pas.
97
+ - Le port reste sur `127.0.0.1` : le seul point d'entrée exposé est le gateway.
98
+
99
+ Une valeur hors de `1`–`65535` fait ignorer le MCP au démarrage, avec une erreur dans les logs. Si le MCP n'écoute pas sur le port déclaré, le relais répond `502`.
100
+
101
+ ## Démarrage
102
+
103
+ ```bash
104
+ npm start
105
+ ```
106
+
107
+ Le gateway connecte chaque MCP configuré et démarre le serveur HTTP. Les MCPs en échec au démarrage sont ignorés sans bloquer les autres.
108
+
109
+ ## Rechargement à chaud
110
+
111
+ Avec `GATEWAY_ADMIN_TOKEN` défini, le gateway relit `mcp-servers.json` sur demande, sans redémarrer :
112
+
113
+ ```bash
114
+ curl -X POST -H "X-Gateway-Admin-Token: $GATEWAY_ADMIN_TOKEN" http://127.0.0.1:3500/admin/reload
115
+ ```
116
+
117
+ Chaque MCP est comparé à sa configuration en service :
118
+
119
+ | Changement | Effet |
120
+ |---|---|
121
+ | MCP retiré du fichier | son processus est arrêté, sa route disparaît |
122
+ | MCP ajouté, ou en échec jusque-là | il est lancé |
123
+ | `command`, `args` ou une variable de `env` modifiés | **seul ce MCP** est relancé |
124
+ | `description` ou `EXPOSE_AT_AGENT_STARTUP` seuls modifiés | mis à jour sans relance |
125
+ | rien | rien |
126
+
127
+ Les autres MCP ne sont pas touchés : leurs processus, leurs sessions et leurs appels en cours continuent.
128
+
129
+ La réponse donne le résultat MCP par MCP :
130
+
131
+ ```json
132
+ {
133
+ "revision": "5ecfab70.3",
134
+ "results": [
135
+ { "name": "documentor", "action": "unchanged", "ok": true },
136
+ { "name": "github", "action": "restarted", "ok": true },
137
+ { "name": "scraper", "action": "started", "ok": false, "error": "MCP error -32000: Connection closed" }
138
+ ]
139
+ }
140
+ ```
141
+
142
+ `action` vaut `started`, `restarted`, `stopped`, `updated` ou `unchanged`. Un MCP qui ne démarre pas est signalé par `ok: false` et reste hors service ; le rechargement suivant tente de le relancer. Un fichier illisible est refusé en `400`, et rien n'est modifié.
143
+
144
+ Le jeton passe par l'en-tête `X-Gateway-Admin-Token`, indépendamment de l'authentification OAuth du gateway.
145
+
146
+ ### Révision
147
+
148
+ Chaque réponse du gateway porte l'en-tête `X-Gateway-Revision`. Sa valeur change à chaque démarrage du gateway et à chaque rechargement qui modifie quelque chose. Le [mode MCP](#mode-mcp) s'en sert pour mettre à jour les outils de l'agent.
149
+
150
+ ## Routes
151
+
152
+ ### `GET /mcps`
153
+
154
+ Retourne la liste des MCPs connectés avec leur route, description et instructions d'utilisation (format Markdown).
155
+
156
+ ```bash
157
+ curl http://localhost:3500/mcps
158
+ ```
159
+
160
+ ---
161
+
162
+ ### `GET /{route}/tools`
163
+
164
+ Liste les outils disponibles d'un MCP avec leur nom, description et schéma d'arguments.
165
+
166
+ ```bash
167
+ curl http://localhost:3500/documentor/tools
168
+ ```
169
+
170
+ ---
171
+
172
+ ### `POST /{route}/tools/:toolName`
173
+
174
+ Appelle un outil du MCP. Les arguments sont passés dans le body JSON.
175
+
176
+ ```bash
177
+ curl -X POST http://localhost:3500/documentor/tools/list_projects -H "Content-Type: application/json" -d '{}'
178
+ ```
179
+
180
+ ```bash
181
+ curl -X POST http://localhost:3500/documentor/tools/get_doc -H "Content-Type: application/json" -d '{"id":"mon-doc-id"}'
182
+ ```
183
+
184
+ #### Réponse
185
+
186
+ La réponse est le format brut du protocole MCP — un tableau `content` de blocs typés :
187
+
188
+ ```json
189
+ {
190
+ "content": [
191
+ { "type": "text", "text": "Résultat de l'outil..." }
192
+ ]
193
+ }
194
+ ```
195
+
196
+ Pour extraire le texte directement avec `jq` :
197
+
198
+ ```bash
199
+ curl -s -X POST http://localhost:3500/documentor/tools/list_projects -H "Content-Type: application/json" -d '{}' | jq -r '.content[0].text'
200
+ ```
201
+
202
+ ---
203
+
204
+ ### `/{route}/http/*`
205
+
206
+ Présente uniquement pour un MCP qui déclare `MCP_HTTP_RELAY_PORT` (voir [Relais HTTP](#relais-http-mcp_http_relay_port)). Toute méthode est acceptée ; la requête est transmise telle quelle au serveur HTTP du MCP, sans l'en-tête `Authorization`.
207
+
208
+ ```bash
209
+ git clone -c http.extraHeader="Authorization: Bearer <jeton>" http://localhost:3500/github/http/repo.git
210
+ ```
211
+
212
+ ## Mode MCP
213
+
214
+ Le gateway peut également démarrer en tant que **serveur MCP** (transport stdio). Dans ce mode, il se connecte à une instance du gateway HTTP et expose ses outils comme des outils MCP natifs.
215
+
216
+ ### Cas d'usage : agent dans un conteneur Docker
217
+
218
+ Le gateway HTTP tourne **hors du conteneur**, avec accès aux MCPs et leurs clés sensibles. L'agent dans le conteneur reçoit uniquement le gateway en mode MCP — aucune clé ne lui est injectée.
219
+
220
+ ```
221
+ [Agent dans conteneur]
222
+ │ stdio (MCP)
223
+ ▼
224
+ [mcp-http-gateway --mcp] ←── GATEWAY_URL
225
+ │ HTTP
226
+ ▼
227
+ [mcp-http-gateway hors conteneur]
228
+ │ stdio
229
+ ▼
230
+ [MCPs réels (clés sensibles)]
231
+ ```
232
+
233
+ ### Démarrage
234
+
235
+ ```bash
236
+ GATEWAY_URL=http://host.docker.internal:3500 npx @imenam/mcp-http-gateway --mcp
237
+ ```
238
+
239
+ ### Configuration dans le conteneur
240
+
241
+ ```json
242
+ {
243
+ "mcpServers": {
244
+ "gateway": {
245
+ "command": "npx",
246
+ "args": ["@imenam/mcp-http-gateway", "--mcp"],
247
+ "env": {
248
+ "GATEWAY_URL": "http://host.docker.internal:3500"
249
+ }
250
+ }
251
+ }
252
+ }
253
+ ```
254
+
255
+ > `host.docker.internal` est l'adresse de l'hôte depuis un conteneur Docker.
256
+
257
+ ### Variables d'environnement du mode `--mcp`
258
+
259
+ | Variable | Requis | Description |
260
+ |---|---|---|
261
+ | `GATEWAY_URL` | oui | Base HTTP du gateway à relayer, sans barre finale. Le relais compose lui-même ses routes REST (`/startup-exposition`, `/mcp-definitions`, `/{route}/tools`, `/{route}/tools/{outil}`) : **ne pas** y ajouter `/mcp`. |
262
+ | `GATEWAY_TOKEN` | non | Token porteur. S'il est défini, **tous** les appels sortants du relais portent `Authorization: Bearer <token>`. Absent, aucun en-tête n'est ajouté — le comportement est exactement celui des versions antérieures. |
263
+ | `GATEWAY_INLINE_ROOTS` | non | Dossiers depuis lesquels le relais peut lire un fichier et en transmettre le contenu (cf. [Fichiers locaux](#fichiers-locaux--lagent-nomme-le-relais-lit)). **Non renseignée, tout appel à un outil qui déclare la paire échoue explicitement.** Séparateur : `;` sous Windows, `:` ailleurs. |
264
+ | `GATEWAY_INLINE_EXTENSIONS` | non | Extensions autorisées à la lecture, séparées par des virgules. Défaut : `*` (toutes) — les racines restent le vrai garde-fou. |
265
+ | `GATEWAY_INLINE_MAX_BYTES` | non | Taille maximale d'un fichier transmis, en octets, dans les deux sens. Défaut : `8388608` (8 Mio). |
266
+ | `GATEWAY_INLINE_WRITE_ROOTS` | non | Dossiers dans lesquels le relais peut écrire un fichier renvoyé par un MCP (cf. [Fichiers reçus](#fichiers-reçus--le-mcp-produit-le-relais-écrit)). **Non renseignée, la fonction est désactivée** et tout outil qui la réclame échoue explicitement. Séparateur : `;` sous Windows, `:` ailleurs. |
267
+ | `GATEWAY_INLINE_WRITE_EXTENSIONS` | non | Extensions autorisées à l'écriture, séparées par des virgules. Défaut : `*` (toutes) — les racines restent le vrai garde-fou. |
268
+ | `GATEWAY_EXEC_ROUTES` | non | Liste blanche des routes, dans le gateway, des MCP autorisés à faire exécuter des commandes sur la machine de l'agent, séparées par des virgules (ex. `/github`) — cf. [Exécution sur la machine de l'agent](#exécution-sur-la-machine-de-lagent). **Vide, la fonction est désactivée.** Une entrée qui ne commence pas par `/` fait refuser le démarrage. |
269
+ | `GATEWAY_WATCH_DIR` | non | Dossier des fichiers de veille déposés pour les [notifications](#notifications--un-veilleur-sur-la-machine-de-lagent). Défaut : `~/.mcp-http-gateway/watch`. |
270
+
271
+ > Ces deux variables sont lues dans l'environnement du process. Le relais fait
272
+ > `import 'dotenv/config'` : un fichier `.env` présent dans le **répertoire courant de
273
+ > l'agent** peut donc les fournir. dotenv n'écrase jamais une variable déjà définie —
274
+ > le bloc `env` de la configuration MCP l'emporte toujours.
275
+
276
+ ### Derrière un frontal qui authentifie
277
+
278
+ Quand le gateway n'est pas joint en direct mais à travers un frontal qui exige un token
279
+ porteur (hub [`mcp-gateway-manager`](https://github.com/Matthieu-Pesnot-Pin), reverse-proxy
280
+ maison…), `GATEWAY_TOKEN` est ce que le relais présente à chaque requête :
281
+
282
+ ```json
283
+ {
284
+ "mcpServers": {
285
+ "gateway": {
286
+ "command": "npx",
287
+ "args": ["@imenam/mcp-http-gateway", "--mcp"],
288
+ "env": {
289
+ "GATEWAY_URL": "https://my-gateway.fr/jskdlf-jsfklhj",
290
+ "GATEWAY_TOKEN": "…"
291
+ }
292
+ }
293
+ }
294
+ }
295
+ ```
296
+
297
+ Un refus (`401`/`403`) est rapporté à l'agent en clair — token manquant, ou token invalide —
298
+ plutôt qu'en `HTTP 401` nu. Le token lui-même n'est **jamais** journalisé, pas même tronqué.
299
+
300
+ ### Outils exposés
301
+
302
+ Le mode MCP expose 3 outils qui reflètent directement les routes du gateway :
303
+
304
+ | Outil | Description |
305
+ |---|---|
306
+ | `list_mcps` | Liste les MCPs disponibles avec leur route et description |
307
+ | `list_tools` | Liste les outils d'un MCP donné (`route` requis) |
308
+ | `call_tool` | Appelle un outil (`route`, `tool_name`, `arguments`) |
309
+
310
+ L'agent découvre d'abord les MCPs disponibles, explore les outils d'un MCP si besoin, puis appelle l'outil voulu.
311
+
312
+ ### Fichiers locaux : l'agent nomme, le relais lit
313
+
314
+ Le relais tourne sur la machine de l'**agent** ; les MCP, eux, tournent sur la machine du
315
+ **gateway**. Un chemin de fichier transmis tel quel ne veut donc rien dire à l'arrivée : le
316
+ MCP le cherche chez lui et répond « fichier introuvable ».
317
+
318
+ Le relais sait résoudre ça. Dès qu'un outil distant déclare une paire de paramètres
319
+ `<x>_path` / `<x>_content`, le relais **lit le fichier localement, l'encode en base64 et
320
+ envoie le contenu** dans `<x>_content` (plus le nom dans `<x>_name`, si le schéma le
321
+ prévoit). Le paramètre `file_path` reste la seule chose que l'agent fournit : les
322
+ paramètres que le relais remplit sont **retirés du schéma** publié à l'agent, par
323
+ `tools/list` comme par `list_tools`. L'agent n'a donc aucun arbitrage à faire, et aucune
324
+ occasion d'envoyer un chemin qui ne serait pas lisible à l'arrivée.
325
+
326
+ Un outil qui ne déclare pas cette paire n'est jamais concerné : un `output_path` seul reste
327
+ un chemin, interprété côté gateway comme avant.
328
+
329
+ `<x>_path` est obligatoire pour l'agent si, et seulement si, le MCP le déclare obligatoire.
330
+ Un outil qui le laisse optionnel accepte sa donnée autrement — un texte passé en ligne, par
331
+ exemple. Sans `<x>_path`, le relais ne lit rien et transmet l'appel tel quel.
332
+
333
+ **Le chemin vient de l'agent : il n'est pas digne de confiance.** La fonction est donc
334
+ fermée par défaut et s'ouvre dossier par dossier :
335
+
336
+ ```json
337
+ {
338
+ "mcpServers": {
339
+ "gateway": {
340
+ "command": "npx",
341
+ "args": ["@imenam/mcp-http-gateway", "--mcp"],
342
+ "env": {
343
+ "GATEWAY_URL": "https://my-gateway.fr/jskdlf-jsfklhj",
344
+ "GATEWAY_TOKEN": "…",
345
+ "GATEWAY_INLINE_ROOTS": "C:\\Users\\moi\\Documents\\tableurs;C:\\travail\\exports"
346
+ }
347
+ }
348
+ }
349
+ }
350
+ ```
351
+
352
+ - **`GATEWAY_INLINE_ROOTS` est l'interrupteur.** Non renseignée, le relais ne lit aucun
353
+ fichier et **l'appel échoue** en nommant la variable à définir. Un outil qui déclare la
354
+ paire attend le contenu : laisser filer le chemin ferait chercher le fichier au MCP sur
355
+ la machine du gateway, où il ne le trouverait pas — ou pire, où il traiterait un homonyme
356
+ sans rien signaler. Pas de repli, pas de dégradation silencieuse.
357
+
358
+ > **Changement de comportement.** Les versions antérieures laissaient passer le chemin tel
359
+ > quel dans ce cas. Si un déploiement existant s'appuyait là-dessus, renseignez
360
+ > `GATEWAY_INLINE_ROOTS` — le message d'erreur le dit explicitement.
361
+ - Les liens symboliques sont résolus **avant** le contrôle : un lien posé dans un dossier
362
+ autorisé ne fait pas sortir un fichier situé ailleurs. Une remontée par `..` est refusée
363
+ pour la même raison.
364
+ - Seules les extensions de `GATEWAY_INLINE_EXTENSIONS` passent (défaut : toutes).
365
+ - Au-delà de `GATEWAY_INLINE_MAX_BYTES` (8 Mio par défaut), refus explicite. Ce défaut
366
+ tient sous le plafond de 16 Mio que le hub `mcp-gateway-manager` applique quand il
367
+ journalise les corps de requête.
368
+ - Tout refus est rapporté à l'agent en clair, et **chaque fichier transmis laisse une ligne
369
+ sur `stderr`** : ce qui sort de la machine est lisible dans le journal de l'agent.
370
+
371
+ > Côté MCP, cette fonction suppose que l'outil accepte le contenu en base64. C'est le cas
372
+ > de [`@imenam/xls-reader`](https://www.npmjs.com/package/@imenam/xls-reader) ≥ 2.2. Un MCP
373
+ > qui n'expose que `file_path` ne déclare pas la paire, et rien ne change pour lui.
374
+
375
+ > **Le mode MCP distant (`/mcp`, Claude.ai) n'a pas de relais local** : rien ne tourne alors
376
+ > sur la machine de l'appelant, donc personne ne peut lire son disque. Par ce chemin, un
377
+ > agent qui veut faire lire un fichier doit en envoyer lui-même le contenu.
378
+
379
+ ### Fichiers reçus : le MCP produit, le relais écrit
380
+
381
+ Le sens retour, symétrique du précédent. Un MCP qui *fabrique* un fichier — un export, une
382
+ pièce jointe, un téléchargement — ne peut pas le déposer sur la machine de l'agent : le
383
+ protocole n'offre au serveur aucune primitive pour y écrire. Un chemin de destination
384
+ transmis tel quel ferait écrire le MCP sur **son** disque, sans que personne s'en aperçoive.
385
+
386
+ Dès qu'un outil distant déclare la paire `<x>_path` / `<x>_relay`, le relais :
387
+
388
+ 1. retire `<x>_relay` du schéma publié à l'agent, qui ne voit qu'un dossier à nommer ;
389
+ 2. retient `<x>_path`, ne le transmet pas, et pose `<x>_relay` à `true` ;
390
+ 3. reçoit le contenu du fichier dans la réponse du MCP, l'écrit **ici**, et le retire du
391
+ résultat avant de le rendre à l'agent.
392
+
393
+ ```json
394
+ {
395
+ "mcpServers": {
396
+ "gateway": {
397
+ "command": "npx",
398
+ "args": ["@imenam/mcp-http-gateway", "--mcp"],
399
+ "env": {
400
+ "GATEWAY_URL": "https://my-gateway.fr/jskdlf-jsfklhj",
401
+ "GATEWAY_TOKEN": "…",
402
+ "GATEWAY_INLINE_WRITE_ROOTS": "C:\\Users\\moi\\Téléchargements"
403
+ }
404
+ }
405
+ }
406
+ }
407
+ ```
408
+
409
+ - **`GATEWAY_INLINE_WRITE_ROOTS` est l'interrupteur**, distinct de celui de la lecture :
410
+ écrire est plus dangereux que lire, et n'hérite donc pas des dossiers autorisés en lecture.
411
+ - **Non renseignée, l'appel échoue** en nommant la variable à définir. Pas de repli, pas de
412
+ dégradation silencieuse : mieux vaut un fichier perdu qu'un fichier écrit au mauvais endroit.
413
+ - `<x>_path` est toujours un **dossier**, créé si besoin ; le fichier garde le nom que lui
414
+ donne le MCP. Le relais ne renomme jamais.
415
+ - Comme au sens aller, `<x>_path` n'est obligatoire que si le MCP le déclare tel.
416
+ Un outil qui le déclare optionnel dit par là qu'il sait quoi faire sans destination —
417
+ renvoyer une capture inline, par exemple. Sans `<x>_path`, le relais ne retient rien,
418
+ ne pose pas `<x>_relay`, et le résultat repart intact.
419
+ - Si l'outil déclare aussi `<x>_overwrite`, le relais l'honore ; sinon il refuse d'écraser.
420
+ - Les liens symboliques sont résolus avant contrôle, y compris sur le plus proche ancêtre
421
+ existant quand la destination n'existe pas encore.
422
+ - **Le contenu est retiré du résultat dans tous les cas**, y compris en cas d'échec
423
+ d'écriture. Un fichier de 8 Mio encodé en base64 n'a rien à faire dans le contexte d'un
424
+ agent : ça coûte cher, et un agent qui le recopierait à la main le corromprait.
425
+ - Chaque fichier écrit laisse une ligne sur `stderr`.
426
+
427
+ > Côté MCP, cette fonction suppose que l'outil sache renvoyer son fichier en bloc
428
+ > `resource` (avec `blob` et `name`) quand `<x>_relay` est posé. C'est le cas de
429
+ > [`file-share-mcp`](https://www.npmjs.com/package/file-share-mcp). Un MCP qui ne déclare
430
+ > pas la paire n'est pas concerné, et son résultat repart intact.
431
+
432
+ ### Exécution sur la machine de l'agent
433
+
434
+ Un MCP du gateway tourne sur la machine du gateway et n'a aucun moyen de lancer quoi que
435
+ ce soit sur celle de l'agent. Si sa route figure dans `GATEWAY_EXEC_ROUTES`, le relais lui
436
+ en donne un, à travers les appels d'outils ordinaires :
437
+
438
+ 1. Le relais ajoute à chaque appel d'outil vers cette route l'argument réservé `_gateway_exec` :
439
+ `{ "version": 1, "cwd": "<dossier de lancement du relais>", "gatewayToken": true|false }`.
440
+ 2. Le MCP peut répondre par une **demande d'exécution** au lieu d'un résultat final, dans
441
+ `_meta["gateway/exec"]` :
442
+ ```json
443
+ { "id": "…", "steps": [ { "command": "git", "args": ["status"], "cwd": "repo", "env": { "…": "…" } } ] }
444
+ ```
445
+ 3. Le relais lance les étapes dans l'ordre, **sans shell**, et s'arrête à la première dont
446
+ le code de sortie n'est pas `0`.
447
+ - `cwd` est résolu par rapport au dossier de lancement du relais, c'est-à-dire le dossier où l'agent l'a démarré.
448
+ - Dans `args` et dans les valeurs de `env`, le relais remplace `{{GATEWAY_URL}}` et `{{GATEWAY_TOKEN}}` par ses propres valeurs. Un marqueur dont la valeur manque fait échouer l'appel.
449
+ 4. Il rappelle le même outil avec `_gateway_exec.continuation = { "id", "results" }`, un
450
+ résultat `{ code, stdout, stderr, error? }` par étape exécutée. Il recommence tant que le MCP renvoie une demande, dans la limite de 50 allers-retours.
451
+
452
+ L'agent ne voit que le résultat final.
453
+
454
+ ```json
455
+ {
456
+ "mcpServers": {
457
+ "gateway": {
458
+ "command": "npx",
459
+ "args": ["-y", "@imenam/mcp-http-gateway", "--mcp"],
460
+ "env": {
461
+ "GATEWAY_URL": "https://gateway.example.com",
462
+ "GATEWAY_TOKEN": "…",
463
+ "GATEWAY_EXEC_ROUTES": "/github"
464
+ }
465
+ }
466
+ }
467
+ }
468
+ ```
469
+
470
+ - **`GATEWAY_EXEC_ROUTES` est la liste blanche, côté agent** : les routes, dans le gateway,
471
+ des MCP autorisés à exécuter, séparées par des virgules. Vide, aucun MCP n'exécute.
472
+ - Seul un appel vers une route listée porte `_gateway_exec`. Un MCP qui a besoin
473
+ d'exécuter refuse les autres appels, en le disant.
474
+ - Une demande d'exécution renvoyée par une route non listée fait échouer l'appel, sans
475
+ rien lancer.
476
+ - `/github` et `/github/` désignent la même route. Une entrée qui ne commence pas par `/`
477
+ fait refuser le démarrage du relais.
478
+ - **Pour un MCP listé, l'exécution est libre** : il peut faire lancer n'importe quelle
479
+ commande, avec les droits de l'utilisateur qui fait tourner le relais. Ne listez que des
480
+ MCP que vous maîtrisez.
481
+ - **Les demandes ne viennent que du gateway de `GATEWAY_URL`** : le relais n'en lit que dans
482
+ les réponses à ses propres appels. `GATEWAY_TOKEN` prouve au gateway l'identité du relais,
483
+ mais pas l'inverse. C'est HTTPS qui garantit que la réponse vient bien de ce gateway. Sur
484
+ un `GATEWAY_URL` en `http://` hors de la machine, quiconque se trouve sur le chemin réseau
485
+ peut injecter des demandes d'exécution.
486
+ - Chaque commande laisse une ligne sur `stderr`, avec ses marqueurs non remplacés : le
487
+ jeton n'y figure jamais.
488
+ - La sortie standard et la sortie d'erreur de chaque étape sont tronquées au-delà de 1 Mio.
489
+ - Un MCP dont le schéma refuse les arguments inconnus rejettera `_gateway_exec` : avec
490
+ l'exécution activée, ses appels échouent.
491
+
492
+ > Côté MCP, c'est le cas de [`@imenam/mcp-github`](https://www.npmjs.com/package/@imenam/mcp-github)
493
+ > en rôle `remote` : git s'exécute sur la copie de travail de l'agent, à travers le
494
+ > [relais HTTP](#relais-http-mcp_http_relay_port) du gateway.
495
+
496
+ ### Notifications : un veilleur sur la machine de l'agent
497
+
498
+ Le gateway ne sait que répondre : il ne pousse rien vers l'agent. Pour qu'un MCP du gateway
499
+ réveille l'agent quand il a du nouveau (un post publié, un message reçu…), le relais fait
500
+ armer par l'agent un **veilleur** : un processus lancé sur la machine de l'agent par l'outil
501
+ `Monitor` de son hôte, qui appelle en boucle un outil d'attente du MCP à travers l'API REST du
502
+ gateway. Chaque appel reste ouvert jusqu'à ce que le MCP ait un événement, ou 50 s au plus,
503
+ sous les 60 s que le gateway accorde à un appel d'outil. Chaque événement devient une ligne
504
+ sur la sortie du veilleur, et `Monitor` réveille l'agent à chaque ligne.
505
+
506
+ La convention, côté MCP :
507
+
508
+ 1. Un **outil d'activation** déclare dans son schéma la propriété réservée `_gateway_watch`.
509
+ Le relais la retire du schéma montré à l'agent, et la remplit à chaque appel de cet outil :
510
+ `{ "version": 1 }`. Sans elle, le MCP sait qu'aucun relais ne veillera pour lui.
511
+ 2. L'outil d'activation répond par une **spécification de veille**, dans `_meta["gateway/watch"]` :
512
+ ```json
513
+ { "tool": "wait_for_posts", "arguments": {}, "cursor": "57", "description": "New posts in the Telegram channel" }
514
+ ```
515
+ L'outil d'attente, ses arguments fixes, le point de reprise de départ et le libellé du `Monitor`.
516
+ 3. Le relais dépose sur la machine de l'agent un **fichier de veille** en `0600` : adresse et
517
+ jeton du gateway, route, outil, arguments, point de reprise. Il rend à l'agent, à la place
518
+ de la réponse du MCP, l'appel `Monitor` à faire :
519
+ ```text
520
+ command: "<node>" "<…>/mcp-http-gateway/server.js" --watch "<…>/watch/<id>.json"
521
+ timeout_ms: 1800000
522
+ ```
523
+ Le jeton ne figure jamais dans la commande, donc jamais dans la conversation.
524
+ 4. Le veilleur (`server.js --watch <fichier>`) appelle l'outil d'attente avec ses arguments,
525
+ plus `cursor` et `timeout_seconds: 50`. L'outil répond, dans `_meta["gateway/events"]` :
526
+ ```json
527
+ { "cursor": "58", "lines": ["[telegram] new post 42: … — read it with get_post"] }
528
+ ```
529
+ Le veilleur écrit chaque ligne telle quelle, enregistre le point de reprise dans le fichier
530
+ de veille, et rappelle aussitôt l'outil.
531
+
532
+ Comportement du veilleur :
533
+
534
+ - **Expiration du `Monitor`** (30 minutes au plus) : l'agent relance la même commande, et le
535
+ veilleur repart du point de reprise enregistré, sans rien perdre entre-temps.
536
+ - **Réactivation pendant la veille** : si un veilleur suit déjà ce fichier, le relais le dit
537
+ au lieu de demander un second veilleur, et ne touche pas au point de reprise.
538
+ - **Pannes** : le veilleur réessaie avec une temporisation croissante (de 1 s à 30 s) et ne
539
+ s'arrête jamais de lui-même. Un refus du jeton, un outil qui ne suit pas la convention, ou
540
+ trois échecs de suite sont annoncés une fois sur sa sortie, donc à l'agent ; le retour à la
541
+ normale aussi.
542
+ - **Ménage** : le fichier de veille, qui contient le jeton, est propre au relais qui l'écrit.
543
+ Il est retiré à l'arrêt de ce relais (fermeture de son entrée standard) ou, si le relais a
544
+ été tué, par le prochain relais qui démarre.
545
+ - Une spécification renvoyée par un outil qui n'a pas déclaré `_gateway_watch` est ignorée.
546
+ - Un MCP n'accède ainsi qu'à la veille de ses propres outils : le veilleur n'appelle que
547
+ l'outil nommé dans la spécification, sur la route qui l'a renvoyée.
548
+
549
+ > Côté MCP, c'est le cas de [`@imenam/mcp-telegram`](https://www.npmjs.com/package/@imenam/mcp-telegram) :
550
+ > `enable_notifications` active la veille, `wait_for_posts` est l'outil d'attente.
551
+
552
+ ---
553
+
554
+ ## Mode MCP distant (HTTPS / Claude.ai)
555
+
556
+ Le gateway expose également un **endpoint MCP natif** sur `/mcp` via le transport **Streamable HTTP**, en mode **stateless**. Ce mode permet à Claude.ai — et à tout client MCP distant compatible — de se connecter directement au gateway, sans intermédiaire.
557
+
558
+ Chaque `POST /mcp` est traité de façon autonome : aucun `mcp-session-id` n'est émis, aucun état n'est conservé entre deux requêtes. Les trois outils exposés étant du pur requête/réponse (aucune notification serveur→client), aucune session n'est nécessaire. Concrètement :
559
+
560
+ - un connecteur laissé inactif ne peut pas « expirer » côté serveur — il n'y a rien à expirer ;
561
+ - un redémarrage ou un redéploiement du gateway est transparent pour les clients connectés ;
562
+ - l'endpoint peut être scalé horizontalement derrière un load balancer sans affinité de session.
563
+
564
+ `GET /mcp` et `DELETE /mcp` répondent `405 Method Not Allowed` : sans session, il n'y a ni flux SSE autonome ni terminaison de session à gérer (la spec Streamable HTTP l'autorise explicitement).
565
+
566
+ ### Flow
567
+
568
+ ```
569
+ Claude.ai
570
+ │ HTTPS + Bearer JWT
571
+ ▼
572
+ [mcp-http-gateway /mcp] ──── 401 + WWW-Authenticate si non authentifié
573
+ │
574
+ │ (découverte OAuth 2.1)
575
+ ▼
576
+ [Authorization Server externe] (Auth0, Keycloak, Authentik, Zitadel, Logto…)
577
+ ```
578
+
579
+ Le gateway agit uniquement comme **Resource Server** : il valide les JWT via JWKS et expose l'URL de l'AS via le Protected Resource Metadata (RFC 9728). Toute la logique OAuth (DCR, PKCE, `/authorize`, `/token`) est déléguée à l'AS externe.
580
+
581
+ ### Variables d'environnement
582
+
583
+ | Variable | Description |
584
+ |---|---|
585
+ | `MCP_OAUTH_ENABLED` | `true` pour activer la protection OAuth (recommandé en production) |
586
+ | `MCP_PUBLIC_URL` | URL publique canonique du endpoint — ex. `https://gateway.example.com/mcp`. Utilisée comme `resource` (RFC 9728) et `aud` JWT. |
587
+ | `OAUTH_JWKS_URI` | URI du JWKS de l'Authorization Server — ex. `https://your-auth-server/.well-known/jwks.json` |
588
+ | `OAUTH_ISSUER` | Issuer attendu dans les JWT — ex. `https://your-auth-server` |
589
+
590
+ > Sans `MCP_OAUTH_ENABLED=true`, `/mcp` est ouvert (utile pour les tests locaux). Claude.ai refusera cependant de s'y connecter sans 401 + `WWW-Authenticate`.
591
+
592
+ ### Configuration Claude.ai
593
+
594
+ Dans Claude.ai → **Paramètres → Connecteurs** → Ajouter un connecteur MCP :
595
+
596
+ ```
597
+ URL : https://gateway.example.com/mcp
598
+ Authentification : OAuth (auto-détectée via /.well-known/oauth-protected-resource)
599
+ ```
600
+
601
+ ### Prérequis côté Authorization Server
602
+
603
+ L'AS doit supporter :
604
+ - **DCR** (RFC 7591) — `registration_endpoint` exposé
605
+ - **PKCE S256** — `code_challenge_methods_supported: ["S256"]`
606
+ - `redirect_uri` autorisé : `https://claude.ai/api/mcp/auth_callback`
607
+ - JWT signés (RS256 ou ES256) avec claims `iss`, `aud=MCP_PUBLIC_URL`, `exp`
608
+
609
+ ### Exposition HTTPS publique
610
+
611
+ Le endpoint `/mcp` doit être accessible depuis l'IP range `160.79.104.0/21` (Anthropic) en HTTPS. Options :
612
+
613
+ | Option | Commande / Notes |
614
+ |---|---|
615
+ | **Cloudflare Tunnel** | `cloudflared tunnel --url http://localhost:3500` |
616
+ | **ngrok** | `ngrok http 3500` — idéal pour les tests |
617
+ | **Reverse-proxy** | Caddy / Traefik en frontal avec cert Let's Encrypt |
618
+
619
+ ### Outils exposés
620
+
621
+ Les mêmes 3 outils que le mode stdio — avec accès direct aux clients MCP internes (sans repasser par HTTP) :
622
+
623
+ | Outil | Description |
624
+ |---|---|
625
+ | `list_mcps` | Liste les MCPs disponibles avec leur route et description |
626
+ | `list_tools` | Liste les outils d'un MCP donné (`route` requis) |
627
+ | `call_tool` | Appelle un outil (`route`, `tool_name`, `arguments`) |
628
+
629
+ ### Pré-exposition de MCP au démarrage (`EXPOSE_AT_AGENT_STARTUP`)
630
+
631
+ Par défaut, l'agent ne reçoit que les 3 outils de découverte et doit appeler `list_mcps` /
632
+ `list_tools` pour découvrir les MCP. Pour les MCP fréquemment utilisés, la clé
633
+ `EXPOSE_AT_AGENT_STARTUP` publie leurs outils **directement dans la réponse `tools/list`**,
634
+ nommés `<serveur>__<outil>`, avec description et `inputSchema` complets. L'agent les appelle
635
+ directement, sans passer par `call_tool` et sans aucun aller-retour de découverte.
636
+
637
+ L'option se déclare **par MCP**, dans son bloc `env` de `config/mcp-servers.json` — au même
638
+ endroit que `MCP_SERVER_ROUTE`, qui est également une directive lue par le gateway :
639
+
640
+ ```json
641
+ {
642
+ "mcpServers": {
643
+ "documentor": {
644
+ "command": "node",
645
+ "args": ["/chemin/vers/documentor/dist/index.js"],
646
+ "description": "Documentation manager MCP",
647
+ "env": {
648
+ "MCP_SERVER_ROUTE": "/documentor",
649
+ "EXPOSE_AT_AGENT_STARTUP": "true"
650
+ }
651
+ },
652
+ "scraper": {
653
+ "command": "node",
654
+ "args": ["/chemin/vers/scraper/dist/index.js"],
655
+ "env": {
656
+ "MCP_SERVER_ROUTE": "/scraper"
657
+ }
658
+ }
659
+ }
660
+ }
661
+ ```
662
+
663
+ Ici `documentor` est pré-exposé ; `scraper`, sans la clé, ne l'est pas et reste accessible
664
+ via `list_mcps` / `list_tools` / `call_tool`. Le `.env` du gateway ne contient **aucune**
665
+ indication relative aux MCP.
666
+
667
+ La valeur est un booléen : `"true"` (également `"1"`, `"yes"`, `"on"`) ou `"false"`. Toute
668
+ autre valeur est refusée avec un avertissement au démarrage et le MCP n'est pas pré-exposé.
669
+
670
+ **Pourquoi ce canal ?** Les définitions d'outils passent par `tools/list`, seul canal sans
671
+ limite de taille. Le champ `instructions` du protocole, lui, est plafonné par les clients
672
+ autour de **2 048 caractères** et tronqué silencieusement au-delà — un MCP de 30 outils
673
+ avec ses schémas pèse ~33 000 caractères, il ne peut donc pas y transiter.
674
+
675
+ **Quand l'activer ?** Le compromis, pour un MCP réellement utilisé :
676
+
677
+ | | Découverte (défaut) | Pré-exposé |
678
+ |---|---|---|
679
+ | Allers-retours | 3 (`list_mcps`, `list_tools`, `call_tool`) | 1 (appel direct) |
680
+ | Contexte | `list_tools` déverse tous les schémas dans la conversation dès le 1ᵉʳ usage, définitivement | les noms d'outils, puis le schéma des seuls outils utilisés |
681
+
682
+ À activer donc pour les MCP utilisés dans la majorité des sessions. À laisser en découverte
683
+ pour les MCP occasionnels : un MCP pré-exposé mais inutilisé coûte ses noms d'outils à
684
+ chaque requête, et multiplier les outils similaires dégrade la sélection par l'agent.
685
+
686
+ Dans tous les cas, le MCP enfant reste hébergé côté gateway : seules ses **définitions**
687
+ sont relayées, les appels sont proxifiés. L'isolation est préservée.
688
+
689
+ Cette config vit **uniquement côté serveur** et s'applique de façon transparente aux deux
690
+ points d'entrée par lesquels un agent atteint le gateway :
691
+
692
+ - **endpoint `/mcp` distant** (cas Claude.ai) — les outils proxy sont publiés directement ;
693
+ - **bridge stdio** (`server.js --mcp`, cas local) — au démarrage, le bridge interroge le
694
+ serveur (`GET /startup-exposition`), publie les outils reçus et route leurs appels vers
695
+ la bonne route. Le bridge ne connaît pas la config : il ne fait que transmettre ce que le
696
+ serveur décide. Quand une réponse du gateway porte une nouvelle
697
+ [révision](#révision), le bridge relit `GET /startup-exposition` et envoie
698
+ `notifications/tools/list_changed` à l'agent.
699
+
700
+ ---
701
+
702
+ ## Ajouter un MCP
703
+
704
+ 1. Ajouter l'entrée dans `config/mcp-servers.json` avec `MCP_SERVER_ROUTE` dans `env`
705
+ 2. Appeler `POST /admin/reload` (voir [Rechargement à chaud](#rechargement-à-chaud)), ou redémarrer le gateway
706
+
707
+ Aucune modification du MCP n'est nécessaire.