@imenam/mcp-http-gateway 2.0.10 → 2.0.11

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.
Files changed (3) hide show
  1. package/README.md +61 -3
  2. package/mcp-mode.js +249 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -189,9 +189,11 @@ GATEWAY_URL=http://host.docker.internal:3500 npx @imenam/mcp-http-gateway --mcp
189
189
  |---|---|---|
190
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
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, la fonction est désactivée** et un chemin part tel quel. Séparateur : `;` sous Windows, `:` ailleurs. |
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
193
  | `GATEWAY_INLINE_EXTENSIONS` | non | Extensions autorisées, séparées par des virgules. Défaut : `.xlsx,.xls,.ods,.csv`. `*` autorise toutes les extensions. |
194
- | `GATEWAY_INLINE_MAX_BYTES` | non | Taille maximale d'un fichier transmis, en octets. Défaut : `8388608` (8 Mio). |
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. |
195
197
 
196
198
  > Ces deux variables sont lues dans l'environnement du process. Le relais fait
197
199
  > `import 'dotenv/config'` : un fichier `.env` présent dans le **répertoire courant de
@@ -271,7 +273,14 @@ fermée par défaut et s'ouvre dossier par dossier :
271
273
  ```
272
274
 
273
275
  - **`GATEWAY_INLINE_ROOTS` est l'interrupteur.** Non renseignée, le relais ne lit aucun
274
- fichier et se comporte exactement comme les versions antérieures.
276
+ fichier et **l'appel échoue** en nommant la variable à définir. Un outil qui déclare la
277
+ paire attend le contenu : laisser filer le chemin ferait chercher le fichier au MCP sur
278
+ la machine du gateway, où il ne le trouverait pas — ou pire, où il traiterait un homonyme
279
+ sans rien signaler. Pas de repli, pas de dégradation silencieuse.
280
+
281
+ > **Changement de comportement.** Les versions antérieures laissaient passer le chemin tel
282
+ > quel dans ce cas. Si un déploiement existant s'appuyait là-dessus, renseignez
283
+ > `GATEWAY_INLINE_ROOTS` — le message d'erreur le dit explicitement.
275
284
  - Les liens symboliques sont résolus **avant** le contrôle : un lien posé dans un dossier
276
285
  autorisé ne fait pas sortir un fichier situé ailleurs. Une remontée par `..` est refusée
277
286
  pour la même raison.
@@ -290,6 +299,55 @@ fermée par défaut et s'ouvre dossier par dossier :
290
299
  > sur la machine de l'appelant, donc personne ne peut lire son disque. Par ce chemin, un
291
300
  > agent qui veut faire lire un fichier doit en envoyer lui-même le contenu.
292
301
 
302
+ ### Fichiers reçus : le MCP produit, le relais écrit
303
+
304
+ Le sens retour, symétrique du précédent. Un MCP qui *fabrique* un fichier — un export, une
305
+ pièce jointe, un téléchargement — ne peut pas le déposer sur la machine de l'agent : le
306
+ protocole n'offre au serveur aucune primitive pour y écrire. Un chemin de destination
307
+ transmis tel quel ferait écrire le MCP sur **son** disque, sans que personne s'en aperçoive.
308
+
309
+ Dès qu'un outil distant déclare la paire `<x>_path` / `<x>_relay`, le relais :
310
+
311
+ 1. retire `<x>_relay` du schéma publié à l'agent, qui ne voit qu'un dossier à nommer ;
312
+ 2. retient `<x>_path`, ne le transmet pas, et pose `<x>_relay` à `true` ;
313
+ 3. reçoit le contenu du fichier dans la réponse du MCP, l'écrit **ici**, et le retire du
314
+ résultat avant de le rendre à l'agent.
315
+
316
+ ```json
317
+ {
318
+ "mcpServers": {
319
+ "gateway": {
320
+ "command": "npx",
321
+ "args": ["@imenam/mcp-http-gateway", "--mcp"],
322
+ "env": {
323
+ "GATEWAY_URL": "https://my-gateway.fr/jskdlf-jsfklhj",
324
+ "GATEWAY_TOKEN": "…",
325
+ "GATEWAY_INLINE_WRITE_ROOTS": "C:\\Users\\moi\\Téléchargements"
326
+ }
327
+ }
328
+ }
329
+ }
330
+ ```
331
+
332
+ - **`GATEWAY_INLINE_WRITE_ROOTS` est l'interrupteur**, distinct de celui de la lecture :
333
+ écrire est plus dangereux que lire, et n'hérite donc pas des dossiers autorisés en lecture.
334
+ - **Non renseignée, l'appel échoue** en nommant la variable à définir. Pas de repli, pas de
335
+ dégradation silencieuse : mieux vaut un fichier perdu qu'un fichier écrit au mauvais endroit.
336
+ - `<x>_path` est toujours un **dossier**, créé si besoin ; le fichier garde le nom que lui
337
+ donne le MCP. Le relais ne renomme jamais.
338
+ - Si l'outil déclare aussi `<x>_overwrite`, le relais l'honore ; sinon il refuse d'écraser.
339
+ - Les liens symboliques sont résolus avant contrôle, y compris sur le plus proche ancêtre
340
+ existant quand la destination n'existe pas encore.
341
+ - **Le contenu est retiré du résultat dans tous les cas**, y compris en cas d'échec
342
+ d'écriture. Un fichier de 8 Mio encodé en base64 n'a rien à faire dans le contexte d'un
343
+ agent : ça coûte cher, et un agent qui le recopierait à la main le corromprait.
344
+ - Chaque fichier écrit laisse une ligne sur `stderr`.
345
+
346
+ > Côté MCP, cette fonction suppose que l'outil sache renvoyer son fichier en bloc
347
+ > `resource` (avec `blob` et `name`) quand `<x>_relay` est posé. C'est le cas de
348
+ > [`file-share-mcp`](https://www.npmjs.com/package/file-share-mcp). Un MCP qui ne déclare
349
+ > pas la paire n'est pas concerné, et son résultat repart intact.
350
+
293
351
  ---
294
352
 
295
353
  ## Mode MCP distant (HTTPS / Claude.ai)
package/mcp-mode.js CHANGED
@@ -146,7 +146,7 @@ function insideAllowedRoot(realPath) {
146
146
  /** Les paires `<x>_path` / `<x>_content` déclarées par le schéma d'un outil. */
147
147
  function inlinablePairs(inputSchema) {
148
148
  const props = inputSchema?.properties;
149
- if (!INLINE_FILES || !props) return [];
149
+ if (!props) return [];
150
150
  return Object.keys(props)
151
151
  .filter((key) => key.endsWith('_content') && `${key.slice(0, -'_content'.length)}_path` in props)
152
152
  .map((contentProp) => {
@@ -161,10 +161,12 @@ function inlinablePairs(inputSchema) {
161
161
 
162
162
  /** Le schéma tel que l'agent doit le voir : sans les paramètres que le relais remplit. */
163
163
  function schemaForAgent(inputSchema) {
164
- const pairs = inlinablePairs(inputSchema);
164
+ const pairs = [...inlinablePairs(inputSchema), ...deliverablePairs(inputSchema)];
165
165
  if (pairs.length === 0) return inputSchema;
166
166
 
167
- const hidden = new Set(pairs.flatMap((p) => [p.contentProp, p.nameProp].filter(Boolean)));
167
+ const hidden = new Set(
168
+ pairs.flatMap((p) => [p.contentProp, p.nameProp, p.relayProp].filter(Boolean))
169
+ );
168
170
  const properties = Object.fromEntries(
169
171
  Object.entries(inputSchema.properties).filter(([key]) => !hidden.has(key))
170
172
  );
@@ -195,6 +197,19 @@ function inlineFileArgs(inputSchema, args = {}, label = '') {
195
197
  const filePath = out[pathProp];
196
198
  if (typeof filePath !== 'string' || filePath === '') continue;
197
199
 
200
+ // Un outil qui déclare la paire attend le CONTENU : il tourne sur la machine du
201
+ // gateway, où le chemin de l'agent ne désigne rien — ou pire, désigne un homonyme
202
+ // qu'il traiterait sans broncher. Laisser filer le chemin serait une dégradation
203
+ // silencieuse ; on refuse en nommant la variable à renseigner.
204
+ if (!INLINE_FILES) {
205
+ throw new Error(
206
+ `Transmission de fichier impossible : aucun dossier de lecture autorisé. L'outil ` +
207
+ `attend le contenu du fichier (paramètre ${pathProp}), mais il tourne sur la machine ` +
208
+ `du gateway et ne peut pas lire la vôtre. Renseignez GATEWAY_INLINE_ROOTS dans le ` +
209
+ `bloc « env » de la configuration de ce serveur MCP.`
210
+ );
211
+ }
212
+
198
213
  const resolved = path.resolve(filePath);
