@imenam/mcp-http-gateway 2.0.22 → 2.0.23

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
@@ -612,6 +612,15 @@ Chaque `POST /mcp` est traité de façon autonome : aucun `mcp-session-id` n'est
612
612
 
613
613
  `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).
614
614
 
615
+ L'endpoint sert deux générations du protocole, toujours en réponse JSON :
616
+
617
+ - **2026-07-28** : le client se présente par `server/discover`, sans `initialize` ;
618
+ - **2025-11-25 et antérieures** : poignée de main par `initialize`.
619
+
620
+ La génération est reconnue requête par requête : un client 2026-07-28 se connecte directement, sans essuyer de refus.
621
+
622
+ Les requêtes dont l'en-tête `Host` ou `Origin` n'est pas autorisé sont refusées en `403` (protection anti-DNS-rebinding). Sont autorisés les hôtes `localhost`, celui de `MCP_PUBLIC_URL` et ceux de `GATEWAY_ALLOWED_HOSTS`, ainsi que les origines `https://claude.ai` et celles de `GATEWAY_ALLOWED_ORIGINS`. La comparaison porte sur le nom d'hôte seul, sans le port. Une requête sans en-tête `Origin` est acceptée.
623
+
615
624
  ### Flow
616
625
 
617
626
  ```
@@ -635,6 +644,8 @@ Le gateway agit uniquement comme **Resource Server** : il valide les JWT via JWK
635
644
  | `MCP_PUBLIC_URL` | URL publique canonique du endpoint — ex. `https://gateway.example.com/mcp`. Utilisée comme `resource` (RFC 9728) et `aud` JWT. |
636
645
  | `OAUTH_JWKS_URI` | URI du JWKS de l'Authorization Server — ex. `https://your-auth-server/.well-known/jwks.json` |
637
646
  | `OAUTH_ISSUER` | Issuer attendu dans les JWT — ex. `https://your-auth-server` |
647
+ | `GATEWAY_ALLOWED_HOSTS` | Hôtes supplémentaires admis sur `/mcp`, séparés par des virgules — ex. `mon-autre-host.example.com` |
648
+ | `GATEWAY_ALLOWED_ORIGINS` | Origines supplémentaires admises sur `/mcp`, séparées par des virgules — ex. `https://mon-client.example.com` |
638
649
 
