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