199
214
 
200
215
  // realpath avant tout contrôle : sans ça, un lien symbolique placé dans une racine
@@ -244,11 +259,233 @@ function inlineFileArgs(inputSchema, args = {}, label = '') {
244
259
  return out;
245
260
  }
246
261
 
262
+ // --- Fichiers : le MCP produit, le relais écrit ---
263
+ //
264
+ // Le sens retour, symétrique du précédent. `inlineFileArgs` ne réécrit que les arguments
265
+ // à l'aller ; un fichier produit par le MCP, lui, n'a aucun moyen d'atteindre la machine
266
+ // de l'agent : le protocole n'offre au serveur aucune primitive pour y écrire.
267
+ //
268
+ // La règle : dès qu'un outil distant déclare une paire `<x>_path` / `<x>_relay`, le relais
269
+ // retient le chemin, annonce sa présence au MCP en posant `<x>_relay` à true, et ne
270
+ // transmet PAS `<x>_path` — qui ne désignerait rien à l'arrivée. Le MCP répond alors avec
271
+ // le contenu du fichier, que le relais écrit ICI, à l'emplacement retenu.
272
+ //
273
+ // Le contenu ne ressort JAMAIS vers l'agent : le relais le retire du résultat, qu'il ait
274
+ // réussi ou non à écrire. Un fichier de 8 Mio encodé en base64 n'a rien à faire dans le
275
+ // contexte d'un agent, et rien ne justifie de l'y déverser « au cas où » : l'appel échoue,
276
+ // le fichier est perdu, l'agent recommence.
277
+ //
278
+ // Sans relais dans la chaîne — MCP lancé en stdio à côté de l'agent — `<x>_relay` reste
279
+ // absent : les deux machines n'en font qu'une, et le MCP écrit lui-même à `<x>_path`.
280
+ //
281
+ // Écrire est plus dangereux que lire : la fonction a donc sa propre racine autorisée,
282
+ // et n'hérite pas de celle de la lecture.
283
+ //
284
+ // GATEWAY_INLINE_WRITE_ROOTS Dossiers où un fichier peut être déposé. C'est
285
+ // l'interrupteur : non renseigné, aucune livraison
286
+ // n'a lieu et l'appel échoue en le disant.
287
+ // GATEWAY_INLINE_WRITE_EXTENSIONS Extensions autorisées (défaut : toutes). Les racines
288
+ // restent le vrai garde-fou.
289
+ // GATEWAY_INLINE_MAX_BYTES Plafond de taille, partagé avec la lecture.
290
+
291
+ const WRITE_ROOTS = (process.env.GATEWAY_INLINE_WRITE_ROOTS ?? '')
292
+ .split(path.delimiter)
293
+ .map((entry) => entry.trim())
294
+ .filter(Boolean)
295
+ .map((entry) => {
296
+ const resolved = path.resolve(entry);
297
+ try {
298
+ return fs.realpathSync(resolved);
299
+ } catch {
300
+ console.error(`[mcp-mode] GATEWAY_INLINE_WRITE_ROOTS : dossier introuvable, ignoré — ${resolved}`);
301
+ return null;
302
+ }
303
+ })
304
+ .filter(Boolean);
305
+
306
+ const WRITE_EXTENSIONS = (process.env.GATEWAY_INLINE_WRITE_EXTENSIONS ?? '*')
307
+ .split(',')
308
+ .map((entry) => entry.trim().toLowerCase())
309
+ .filter(Boolean);
310
+
311
+ const WRITE_ANY_EXTENSION = WRITE_EXTENSIONS.includes('*');
312
+
313
+ if (WRITE_ROOTS.length > 0) {
314
+ console.error(
315
+ `[mcp-mode] Écriture des fichiers reçus activée vers : ${WRITE_ROOTS.join(', ')} ` +
316
+ `(extensions : ${WRITE_ANY_EXTENSION ? 'toutes' : WRITE_EXTENSIONS.join(' ')})`
317
+ );
318
+ }
319
+
320
+ /** Les paires `<x>_path` / `<x>_relay` déclarées par le schéma d'un outil. */
321
+ function deliverablePairs(inputSchema) {
322
+ const props = inputSchema?.properties;
323
+ if (!props) return [];
324
+ return Object.keys(props)
325
+ .filter((key) => key.endsWith('_relay') && `${key.slice(0, -'_relay'.length)}_path` in props)
326
+ .map((relayProp) => {
327
+ const stem = relayProp.slice(0, -'_relay'.length);
328
+ return {
329
+ pathProp: `${stem}_path`,
330
+ relayProp,
331
+ overwriteProp: `${stem}_overwrite` in props ? `${stem}_overwrite` : null,
332
+ };
333
+ });
334
+ }
335
+
336
+ /**
337
+ * Le chemin de destination, résolu et vérifié contre les racines autorisées.
338
+ *
339
+ * `destDir` est toujours un dossier et `fileName` le nom que le MCP donne au fichier :
340
+ * le relais ne renomme jamais. Passer `fileName` vide ne vérifie que le dossier — ce
341
+ * qu'on fait à l'aller, pour échouer avant d'avoir fait voyager les octets pour rien.
342
+ *
343
+ * Le fichier n'existe pas encore : impossible d'appeler `realpath` dessus. On résout
344
+ * donc les liens du plus proche ancêtre existant, sans quoi un lien symbolique déposé
345
+ * dans une racine autorisée permettrait d'écrire n'importe où sur la machine.
346
+ */
347
+ function resolveWriteTarget(destDir, fileName = '') {
348
+ const resolved = fileName
349
+ ? path.resolve(destDir, path.basename(fileName))
350
+ : path.resolve(destDir);
351
+
352
+ let existing = path.dirname(resolved);
353
+ const trailing = [path.basename(resolved)];
354
+ while (!fs.existsSync(existing)) {
355
+ const parent = path.dirname(existing);
356
+ if (parent === existing) {
357
+ throw new Error(`Destination inatteignable sur la machine de l'agent : ${resolved}.`);
358
+ }
359
+ trailing.unshift(path.basename(existing));
360
+ existing = parent;
361
+ }
362
+
363
+ const real = path.join(fs.realpathSync(existing), ...trailing);
364
+
365
+ const inside = WRITE_ROOTS.some((root) => {
366
+ const rel = path.relative(root, real);
367
+ // Un dossier de destination PEUT être la racine elle-même ; un fichier, non.
368
+ if (rel === '') return fileName === '';
369
+ return !rel.startsWith('..') && !path.isAbsolute(rel);
370
+ });
371
+ if (!inside) {
372
+ throw new Error(
373
+ `Écriture refusée : ${real} est hors des dossiers autorisés (${WRITE_ROOTS.join(', ')}). ` +
374
+ `Choisissez une destination dans l'un d'eux, ou ajoutez son dossier à ` +
375
+ `GATEWAY_INLINE_WRITE_ROOTS dans la configuration de ce serveur MCP.`
376
+ );
377
+ }
378
+
379
+ const ext = path.extname(real).toLowerCase();
380
+ if (fileName && !WRITE_ANY_EXTENSION && !WRITE_EXTENSIONS.includes(ext)) {
381
+ throw new Error(
382
+ `Écriture refusée : l'extension « ${ext || '(aucune)'} » n'est pas autorisée ` +
383
+ `(autorisées : ${WRITE_EXTENSIONS.join(' ')}, via GATEWAY_INLINE_WRITE_EXTENSIONS).`
384
+ );
385
+ }
386
+
387
+ return real;
388
+ }
389
+
390
+ /**
391
+ * Retire `<x>_path` des arguments et pose `<x>_relay`, pour que le MCP renvoie le
392
+ * contenu au lieu de tenter une écriture qui atterrirait sur SA machine.
393
+ */
394
+ function prepareDeliveries(inputSchema, args = {}) {
395
+ const pairs = deliverablePairs(inputSchema);
396
+ if (pairs.length === 0) return { args, deliveries: [] };
397
+
398
+ const out = { ...args };
399
+ const deliveries = [];
400
+ for (const { pathProp, relayProp, overwriteProp } of pairs) {
401
+ const destPath = out[pathProp];
402
+ if (typeof destPath !== 'string' || destPath === '') continue;
403
+
404
+ if (WRITE_ROOTS.length === 0) {
405
+ throw new Error(
406
+ `Livraison de fichier impossible : aucun dossier d'écriture autorisé. Le MCP tourne ` +
407
+ `sur la machine du gateway et ne peut pas écrire sur la vôtre. Renseignez ` +
408
+ `GATEWAY_INLINE_WRITE_ROOTS dans le bloc « env » de la configuration de ce ` +
409
+ `serveur MCP avec le ou les dossiers où déposer les fichiers reçus.`
410
+ );
411
+ }
412
+
413
+ // Vérification du dossier seul : le nom du fichier n'est connu qu'au retour.
414
+ resolveWriteTarget(destPath);
415
+ deliveries.push({
416
+ destDir: destPath,
417
+ overwrite: overwriteProp ? out[overwriteProp] === true : false,
418
+ });
419
+ out[relayProp] = true;
420
+ delete out[pathProp];
421
+ }
422
+ return { args: out, deliveries };
423
+ }
424
+
425
+ /**
426
+ * Écrit les fichiers renvoyés par le MCP et purge le résultat de leur contenu.
427
+ *
428
+ * Le contenu est retiré dans tous les cas, y compris en cas d'échec d'écriture : c'est
429
+ * la seule garantie qu'un base64 ne finira jamais dans le contexte de l'agent.
430
+ */
431
+ function applyDeliveries(result, deliveries, label = '') {
432
+ if (deliveries.length === 0) return result;
433
+
434
+ const blocks = Array.isArray(result?.content) ? result.content : [];
435
+ const blobs = blocks.filter((b) => b?.type === 'resource' && typeof b.resource?.blob === 'string');
436
+ const kept = blocks.filter((b) => !blobs.includes(b));
437
+
438
+ if (result?.isError) return { ...result, content: kept.length > 0 ? kept : blocks };
439
+
440
+ const notes = [];
441
+ deliveries.forEach((delivery, i) => {
442
+ const blob = blobs[i];
443
+ if (!blob) {
444
+ notes.push(`Aucun contenu renvoyé par le MCP pour ${delivery.destDir} : rien n'a été écrit.`);
445
+ return;
446
+ }
447
+ let target = delivery.destDir;
448
+ try {
449
+ const fileName = blob.resource.name || path.basename(new URL(blob.resource.uri).pathname);
450
+ if (!fileName) throw new Error("le MCP n'a pas nommé le fichier renvoyé");
451
+ target = resolveWriteTarget(delivery.destDir, fileName);
452
+
453
+ const data = Buffer.from(blob.resource.blob, 'base64');
454
+ if (data.length > INLINE_MAX_BYTES) {
455
+ throw new Error(
456
+ `fichier trop volumineux : ${data.length} octets, la limite est de ` +
457
+ `${INLINE_MAX_BYTES} (GATEWAY_INLINE_MAX_BYTES)`
458
+ );
459
+ }
460
+ if (!delivery.overwrite && fs.existsSync(target)) {
461
+ throw new Error('la destination existe déjà');
462
+ }
463
+ fs.mkdirSync(path.dirname(target), { recursive: true });
464
+ fs.writeFileSync(target, data);
465
+ notes.push(`Fichier écrit : ${target} (${data.length} octets).`);
466
+ console.error(
467
+ `[mcp-mode] Fichier reçu${label ? ` de ${label}` : ''} : ${target} (${data.length} octets)`
468
+ );
469
+ } catch (err) {
470
+ notes.push(`Échec de l'écriture dans ${target} : ${err.message}.`);
471
+ }
472
+ });
473
+
474
+ const failed = notes.some((note) => note.startsWith('Échec') || note.startsWith('Aucun'));
475
+ return {
476
+ ...result,
477
+ content: [...kept, { type: 'text', text: notes.join('\n') }],
478
+ ...(failed ? { isError: true } : {}),
479
+ };
480
+ }
481
+
247
482
  /** Schémas des outils d'une route, mis en cache — nécessaires pour `call_tool`. */