639
650
  > 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`.
640
651
 
package/auth.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRemoteJWKSet, jwtVerify } from 'jose';
2
- import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
2
+ import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/server';
3
3
 
4
4
  const OAUTH_ENABLED = process.env.MCP_OAUTH_ENABLED === 'true';
5
5
 
@@ -22,7 +22,8 @@ export function createMcpTokenVerifier() {
22
22
  } catch (err) {
23
23
  const ts = new Date().toISOString();
24
24
  console.error(`[auth] ${ts} jwtVerify FAILED:`, err.code ?? err.constructor.name, err.message);
25
- throw new InvalidTokenError(
25
+ throw new OAuthError(
26
+ OAuthErrorCode.InvalidToken,
26
27
  err.code === 'ERR_JWT_EXPIRED' ? 'Token has expired' : 'Invalid token'
27
28
  );
28
29
  }
package/mcp-endpoint.js CHANGED
@@ -1,9 +1,6 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
- import {
4
- ListToolsRequestSchema,
5
- CallToolRequestSchema,
6
- } from '@modelcontextprotocol/sdk/types.js';
1
+ import { Server, createMcpHandler, isLegacyRequest } from '@modelcontextprotocol/server';
2
+ import { NodeStreamableHTTPServerTransport, toNodeHandler, toWebRequest } from '@modelcontextprotocol/node';
3
+ import { hostHeaderValidation, originValidation } from '@modelcontextprotocol/express';
7
4
  import { getProtectedResourceMetadata } from './auth.js';
8
5
  import { toolDefinition } from './startup-exposition.js';
9
6
  import { allowsMcp, allowsTool, filterTools } from './scope.js';
@@ -61,9 +58,9 @@ function buildMcpServer(registry, scope) {
61
58
  return entry && allowsMcp(scope, entry.name) ? entry : null;
62
59
  };
63
60
 
64
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: allTools }));
61
+ server.setRequestHandler('tools/list', async () => ({ tools: allTools }));
65
62
 
66
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
63
+ server.setRequestHandler('tools/call', async (request) => {
67
64
  const { name, arguments: args = {} } = request.params;
68
65
 
69
66
  try {
@@ -117,24 +114,29 @@ function buildMcpServer(registry, scope) {
117
114
  return server;
118
115
  }
119
116
 
117
+ /** Nom d'hôte, sans port, d'un hôte (`exemple.com:8443`) ou d'une origine (`https://exemple.com`). */
118
+ function hostnameOf(value) {
119
+ try {
120
+ return new URL(value.includes('://') ? value : `http://${value}`).hostname;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ const envList = (name) => (process.env[name] ?? '').split(',').map(s => s.trim()).filter(Boolean);
127
+
128
+ /**
129
+ * Noms d'hôte acceptés pour la protection DNS rebinding : la vérification du SDK ignore le port.
130
+ */
120
131
  function buildDnsRebindingConfig() {
121
- const port = process.env.GATEWAY_PORT ?? '3500';
122
- const extraHosts = process.env.GATEWAY_ALLOWED_HOSTS
123
- ? process.env.GATEWAY_ALLOWED_HOSTS.split(',').map(s => s.trim()).filter(Boolean)
124
- : [];
125
- const publicHost = process.env.MCP_PUBLIC_URL
126
- ? (() => { try { return new URL(process.env.MCP_PUBLIC_URL).host; } catch { return null; } })()
127
- : null;
128
132
  const allowedHosts = [
129
- `localhost:${port}`,
130
- ...(publicHost ? [publicHost] : []),
131
- ...extraHosts,
132
- ];
133
+ 'localhost',
134
+ ...(process.env.MCP_PUBLIC_URL ? [process.env.MCP_PUBLIC_URL] : []),
135
+ ...envList('GATEWAY_ALLOWED_HOSTS'),
136
+ ].map(hostnameOf).filter(Boolean);
133
137
 
134
- const extraOrigins = process.env.GATEWAY_ALLOWED_ORIGINS
135
- ? process.env.GATEWAY_ALLOWED_ORIGINS.split(',').map(s => s.trim()).filter(Boolean)
136
- : [];
137
- const allowedOrigins = ['https://claude.ai', ...extraOrigins];
138
+ const allowedOrigins = ['https://claude.ai', ...envList('GATEWAY_ALLOWED_ORIGINS')]
139
+ .map(hostnameOf).filter(Boolean);
138
140
 
139
141
  return { allowedHosts, allowedOrigins };
140
142
  }
@@ -148,31 +150,40 @@ export function registerMcpEndpoint(app, registry) {
148
150
  console.log('[gateway] DNS rebinding protection — allowedHosts:', allowedHosts);
149
151
  console.log('[gateway] DNS rebinding protection — allowedOrigins:', allowedOrigins);
150
152
 
151
- // Mode stateless : un Server + un transport éphémères par requête, aucun état inter-requêtes.
153
+ const logError = (err) => console.error('[mcp-endpoint] handleMcpRequest error:', err);
154
+
155
+ // Mode stateless : un Server éphémère par requête, aucun état inter-requêtes.
152
156
  // Les trois outils exposés sont du pur requête/réponse (aucune notification serveur→client,
153
157
  // ni sampling, ni subscription), donc aucune session n'est nécessaire.
154
- // Conséquence : plus de 404 "session inconnue" après une période d'inactivité ou un redémarrage,
158
+ // Conséquence : pas de 404 "session inconnue" après une période d'inactivité ou un redémarrage,
155
159
  // et l'endpoint supporte le scale-out multi-instances.
156
- app.post('/mcp', async (req, res) => {
157
- const server = buildMcpServer(registry, req.scope);
158
- const transport = new StreamableHTTPServerTransport({
159
- sessionIdGenerator: undefined,
160
- enableJsonResponse: true,
161
- enableDnsRebindingProtection: true,
162
- allowedHosts,
163
- allowedOrigins,
164
- });
165
-
166
- res.on('close', () => {
167
- transport.close().catch(() => {});
168
- server.close().catch(() => {});
169
- });
160
+ // Deux générations du protocole sur le même endpoint, toutes deux en réponse JSON :
161
+ // - révisions 2025 (qui commencent par `initialize`) : transport Streamable HTTP sans session ;
162
+ // - révision 2026-07-28 (dont `server/discover`) : handler par requête du SDK.
163
+ app.post('/mcp', hostHeaderValidation(allowedHosts), originValidation(allowedOrigins), async (req, res) => {
164
+ const factory = () => buildMcpServer(registry, req.scope);
170
165
 
171
166
  try {
172
- await server.connect(transport);
173
- await transport.handleRequest(req, res, req.body);
167
+ if (await isLegacyRequest(await toWebRequest(req, req.body), req.body)) {
168
+ const server = factory();
169
+ const transport = new NodeStreamableHTTPServerTransport({
170
+ sessionIdGenerator: undefined,
171
+ enableJsonResponse: true,
172
+ });
173
+ res.on('close', () => {
174
+ transport.close().catch(() => {});
175
+ server.close().catch(() => {});
176
+ });
177
+ await server.connect(transport);
178
+ await transport.handleRequest(req, res, req.body);
179
+ return;
180
+ }
181
+
182
+ const handler = createMcpHandler(factory, { legacy: 'reject', responseMode: 'json', onerror: logError });
183
+ res.on('close', () => { handler.close().catch(() => {}); });
184
+ await toNodeHandler(handler, { onerror: logError })(req, res, req.body);
174
185
  } catch (err) {
175
- console.error('[mcp-endpoint] handleMcpRequest error:', err);
186
+ logError(err);
176
187
  if (!res.headersSent) {
177
188
  res.status(500).json({
178
189
  jsonrpc: '2.0',
package/mcp-mode.js CHANGED
@@ -2,12 +2,8 @@
2
2
  import 'dotenv/config';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import {
8
- ListToolsRequestSchema,
9
- CallToolRequestSchema,
10
- } from '@modelcontextprotocol/sdk/types.js';
5
+ import { Server } from '@modelcontextprotocol/server';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
11
7
  import {
12
8
  EXEC_ARG, MAX_EXEC_ROUNDS, parseExecRoutes, normalizeRoute, execAnnouncement, execRequestOf, runExecRequest,
13
9
  } from './gateway-exec.js';
@@ -705,9 +701,9 @@ if (initialProxyTools.length) {
705
701
  console.error(`[mcp-mode] ${initialProxyTools.length} proxy tool(s) exposed directly from gateway`);
706
702
  }
707
703
 
708
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: allTools }));
704
+ server.setRequestHandler('tools/list', async () => ({ tools: allTools }));
709
705
 
710
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
706
+ server.setRequestHandler('tools/call', async (request) => {
711
707
  const { name, arguments: args = {} } = request.params;
712
708
 
713
709
  try {
package/mcp-registry.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import crypto from 'crypto';
2
2
  import readline from 'readline';
3
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
+ import { Client } from '@modelcontextprotocol/client';
4
+ import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
5
5
  import { buildProxyTools, overlongProxyNames, parseExposeFlag } from './startup-exposition.js';
6
6
  import { HTTP_RELAY_PORT_KEY, parseRelayPort, relayPrefix } from './http-relay.js';
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-http-gateway",
3
- "version": "2.0.22",
3
+ "version": "2.0.23",
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",
@@ -46,13 +46,17 @@
46
46
  "homepage": "https://github.com/Matthieu-Pesnot-Pin/mcp-http-gateway#readme",
47
47
  "license": "ISC",
48
48
  "engines": {
49
- "node": ">=18"
49
+ "node": ">=20"
50
50
  },
51
51
  "dependencies": {
52
- "@modelcontextprotocol/sdk": "^1.27.1",
52
+ "@modelcontextprotocol/client": "^2.4.0",
53
+ "@modelcontextprotocol/express": "^2.0.2",
54
+ "@modelcontextprotocol/node": "^2.1.2",
55
+ "@modelcontextprotocol/server": "^2.4.0",
53
56
  "better-sqlite3": "^12.8.0",
54
57
  "dotenv": "^17.3.1",
55
58
  "express": "^5.2.1",
59
+ "express-rate-limit": "^8.7.1",
56
60
  "jose": "^6.2.3",
57
61
  "openai": "^6.31.0"
58
62
  }
package/server.js CHANGED
@@ -26,7 +26,7 @@ import { McpRegistry, REVISION_HEADER } from './mcp-registry.js';
26
26
  import {
27
27
  FULL_SCOPE, SCOPE_HEADER, allowsMcp, allowsTool, filterTools, readScope, scopedRevision,
28
28
  } from './scope.js';
29
- import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
29
+ import { requireBearerAuth } from '@modelcontextprotocol/express';
30
30
  import { rateLimit } from 'express-rate-limit';
31
31
 
32
32
  // Normalise les différents formats de config rencontrés dans la nature :