@j0hanz/superfetch 2.5.3 → 2.7.0

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 (46) hide show
  1. package/README.md +350 -226
  2. package/dist/assets/logo.svg +24837 -24835
  3. package/dist/cache.d.ts +28 -20
  4. package/dist/cache.js +292 -512
  5. package/dist/config.d.ts +41 -7
  6. package/dist/config.js +316 -154
  7. package/dist/crypto.js +25 -12
  8. package/dist/dom-noise-removal.js +382 -421
  9. package/dist/errors.d.ts +2 -2
  10. package/dist/errors.js +25 -8
  11. package/dist/fetch.d.ts +19 -16
  12. package/dist/fetch.js +1207 -538
  13. package/dist/host-normalization.js +40 -10
  14. package/dist/http-native.js +641 -283
  15. package/dist/index.js +67 -7
  16. package/dist/instructions.md +44 -31
  17. package/dist/ip-blocklist.d.ts +8 -0
  18. package/dist/ip-blocklist.js +65 -0
  19. package/dist/json.js +14 -9
  20. package/dist/language-detection.d.ts +2 -11
  21. package/dist/language-detection.js +313 -280
  22. package/dist/markdown-cleanup.d.ts +0 -1
  23. package/dist/markdown-cleanup.js +391 -429
  24. package/dist/mcp-validator.js +4 -2
  25. package/dist/mcp.js +191 -136
  26. package/dist/observability.js +89 -21
  27. package/dist/resources.js +16 -6
  28. package/dist/server-tuning.d.ts +2 -0
  29. package/dist/server-tuning.js +25 -23
  30. package/dist/session.d.ts +1 -0
  31. package/dist/session.js +41 -33
  32. package/dist/tasks.d.ts +3 -0
  33. package/dist/tasks.js +167 -51
  34. package/dist/timer-utils.d.ts +5 -0
  35. package/dist/timer-utils.js +20 -0
  36. package/dist/tools.d.ts +30 -5
  37. package/dist/tools.js +319 -184
  38. package/dist/transform-types.d.ts +6 -1
  39. package/dist/transform.d.ts +3 -2
  40. package/dist/transform.js +1243 -421
  41. package/dist/type-guards.d.ts +1 -0
  42. package/dist/type-guards.js +7 -0
  43. package/dist/workers/transform-child.d.ts +1 -0
  44. package/dist/workers/transform-child.js +118 -0
  45. package/dist/workers/transform-worker.js +87 -78
  46. package/package.json +14 -6
@@ -1,28 +1,33 @@
1
+ import { Buffer } from 'node:buffer';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { createServer, } from 'node:http';
4
+ import { isIP } from 'node:net';
5
+ import { freemem, hostname, totalmem } from 'node:os';
6
+ import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
7
+ import process from 'node:process';
8
+ import { Writable } from 'node:stream';
9
+ import { pipeline } from 'node:stream/promises';
3
10
  import { setInterval as setIntervalPromise } from 'node:timers/promises';
4
- import { URL, URLSearchParams } from 'node:url';
5
11
  import { InvalidTokenError, ServerError, } from '@modelcontextprotocol/sdk/server/auth/errors.js';
6
12
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
7
13
  import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
8
14
  import { keys as cacheKeys, handleDownload } from './cache.js';
9
15
  import { config, enableHttpMode, serverVersion } from './config.js';
10
- import { timingSafeEqualUtf8 } from './crypto.js';
16
+ import { sha256Hex, timingSafeEqualUtf8 } from './crypto.js';
11
17
  import { normalizeHost } from './host-normalization.js';
18
+ import { createDefaultBlockList, normalizeIpForBlockList, } from './ip-blocklist.js';
12
19
  import { acceptsEventStream, isJsonRpcBatchRequest, isMcpRequestBody, } from './mcp-validator.js';
13
20
  import { createMcpServer } from './mcp.js';
14
- import { logError, logInfo, logWarn } from './observability.js';
21
+ import { logError, logInfo, logWarn, runWithRequestContext, } from './observability.js';
15
22
  import { applyHttpServerTuning, drainConnectionsOnShutdown, } from './server-tuning.js';
16
23
  import { composeCloseHandlers, createSessionStore, createSlotTracker, ensureSessionCapacity, reserveSessionSlot, startSessionCleanupLoop, } from './session.js';
17
24
  import { getTransformPoolStats } from './transform.js';
18
25
  import { isObject } from './type-guards.js';
19
- /* -------------------------------------------------------------------------------------------------
20
- * Transport adaptation
21
- * ------------------------------------------------------------------------------------------------- */
22
26
  function createTransportAdapter(transportImpl) {
23
27
  const noopOnClose = () => { };
24
28
  const noopOnError = () => { };
25
29
  const noopOnMessage = () => { };
30
+ const baseOnClose = transportImpl.onclose;
26
31
  let oncloseHandler = noopOnClose;
27
32
  let onerrorHandler = noopOnError;
28
33
  let onmessageHandler = noopOnMessage;
@@ -35,7 +40,7 @@ function createTransportAdapter(transportImpl) {
35
40
  },
36
41
  set onclose(handler) {
37
42
  oncloseHandler = handler;
38
- transportImpl.onclose = handler;
43
+ transportImpl.onclose = composeCloseHandlers(baseOnClose, handler);
39
44
  },
40
45
  get onerror() {
41
46
  return onerrorHandler;
@@ -53,111 +58,313 @@ function createTransportAdapter(transportImpl) {
53
58
  },
54
59
  };
55
60
  }