248
483
  const routeSchemas = new Map();
249
484
 
250
485
  async function toolSchema(route, toolName) {
251
- if (!INLINE_FILES) return null;
486
+ // Toujours récupéré, même si l'inlining en lecture est désactivé : c'est le schéma qui
487
+ // révèle une paire `<x>_path` / `<x>_relay`, et la manquer ferait repartir un chemin
488
+ // de destination vers le MCP distant, qui écrirait alors sur SA machine sans le dire.
252
489
  if (!routeSchemas.has(route)) {
253
490
  try {
254
491
  const res = await gatewayFetch(`${route}/tools`);
@@ -345,18 +582,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
345
582
  try {
346
583
  const proxy = proxyIndex.get(name);
347
584
  if (proxy) {
585
+ const label = `${proxy.route}/${proxy.toolName}`;
586
+ const { args: outArgs, deliveries } = prepareDeliveries(proxy.inputSchema, args ?? {});
348
587
  const res = await gatewayFetch(`${proxy.route}/tools/${proxy.toolName}`, {
349
588
  method: 'POST',
350
589
  headers: { 'Content-Type': 'application/json' },
351
- body: JSON.stringify(
352
- inlineFileArgs(proxy.inputSchema, args ?? {}, `${proxy.route}/${proxy.toolName}`)
353
- ),
590
+ body: JSON.stringify(inlineFileArgs(proxy.inputSchema, outArgs, label)),
354
591
  });
355
592
  if (!res.ok) {
356
593
  const text = await res.text();
357
594
  return { content: [{ type: 'text', text: gatewayErrorText(res.status, text) }], isError: true };
358
595
  }
359
- return await res.json();
596
+ return applyDeliveries(await res.json(), deliveries, label);
360
597
  }
361
598
 
362
599
  if (name === 'list_mcps') {
@@ -384,18 +621,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
384
621
 
385
622
  if (name === 'call_tool') {
386
623
  const schema = await toolSchema(args.route, args.tool_name);
624
+ const label = `${args.route}/${args.tool_name}`;
625
+ const { args: outArgs, deliveries } = prepareDeliveries(schema, args.arguments ?? {});
387
626
  const res = await gatewayFetch(`${args.route}/tools/${args.tool_name}`, {
388
627
  method: 'POST',
389
628
  headers: { 'Content-Type': 'application/json' },
390
- body: JSON.stringify(
391
- inlineFileArgs(schema, args.arguments ?? {}, `${args.route}/${args.tool_name}`)
392
- ),
629
+ body: JSON.stringify(inlineFileArgs(schema, outArgs, label)),
393
630
  });
394
631
  if (!res.ok) {
395
632
  const text = await res.text();
396
633
  return { content: [{ type: 'text', text: gatewayErrorText(res.status, text) }], isError: true };
397
634
  }
398
- return await res.json();
635
+ return applyDeliveries(await res.json(), deliveries, label);
399
636
  }
400
637
 
401
638
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-http-gateway",
3
- "version": "2.0.10",
3
+ "version": "2.0.11",
4
4
  "type": "module",
5
5
  "description": "HTTP gateway that exposes MCP servers as REST routes — bridge between MCP and any HTTP-capable agent",
6
6
  "main": "server.js",