@j0hanz/superfetch 2.2.0 → 2.2.2

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 (68) hide show
  1. package/README.md +363 -614
  2. package/dist/cache.d.ts +2 -2
  3. package/dist/cache.d.ts.map +1 -1
  4. package/dist/cache.js +49 -227
  5. package/dist/cache.js.map +1 -1
  6. package/dist/config.d.ts +6 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +20 -27
  9. package/dist/config.js.map +1 -1
  10. package/dist/dom-noise-removal.d.ts +6 -0
  11. package/dist/dom-noise-removal.d.ts.map +1 -0
  12. package/dist/dom-noise-removal.js +482 -0
  13. package/dist/dom-noise-removal.js.map +1 -0
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +8 -5
  16. package/dist/errors.js.map +1 -1
  17. package/dist/fetch.d.ts.map +1 -1
  18. package/dist/fetch.js +26 -32
  19. package/dist/fetch.js.map +1 -1
  20. package/dist/http-native.d.ts +6 -0
  21. package/dist/http-native.d.ts.map +1 -0
  22. package/dist/http-native.js +645 -0
  23. package/dist/http-native.js.map +1 -0
  24. package/dist/http-utils.d.ts +61 -0
  25. package/dist/http-utils.d.ts.map +1 -0
  26. package/dist/http-utils.js +252 -0
  27. package/dist/http-utils.js.map +1 -0
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/instructions.md +41 -39
  31. package/dist/json.d.ts +2 -0
  32. package/dist/json.d.ts.map +1 -0
  33. package/dist/json.js +30 -0
  34. package/dist/json.js.map +1 -0
  35. package/dist/language-detection.d.ts +13 -0
  36. package/dist/language-detection.d.ts.map +1 -0
  37. package/dist/language-detection.js +283 -0
  38. package/dist/language-detection.js.map +1 -0
  39. package/dist/markdown-cleanup.d.ts +19 -0
  40. package/dist/markdown-cleanup.d.ts.map +1 -0
  41. package/dist/markdown-cleanup.js +283 -0
  42. package/dist/markdown-cleanup.js.map +1 -0
  43. package/dist/observability.d.ts +1 -0
  44. package/dist/observability.d.ts.map +1 -1
  45. package/dist/observability.js +10 -0
  46. package/dist/observability.js.map +1 -1
  47. package/dist/tools.d.ts.map +1 -1
  48. package/dist/tools.js +23 -8
  49. package/dist/tools.js.map +1 -1
  50. package/dist/transform-types.d.ts +81 -0
  51. package/dist/transform-types.d.ts.map +1 -0
  52. package/dist/transform-types.js +6 -0
  53. package/dist/transform-types.js.map +1 -0
  54. package/dist/transform.d.ts +8 -52
  55. package/dist/transform.d.ts.map +1 -1
  56. package/dist/transform.js +419 -825
  57. package/dist/transform.js.map +1 -1
  58. package/dist/type-guards.d.ts +1 -1
  59. package/dist/type-guards.d.ts.map +1 -1
  60. package/dist/type-guards.js +1 -1
  61. package/dist/type-guards.js.map +1 -1
  62. package/dist/workers/transform-worker.js +23 -24
  63. package/dist/workers/transform-worker.js.map +1 -1
  64. package/package.json +85 -86
  65. package/dist/http.d.ts +0 -90
  66. package/dist/http.d.ts.map +0 -1
  67. package/dist/http.js +0 -1576
  68. package/dist/http.js.map +0 -1
