@imenam/mcp-http-gateway 2.0.19 → 2.0.21
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 +17 -3
- package/gateway-watch.js +4 -3
- package/mcp-registry.js +16 -0
- package/package.json +1 -1
- package/server.js +20 -15
package/README.md
CHANGED
|
@@ -55,6 +55,10 @@ GATEWAY_CONFIG_DIR=./config
|
|
|
55
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
56
|
| `GATEWAY_SCOPE_SECRET` | — | Impose un périmètre d'accès signé sur chaque requête. Absent, toute requête voit tous les MCP et tous les outils. Voir [Périmètre d'accès](#périmètre-daccès). |
|
|
57
57
|
| `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). |
|
|
58
|
+
| `RATE_LIMIT_GLOBAL_MAX` / `RATE_LIMIT_GLOBAL_WINDOW_MINUTES` | `1000` / `1` | Requêtes acceptées par IP sur la fenêtre, toutes routes confondues ; au-delà, la réponse est un `429`. `0` désactive la limite. |
|
|
59
|
+
| `RATE_LIMIT_SEARCH_MAX` / `RATE_LIMIT_SEARCH_WINDOW_MINUTES` | `30` / `1` | Même principe pour `/tools/search`, en plus de la limite globale. `0` désactive la limite. |
|
|
60
|
+
|
|
61
|
+
Derrière un frontal qui authentifie déjà les appelants et borne leur fréquence, comme mcp-gateway-manager, les deux limites se mettent à `0`.
|
|
58
62
|
|
|
59
63
|
### `mcp-servers.json`
|
|
60
64
|
|
|
@@ -107,6 +111,14 @@ npm start
|
|
|
107
111
|
|
|
108
112
|
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.
|
|
109
113
|
|
|
114
|
+
Le `stderr` de chaque MCP est recopié sur celui du gateway, ligne par ligne, préfixé par le nom du MCP sous forme de chaîne JSON :
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
[mcp "Ticket PXP"] [ProxyClient] Mount path resolved to /jf5gh9dqct/ticket-manager (requested: /ticket-manager)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Un superviseur qui lit le journal du gateway sait ainsi quel MCP a écrit chaque ligne.
|
|
121
|
+
|
|
110
122
|
## Rechargement à chaud
|
|
111
123
|
|
|
112
124
|
Avec `GATEWAY_ADMIN_TOKEN` défini, le gateway relit `mcp-servers.json` sur demande, sans redémarrer :
|
|
@@ -552,11 +564,13 @@ La convention, côté MCP :
|
|
|
552
564
|
jeton du gateway, route, outil, arguments, point de reprise. Il rend à l'agent, à la place
|
|
553
565
|
de la réponse du MCP, l'appel `Monitor` à faire :
|
|
554
566
|
```text
|
|
555
|
-
command:
|
|
567
|
+
command: npx -y @imenam/mcp-http-gateway@<version du relais> --watch "<…>/watch/<id>.json"
|
|
556
568
|
timeout_ms: 1800000
|
|
557
569
|
```
|
|
558
|
-
Le jeton ne figure jamais dans la commande, donc jamais dans la conversation.
|
|
559
|
-
|
|
570
|
+
Le jeton ne figure jamais dans la commande, donc jamais dans la conversation. La commande
|
|
571
|
+
lance le veilleur par npx, dans la version exacte du relais : elle ne dépend d'aucun chemin
|
|
572
|
+
du cache de npx.
|
|
573
|
+
4. Le veilleur (`mcp-http-gateway --watch <fichier>`) appelle l'outil d'attente avec ses arguments,
|
|
560
574
|
plus `cursor` et `timeout_seconds: 50`. L'outil répond, dans `_meta["gateway/events"]` :
|
|
561
575
|
```json
|
|
562
576
|
{ "cursor": "58", "lines": ["[telegram] new post 42: … — read it with get_post"] }
|
package/gateway-watch.js
CHANGED
|
@@ -51,7 +51,8 @@ const BACKOFF_MAX_MS = 30_000;
|
|
|
51
51
|
/** Échecs consécutifs au-delà desquels l'agent est prévenu que la veille est interrompue. */
|
|
52
52
|
const FAILURES_BEFORE_NOTICE = 3;
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
/** Nom et version de ce paquet, qui fixent la commande du veilleur. */
|
|
55
|
+
const PACKAGE = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8'));
|
|
55
56
|
|
|
56
57
|
export function watchDir() {
|
|
57
58
|
const override = process.env.GATEWAY_WATCH_DIR?.trim();
|
|
@@ -123,10 +124,10 @@ function activeWatcher(file) {
|
|
|
123
124
|
return presence && typeof presence.pid === 'number' && processExists(presence.pid) ? presence : null;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
/** Commande du veilleur :
|
|
127
|
+
/** Commande du veilleur : ce paquet, lancé par npx dans la version exacte de ce relais. */
|
|
127
128
|
export function watchCommand(file) {
|
|
128
129
|
const quote = (value) => `"${value.replace(/\\/g, '/')}"`;
|
|
129
|
-
return
|
|
130
|
+
return `npx -y ${PACKAGE.name}@${PACKAGE.version} --watch ${quote(file)}`;
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
/** Fichiers de veille écrits par ce relais, retirés à son arrêt. */
|
package/mcp-registry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
|
+
import readline from 'readline';
|
|
2
3
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
4
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
4
5
|
import { buildProxyTools, overlongProxyNames, parseExposeFlag } from './startup-exposition.js';
|
|
@@ -44,12 +45,27 @@ function buildChildEnv(cfgEnv = {}) {
|
|
|
44
45
|
return { ...safeEnv, ...cfgEnv };
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/** Préfixe des lignes de stderr d'un MCP : `[mcp "<nom>"] `, le nom en chaîne JSON. */
|
|
49
|
+
export function stderrPrefix(name) {
|
|
50
|
+
return `[mcp ${JSON.stringify(name)}] `;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Recopie le stderr d'un MCP sur celui du gateway, ligne par ligne, chacune préfixée par le nom du MCP. */
|
|
54
|
+
function relayStderr(name, stream) {
|
|
55
|
+
const prefix = stderrPrefix(name);
|
|
56
|
+
readline.createInterface({ input: stream, crlfDelay: Infinity }).on('line', (line) => {
|
|
57
|
+
process.stderr.write(`${prefix}${line}\n`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
async function connectMcp(name, cfg) {
|
|
48
62
|
const transport = new StdioClientTransport({
|
|
49
63
|
command: cfg.command,
|
|
50
64
|
args: cfg.args ?? [],
|
|
51
65
|
env: buildChildEnv(cfg.env),
|
|
66
|
+
stderr: 'pipe',
|
|
52
67
|
});
|
|
68
|
+
relayStderr(name, transport.stderr);
|
|
53
69
|
const client = new Client({ name: 'mcp-gateway', version: '1.0.0' }, { capabilities: {} });
|
|
54
70
|
await client.connect(transport);
|
|
55
71
|
console.log(`[gateway] Connected: ${name} → ${cfg.env?.MCP_SERVER_ROUTE}`);
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -198,28 +198,33 @@ async function main() {
|
|
|
198
198
|
isRelayPath(registry.relayPrefixes, req.path) ? next() : jsonParser(req, res, next)
|
|
199
199
|
);
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
const
|
|
201
|
+
// Une limite à 0 désactive le compteur correspondant.
|
|
202
|
+
const globalWindowMin = parseInt(process.env.RATE_LIMIT_GLOBAL_WINDOW_MINUTES ?? '1', 10);
|
|
203
|
+
const globalMax = parseInt(process.env.RATE_LIMIT_GLOBAL_MAX ?? '1000', 10);
|
|
203
204
|
const searchWindowMin = parseInt(process.env.RATE_LIMIT_SEARCH_WINDOW_MINUTES ?? '1', 10);
|
|
204
205
|
const searchMax = parseInt(process.env.RATE_LIMIT_SEARCH_MAX ?? '30', 10);
|
|
205
206
|
|
|
206
|
-
|
|
207
|
-
windowMs:
|
|
208
|
-
|
|
209
|
-
standardHeaders: 'draft-8',
|
|
210
|
-
legacyHeaders: false,
|
|
211
|
-
message: { error: 'Too Many Requests' },
|
|
212
|
-
}));
|
|
213
|
-
console.log(`[gateway] Rate limit — global: ${globalMax} req/${globalWindowMin} min/IP`);
|
|
214
|
-
|
|
215
|
-
const searchLimiter = rateLimit({
|
|
216
|
-
windowMs: searchWindowMin * 60_000,
|
|
217
|
-
max: searchMax,
|
|
207
|
+
const limiter = (windowMin, max) => rateLimit({
|
|
208
|
+
windowMs: windowMin * 60_000,
|
|
209
|
+
limit: max,
|
|
218
210
|
standardHeaders: 'draft-8',
|
|
219
211
|
legacyHeaders: false,
|
|
220
212
|
message: { error: 'Too Many Requests' },
|
|
221
213
|
});
|
|
222
|
-
|
|
214
|
+
|
|
215
|
+
if (globalMax > 0) {
|
|
216
|
+
app.use(limiter(globalWindowMin, globalMax));
|
|
217
|
+
console.log(`[gateway] Rate limit — global: ${globalMax} req/${globalWindowMin} min/IP`);
|
|
218
|
+
} else {
|
|
219
|
+
console.log('[gateway] Rate limit — global: disabled');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const searchLimiter = searchMax > 0
|
|
223
|
+
? limiter(searchWindowMin, searchMax)
|
|
224
|
+
: (req, res, next) => next();
|
|
225
|
+
console.log(searchMax > 0
|
|
226
|
+
? `[gateway] Rate limit — /tools/search: ${searchMax} req/${searchWindowMin} min/IP`
|
|
227
|
+
: '[gateway] Rate limit — /tools/search: disabled');
|
|
223
228
|
|
|
224
229
|
const verifier = createMcpTokenVerifier();
|
|
225
230
|
if (verifier) {
|