56
- function shimResponse(res) {
57
- const shim = res;
58
- shim.status = function (code) {
59
- this.statusCode = code;
60
- return this;
61
+ function sendJson(res, status, body) {
62
+ res.statusCode = status;
63
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
64
+ res.setHeader('X-Content-Type-Options', 'nosniff');
65
+ res.setHeader('Cache-Control', 'no-store');
66
+ res.end(JSON.stringify(body));
67
+ }
68
+ function sendText(res, status, body) {
69
+ res.statusCode = status;
70
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
71
+ res.setHeader('X-Content-Type-Options', 'nosniff');
72
+ res.setHeader('Cache-Control', 'no-store');
73
+ res.end(body);
74
+ }
75
+ function sendEmpty(res, status) {
76
+ res.statusCode = status;
77
+ res.setHeader('Content-Length', '0');
78
+ res.end();
79
+ }
80
+ function drainRequest(req) {
81
+ if (req.readableEnded)
82
+ return;
83
+ try {
84
+ req.resume();
85
+ }
86
+ catch {
87
+ // Best-effort only.
88
+ }
89
+ }
90
+ function createRequestAbortSignal(req) {
91
+ const controller = new AbortController();
92
+ let cleanedUp = false;
93
+ const abortRequest = () => {
94
+ if (cleanedUp)
95
+ return;
96
+ if (!controller.signal.aborted)
97
+ controller.abort();
61
98
  };
62
- shim.json = function (body) {
63
- this.setHeader('Content-Type', 'application/json');
64
- this.end(JSON.stringify(body));
65
- return this;
99
+ if (req.destroyed) {
100
+ abortRequest();
101
+ return {
102
+ signal: controller.signal,
103
+ cleanup: () => {
104
+ cleanedUp = true;
105
+ },
106
+ };
107
+ }
108
+ const onAborted = () => {
109
+ abortRequest();
66
110
  };
67
- shim.send = function (body) {
68
- this.end(body);
69
- return this;
111
+ const onClose = () => {
112
+ abortRequest();
70
113
  };
71
- shim.sendStatus = function (code) {
72
- this.statusCode = code;
73
- this.end();
74
- return this;
114
+ const onError = () => {
115
+ abortRequest();
75
116
  };
76
- return shim;
117
+ req.once('aborted', onAborted);
118
+ req.once('close', onClose);
119
+ req.once('error', onError);
120
+ return {
121
+ signal: controller.signal,
122
+ cleanup: () => {
123
+ cleanedUp = true;
124
+ req.removeListener('aborted', onAborted);
125
+ req.removeListener('close', onClose);
126
+ req.removeListener('error', onError);
127
+ },
128
+ };
129
+ }
130
+ function normalizeRemoteAddress(address) {
131
+ if (!address)
132
+ return null;
133
+ const trimmed = address.trim();
134
+ if (!trimmed)
135
+ return null;
136
+ const zoneIndex = trimmed.indexOf('%');
137
+ const withoutZone = zoneIndex > 0 ? trimmed.slice(0, zoneIndex) : trimmed;
138
+ const normalized = withoutZone.toLowerCase();
139
+ if (normalized.startsWith('::ffff:')) {
140
+ const mapped = normalized.slice('::ffff:'.length);
141
+ if (isIP(mapped) === 4)
142
+ return mapped;
143
+ }
144
+ if (isIP(normalized))
145
+ return normalized;
146
+ return trimmed;
77
147
  }
78
- /* -------------------------------------------------------------------------------------------------
79
- * Request parsing helpers
80
- * ------------------------------------------------------------------------------------------------- */
148
+ function registerInboundBlockList(server) {
149
+ if (!config.server.http.blockPrivateConnections)
150
+ return;
151
+ const blockList = createDefaultBlockList();
152
+ server.on('connection', (socket) => {
153
+ const remoteAddress = normalizeRemoteAddress(socket.remoteAddress);
154
+ if (!remoteAddress)
155
+ return;
156
+ const normalized = normalizeIpForBlockList(remoteAddress);
157
+ if (!normalized)
158
+ return;
159
+ if (blockList.check(normalized.ip, normalized.family)) {
160
+ logWarn('Blocked inbound connection', {
161
+ remoteAddress: normalized.ip,
162
+ family: normalized.family,
163
+ });
164
+ socket.destroy();
165
+ }
166
+ });
167
+ }
168
+ function getHeaderValue(req, name) {
169
+ const val = req.headers[name];
170
+ if (!val)
171
+ return null;
172
+ if (Array.isArray(val))
173
+ return val[0] ?? null;
174
+ return val;
175
+ }
176
+ function getMcpSessionId(req) {
177
+ return (getHeaderValue(req, 'mcp-session-id') ??
178
+ getHeaderValue(req, 'x-mcp-session-id'));
179
+ }
180
+ function buildRequestContext(req, res, signal) {
181
+ let url;
182
+ try {
183
+ url = new URL(req.url ?? '', 'http://localhost');
184
+ }
185
+ catch {
186
+ sendJson(res, 400, { error: 'Invalid request URL' });
187
+ return null;
188
+ }
189
+ return {
190
+ req,
191
+ res,
192
+ url,
193
+ method: req.method,
194
+ ip: normalizeRemoteAddress(req.socket.remoteAddress),
195
+ body: undefined,
196
+ ...(signal ? { signal } : {}),
197
+ };
198
+ }
199
+ async function closeTransportBestEffort(transport, context) {
200
+ try {
201
+ await transport.close();
202
+ }
203
+ catch (error) {
204
+ logWarn('Transport close failed', { context, error });
205
+ }
206
+ }
207
+ class JsonBodyError extends Error {
208
+ kind;
209
+ constructor(kind, message) {
210
+ super(message);
211
+ this.name = 'JsonBodyError';
212
+ this.kind = kind;
213
+ }
214
+ }
215
+ const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
81
216
  class JsonBodyReader {
82
- async read(req, limit = 1024 * 1024) {
83
- const contentType = req.headers['content-type'];
217
+ async read(req, limit = DEFAULT_BODY_LIMIT_BYTES, signal) {
218
+ const contentType = getHeaderValue(req, 'content-type');
84
219
  if (!contentType?.includes('application/json'))
85
220
  return undefined;
86
- return new Promise((resolve, reject) => {
87
- let size = 0;
88
- const chunks = [];
89
- req.on('data', (chunk) => {
90
- size += chunk.length;
91
- if (size > limit) {
221
+ const contentLengthHeader = getHeaderValue(req, 'content-length');
222
+ if (contentLengthHeader) {
223
+ const contentLength = Number.parseInt(contentLengthHeader, 10);
224
+ if (Number.isFinite(contentLength) && contentLength > limit) {
225
+ try {
92
226
  req.destroy();
93
- reject(new Error('Payload too large'));
94
- return;
95
227
  }
96
- chunks.push(chunk);
97
- });
98
- req.on('end', () => {
228
+ catch {
229
+ // Best-effort only.
230
+ }
231
+ throw new JsonBodyError('payload-too-large', 'Payload too large');
232
+ }
233
+ }
234
+ if (signal?.aborted || req.destroyed) {
235
+ throw new JsonBodyError('read-failed', 'Request aborted');
236
+ }
237
+ const body = await this.readBody(req, limit, signal);
238
+ if (!body)
239
+ return undefined;
240
+ try {
241
+ return JSON.parse(body);
242
+ }
243
+ catch (err) {
244
+ throw new JsonBodyError('invalid-json', err instanceof Error ? err.message : String(err));
245
+ }
246
+ }
247
+ async readBody(req, limit, signal) {
248
+ const abortListener = this.attachAbortListener(req, signal);
249
+ try {
250
+ const { chunks, size } = await this.collectChunks(req, limit, signal);
251
+ if (chunks.length === 0)
252
+ return undefined;
253
+ return Buffer.concat(chunks, size).toString('utf8');
254
+ }
255
+ finally {
256
+ this.detachAbortListener(signal, abortListener);
257
+ }
258
+ }
259
+ attachAbortListener(req, signal) {
260
+ if (!signal)
261
+ return null;
262
+ const listener = () => {
263
+ try {
264
+ req.destroy();
265
+ }
266
+ catch {
267
+ // Best-effort only.
268
+ }
269
+ };
270
+ if (signal.aborted) {
271
+ listener();
272
+ }
273
+ else {
274
+ signal.addEventListener('abort', listener, { once: true });
275
+ }
276
+ return listener;
277
+ }
278
+ detachAbortListener(signal, listener) {
279
+ if (!signal || !listener)
280
+ return;
281
+ try {
282
+ signal.removeEventListener('abort', listener);
283
+ }
284
+ catch {
285
+ // Best-effort cleanup.
286
+ }
287
+ }
288
+ async collectChunks(req, limit, signal) {
289
+ let size = 0;
290
+ const chunks = [];
291
+ const sink = new Writable({
292
+ write: (chunk, _encoding, callback) => {
99
293
  try {
100
- const body = Buffer.concat(chunks).toString();
101
- if (!body) {
102
- resolve(undefined);
294
+ if (signal?.aborted || req.destroyed) {
295
+ callback(new JsonBodyError('read-failed', 'Request aborted'));
296
+ return;
297
+ }
298
+ const buf = this.normalizeChunk(chunk);
299
+ size += buf.length;
300
+ if (size > limit) {
301
+ req.destroy();
302
+ callback(new JsonBodyError('payload-too-large', 'Payload too large'));
103
303
  return;
104
304
  }
105
- resolve(JSON.parse(body));
305
+ chunks.push(buf);
306
+ callback();
106
307
  }
107
308
  catch (err) {
108
- reject(err instanceof Error ? err : new Error(String(err)));
309
+ callback(err instanceof Error ? err : new Error(String(err)));
109
310
  }
110
- });
111
- req.on('error', (err) => {
112
- reject(err instanceof Error ? err : new Error(String(err)));
113
- });
311
+ },
114
312
  });
313
+ try {
314
+ if (signal?.aborted || req.destroyed) {
315
+ throw new JsonBodyError('read-failed', 'Request aborted');
316
+ }
317
+ await pipeline(req, sink, signal ? { signal } : undefined);
318
+ return { chunks, size };
319
+ }
320
+ catch (err) {
321
+ if (err instanceof JsonBodyError)
322
+ throw err;
323
+ if (signal?.aborted || req.destroyed) {
324
+ throw new JsonBodyError('read-failed', 'Request aborted');
325
+ }
326
+ throw new JsonBodyError('read-failed', err instanceof Error ? err.message : String(err));
327
+ }
328
+ }
329
+ normalizeChunk(chunk) {
330
+ if (Buffer.isBuffer(chunk))
331
+ return chunk;
332
+ if (typeof chunk === 'string')
333
+ return Buffer.from(chunk, 'utf8');
334
+ return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
115
335
  }
116
336
  }
117
337
  const jsonBodyReader = new JsonBodyReader();
118
- function parseQuery(url) {
119
- const query = {};
120
- for (const [key, value] of url.searchParams) {
121
- const existing = query[key];
122
- if (existing) {
123
- if (Array.isArray(existing))
124
- existing.push(value);
125
- else
126
- query[key] = [existing, value];
338
+ class CorsPolicy {
339
+ handle(ctx) {
340
+ const { req, res } = ctx;
341
+ const origin = getHeaderValue(req, 'origin');
342
+ if (origin) {
343
+ res.setHeader('Access-Control-Allow-Origin', origin);
344
+ res.setHeader('Vary', 'Origin');
127
345
  }
128
346
  else {
129
- query[key] = value;
347
+ res.setHeader('Access-Control-Allow-Origin', '*');
130
348
  }
131
- }
132
- return query;
133
- }
134
- function getHeaderValue(req, name) {
135
- const val = req.headers[name.toLowerCase()];
136
- if (!val)
137
- return null;
138
- if (Array.isArray(val))
139
- return val[0] ?? null;
140
- return val;
141
- }
142
- /* -------------------------------------------------------------------------------------------------
143
- * CORS & Host/Origin policy
144
- * ------------------------------------------------------------------------------------------------- */
145
- class CorsPolicy {
146
- handle(req, res) {
147
- res.setHeader('Access-Control-Allow-Origin', '*');
148
349
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE');
149
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, MCP-Protocol-Version, X-MCP-Session-ID');
150
- if (req.method === 'OPTIONS') {
151
- res.writeHead(204);
152
- res.end();
153
- return true;
154
- }
155
- return false;
350
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, MCP-Protocol-Version, MCP-Session-ID, X-MCP-Session-ID, Last-Event-ID');
351
+ if (req.method !== 'OPTIONS')
352
+ return false;
353
+ sendEmpty(res, 204);
354
+ return true;
156
355
  }
157
356
  }
158
357
  const corsPolicy = new CorsPolicy();
159
358
  const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
160
359
  const WILDCARD_HOSTS = new Set(['0.0.0.0', '::']);
360
+ function hasConstantTimeMatch(candidates, input) {
361
+ // Avoid leaking match index via early-return.
362
+ let matched = 0;
363
+ for (const candidate of candidates) {
364
+ matched |= timingSafeEqualUtf8(candidate, input) ? 1 : 0;
365
+ }
366
+ return matched === 1;
367
+ }
161
368
  function isWildcardHost(host) {
162
369
  return WILDCARD_HOSTS.has(host);
163
370
  }
@@ -176,20 +383,21 @@ function buildAllowedHosts() {
176
383
  }
177
384
  const ALLOWED_HOSTS = buildAllowedHosts();
178
385
  class HostOriginPolicy {
179
- validate(req, res) {
386
+ validate(ctx) {
387
+ const { req, res } = ctx;
180
388
  const host = this.resolveHostHeader(req);
181
389
  if (!host)
182
390
  return this.reject(res, 400, 'Missing or invalid Host header');
183
391
  if (!ALLOWED_HOSTS.has(host))
184
392
  return this.reject(res, 403, 'Host not allowed');
185
393
  const originHeader = getHeaderValue(req, 'origin');
186
- if (originHeader) {
187
- const originHost = this.resolveOriginHost(originHeader);
188
- if (!originHost)
189
- return this.reject(res, 403, 'Invalid Origin header');
190
- if (!ALLOWED_HOSTS.has(originHost))
191
- return this.reject(res, 403, 'Origin not allowed');
192
- }
394
+ if (!originHeader)
395
+ return true;
396
+ const originHost = this.resolveOriginHost(originHeader);
397
+ if (!originHost)
398
+ return this.reject(res, 403, 'Invalid Origin header');
399
+ if (!ALLOWED_HOSTS.has(originHost))
400
+ return this.reject(res, 403, 'Origin not allowed');
193
401
  return true;
194
402
  }
195
403
  resolveHostHeader(req) {
@@ -210,14 +418,11 @@ class HostOriginPolicy {
210
418
  }
211
419
  }
212
420
  reject(res, status, message) {
213
- res.status(status).json({ error: message });
421
+ sendJson(res, status, { error: message });
214
422
  return false;
215
423
  }
216
424
  }
217
425
  const hostOriginPolicy = new HostOriginPolicy();
218
- /* -------------------------------------------------------------------------------------------------
219
- * HTTP mode configuration assertions
220
- * ------------------------------------------------------------------------------------------------- */
221
426
  function assertHttpModeConfiguration() {
222
427
  const configuredHost = normalizeHost(config.server.host);
223
428
  const isLoopback = configuredHost !== null && LOOPBACK_HOSTS.has(configuredHost);
@@ -248,12 +453,7 @@ class RateLimiter {
248
453
  void (async () => {
249
454
  try {
250
455
  for await (const getNow of interval) {
251
- const now = getNow();
252
- for (const [key, entry] of this.store.entries()) {
253
- if (now - entry.lastAccessed > this.options.windowMs * 2) {
254
- this.store.delete(key);
255
- }
256
- }
456
+ this.cleanupEntries(getNow());
257
457
  }
258
458
  }
259
459
  catch (err) {
@@ -263,13 +463,32 @@ class RateLimiter {
263
463
  }
264
464
  })();
265
465
  }
266
- check(req, res) {
267
- if (!this.options.enabled || req.method === 'OPTIONS')
466
+ cleanupEntries(now) {
467
+ const maxIdle = this.options.windowMs * 2;
468
+ for (const [key, entry] of this.store.entries()) {
469
+ if (now - entry.lastAccessed > maxIdle) {
470
+ this.store.delete(key);
471
+ }
472
+ }
473
+ }
474
+ check(ctx) {
475
+ if (!this.options.enabled || ctx.method === 'OPTIONS')
268
476
  return true;
269
- const key = req.ip ?? 'unknown';
477
+ const key = ctx.ip ?? 'unknown';
270
478
  const now = Date.now();
271
479
  let entry = this.store.get(key);
272
- if (!entry || now > entry.resetTime) {
480
+ if (entry) {
481
+ if (now > entry.resetTime) {
482
+ entry.count = 1;
483
+ entry.resetTime = now + this.options.windowMs;
484
+ entry.lastAccessed = now;
485
+ }
486
+ else {
487
+ entry.count += 1;
488
+ entry.lastAccessed = now;
489
+ }
490
+ }
491
+ else {
273
492
  entry = {
274
493
  count: 1,
275
494
  resetTime: now + this.options.windowMs,
@@ -277,14 +496,10 @@ class RateLimiter {
277
496
  };
278
497
  this.store.set(key, entry);
279
498
  }
280
- else {
281
- entry.count += 1;
282
- entry.lastAccessed = now;
283
- }
284
499
  if (entry.count > this.options.maxRequests) {
285
500
  const retryAfter = Math.max(1, Math.ceil((entry.resetTime - now) / 1000));
286
- res.setHeader('Retry-After', String(retryAfter));
287
- res.status(429).json({ error: 'Rate limit exceeded', retryAfter });
501
+ ctx.res.setHeader('Retry-After', String(retryAfter));
502
+ sendJson(ctx.res, 429, { error: 'Rate limit exceeded', retryAfter });
288
503
  return false;
289
504
  }
290
505
  return true;
@@ -296,22 +511,20 @@ class RateLimiter {
296
511
  function createRateLimitManagerImpl(options) {
297
512
  return new RateLimiter(options);
298
513
  }
299
- /* -------------------------------------------------------------------------------------------------
300
- * Auth (static + OAuth introspection)
301
- * ------------------------------------------------------------------------------------------------- */
302
514
  const STATIC_TOKEN_TTL_SECONDS = 60 * 60 * 24;
303
515
  class AuthService {
304
- async authenticate(req) {
305
- const authHeader = req.headers.authorization;
516
+ staticTokenDigests = config.auth.staticTokens.map((token) => sha256Hex(token));
517
+ async authenticate(req, signal) {
518
+ const authHeader = getHeaderValue(req, 'authorization');
306
519
  if (!authHeader) {
307
520
  return this.authenticateWithApiKey(req);
308
521
  }
309
522
  const token = this.resolveBearerToken(authHeader);
310
- return this.authenticateWithToken(token);
523
+ return this.authenticateWithToken(token, signal);
311
524
  }
312
- authenticateWithToken(token) {
525
+ authenticateWithToken(token, signal) {
313
526
  return config.auth.mode === 'oauth'
314
- ? this.verifyWithIntrospection(token)
527
+ ? this.verifyWithIntrospection(token, signal)
315
528
  : Promise.resolve(this.verifyStaticToken(token));
316
529
  }
317
530
  authenticateWithApiKey(req) {
@@ -325,8 +538,11 @@ class AuthService {
325
538
  throw new InvalidTokenError('Missing Authorization header');
326
539
  }
327
540
  resolveBearerToken(authHeader) {
328
- const [type, token] = authHeader.split(' ');
329
- if (type !== 'Bearer' || !token) {
541
+ if (!authHeader.startsWith('Bearer ')) {
542
+ throw new InvalidTokenError('Invalid Authorization header format');
543
+ }
544
+ const token = authHeader.substring(7);
545
+ if (!token) {
330
546
  throw new InvalidTokenError('Invalid Authorization header format');
331
547
  }
332
548
  return token;
@@ -341,10 +557,11 @@ class AuthService {
341
557
  };
342
558
  }
343
559
  verifyStaticToken(token) {
344
- if (config.auth.staticTokens.length === 0) {
560
+ if (this.staticTokenDigests.length === 0) {
345
561
  throw new InvalidTokenError('No static tokens configured');
346
562
  }
347
- const matched = config.auth.staticTokens.some((candidate) => timingSafeEqualUtf8(candidate, token));
563
+ const tokenDigest = sha256Hex(token);
564
+ const matched = hasConstantTimeMatch(this.staticTokenDigests, tokenDigest);
348
565
  if (!matched)
349
566
  throw new InvalidTokenError('Invalid token');
350
567
  return this.buildStaticAuthInfo(token);
@@ -355,8 +572,9 @@ class AuthService {
355
572
  return clean.href;
356
573
  }
357
574
  buildBasicAuthHeader(clientId, clientSecret) {
575
+ // Base64 is only an encoding for header transport; it is NOT encryption.
358
576
  const credentials = `${clientId}:${clientSecret ?? ''}`;
359
- return `Basic ${Buffer.from(credentials).toString('base64')}`;
577
+ return `Basic ${Buffer.from(credentials, 'utf8').toString('base64')}`;
360
578
  }
361
579
  buildIntrospectionRequest(token, resourceUrl, clientId, clientSecret) {
362
580
  const body = new URLSearchParams({
@@ -367,19 +585,26 @@ class AuthService {
367
585
  const headers = {
368
586
  'content-type': 'application/x-www-form-urlencoded',
369
587
  };
370
- if (clientId)
588
+ if (clientId) {
371
589
  headers.authorization = this.buildBasicAuthHeader(clientId, clientSecret);
590
+ }
372
591
  return { body, headers };
373
592
  }
374
- async requestIntrospection(url, request, timeoutMs) {
593
+ async requestIntrospection(url, request, timeoutMs, signal) {
594
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
595
+ const combinedSignal = signal
596
+ ? AbortSignal.any([signal, timeoutSignal])
597
+ : timeoutSignal;
375
598
  const response = await fetch(url, {
376
599
  method: 'POST',
377
600
  headers: request.headers,
378
601
  body: request.body,
379
- signal: AbortSignal.timeout(timeoutMs),
602
+ signal: combinedSignal,
380
603
  });
381
604
  if (!response.ok) {
382
- await response.body?.cancel();
605
+ if (response.body) {
606
+ await response.body.cancel();
607
+ }
383
608
  throw new ServerError(`Token introspection failed: ${response.status}`);
384
609
  }
385
610
  return response.json();
@@ -397,12 +622,12 @@ class AuthService {
397
622
  info.expiresAt = expiresAt;
398
623
  return info;
399
624
  }
400
- async verifyWithIntrospection(token) {
625
+ async verifyWithIntrospection(token, signal) {
401
626
  if (!config.auth.introspectionUrl) {
402
627
  throw new ServerError('Introspection not configured');
403
628
  }
404
629
  const req = this.buildIntrospectionRequest(token, config.auth.resourceUrl, config.auth.clientId, config.auth.clientSecret);
405
- const payload = await this.requestIntrospection(config.auth.introspectionUrl, req, config.auth.introspectionTimeoutMs);
630
+ const payload = await this.requestIntrospection(config.auth.introspectionUrl, req, config.auth.introspectionTimeoutMs, signal);
406
631
  if (!isObject(payload) || payload.active !== true) {
407
632
  throw new InvalidTokenError('Token is inactive');
408
633
  }
@@ -410,11 +635,47 @@ class AuthService {
410
635
  }
411
636
  }
412
637
  const authService = new AuthService();
413
- /* -------------------------------------------------------------------------------------------------
414
- * MCP routing + session gateway
415
- * ------------------------------------------------------------------------------------------------- */
638
+ const EVENT_LOOP_DELAY_RESOLUTION_MS = 20;
639
+ const eventLoopDelay = monitorEventLoopDelay({
640
+ resolution: EVENT_LOOP_DELAY_RESOLUTION_MS,
641
+ });
642
+ let lastEventLoopUtilization = performance.eventLoopUtilization();
643
+ function roundTo(value, precision) {
644
+ const factor = 10 ** precision;
645
+ return Math.round(value * factor) / factor;
646
+ }
647
+ function formatEventLoopUtilization(snapshot) {
648
+ return {
649
+ utilization: roundTo(snapshot.utilization, 4),
650
+ activeMs: Math.round(snapshot.active),
651
+ idleMs: Math.round(snapshot.idle),
652
+ };
653
+ }
654
+ function toMs(valueNs) {
655
+ return roundTo(valueNs / 1_000_000, 3);
656
+ }
657
+ function getEventLoopStats() {
658
+ const current = performance.eventLoopUtilization();
659
+ const delta = performance.eventLoopUtilization(current, lastEventLoopUtilization);
660
+ lastEventLoopUtilization = current;
661
+ return {
662
+ utilization: {
663
+ total: formatEventLoopUtilization(current),
664
+ sinceLast: formatEventLoopUtilization(delta),
665
+ },
666
+ delay: {
667
+ minMs: toMs(eventLoopDelay.min),
668
+ maxMs: toMs(eventLoopDelay.max),
669
+ meanMs: toMs(eventLoopDelay.mean),
670
+ stddevMs: toMs(eventLoopDelay.stddev),
671
+ p50Ms: toMs(eventLoopDelay.percentile(50)),
672
+ p95Ms: toMs(eventLoopDelay.percentile(95)),
673
+ p99Ms: toMs(eventLoopDelay.percentile(99)),
674
+ },
675
+ };
676
+ }
416
677
  function sendError(res, code, message, status = 400, id = null) {
417
- res.status(status).json({
678
+ sendJson(res, status, {
418
679
  jsonrpc: '2.0',
419
680
  error: { code, message },
420
681
  id,
@@ -433,6 +694,15 @@ function ensureMcpProtocolVersion(req, res) {
433
694
  }
434
695
  return true;
435
696
  }
697
+ function buildAuthFingerprint(auth) {
698
+ if (!auth)
699
+ return null;
700
+ const safeClientId = typeof auth.clientId === 'string' ? auth.clientId : '';
701
+ const safeToken = typeof auth.token === 'string' ? auth.token : '';
702
+ if (!safeClientId && !safeToken)
703
+ return null;
704
+ return sha256Hex(`${safeClientId}:${safeToken}`);
705
+ }
436
706
  class McpSessionGateway {
437
707
  store;
438
708
  mcpServer;
@@ -440,56 +710,56 @@ class McpSessionGateway {
440
710
  this.store = store;
441
711
  this.mcpServer = mcpServer;
442
712
  }
443
- async handlePost(req, res) {
444
- if (!ensureMcpProtocolVersion(req, res))
713
+ async handlePost(ctx) {
714
+ if (!ensureMcpProtocolVersion(ctx.req, ctx.res))
445
715
  return;
446
- const { body } = req;
716
+ const { body } = ctx;
447
717
  if (isJsonRpcBatchRequest(body)) {
448
- sendError(res, -32600, 'Batch requests not supported');
718
+ sendError(ctx.res, -32600, 'Batch requests not supported');
449
719
  return;
450
720
  }
451
721
  if (!isMcpRequestBody(body)) {
452
- sendError(res, -32600, 'Invalid request body');
722
+ sendError(ctx.res, -32600, 'Invalid request body');
453
723
  return;
454
724
  }
455
725
  const requestId = body.id ?? null;
456
726
  logInfo('[MCP POST]', {
457
727
  method: body.method,
458
728
  id: body.id,
459
- sessionId: getHeaderValue(req, 'mcp-session-id'),
729
+ sessionId: getMcpSessionId(ctx.req),
460
730
  });
461
- const transport = await this.getOrCreateTransport(req, res, requestId);
731
+ const transport = await this.getOrCreateTransport(ctx, requestId);
462
732
  if (!transport)
463
733
  return;
464
- await transport.handleRequest(req, res, body);
734
+ await transport.handleRequest(ctx.req, ctx.res, body);
465
735
  }
466
- async handleGet(req, res) {
467
- if (!ensureMcpProtocolVersion(req, res))
736
+ async handleGet(ctx) {
737
+ if (!ensureMcpProtocolVersion(ctx.req, ctx.res))
468
738
  return;
469
- const sessionId = getHeaderValue(req, 'mcp-session-id');
739
+ const sessionId = getMcpSessionId(ctx.req);
470
740
  if (!sessionId) {
471
- sendError(res, -32600, 'Missing session ID');
741
+ sendError(ctx.res, -32600, 'Missing session ID');
472
742
  return;
473
743
  }
474
744
  const session = this.store.get(sessionId);
475
745
  if (!session) {
476
- sendError(res, -32600, 'Session not found', 404);
746
+ sendError(ctx.res, -32600, 'Session not found', 404);
477
747
  return;
478
748
  }
479
- const acceptHeader = getHeaderValue(req, 'accept');
749
+ const acceptHeader = getHeaderValue(ctx.req, 'accept');
480
750
  if (!acceptsEventStream(acceptHeader)) {
481
- res.status(405).json({ error: 'Method Not Allowed' });
751
+ sendJson(ctx.res, 405, { error: 'Method Not Allowed' });
482
752
  return;
483
753
  }
484
754
  this.store.touch(sessionId);
485
- await session.transport.handleRequest(req, res);
755
+ await session.transport.handleRequest(ctx.req, ctx.res);
486
756
  }
487
- async handleDelete(req, res) {
488
- if (!ensureMcpProtocolVersion(req, res))
757
+ async handleDelete(ctx) {
758
+ if (!ensureMcpProtocolVersion(ctx.req, ctx.res))
489
759
  return;
490
- const sessionId = getHeaderValue(req, 'mcp-session-id');
760
+ const sessionId = getMcpSessionId(ctx.req);
491
761
  if (!sessionId) {
492
- sendError(res, -32600, 'Missing session ID');
762
+ sendError(ctx.res, -32600, 'Missing session ID');
493
763
  return;
494
764
  }
495
765
  const session = this.store.get(sessionId);
@@ -497,46 +767,41 @@ class McpSessionGateway {
497
767
  await session.transport.close();
498
768
  this.store.remove(sessionId);
499
769
  }
500
- res.status(200).send('Session closed');
770
+ sendText(ctx.res, 200, 'Session closed');
501
771
  }
502
- async getOrCreateTransport(req, res, requestId) {
503
- const sessionId = getHeaderValue(req, 'mcp-session-id');
772
+ async getOrCreateTransport(ctx, requestId) {
773
+ const sessionId = getMcpSessionId(ctx.req);
504
774
  if (sessionId) {
505
- const session = this.store.get(sessionId);
506
- if (!session) {
507
- sendError(res, -32600, 'Session not found', 404, requestId);
508
- return null;
509
- }
510
- this.store.touch(sessionId);
511
- return session.transport;
775
+ const fingerprint = buildAuthFingerprint(ctx.auth);
776
+ return this.getExistingTransport(sessionId, fingerprint, ctx.res, requestId);
512
777
  }
513
- if (!isInitializeRequest(req.body)) {
514
- sendError(res, -32600, 'Missing session ID', 400, requestId);
778
+ if (!isInitializeRequest(ctx.body)) {
779
+ sendError(ctx.res, -32600, 'Missing session ID', 400, requestId);
515
780
  return null;
516
781
  }
517
- return this.createNewSession(res, requestId);
782
+ return this.createNewSession(ctx, requestId);
518
783
  }
519
- async createNewSession(res, requestId) {
520
- const allowed = ensureSessionCapacity({
521
- store: this.store,
522
- maxSessions: config.server.maxSessions,
523
- evictOldest: (s) => {
524
- const evicted = s.evictOldest();
525
- if (evicted) {
526
- void evicted.transport.close().catch(() => { });
527
- return true;
528
- }
529
- return false;
530
- },
531
- });
532
- if (!allowed) {
533
- sendError(res, -32000, 'Server busy', 503, requestId);
784
+ getExistingTransport(sessionId, authFingerprint, res, requestId) {
785
+ const session = this.store.get(sessionId);
786
+ if (!session) {
787
+ sendError(res, -32600, 'Session not found', 404, requestId);
534
788
  return null;
535
789
  }
536
- if (!reserveSessionSlot(this.store, config.server.maxSessions)) {
537
- sendError(res, -32000, 'Server busy', 503, requestId);
790
+ if (!authFingerprint || session.authFingerprint !== authFingerprint) {
791
+ sendError(res, -32600, 'Session not found', 404, requestId);
538
792
  return null;
539
793
  }
794
+ this.store.touch(sessionId);
795
+ return session.transport;
796
+ }
797
+ async createNewSession(ctx, requestId) {
798
+ const authFingerprint = buildAuthFingerprint(ctx.auth);
799
+ if (!authFingerprint) {
800
+ sendError(ctx.res, -32603, 'Missing auth context', 500, requestId);
801
+ return null;
802
+ }
803
+ if (!this.reserveCapacity(ctx.res, requestId))
804
+ return null;
540
805
  const tracker = createSlotTracker(this.store);
541
806
  const transportImpl = new StreamableHTTPServerTransport({
542
807
  sessionIdGenerator: () => randomUUID(),
@@ -544,9 +809,10 @@ class McpSessionGateway {
544
809
  const initTimeout = setTimeout(() => {
545
810
  if (!tracker.isInitialized()) {
546
811
  tracker.releaseSlot();
547
- void transportImpl.close().catch(() => { });
812
+ void closeTransportBestEffort(transportImpl, 'session-init-timeout');
548
813
  }
549
814
  }, config.server.sessionInitTimeoutMs);
815
+ initTimeout.unref();
550
816
  transportImpl.onclose = () => {
551
817
  clearTimeout(initTimeout);
552
818
  if (!tracker.isInitialized())
@@ -559,7 +825,7 @@ class McpSessionGateway {
559
825
  catch (err) {
560
826
  clearTimeout(initTimeout);
561
827
  tracker.releaseSlot();
562
- void transportImpl.close().catch(() => { });
828
+ void closeTransportBestEffort(transportImpl, 'session-connect-failed');
563
829
  throw err;
564
830
  }
565
831
  const newSessionId = transportImpl.sessionId;
@@ -573,16 +839,37 @@ class McpSessionGateway {
573
839
  createdAt: Date.now(),
574
840
  lastSeen: Date.now(),
575
841
  protocolInitialized: false,
842
+ authFingerprint,
576
843
  });
577
844
  transportImpl.onclose = composeCloseHandlers(transportImpl.onclose, () => {
578
845
  this.store.remove(newSessionId);
579
846
  });
580
847
  return transportImpl;
581
848
  }
849
+ reserveCapacity(res, requestId) {
850
+ const allowed = ensureSessionCapacity({
851
+ store: this.store,
852
+ maxSessions: config.server.maxSessions,
853
+ evictOldest: (store) => {
854
+ const evicted = store.evictOldest();
855
+ if (evicted) {
856
+ void closeTransportBestEffort(evicted.transport, 'session-eviction');
857
+ return true;
858
+ }
859
+ return false;
860
+ },
861
+ });
862
+ if (!allowed) {
863
+ sendError(res, -32000, 'Server busy', 503, requestId);
864
+ return false;
865
+ }
866
+ if (!reserveSessionSlot(this.store, config.server.maxSessions)) {
867
+ sendError(res, -32000, 'Server busy', 503, requestId);
868
+ return false;
869
+ }
870
+ return true;
871
+ }
582
872
  }
583
- /* -------------------------------------------------------------------------------------------------
584
- * Downloads + dispatcher
585
- * ------------------------------------------------------------------------------------------------- */
586
873
  function checkDownloadRoute(path) {
587
874
  const downloadMatch = /^\/mcp\/downloads\/([^/]+)\/([^/]+)$/.exec(path);
588
875
  if (!downloadMatch)
@@ -600,48 +887,61 @@ class HttpDispatcher {
600
887
  this.store = store;
601
888
  this.mcpGateway = mcpGateway;
602
889
  }
603
- async dispatch(req, res, url) {
604
- const { pathname: path } = url;
605
- const { method } = req;
890
+ async dispatch(ctx) {
606
891
  try {
607
- // 1) Health endpoint bypasses auth (preserve existing behavior)
608
- if (method === 'GET' && path === '/health') {
609
- this.handleHealthCheck(res);
892
+ if (ctx.method === 'GET' && ctx.url.pathname === '/health') {
893
+ this.handleHealthCheck(ctx.res);
610
894
  return;
611
895
  }
612
- // 2) Auth required for everything else (preserve existing behavior)
613
- if (!(await this.authenticateRequest(req, res)))
896
+ const auth = await this.authenticateRequest(ctx);
897
+ if (!auth)
614
898
  return;
615
- // 3) Downloads
616
- if (method === 'GET') {
617
- const download = checkDownloadRoute(path);
899
+ const authCtx = { ...ctx, auth };
900
+ if (ctx.method === 'GET') {
901
+ const download = checkDownloadRoute(ctx.url.pathname);
618
902
  if (download) {
619
- handleDownload(res, download.namespace, download.hash);
903
+ handleDownload(ctx.res, download.namespace, download.hash);
620
904
  return;
621
905
  }
622
906
  }
623
- // 4) MCP routes
624
- if (path === '/mcp') {
625
- if (await this.handleMcpRoutes(req, res, method)) {
907
+ if (ctx.url.pathname === '/mcp') {
908
+ const handled = await this.handleMcpRoutes(authCtx);
909
+ if (handled)
626
910
  return;
627
- }
628
911
  }
629
- res.status(404).json({ error: 'Not Found' });
912
+ sendJson(ctx.res, 404, { error: 'Not Found' });
630
913
  }
631
914
  catch (err) {
632
- logError('Request failed', err instanceof Error ? err : new Error(String(err)));
633
- if (!res.writableEnded) {
634
- res.status(500).json({ error: 'Internal Server Error' });
915
+ const error = err instanceof Error ? err : new Error(String(err));
916
+ logError('Request failed', error);
917
+ if (!ctx.res.writableEnded) {
918
+ sendJson(ctx.res, 500, { error: 'Internal Server Error' });
635
919
  }
636
920
  }
637
921
  }
638
922
  handleHealthCheck(res) {
639
923
  const poolStats = getTransformPoolStats();
640
- res.status(200).json({
924
+ res.setHeader('Cache-Control', 'no-store');
925
+ sendJson(res, 200, {
641
926
  status: 'ok',
642
927
  version: serverVersion,
643
928
  uptime: Math.floor(process.uptime()),
644
929
  timestamp: new Date().toISOString(),
930
+ os: {
931
+ hostname: hostname(),
932
+ platform: process.platform,
933
+ arch: process.arch,
934
+ memoryFree: freemem(),
935
+ memoryTotal: totalmem(),
936
+ },
937
+ process: {
938
+ pid: process.pid,
939
+ ppid: process.ppid,
940
+ memory: process.memoryUsage(),
941
+ cpu: process.cpuUsage(),
942
+ resource: process.resourceUsage(),
943
+ },
944
+ perf: getEventLoopStats(),
645
945
  stats: {
646
946
  activeSessions: this.store.size(),
647
947
  cacheKeys: cacheKeys().length,
@@ -653,37 +953,33 @@ class HttpDispatcher {
653
953
  },
654
954
  });
655
955
  }
656
- async handleMcpRoutes(req, res, method) {
657
- if (method === 'POST') {
658
- await this.mcpGateway.handlePost(req, res);
659
- return true;
660
- }
661
- if (method === 'GET') {
662
- await this.mcpGateway.handleGet(req, res);
663
- return true;
664
- }
665
- if (method === 'DELETE') {
666
- await this.mcpGateway.handleDelete(req, res);
667
- return true;
956
+ async handleMcpRoutes(ctx) {
957
+ switch (ctx.method) {
958
+ case 'POST':
959
+ await this.mcpGateway.handlePost(ctx);
960
+ return true;
961
+ case 'GET':
962
+ await this.mcpGateway.handleGet(ctx);
963
+ return true;
964
+ case 'DELETE':
965
+ await this.mcpGateway.handleDelete(ctx);
966
+ return true;
967
+ default:
968
+ return false;
668
969
  }
669
- return false;
670
970
  }
671
- async authenticateRequest(req, res) {
971
+ async authenticateRequest(ctx) {
672
972
  try {
673
- req.auth = await authService.authenticate(req);
674
- return true;
973
+ return await authService.authenticate(ctx.req, ctx.signal);
675
974
  }
676
975
  catch (err) {
677
- res.status(401).json({
976
+ sendJson(ctx.res, 401, {
678
977
  error: err instanceof Error ? err.message : 'Unauthorized',
679
978
  });
680
- return false;
979
+ return null;
681
980
  }
682
981
  }
683
982
  }
684
- /* -------------------------------------------------------------------------------------------------
685
- * Request pipeline (order is part of behavior)
686
- * ------------------------------------------------------------------------------------------------- */
687
983
  class HttpRequestPipeline {
688
984
  rateLimiter;
689
985
  dispatcher;
@@ -692,40 +988,110 @@ class HttpRequestPipeline {
692
988
  this.dispatcher = dispatcher;
693
989
  }
694
990
  async handle(rawReq, rawRes) {
695
- const res = shimResponse(rawRes);
696
- const req = rawReq;
697
- // 1. Basic setup
698
- const url = new URL(req.url ?? '', 'http://localhost');
699
- req.query = parseQuery(url);
700
- if (req.socket.remoteAddress)
701
- req.ip = req.socket.remoteAddress;
702
- req.params = {};
703
- // 2. Host/Origin + CORS (preserve exact order)
704
- if (!hostOriginPolicy.validate(req, res))
705
- return;
706
- if (corsPolicy.handle(req, res))
707
- return;
708
- // 3. Body parsing
991
+ const requestId = getHeaderValue(rawReq, 'x-request-id') ?? randomUUID();
992
+ const sessionId = getMcpSessionId(rawReq) ?? undefined;
993
+ const { signal, cleanup } = createRequestAbortSignal(rawReq);
709
994
  try {
710
- req.body = await jsonBodyReader.read(req);
995
+ await runWithRequestContext({
996
+ requestId,
997
+ operationId: requestId,
998
+ ...(sessionId ? { sessionId } : {}),
999
+ }, async () => {
1000
+ const ctx = buildRequestContext(rawReq, rawRes, signal);
1001
+ if (!ctx) {
1002
+ drainRequest(rawReq);
1003
+ return;
1004
+ }
1005
+ if (!hostOriginPolicy.validate(ctx)) {
1006
+ drainRequest(rawReq);
1007
+ return;
1008
+ }
1009
+ if (corsPolicy.handle(ctx)) {
1010
+ drainRequest(rawReq);
1011
+ return;
1012
+ }
1013
+ if (!this.rateLimiter.check(ctx)) {
1014
+ drainRequest(rawReq);
1015
+ return;
1016
+ }
1017
+ try {
1018
+ ctx.body = await jsonBodyReader.read(ctx.req, DEFAULT_BODY_LIMIT_BYTES, ctx.signal);
1019
+ }
1020
+ catch {
1021
+ if (ctx.url.pathname === '/mcp' && ctx.method === 'POST') {
1022
+ sendError(ctx.res, -32700, 'Parse error', 400, null);
1023
+ }
1024
+ else {
1025
+ sendJson(ctx.res, 400, {
1026
+ error: 'Invalid JSON or Payload too large',
1027
+ });
1028
+ }
1029
+ drainRequest(rawReq);
1030
+ return;
1031
+ }
1032
+ await this.dispatcher.dispatch(ctx);
1033
+ });
711
1034
  }
712
- catch {
713
- res.status(400).json({ error: 'Invalid JSON or Payload too large' });
714
- return;
1035
+ finally {
1036
+ cleanup();
715
1037
  }
716
- // 4. Rate limit
717
- if (!this.rateLimiter.check(req, res))
718
- return;
719
- // 5. Dispatch
720
- await this.dispatcher.dispatch(req, res, url);
721
1038
  }
722
1039
  }
723
- /* -------------------------------------------------------------------------------------------------
724
- * Server lifecycle
725
- * ------------------------------------------------------------------------------------------------- */
1040
+ function handlePipelineError(error, res) {
1041
+ logError('Request pipeline failed', error instanceof Error ? error : new Error(String(error)));
1042
+ if (res.writableEnded)
1043
+ return;
1044
+ if (!res.headersSent) {
1045
+ sendJson(res, 500, { error: 'Internal Server Error' });
1046
+ return;
1047
+ }
1048
+ res.end();
1049
+ }
1050
+ async function listen(server, host, port) {
1051
+ await new Promise((resolve, reject) => {
1052
+ function onError(err) {
1053
+ server.off('error', onError);
1054
+ reject(err);
1055
+ }
1056
+ server.once('error', onError);
1057
+ server.listen(port, host, () => {
1058
+ server.off('error', onError);
1059
+ resolve();
1060
+ });
1061
+ });
1062
+ }
1063
+ function resolveListeningPort(server, fallback) {
1064
+ const addr = server.address();
1065
+ if (addr && typeof addr === 'object')
1066
+ return addr.port;
1067
+ return fallback;
1068
+ }
1069
+ function createShutdownHandler(options) {
1070
+ return async (signal) => {
1071
+ logInfo(`Stopping HTTP server (${signal})...`);
1072
+ options.rateLimiter.stop();
1073
+ options.sessionCleanup.abort();
1074
+ drainConnectionsOnShutdown(options.server);
1075
+ eventLoopDelay.disable();
1076
+ const sessions = options.sessionStore.clear();
1077
+ await Promise.all(sessions.map((session) => closeTransportBestEffort(session.transport, 'shutdown-session-close')));
1078
+ await new Promise((resolve, reject) => {
1079
+ options.server.close((err) => {
1080
+ if (err)
1081
+ reject(err);
1082
+ else
1083
+ resolve();
1084
+ });
1085
+ });
1086
+ await options.mcpServer.close();
1087
+ };
1088
+ }
726
1089
  export async function startHttpServer() {
727
1090
  assertHttpModeConfiguration();
728
1091
  enableHttpMode();
1092
+ lastEventLoopUtilization = performance.eventLoopUtilization();
1093
+ eventLoopDelay.reset();
1094
+ eventLoopDelay.enable();
729
1095
  const mcpServer = await createMcpServer();
730
1096
  const rateLimiter = createRateLimitManagerImpl(config.rateLimit);
731
1097
  const sessionStore = createSessionStore(config.server.sessionTtlMs);
@@ -734,37 +1100,29 @@ export async function startHttpServer() {
734
1100
  const dispatcher = new HttpDispatcher(sessionStore, mcpGateway);
735
1101
  const pipeline = new HttpRequestPipeline(rateLimiter, dispatcher);
736
1102
  const server = createServer((req, res) => {
737
- void pipeline.handle(req, res);
1103
+ void pipeline.handle(req, res).catch((error) => {
1104
+ handlePipelineError(error, res);
1105
+ });
738
1106
  });
1107
+ registerInboundBlockList(server);
739
1108
  applyHttpServerTuning(server);
740
- await new Promise((resolve, reject) => {
741
- server.listen(config.server.port, config.server.host, () => {
742
- resolve();
743
- });
744
- server.on('error', reject);
1109
+ await listen(server, config.server.host, config.server.port);
1110
+ const port = resolveListeningPort(server, config.server.port);
1111
+ logInfo(`HTTP server listening on port ${port}`, {
1112
+ platform: process.platform,
1113
+ arch: process.arch,
1114
+ hostname: hostname(),
1115
+ nodeVersion: process.version,
745
1116
  });
746
- const addr = server.address();
747
- const port = typeof addr === 'object' && addr ? addr.port : config.server.port;
748
- logInfo(`HTTP server listening on port ${port}`);
749
1117
  return {
750
1118
  port,
751
1119
  host: config.server.host,
752
- shutdown: async (signal) => {
753
- logInfo(`Stopping HTTP server (${signal})...`);
754
- rateLimiter.stop();
755
- sessionCleanup.abort();
756
- drainConnectionsOnShutdown(server);
757
- const sessions = sessionStore.clear();
758
- await Promise.all(sessions.map(async (s) => {
759
- try {
760
- await s.transport.close();
761
- }
762
- catch {
763
- /* ignore */
764
- }
765
- }));
766
- server.close();
767
- await mcpServer.close();
768
- },
1120
+ shutdown: createShutdownHandler({
1121
+ server,
1122
+ rateLimiter,
1123
+ sessionCleanup,
1124
+ sessionStore,
1125
+ mcpServer,
1126
+ }),
769
1127
  };
770
1128
  }