package/dist/http.js DELETED
@@ -1,1576 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { once } from 'node:events';
3
- import { isIP } from 'node:net';
4
- import { setInterval as setIntervalPromise } from 'node:timers/promises';
5
- import { z } from 'zod';
6
- import { InvalidTokenError, ServerError, } from '@modelcontextprotocol/sdk/server/auth/errors.js';
7
- import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
8
- import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter, } from '@modelcontextprotocol/sdk/server/auth/router.js';
9
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
10
- import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
11
- import { registerDownloadRoutes } from './cache.js';
12
- import { config, enableHttpMode } from './config.js';
13
- import { timingSafeEqualUtf8 } from './crypto.js';
14
- import { FetchError, getErrorMessage } from './errors.js';
15
- import { destroyAgents } from './fetch.js';
16
- import { createMcpServer } from './mcp.js';
17
- import { logDebug, logError, logInfo, logWarn, runWithRequestContext, } from './observability.js';
18
- import { shutdownTransformWorkerPool } from './transform.js';
19
- import { isRecord } from './type-guards.js';
20
- function formatHostForUrl(hostname) {
21
- if (hostname.includes(':') && !hostname.startsWith('[')) {
22
- return `[${hostname}]`;
23
- }
24
- return hostname;
25
- }
26
- function getRateLimitKey(req) {
27
- return req.ip ?? req.socket.remoteAddress ?? 'unknown';
28
- }
29
- function createCleanupInterval(store, options) {
30
- const controller = new AbortController();
31
- void startCleanupLoop(store, options, controller.signal).catch(handleCleanupError);
32
- return controller;
33
- }
34
- function createRateLimitMiddleware(options) {
35
- const store = new Map();
36
- const cleanupController = createCleanupInterval(store, options);
37
- const stop = () => {
38
- cleanupController.abort();
39
- };
40
- const middleware = createRateLimitHandler(store, options);
41
- return { middleware, stop, store };
42
- }
43
- function createRateLimitHandler(store, options) {
44
- return (req, res, next) => {
45
- if (shouldSkipRateLimit(req, options)) {
46
- next();
47
- return;
48
- }
49
- const now = Date.now();
50
- const key = getRateLimitKey(req);
51
- const resolution = resolveRateLimitEntry(store, key, now, options);
52
- if (resolution.isNew) {
53
- next();
54
- return;
55
- }
56
- if (handleRateLimitExceeded(res, resolution.entry, now, options)) {
57
- return;
58
- }
59
- next();
60
- };
61
- }
62
- async function startCleanupLoop(store, options, signal) {
63
- for await (const getNow of setIntervalPromise(options.cleanupIntervalMs, Date.now, { signal, ref: false })) {
64
- evictStaleEntries(store, options, getNow());
65
- }
66
- }
67
- function evictStaleEntries(store, options, now) {
68
- for (const [key, entry] of store.entries()) {
69
- if (now - entry.lastAccessed > options.windowMs * 2) {
70
- store.delete(key);
71
- }
72
- }
73
- }
74
- function handleCleanupError(error) {
75
- if (isAbortError(error)) {
76
- return;
77
- }
78
- logWarn('Rate limit cleanup loop failed', {
79
- error: error instanceof Error ? error.message : 'Unknown error',
80
- });
81
- }
82
- function shouldSkipRateLimit(req, options) {
83
- return !options.enabled || req.method === 'OPTIONS';
84
- }
85
- function resolveRateLimitEntry(store, key, now, options) {
86
- const existing = store.get(key);
87
- if (!existing || now > existing.resetTime) {
88
- const entry = createNewEntry(now, options);
89
- store.set(key, entry);
90
- return { entry, isNew: true };
91
- }
92
- updateEntry(existing, now);
93
- return { entry: existing, isNew: false };
94
- }
95
- function createNewEntry(now, options) {
96
- return {
97
- count: 1,
98
- resetTime: now + options.windowMs,
99
- lastAccessed: now,
100
- };
101
- }
102
- function updateEntry(entry, now) {
103
- entry.count += 1;
104
- entry.lastAccessed = now;
105
- }
106
- function handleRateLimitExceeded(res, entry, now, options) {
107
- if (entry.count <= options.maxRequests) {
108
- return false;
109
- }
110
- const retryAfter = Math.max(1, Math.ceil((entry.resetTime - now) / 1000));
111
- res.set('Retry-After', String(retryAfter));
112
- res.status(429).json({
113
- error: 'Rate limit exceeded',
114
- retryAfter,
115
- });
116
- return true;
117
- }
118
- function assertHttpConfiguration() {
119
- ensureBindAllowed();
120
- ensureStaticTokens();
121
- if (config.auth.mode === 'oauth') {
122
- ensureOauthConfiguration();
123
- }
124
- }
125
- function ensureBindAllowed() {
126
- const isLoopback = ['127.0.0.1', '::1', 'localhost'].includes(config.server.host);
127
- if (!config.security.allowRemote && !isLoopback) {
128
- logError('Refusing to bind to non-loopback host without ALLOW_REMOTE=true', { host: config.server.host });
129
- process.exit(1);
130
- }
131
- if (!isLoopback &&
132
- config.security.allowRemote &&
133
- config.auth.mode !== 'oauth') {
134
- logError('Remote HTTP mode requires OAuth configuration; refusing to start');
135
- process.exit(1);
136
- }
137
- }
138
- function ensureStaticTokens() {
139
- if (config.auth.mode === 'static' && config.auth.staticTokens.length === 0) {
140
- logError('At least one static access token is required for HTTP mode');
141
- process.exit(1);
142
- }
143
- }
144
- function ensureOauthConfiguration() {
145
- if (!config.auth.issuerUrl || !config.auth.authorizationUrl) {
146
- logError('OAUTH_ISSUER_URL and OAUTH_AUTHORIZATION_URL are required for OAuth mode');
147
- process.exit(1);
148
- }
149
- if (!config.auth.tokenUrl) {
150
- logError('OAUTH_TOKEN_URL is required for OAuth mode');
151
- process.exit(1);
152
- }
153
- if (!config.auth.introspectionUrl) {
154
- logError('OAUTH_INTROSPECTION_URL is required for OAuth mode');
155
- process.exit(1);
156
- }
157
- }
158
- function createShutdownHandler(server, sessionStore, sessionCleanupController, stopRateLimitCleanup) {
159
- let inFlight = null;
160
- let initialSignal = null;
161
- return (signal) => {
162
- if (inFlight) {
163
- logWarn('Shutdown already in progress; ignoring signal', {
164
- signal,
165
- initialSignal,
166
- });
167
- return inFlight;
168
- }
169
- initialSignal = signal;
170
- inFlight = shutdownServer(signal, server, sessionStore, sessionCleanupController, stopRateLimitCleanup).catch((error) => {
171
- logError('Shutdown handler failed', error instanceof Error ? error : { error: getErrorMessage(error) });
172
- throw error;
173
- });
174
- return inFlight;
175
- };
176
- }
177
- async function shutdownServer(signal, server, sessionStore, sessionCleanupController, stopRateLimitCleanup) {
178
- logInfo(`${signal} received, shutting down gracefully...`);
179
- stopRateLimitCleanup();
180
- sessionCleanupController.abort();
181
- await closeSessions(sessionStore);
182
- destroyAgents();
183
- await shutdownTransformWorkerPool();
184
- drainConnectionsOnShutdown(server);
185
- closeServer(server);
186
- scheduleForcedShutdown(10000);
187
- }
188
- async function closeSessions(sessionStore) {
189
- const sessions = sessionStore.clear();
190
- await Promise.allSettled(sessions.map((session) => session.transport.close().catch((error) => {
191
- logWarn('Failed to close session during shutdown', {
192
- error: getErrorMessage(error),
193
- });
194
- })));
195
- }
196
- function closeServer(server) {
197
- server.close(() => {
198
- logInfo('HTTP server closed');
199
- process.exit(0);
200
- });
201
- }
202
- function scheduleForcedShutdown(timeoutMs) {
203
- setTimeout(() => {
204
- logError('Forced shutdown after timeout');
205
- process.exit(1);
206
- }, timeoutMs).unref();
207
- }
208
- function registerSignalHandlers(shutdown) {
209
- process.once('SIGINT', () => {
210
- void shutdown('SIGINT');
211
- });
212
- process.once('SIGTERM', () => {
213
- void shutdown('SIGTERM');
214
- });
215
- }
216
- function startListening(app) {
217
- const server = app.listen(config.server.port, config.server.host, () => {
218
- const address = server.address();
219
- const resolvedPort = typeof address === 'object' && address
220
- ? address.port
221
- : config.server.port;
222
- logInfo('superFetch MCP server started', {
223
- host: config.server.host,
224
- port: resolvedPort,
225
- });
226
- const baseUrl = `http://${formatHostForUrl(config.server.host)}:${resolvedPort}`;
227
- logInfo(`superFetch MCP server running at ${baseUrl} (health: ${baseUrl}/health, mcp: ${baseUrl}/mcp)`);
228
- logInfo('Run with --stdio flag for direct stdio integration');
229
- });
230
- server.on('error', (err) => {
231
- logError('Failed to start server', err);
232
- process.exit(1);
233
- });
234
- return server;
235
- }
236
- async function stopServerWithoutExit(server, sessionStore, sessionCleanupController, stopRateLimitCleanup) {
237
- stopRateLimitCleanup();
238
- sessionCleanupController.abort();
239
- await closeSessions(sessionStore);
240
- await new Promise((resolve) => {
241
- server.close(() => {
242
- resolve();
243
- });
244
- });
245
- }
246
- function buildMiddleware() {
247
- const { middleware: rateLimitMiddleware, stop: stopRateLimitCleanup } = createRateLimitMiddleware(config.rateLimit);
248
- const authMiddleware = createAuthMiddleware();
249
- // No CORS - MCP clients don't run in browsers
250
- const corsMiddleware = createCorsMiddleware();
251
- return {
252
- rateLimitMiddleware,
253
- stopRateLimitCleanup,
254
- authMiddleware,
255
- corsMiddleware,
256
- };
257
- }
258
- function createSessionInfrastructure() {
259
- const sessionStore = createSessionStore(config.server.sessionTtlMs);
260
- const sessionCleanupController = startSessionCleanupLoop(sessionStore, config.server.sessionTtlMs);
261
- return { sessionStore, sessionCleanupController };
262
- }
263
- function registerHttpRoutes(app, sessionStore, authMiddleware) {
264
- app.use('/mcp', authMiddleware);
265
- app.use('/mcp/downloads', authMiddleware);
266
- registerMcpRoutes(app, {
267
- sessionStore,
268
- maxSessions: config.server.maxSessions,
269
- });
270
- registerDownloadRoutes(app);
271
- app.use(errorHandler);
272
- }
273
- function attachAuthMetadata(app) {
274
- const authMetadataRouter = createAuthMetadataRouter();
275
- if (authMetadataRouter) {
276
- app.use(authMetadataRouter);
277
- }
278
- }
279
- async function buildServerContext() {
280
- const { app, authMiddleware, stopRateLimitCleanup } = await createAppWithMiddleware();
281
- const { sessionStore, sessionCleanupController } = attachSessionRoutes(app, authMiddleware);
282
- return { app, sessionStore, sessionCleanupController, stopRateLimitCleanup };
283
- }
284
- async function createAppWithMiddleware() {
285
- const { app, jsonParser } = await createExpressApp();
286
- const { rateLimitMiddleware, stopRateLimitCleanup, authMiddleware, corsMiddleware, } = buildMiddleware();
287
- attachBaseMiddleware({
288
- app,
289
- jsonParser,
290
- rateLimitMiddleware,
291
- corsMiddleware,
292
- });
293
- attachAuthMetadata(app);
294
- assertHttpConfiguration();
295
- return { app, authMiddleware, stopRateLimitCleanup };
296
- }
297
- function attachSessionRoutes(app, authMiddleware) {
298
- const { sessionStore, sessionCleanupController } = createSessionInfrastructure();
299
- registerHttpRoutes(app, sessionStore, authMiddleware);
300
- return { sessionStore, sessionCleanupController };
301
- }
302
- async function ensureServerListening(server) {
303
- if (server.listening)
304
- return;
305
- await once(server, 'listening');
306
- }
307
- function resolveServerAddress(server) {
308
- const address = server.address();
309
- const resolvedPort = typeof address === 'object' && address ? address.port : config.server.port;
310
- const { host } = config.server;
311
- const url = `http://${formatHostForUrl(host)}:${resolvedPort}`;
312
- return { host, port: resolvedPort, url };
313
- }
314
- function createStopHandler(server, sessionStore, sessionCleanupController, stopRateLimitCleanup) {
315
- return async () => {
316
- await stopServerWithoutExit(server, sessionStore, sessionCleanupController, stopRateLimitCleanup);
317
- };
318
- }
319
- function buildServerLifecycle(options) {
320
- const { server, sessionStore, sessionCleanupController, stopRateLimitCleanup, registerSignals, } = options;
321
- const shutdown = createShutdownHandler(server, sessionStore, sessionCleanupController, stopRateLimitCleanup);
322
- const stop = createStopHandler(server, sessionStore, sessionCleanupController, stopRateLimitCleanup);
323
- if (registerSignals)
324
- registerSignalHandlers(shutdown);
325
- return { shutdown, stop };
326
- }
327
- export async function startHttpServer(options) {
328
- enableHttpMode();
329
- const { app, sessionStore, sessionCleanupController, stopRateLimitCleanup } = await buildServerContext();
330
- const server = startListening(app);
331
- applyHttpServerTuning(server);
332
- await ensureServerListening(server);
333
- const { host, port, url } = resolveServerAddress(server);
334
- const { shutdown, stop } = buildServerLifecycle({
335
- server,
336
- sessionStore,
337
- sessionCleanupController,
338
- stopRateLimitCleanup,
339
- registerSignals: options?.registerSignalHandlers !== false,
340
- });
341
- return { shutdown, stop, url, host, port };
342
- }
343
- async function createExpressApp() {
344
- const { default: express } = await import('express');
345
- const app = express();
346
- const jsonParser = express.json({ limit: '1mb' });
347
- return { app, jsonParser };
348
- }
349
- function getStatusCode(fetchError) {
350
- return fetchError ? fetchError.statusCode : 500;
351
- }
352
- function getErrorCode(fetchError) {
353
- return fetchError ? fetchError.code : 'INTERNAL_ERROR';
354
- }
355
- function getFetchErrorMessage(fetchError) {
356
- return fetchError ? fetchError.message : 'Internal Server Error';
357
- }
358
- function getErrorDetails(fetchError) {
359
- if (fetchError && Object.keys(fetchError.details).length > 0) {
360
- return fetchError.details;
361
- }
362
- return undefined;
363
- }
364
- function resolveRetryAfter(fetchError) {
365
- if (fetchError?.statusCode !== 429)
366
- return undefined;
367
- const { retryAfter } = fetchError.details;
368
- return isRetryAfterValue(retryAfter) ? String(retryAfter) : undefined;
369
- }
370
- function isRetryAfterValue(value) {
371
- return typeof value === 'number' || typeof value === 'string';
372
- }
373
- function setRetryAfterHeader(res, fetchError) {
374
- const retryAfter = resolveRetryAfter(fetchError);
375
- if (retryAfter === undefined)
376
- return;
377
- res.set('Retry-After', retryAfter);
378
- }
379
- function buildErrorResponse(fetchError) {
380
- const details = getErrorDetails(fetchError);
381
- const response = {
382
- error: {
383
- message: getFetchErrorMessage(fetchError),
384
- code: getErrorCode(fetchError),
385
- statusCode: getStatusCode(fetchError),
386
- ...(details && { details }),
387
- },
388
- };
389
- return response;
390
- }
391
- export function errorHandler(err, req, res, next) {
392
- if (res.headersSent) {
393
- next(err);
394
- return;
395
- }
396
- const fetchError = err instanceof FetchError ? err : null;
397
- const statusCode = getStatusCode(fetchError);
398
- logError(`HTTP ${statusCode}: ${err.message} - ${req.method} ${req.path}`, err);
399
- setRetryAfterHeader(res, fetchError);
400
- res.status(statusCode).json(buildErrorResponse(fetchError));
401
- }
402
- export function normalizeHost(value) {
403
- const trimmed = value.trim().toLowerCase();
404
- if (!trimmed)
405
- return null;
406
- const first = takeFirstHostValue(trimmed);
407
- if (!first)
408
- return null;
409
- const ipv6 = stripIpv6Brackets(first);
410
- if (ipv6)
411
- return ipv6;
412
- if (isIpV6Literal(first)) {
413
- return first;
414
- }
415
- return stripPortIfPresent(first);
416
- }
417
- function takeFirstHostValue(value) {
418
- const first = value.split(',')[0];
419
- if (!first)
420
- return null;
421
- const trimmed = first.trim();
422
- return trimmed ? trimmed : null;
423
- }
424
- function stripIpv6Brackets(value) {
425
- if (!value.startsWith('['))
426
- return null;
427
- const end = value.indexOf(']');
428
- if (end === -1)
429
- return null;
430
- return value.slice(1, end);
431
- }
432
- function stripPortIfPresent(value) {
433
- const colonIndex = value.indexOf(':');
434
- if (colonIndex === -1)
435
- return value;
436
- return value.slice(0, colonIndex);
437
- }
438
- function isIpV6Literal(value) {
439
- return isIP(value) === 6;
440
- }
441
- const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
442
- function getNonEmptyStringHeader(value) {
443
- if (typeof value !== 'string')
444
- return null;
445
- const trimmed = value.trim();
446
- return trimmed === '' ? null : trimmed;
447
- }
448
- function respondHostNotAllowed(res) {
449
- res.status(403).json({
450
- error: 'Host not allowed',
451
- code: 'HOST_NOT_ALLOWED',
452
- });
453
- }
454
- function respondOriginNotAllowed(res) {
455
- res.status(403).json({
456
- error: 'Origin not allowed',
457
- code: 'ORIGIN_NOT_ALLOWED',
458
- });
459
- }
460
- function tryParseOriginHostname(originHeader) {
461
- try {
462
- return new URL(originHeader).hostname.toLowerCase();
463
- }
464
- catch {
465
- return null;
466
- }
467
- }
468
- function isWildcardHost(host) {
469
- return host === '0.0.0.0' || host === '::';
470
- }
471
- function addLoopbackHosts(allowedHosts) {
472
- for (const host of LOOPBACK_HOSTS) {
473
- allowedHosts.add(host);
474
- }
475
- }
476
- function addConfiguredHost(allowedHosts) {
477
- const configuredHost = normalizeHost(config.server.host);
478
- if (!configuredHost)
479
- return;
480
- if (isWildcardHost(configuredHost))
481
- return;
482
- allowedHosts.add(configuredHost);
483
- }
484
- function addExplicitAllowedHosts(allowedHosts) {
485
- for (const host of config.security.allowedHosts) {
486
- const normalized = normalizeHost(host);
487
- if (!normalized) {
488
- logDebug('Ignoring invalid allowed host entry', { host });
489
- continue;
490
- }
491
- allowedHosts.add(normalized);
492
- }
493
- }
494
- function buildAllowedHosts() {
495
- const allowedHosts = new Set();
496
- addLoopbackHosts(allowedHosts);
497
- addConfiguredHost(allowedHosts);
498
- addExplicitAllowedHosts(allowedHosts);
499
- return allowedHosts;
500
- }
501
- function createHostValidationMiddleware() {
502
- const allowedHosts = buildAllowedHosts();
503
- return (req, res, next) => {
504
- const hostHeader = typeof req.headers.host === 'string' ? req.headers.host : '';
505
- const normalized = normalizeHost(hostHeader);
506
- if (!normalized || !allowedHosts.has(normalized)) {
507
- respondHostNotAllowed(res);
508
- return;
509
- }
510
- next();
511
- };
512
- }
513
- function createOriginValidationMiddleware() {
514
- const allowedHosts = buildAllowedHosts();
515
- return (req, res, next) => {
516
- const originHeader = getNonEmptyStringHeader(req.headers.origin);
517
- if (!originHeader) {
518
- next();
519
- return;
520
- }
521
- const originHostname = tryParseOriginHostname(originHeader);
522
- if (!originHostname || !allowedHosts.has(originHostname)) {
523
- respondOriginNotAllowed(res);
524
- return;
525
- }
526
- next();
527
- };
528
- }
529
- function createJsonParseErrorHandler() {
530
- return (err, _req, res, next) => {
531
- if (err instanceof SyntaxError && 'body' in err) {
532
- res.status(400).json({
533
- jsonrpc: '2.0',
534
- error: {
535
- code: -32700,
536
- message: 'Parse error: Invalid JSON',
537
- },
538
- id: null,
539
- });
540
- return;
541
- }
542
- next();
543
- };
544
- }
545
- function createContextMiddleware() {
546
- return (req, _res, next) => {
547
- const requestId = randomUUID();
548
- const sessionId = getSessionId(req);
549
- const context = sessionId === undefined
550
- ? { requestId, operationId: requestId }
551
- : { requestId, operationId: requestId, sessionId };
552
- runWithRequestContext(context, () => {
553
- next();
554
- });
555
- };
556
- }
557
- function registerHealthRoute(app) {
558
- app.get('/health', (_req, res) => {
559
- res.json({
560
- status: 'healthy',
561
- name: config.server.name,
562
- version: config.server.version,
563
- uptime: process.uptime(),
564
- });
565
- });
566
- }
567
- export function attachBaseMiddleware(options) {
568
- const { app, jsonParser, rateLimitMiddleware, corsMiddleware } = options;
569
- app.use(createHostValidationMiddleware());
570
- app.use(createOriginValidationMiddleware());
571
- app.use(jsonParser);
572
- app.use(createContextMiddleware());
573
- app.use(createJsonParseErrorHandler());
574
- app.use(corsMiddleware);
575
- app.use('/mcp', rateLimitMiddleware);
576
- registerHealthRoute(app);
577
- }
578
- export function createCorsMiddleware() {
579
- return (req, res, next) => {
580
- if (req.method === 'OPTIONS') {
581
- res.sendStatus(200);
582
- return;
583
- }
584
- next();
585
- };
586
- }
587
- function parseScopes(value) {
588
- if (typeof value === 'string') {
589
- return value
590
- .split(' ')
591
- .map((scope) => scope.trim())
592
- .filter((scope) => scope.length > 0);
593
- }
594
- if (Array.isArray(value)) {
595
- return value.filter((scope) => typeof scope === 'string');
596
- }
597
- return [];
598
- }
599
- function parseResourceUrl(value) {
600
- if (typeof value !== 'string')
601
- return undefined;
602
- if (!URL.canParse(value))
603
- return undefined;
604
- return new URL(value);
605
- }
606
- function parseAudResource(aud) {
607
- if (typeof aud === 'string') {
608
- return parseResourceUrl(aud);
609
- }
610
- if (Array.isArray(aud)) {
611
- for (const entry of aud) {
612
- const parsed = parseResourceUrl(entry);
613
- if (parsed)
614
- return parsed;
615
- }
616
- }
617
- return undefined;
618
- }
619
- function extractResource(data) {
620
- const resource = parseResourceUrl(data.resource);
621
- if (resource)
622
- return resource;
623
- return parseAudResource(data.aud);
624
- }
625
- function extractScopes(data) {
626
- if (data.scope !== undefined) {
627
- return parseScopes(data.scope);
628
- }
629
- if (data.scopes !== undefined) {
630
- return parseScopes(data.scopes);
631
- }
632
- if (data.scp !== undefined) {
633
- return parseScopes(data.scp);
634
- }
635
- return [];
636
- }
637
- function readExpiresAt(data) {
638
- const expiresAt = typeof data.exp === 'number' ? data.exp : Number.NaN;
639
- if (!Number.isFinite(expiresAt)) {
640
- throw new InvalidTokenError('Token has no expiration time');
641
- }
642
- return expiresAt;
643
- }
644
- function resolveClientId(data) {
645
- if (typeof data.client_id === 'string')
646
- return data.client_id;
647
- if (typeof data.cid === 'string')
648
- return data.cid;
649
- if (typeof data.sub === 'string')
650
- return data.sub;
651
- return 'unknown';
652
- }
653
- function stripHash(url) {
654
- const copy = new URL(url.href);
655
- copy.hash = '';
656
- return copy.href;
657
- }
658
- function ensureResourceMatch(resource) {
659
- if (!resource) {
660
- throw new InvalidTokenError('Token resource mismatch');
661
- }
662
- if (stripHash(resource) !== stripHash(config.auth.resourceUrl)) {
663
- throw new InvalidTokenError('Token resource mismatch');
664
- }
665
- return resource;
666
- }
667
- function buildIntrospectionAuthInfo(token, data) {
668
- const resource = ensureResourceMatch(extractResource(data));
669
- return {
670
- token,
671
- clientId: resolveClientId(data),
672
- scopes: extractScopes(data),
673
- expiresAt: readExpiresAt(data),
674
- resource,
675
- extra: data,
676
- };
677
- }
678
- function buildBasicAuthHeader(clientId, clientSecret) {
679
- const secret = clientSecret ?? '';
680
- const basic = Buffer.from(`${clientId}:${secret}`, 'utf8').toString('base64');
681
- return `Basic ${basic}`;
682
- }
683
- function buildIntrospectionRequest(token, resourceUrl, clientId, clientSecret) {
684
- const body = new URLSearchParams({
685
- token,
686
- token_type_hint: 'access_token',
687
- resource: stripHash(resourceUrl),
688
- }).toString();
689
- const headers = {
690
- 'content-type': 'application/x-www-form-urlencoded',
691
- };
692
- if (clientId) {
693
- headers.authorization = buildBasicAuthHeader(clientId, clientSecret);
694
- }
695
- return { body, headers };
696
- }
697
- async function requestIntrospection(introspectionUrl, request, timeoutMs) {
698
- const response = await fetch(introspectionUrl, {
699
- method: 'POST',
700
- headers: request.headers,
701
- body: request.body,
702
- signal: AbortSignal.timeout(timeoutMs),
703
- });
704
- if (!response.ok) {
705
- await response.body?.cancel();
706
- throw new ServerError(`Token introspection failed: ${response.status}`);
707
- }
708
- return response.json();
709
- }
710
- function parseIntrospectionPayload(payload) {
711
- if (!isRecord(payload) || Array.isArray(payload)) {
712
- throw new ServerError('Invalid introspection response');
713
- }
714
- if (payload.active !== true) {
715
- throw new InvalidTokenError('Token is inactive');
716
- }
717
- return payload;
718
- }
719
- async function verifyWithIntrospection(token) {
720
- const { auth } = config;
721
- if (!auth.introspectionUrl) {
722
- throw new ServerError('Token introspection is not configured');
723
- }
724
- const request = buildIntrospectionRequest(token, auth.resourceUrl, auth.clientId, auth.clientSecret);
725
- const payload = await requestIntrospection(auth.introspectionUrl, request, auth.introspectionTimeoutMs);
726
- return buildIntrospectionAuthInfo(token, parseIntrospectionPayload(payload));
727
- }
728
- const STATIC_TOKEN_TTL_SECONDS = 60 * 60 * 24;
729
- function buildStaticAuthInfo(token) {
730
- return {
731
- token,
732
- clientId: 'static-token',
733
- scopes: config.auth.requiredScopes,
734
- expiresAt: Math.floor(Date.now() / 1000) + STATIC_TOKEN_TTL_SECONDS,
735
- resource: config.auth.resourceUrl,
736
- };
737
- }
738
- function verifyStaticToken(token) {
739
- if (config.auth.staticTokens.length === 0) {
740
- throw new InvalidTokenError('No static tokens configured');
741
- }
742
- const matched = config.auth.staticTokens.some((candidate) => timingSafeEqualUtf8(candidate, token));
743
- if (!matched) {
744
- throw new InvalidTokenError('Invalid token');
745
- }
746
- return buildStaticAuthInfo(token);
747
- }
748
- function normalizeHeaderValue(header) {
749
- return Array.isArray(header) ? header[0] : header;
750
- }
751
- function getApiKeyHeader(req) {
752
- const apiKeyHeader = normalizeHeaderValue(req.headers['x-api-key']);
753
- return apiKeyHeader ? apiKeyHeader.trim() : null;
754
- }
755
- function createLegacyApiKeyMiddleware() {
756
- return (req, _res, next) => {
757
- if (config.auth.mode !== 'static') {
758
- next();
759
- return;
760
- }
761
- if (!req.headers.authorization) {
762
- const apiKey = getApiKeyHeader(req);
763
- if (apiKey) {
764
- req.headers.authorization = `Bearer ${apiKey}`;
765
- }
766
- }
767
- next();
768
- };
769
- }
770
- async function verifyAccessToken(token) {
771
- if (config.auth.mode === 'oauth') {
772
- return verifyWithIntrospection(token);
773
- }
774
- return verifyStaticToken(token);
775
- }
776
- function resolveMetadataUrl() {
777
- if (config.auth.mode !== 'oauth')
778
- return null;
779
- return getOAuthProtectedResourceMetadataUrl(new URL(config.auth.resourceUrl));
780
- }
781
- function resolveOptionalScopes(requiredScopes) {
782
- return requiredScopes.length > 0 ? [...requiredScopes] : undefined;
783
- }
784
- function resolveOAuthMetadataParams(authConfig) {
785
- const { issuerUrl, authorizationUrl, tokenUrl, revocationUrl, registrationUrl, requiredScopes, } = authConfig;
786
- if (!issuerUrl || !authorizationUrl || !tokenUrl)
787
- return null;
788
- return {
789
- issuerUrl,
790
- authorizationUrl,
791
- tokenUrl,
792
- revocationUrl,
793
- registrationUrl,
794
- requiredScopes,
795
- };
796
- }
797
- function buildBaseOAuthMetadata(params) {
798
- return {
799
- issuer: params.issuerUrl.href,
800
- authorization_endpoint: params.authorizationUrl.href,
801
- response_types_supported: ['code'],
802
- code_challenge_methods_supported: ['S256'],
803
- token_endpoint: params.tokenUrl.href,
804
- token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
805
- grant_types_supported: ['authorization_code', 'refresh_token'],
806
- };
807
- }
808
- function applyOptionalScopes(metadata, requiredScopes) {
809
- const scopesSupported = resolveOptionalScopes(requiredScopes);
810
- if (scopesSupported !== undefined) {
811
- metadata.scopes_supported = scopesSupported;
812
- }
813
- }
814
- function applyOptionalEndpoint(metadata, key, url) {
815
- if (!url)
816
- return;
817
- metadata[key] = url.href;
818
- }
819
- function buildOAuthMetadata(params) {
820
- const oauthMetadata = buildBaseOAuthMetadata(params);
821
- applyOptionalScopes(oauthMetadata, params.requiredScopes);
822
- applyOptionalEndpoint(oauthMetadata, 'revocation_endpoint', params.revocationUrl);
823
- applyOptionalEndpoint(oauthMetadata, 'registration_endpoint', params.registrationUrl);
824
- return oauthMetadata;
825
- }
826
- function createAuthMiddleware() {
827
- const metadataUrl = resolveMetadataUrl();
828
- const authHandler = requireBearerAuth({
829
- verifier: { verifyAccessToken },
830
- requiredScopes: config.auth.requiredScopes,
831
- ...(metadataUrl ? { resourceMetadataUrl: metadataUrl } : {}),
832
- });
833
- const legacyHandler = createLegacyApiKeyMiddleware();
834
- return (req, res, next) => {
835
- legacyHandler(req, res, () => {
836
- authHandler(req, res, next);
837
- });
838
- };
839
- }
840
- function createAuthMetadataRouter() {
841
- if (config.auth.mode !== 'oauth')
842
- return null;
843
- const oauthMetadataParams = resolveOAuthMetadataParams(config.auth);
844
- if (!oauthMetadataParams)
845
- return null;
846
- return mcpAuthMetadataRouter({
847
- oauthMetadata: buildOAuthMetadata(oauthMetadataParams),
848
- resourceServerUrl: config.auth.resourceUrl,
849
- scopesSupported: config.auth.requiredScopes,
850
- resourceName: config.server.name,
851
- });
852
- }
853
- function sendJsonRpcError(res, code, message, status = 400, id = null) {
854
- res.status(status).json({
855
- jsonrpc: '2.0',
856
- error: {
857
- code,
858
- message,
859
- },
860
- id,
861
- });
862
- }
863
- function sendJsonRpcErrorOrNoContent(res, code, message, status, id) {
864
- if (id === null) {
865
- res.sendStatus(204);
866
- return;
867
- }
868
- sendJsonRpcError(res, code, message, status, id ?? null);
869
- }
870
- function getSessionId(req) {
871
- const header = req.headers['mcp-session-id'];
872
- return Array.isArray(header) ? header[0] : header;
873
- }
874
- export function createSessionStore(sessionTtlMs) {
875
- const sessions = new Map();
876
- return {
877
- get: (sessionId) => sessions.get(sessionId),
878
- touch: (sessionId) => {
879
- touchSession(sessions, sessionId);
880
- },
881
- set: (sessionId, entry) => {
882
- sessions.set(sessionId, entry);
883
- },
884
- remove: (sessionId) => removeSession(sessions, sessionId),
885
- size: () => sessions.size,
886
- clear: () => clearSessions(sessions),
887
- evictExpired: () => evictExpiredSessions(sessions, sessionTtlMs),
888
- evictOldest: () => evictOldestSession(sessions),
889
- };
890
- }
891
- function touchSession(sessions, sessionId) {
892
- const session = sessions.get(sessionId);
893
- if (session) {
894
- session.lastSeen = Date.now();
895
- sessions.delete(sessionId);
896
- sessions.set(sessionId, session);
897
- }
898
- }
899
- function removeSession(sessions, sessionId) {
900
- const session = sessions.get(sessionId);
901
- sessions.delete(sessionId);
902
- return session;
903
- }
904
- function clearSessions(sessions) {
905
- const entries = Array.from(sessions.values());
906
- sessions.clear();
907
- return entries;
908
- }
909
- function evictExpiredSessions(sessions, sessionTtlMs) {
910
- const now = Date.now();
911
- const evicted = [];
912
- for (const [id, session] of sessions.entries()) {
913
- if (now - session.lastSeen > sessionTtlMs) {
914
- sessions.delete(id);
915
- evicted.push(session);
916
- }
917
- }
918
- return evicted;
919
- }
920
- function evictOldestSession(sessions) {
921
- const oldestEntry = sessions.keys().next();
922
- if (oldestEntry.done)
923
- return undefined;
924
- const oldestId = oldestEntry.value;
925
- const session = sessions.get(oldestId);
926
- sessions.delete(oldestId);
927
- return session;
928
- }
929
- let inFlightSessions = 0;
930
- export function reserveSessionSlot(store, maxSessions) {
931
- if (store.size() + inFlightSessions >= maxSessions) {
932
- return false;
933
- }
934
- inFlightSessions += 1;
935
- return true;
936
- }
937
- function releaseSessionSlot() {
938
- if (inFlightSessions > 0) {
939
- inFlightSessions -= 1;
940
- }
941
- }
942
- export function createSlotTracker() {
943
- let slotReleased = false;
944
- let initialized = false;
945
- return {
946
- releaseSlot: () => {
947
- if (slotReleased)
948
- return;
949
- slotReleased = true;
950
- releaseSessionSlot();
951
- },
952
- markInitialized: () => {
953
- initialized = true;
954
- },
955
- isInitialized: () => initialized,
956
- };
957
- }
958
- function isServerAtCapacity(store, maxSessions) {
959
- return store.size() + inFlightSessions >= maxSessions;
960
- }
961
- function tryEvictSlot(store, maxSessions, evictOldest) {
962
- const currentSize = store.size();
963
- const canFreeSlot = currentSize >= maxSessions &&
964
- currentSize - 1 + inFlightSessions < maxSessions;
965
- return canFreeSlot && evictOldest(store);
966
- }
967
- export function ensureSessionCapacity({ store, maxSessions, res, evictOldest, requestId, }) {
968
- if (!isServerAtCapacity(store, maxSessions)) {
969
- return true;
970
- }
971
- if (tryEvictSlot(store, maxSessions, evictOldest)) {
972
- return !isServerAtCapacity(store, maxSessions);
973
- }
974
- respondServerBusy(res, requestId);
975
- return false;
976
- }
977
- function respondServerBusy(res, requestId) {
978
- sendJsonRpcErrorOrNoContent(res, -32000, 'Server busy: maximum sessions reached', 503, requestId);
979
- }
980
- function respondBadRequest(res, id) {
981
- sendJsonRpcErrorOrNoContent(res, -32000, 'Bad Request: Missing session ID or not an initialize request', 400, id);
982
- }
983
- function respondSessionNotInitialized(res, requestId) {
984
- sendJsonRpcErrorOrNoContent(res, -32000, 'Bad Request: Session not initialized', 400, requestId);
985
- }
986
- function isNotificationMethod(method) {
987
- return method.startsWith('notifications/');
988
- }
989
- function isAllowedBeforeInitialized(method) {
990
- return (method === 'initialize' ||
991
- method === 'notifications/initialized' ||
992
- method === 'ping');
993
- }
994
- function createTimeoutController() {
995
- let initTimeout = null;
996
- return {
997
- clear: () => {
998
- if (!initTimeout)
999
- return;
1000
- clearTimeout(initTimeout);
1001
- initTimeout = null;
1002
- },
1003
- set: (timeout) => {
1004
- initTimeout = timeout;
1005
- },
1006
- };
1007
- }
1008
- function createTransportAdapter(transport) {
1009
- const adapter = buildTransportAdapter(transport);
1010
- attachTransportAccessors(adapter, transport);
1011
- return adapter;
1012
- }
1013
- function buildTransportAdapter(transport) {
1014
- return {
1015
- start: () => transport.start(),
1016
- send: (message, options) => transport.send(message, options),
1017
- close: () => transport.close(),
1018
- };
1019
- }
1020
- function createAccessorDescriptor(getter, setter) {
1021
- return {
1022
- get: getter,
1023
- ...(setter ? { set: setter } : {}),
1024
- enumerable: true,
1025
- configurable: true,
1026
- };
1027
- }
1028
- export function composeCloseHandlers(first, second) {
1029
- if (!first)
1030
- return second;
1031
- if (!second)
1032
- return first;
1033
- return () => {
1034
- try {
1035
- first();
1036
- }
1037
- finally {
1038
- second();
1039
- }
1040
- };
1041
- }
1042
- function createOnCloseDescriptor(transport) {
1043
- return createAccessorDescriptor(() => transport.onclose, (handler) => {
1044
- transport.onclose = handler;
1045
- });
1046
- }
1047
- function createOnErrorDescriptor(transport) {
1048
- return createAccessorDescriptor(() => transport.onerror, (handler) => {
1049
- transport.onerror = handler;
1050
- });
1051
- }
1052
- function createOnMessageDescriptor(transport) {
1053
- return createAccessorDescriptor(() => transport.onmessage, (handler) => {
1054
- transport.onmessage = handler;
1055
- });
1056
- }
1057
- function attachTransportAccessors(adapter, transport) {
1058
- Object.defineProperties(adapter, {
1059
- onclose: createOnCloseDescriptor(transport),
1060
- onerror: createOnErrorDescriptor(transport),
1061
- onmessage: createOnMessageDescriptor(transport),
1062
- sessionId: createAccessorDescriptor(() => transport.sessionId),
1063
- });
1064
- }
1065
- function startSessionInitTimeout({ transport, tracker, clearInitTimeout, timeoutMs, }) {
1066
- if (timeoutMs <= 0)
1067
- return null;
1068
- const timeout = setTimeout(() => {
1069
- clearInitTimeout();
1070
- if (tracker.isInitialized())
1071
- return;
1072
- tracker.releaseSlot();
1073
- void transport.close().catch((error) => {
1074
- logWarn('Failed to close stalled session', {
1075
- error: getErrorMessage(error),
1076
- });
1077
- });
1078
- logWarn('Session initialization timed out', { timeoutMs });
1079
- }, timeoutMs);
1080
- timeout.unref();
1081
- return timeout;
1082
- }
1083
- function createSessionTransport({ tracker, timeoutController, }) {
1084
- const transport = new StreamableHTTPServerTransport({
1085
- sessionIdGenerator: () => randomUUID(),
1086
- });
1087
- transport.onclose = () => {
1088
- timeoutController.clear();
1089
- if (!tracker.isInitialized()) {
1090
- tracker.releaseSlot();
1091
- }
1092
- };
1093
- timeoutController.set(startSessionInitTimeout({
1094
- transport,
1095
- tracker,
1096
- clearInitTimeout: timeoutController.clear,
1097
- timeoutMs: config.server.sessionInitTimeoutMs,
1098
- }));
1099
- return transport;
1100
- }
1101
- async function connectTransportOrThrow({ transport, clearInitTimeout, releaseSlot, }) {
1102
- const mcpServer = createMcpServer();
1103
- const transportAdapter = createTransportAdapter(transport);
1104
- const oncloseBeforeConnect = transport.onclose;
1105
- try {
1106
- await mcpServer.connect(transportAdapter);
1107
- if (oncloseBeforeConnect && transport.onclose !== oncloseBeforeConnect) {
1108
- transport.onclose = composeCloseHandlers(transport.onclose, oncloseBeforeConnect);
1109
- }
1110
- }
1111
- catch (error) {
1112
- clearInitTimeout();
1113
- releaseSlot();
1114
- void transport.close().catch((closeError) => {
1115
- logWarn('Failed to close transport after connect error', {
1116
- error: getErrorMessage(closeError),
1117
- });
1118
- });
1119
- logError('Failed to initialize MCP session', error instanceof Error ? error : undefined);
1120
- throw error;
1121
- }
1122
- return mcpServer;
1123
- }
1124
- function evictExpiredSessionsWithClose(store) {
1125
- const evicted = store.evictExpired();
1126
- for (const session of evicted) {
1127
- void session.transport.close().catch((error) => {
1128
- logWarn('Failed to close expired session', {
1129
- error: getErrorMessage(error),
1130
- });
1131
- });
1132
- }
1133
- return evicted.length;
1134
- }
1135
- function evictOldestSessionWithClose(store) {
1136
- const session = store.evictOldest();
1137
- if (!session)
1138
- return false;
1139
- void session.transport.close().catch((error) => {
1140
- logWarn('Failed to close evicted session', {
1141
- error: getErrorMessage(error),
1142
- });
1143
- });
1144
- return true;
1145
- }
1146
- function reserveSessionIfPossible({ options, res, requestId, }) {
1147
- const capacityArgs = {
1148
- store: options.sessionStore,
1149
- maxSessions: options.maxSessions,
1150
- res,
1151
- evictOldest: evictOldestSessionWithClose,
1152
- ...(requestId !== undefined ? { requestId } : {}),
1153
- };
1154
- if (!ensureSessionCapacity(capacityArgs)) {
1155
- return false;
1156
- }
1157
- if (!reserveSessionSlot(options.sessionStore, options.maxSessions)) {
1158
- respondServerBusy(res);
1159
- return false;
1160
- }
1161
- return true;
1162
- }
1163
- function resolveExistingSessionTransport(store, sessionId, res, requestId, method) {
1164
- const existingSession = store.get(sessionId);
1165
- if (existingSession) {
1166
- if (!existingSession.protocolInitialized &&
1167
- !isAllowedBeforeInitialized(method)) {
1168
- respondSessionNotInitialized(res, requestId);
1169
- return null;
1170
- }
1171
- store.touch(sessionId);
1172
- return existingSession.transport;
1173
- }
1174
- // Client supplied a session id but it doesn't exist; Streamable HTTP: invalid session IDs => 404.
1175
- sendJsonRpcErrorOrNoContent(res, -32600, 'Session not found', 404, requestId);
1176
- return null;
1177
- }
1178
- function createSessionContext() {
1179
- const tracker = createSlotTracker();
1180
- const timeoutController = createTimeoutController();
1181
- const transport = createSessionTransport({ tracker, timeoutController });
1182
- return { tracker, timeoutController, transport };
1183
- }
1184
- function attachSessionInitializedHandler(server, store, sessionId) {
1185
- const previousInitialized = server.server.oninitialized;
1186
- server.server.oninitialized = () => {
1187
- const entry = store.get(sessionId);
1188
- if (entry) {
1189
- entry.protocolInitialized = true;
1190
- }
1191
- previousInitialized?.();
1192
- };
1193
- }
1194
- function finalizeSessionIfValid({ store, transport, mcpServer, tracker, clearInitTimeout, res, requestId, }) {
1195
- const { sessionId } = transport;
1196
- if (typeof sessionId !== 'string') {
1197
- clearInitTimeout();
1198
- tracker.releaseSlot();
1199
- respondBadRequest(res, requestId ?? null);
1200
- return false;
1201
- }
1202
- finalizeSession({
1203
- store,
1204
- transport,
1205
- sessionId,
1206
- mcpServer,
1207
- tracker,
1208
- clearInitTimeout,
1209
- });
1210
- return true;
1211
- }
1212
- function finalizeSession({ store, transport, sessionId, mcpServer, tracker, clearInitTimeout, }) {
1213
- clearInitTimeout();
1214
- tracker.markInitialized();
1215
- tracker.releaseSlot();
1216
- const now = Date.now();
1217
- store.set(sessionId, {
1218
- transport,
1219
- createdAt: now,
1220
- lastSeen: now,
1221
- protocolInitialized: false,
1222
- });
1223
- attachSessionInitializedHandler(mcpServer, store, sessionId);
1224
- const previousOnClose = transport.onclose;
1225
- transport.onclose = composeCloseHandlers(previousOnClose, () => {
1226
- store.remove(sessionId);
1227
- logInfo('Session closed');
1228
- });
1229
- logInfo('Session initialized');
1230
- }
1231
- async function createAndConnectTransport({ options, res, requestId, }) {
1232
- const reserveArgs = {
1233
- options,
1234
- res,
1235
- ...(requestId !== undefined ? { requestId } : {}),
1236
- };
1237
- if (!reserveSessionIfPossible(reserveArgs))
1238
- return null;
1239
- const { tracker, timeoutController, transport } = createSessionContext();
1240
- const mcpServer = await connectTransportOrThrow({
1241
- transport,
1242
- clearInitTimeout: timeoutController.clear,
1243
- releaseSlot: tracker.releaseSlot,
1244
- });
1245
- if (!finalizeSessionIfValid({
1246
- store: options.sessionStore,
1247
- transport,
1248
- mcpServer,
1249
- tracker,
1250
- clearInitTimeout: timeoutController.clear,
1251
- res,
1252
- ...(requestId !== undefined ? { requestId } : {}),
1253
- })) {
1254
- return null;
1255
- }
1256
- return transport;
1257
- }
1258
- export async function resolveTransportForPost({ res, body, sessionId, options, }) {
1259
- const requestId = body.id ?? null;
1260
- if (sessionId) {
1261
- return resolveExistingSessionTransport(options.sessionStore, sessionId, res, requestId, body.method);
1262
- }
1263
- if (!isInitializeRequest(body)) {
1264
- respondBadRequest(res, requestId);
1265
- return null;
1266
- }
1267
- evictExpiredSessionsWithClose(options.sessionStore);
1268
- return createAndConnectTransport({ options, res, requestId });
1269
- }
1270
- function startSessionCleanupLoop(store, sessionTtlMs) {
1271
- const controller = new AbortController();
1272
- void runSessionCleanupLoop(store, sessionTtlMs, controller.signal).catch(handleSessionCleanupError);
1273
- return controller;
1274
- }
1275
- async function runSessionCleanupLoop(store, sessionTtlMs, signal) {
1276
- const intervalMs = getCleanupIntervalMs(sessionTtlMs);
1277
- for await (const getNow of setIntervalPromise(intervalMs, Date.now, {
1278
- signal,
1279
- ref: false,
1280
- })) {
1281
- handleSessionEvictions(store, getNow());
1282
- }
1283
- }
1284
- function getCleanupIntervalMs(sessionTtlMs) {
1285
- return Math.min(Math.max(Math.floor(sessionTtlMs / 2), 10000), 60000);
1286
- }
1287
- function isAbortError(error) {
1288
- return error instanceof Error && error.name === 'AbortError';
1289
- }
1290
- function handleSessionEvictions(store, now) {
1291
- const evicted = evictExpiredSessionsWithClose(store);
1292
- if (evicted > 0) {
1293
- logInfo('Expired sessions evicted', {
1294
- evicted,
1295
- timestamp: new Date(now).toISOString(),
1296
- });
1297
- }
1298
- }
1299
- function handleSessionCleanupError(error) {
1300
- if (isAbortError(error)) {
1301
- return;
1302
- }
1303
- logWarn('Session cleanup loop failed', {
1304
- error: error instanceof Error ? error.message : 'Unknown error',
1305
- });
1306
- }
1307
- const paramsSchema = z.looseObject({});
1308
- const mcpRequestSchema = z.looseObject({
1309
- jsonrpc: z.literal('2.0'),
1310
- method: z.string().min(1),
1311
- id: z.union([z.string(), z.number(), z.null()]).optional(),
1312
- params: paramsSchema.optional(),
1313
- });
1314
- function wrapAsync(fn) {
1315
- return (req, res, next) => {
1316
- Promise.resolve(fn(req, res)).catch(next);
1317
- };
1318
- }
1319
- export function isJsonRpcBatchRequest(body) {
1320
- return Array.isArray(body);
1321
- }
1322
- export function isMcpRequestBody(body) {
1323
- return mcpRequestSchema.safeParse(body).success;
1324
- }
1325
- function respondInvalidRequestBody(res) {
1326
- sendJsonRpcError(res, -32600, 'Invalid Request: Malformed request body', 400);
1327
- }
1328
- function respondMissingSession(res) {
1329
- sendJsonRpcError(res, -32600, 'Missing mcp-session-id header', 400);
1330
- }
1331
- function respondSessionNotFound(res) {
1332
- sendJsonRpcError(res, -32600, 'Session not found', 404);
1333
- }
1334
- function validatePostPayload(payload, res) {
1335
- if (isJsonRpcBatchRequest(payload)) {
1336
- sendJsonRpcError(res, -32600, 'Batch requests are not supported', 400);
1337
- return null;
1338
- }
1339
- if (!isMcpRequestBody(payload)) {
1340
- respondInvalidRequestBody(res);
1341
- return null;
1342
- }
1343
- return payload;
1344
- }
1345
- function ensureRequestHasId(body, res) {
1346
- if (body.id !== undefined || isNotificationMethod(body.method)) {
1347
- return true;
1348
- }
1349
- sendJsonRpcError(res, -32600, `Invalid Request: ${body.method} requires id`, 400, null);
1350
- return false;
1351
- }
1352
- function logPostRequest(body, sessionId, options) {
1353
- logInfo('[MCP POST]', {
1354
- method: body.method,
1355
- id: body.id,
1356
- isInitialize: body.method === 'initialize',
1357
- sessionCount: options.sessionStore.size(),
1358
- });
1359
- }
1360
- async function handleTransportRequest(transport, req, res, body) {
1361
- try {
1362
- await dispatchTransportRequest(transport, req, res, body);
1363
- }
1364
- catch (error) {
1365
- logError('MCP request handling failed', error instanceof Error ? error : undefined);
1366
- handleTransportError(res, body?.id ?? null);
1367
- }
1368
- }
1369
- function handleTransportError(res, id) {
1370
- if (res.headersSent)
1371
- return;
1372
- sendJsonRpcError(res, -32603, 'Internal error', 500, id);
1373
- }
1374
- function dispatchTransportRequest(transport, req, res, body) {
1375
- return body
1376
- ? transport.handleRequest(req, res, body)
1377
- : transport.handleRequest(req, res);
1378
- }
1379
- function resolveSessionTransport(sessionId, options, res) {
1380
- const { sessionStore } = options;
1381
- if (!sessionId) {
1382
- respondMissingSession(res);
1383
- return null;
1384
- }
1385
- const session = sessionStore.get(sessionId);
1386
- if (!session) {
1387
- respondSessionNotFound(res);
1388
- return null;
1389
- }
1390
- sessionStore.touch(sessionId);
1391
- return session.transport;
1392
- }
1393
- const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version';
1394
- const MCP_PROTOCOL_VERSIONS = {
1395
- supported: new Set(['2025-11-25']),
1396
- };
1397
- function getHeaderValue(req, headerNameLower) {
1398
- const value = req.headers[headerNameLower];
1399
- if (typeof value === 'string')
1400
- return value;
1401
- if (Array.isArray(value))
1402
- return value[0] ?? null;
1403
- return null;
1404
- }
1405
- export function ensureMcpProtocolVersionHeader(req, res) {
1406
- const raw = getHeaderValue(req, MCP_PROTOCOL_VERSION_HEADER);
1407
- const version = raw?.trim();
1408
- if (!version) {
1409
- sendJsonRpcError(res, -32600, 'Missing required MCP-Protocol-Version header', 400);
1410
- return false;
1411
- }
1412
- if (!MCP_PROTOCOL_VERSIONS.supported.has(version)) {
1413
- sendJsonRpcError(res, -32600, `Unsupported MCP-Protocol-Version: ${version}`, 400);
1414
- return false;
1415
- }
1416
- return true;
1417
- }
1418
- function closeSessionIfPresent(req, options) {
1419
- const sessionId = getSessionId(req);
1420
- if (!sessionId)
1421
- return;
1422
- const session = options.sessionStore.get(sessionId);
1423
- if (!session)
1424
- return;
1425
- logWarn('Closing session due to protocol version mismatch', { sessionId });
1426
- void session.transport.close().catch((error) => {
1427
- logWarn('Failed to close session after protocol version mismatch', {
1428
- sessionId,
1429
- error: getErrorMessage(error),
1430
- });
1431
- });
1432
- }
1433
- function getAcceptHeader(req) {
1434
- const value = req.headers.accept;
1435
- if (typeof value === 'string')
1436
- return value;
1437
- return '';
1438
- }
1439
- function setAcceptHeader(req, value) {
1440
- req.headers.accept = value;
1441
- const { rawHeaders } = req;
1442
- if (!Array.isArray(rawHeaders))
1443
- return;
1444
- for (let i = 0; i + 1 < rawHeaders.length; i += 2) {
1445
- const key = rawHeaders[i];
1446
- if (typeof key === 'string' && key.toLowerCase() === 'accept') {
1447
- rawHeaders[i + 1] = value;
1448
- return;
1449
- }
1450
- }
1451
- rawHeaders.push('Accept', value);
1452
- }
1453
- function hasToken(header, token) {
1454
- return header
1455
- .split(',')
1456
- .map((part) => part.trim().toLowerCase())
1457
- .some((part) => part === token || part.startsWith(`${token};`));
1458
- }
1459
- export function ensurePostAcceptHeader(req) {
1460
- const accept = getAcceptHeader(req);
1461
- // Some clients send */* or omit Accept; the SDK transport is picky.
1462
- if (!accept || hasToken(accept, '*/*')) {
1463
- setAcceptHeader(req, 'application/json, text/event-stream');
1464
- return;
1465
- }
1466
- const hasJson = hasToken(accept, 'application/json');
1467
- const hasSse = hasToken(accept, 'text/event-stream');
1468
- if (!hasJson || !hasSse) {
1469
- setAcceptHeader(req, 'application/json, text/event-stream');
1470
- }
1471
- }
1472
- export function acceptsEventStream(req) {
1473
- const accept = getAcceptHeader(req);
1474
- if (!accept)
1475
- return false;
1476
- return hasToken(accept, 'text/event-stream');
1477
- }
1478
- async function handlePost(req, res, options) {
1479
- ensurePostAcceptHeader(req);
1480
- if (!ensureMcpProtocolVersionHeader(req, res)) {
1481
- closeSessionIfPresent(req, options);
1482
- return;
1483
- }
1484
- const sessionId = getSessionId(req);
1485
- const payload = validatePostPayload(req.body, res);
1486
- if (!payload)
1487
- return;
1488
- if (!ensureRequestHasId(payload, res))
1489
- return;
1490
- logPostRequest(payload, sessionId, options);
1491
- const transport = await resolveTransportForPost({
1492
- res,
1493
- body: payload,
1494
- sessionId,
1495
- options,
1496
- });
1497
- if (!transport)
1498
- return;
1499
- await handleTransportRequest(transport, req, res, payload);
1500
- }
1501
- async function handleGet(req, res, options) {
1502
- if (!ensureMcpProtocolVersionHeader(req, res)) {
1503
- closeSessionIfPresent(req, options);
1504
- return;
1505
- }
1506
- if (!acceptsEventStream(req)) {
1507
- res.status(406).json({
1508
- error: 'Not Acceptable',
1509
- code: 'ACCEPT_NOT_SUPPORTED',
1510
- });
1511
- return;
1512
- }
1513
- const transport = resolveSessionTransport(getSessionId(req), options, res);
1514
- if (!transport)
1515
- return;
1516
- await handleTransportRequest(transport, req, res);
1517
- }
1518
- async function handleDelete(req, res, options) {
1519
- if (!ensureMcpProtocolVersionHeader(req, res)) {
1520
- closeSessionIfPresent(req, options);
1521
- return;
1522
- }
1523
- const transport = resolveSessionTransport(getSessionId(req), options, res);
1524
- if (!transport)
1525
- return;
1526
- await handleTransportRequest(transport, req, res);
1527
- }
1528
- function registerMcpRoutes(app, options) {
1529
- app.post('/mcp', wrapAsync((req, res) => handlePost(req, res, options)));
1530
- app.get('/mcp', wrapAsync((req, res) => handleGet(req, res, options)));
1531
- app.delete('/mcp', wrapAsync((req, res) => handleDelete(req, res, options)));
1532
- }
1533
- export function applyHttpServerTuning(server) {
1534
- const { headersTimeoutMs, requestTimeoutMs, keepAliveTimeoutMs } = config.server.http;
1535
- if (headersTimeoutMs !== undefined) {
1536
- server.headersTimeout = headersTimeoutMs;
1537
- }
1538
- if (requestTimeoutMs !== undefined) {
1539
- server.requestTimeout = requestTimeoutMs;
1540
- }
1541
- if (keepAliveTimeoutMs !== undefined) {
1542
- server.keepAliveTimeout = keepAliveTimeoutMs;
1543
- }
1544
- if (headersTimeoutMs !== undefined ||
1545
- requestTimeoutMs !== undefined ||
1546
- keepAliveTimeoutMs !== undefined) {
1547
- logDebug('Applied HTTP server tuning', {
1548
- headersTimeoutMs,
1549
- requestTimeoutMs,
1550
- keepAliveTimeoutMs,
1551
- });
1552
- }
1553
- }
1554
- export function drainConnectionsOnShutdown(server) {
1555
- const { shutdownCloseAllConnections, shutdownCloseIdleConnections } = config.server.http;
1556
- if (shutdownCloseAllConnections) {
1557
- if (typeof server.closeAllConnections === 'function') {
1558
- server.closeAllConnections();
1559
- logDebug('Closed all HTTP connections during shutdown');
1560
- }
1561
- else {
1562
- logDebug('HTTP server does not support closeAllConnections()');
1563
- }
1564
- return;
1565
- }
1566
- if (shutdownCloseIdleConnections) {
1567
- if (typeof server.closeIdleConnections === 'function') {
1568
- server.closeIdleConnections();
1569
- logDebug('Closed idle HTTP connections during shutdown');
1570
- }
1571
- else {
1572
- logDebug('HTTP server does not support closeIdleConnections()');
1573
- }
1574
- }
1575
- }
1576
- //# sourceMappingURL=http.js.map