@j0hanz/fetch-url-mcp 1.12.6 → 1.12.8

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 (146) hide show
  1. package/dist/http/auth.d.ts +2 -2
  2. package/dist/http/auth.d.ts.map +1 -1
  3. package/dist/http/auth.js +15 -16
  4. package/dist/http/index.d.ts +6 -0
  5. package/dist/http/index.d.ts.map +1 -0
  6. package/dist/http/index.js +5 -0
  7. package/dist/http/native.d.ts +73 -0
  8. package/dist/http/native.d.ts.map +1 -1
  9. package/dist/http/native.js +585 -62
  10. package/dist/http/rate-limit.d.ts +1 -1
  11. package/dist/http/rate-limit.d.ts.map +1 -1
  12. package/dist/http/rate-limit.js +5 -6
  13. package/dist/index.d.ts +17 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +69 -8
  16. package/dist/lib/config.js +2 -2
  17. package/dist/lib/core.d.ts +56 -4
  18. package/dist/lib/core.d.ts.map +1 -1
  19. package/dist/lib/core.js +162 -11
  20. package/dist/lib/error/classes.d.ts +19 -0
  21. package/dist/lib/error/classes.d.ts.map +1 -0
  22. package/dist/lib/error/classes.js +107 -0
  23. package/dist/lib/error/classify.d.ts +4 -0
  24. package/dist/lib/error/classify.d.ts.map +1 -0
  25. package/dist/lib/error/classify.js +154 -0
  26. package/dist/lib/error/codes.d.ts +23 -0
  27. package/dist/lib/error/codes.d.ts.map +1 -0
  28. package/dist/lib/error/codes.js +22 -0
  29. package/dist/lib/error/index.d.ts +6 -0
  30. package/dist/lib/error/index.d.ts.map +1 -0
  31. package/dist/lib/error/index.js +5 -0
  32. package/dist/lib/{error-messages.d.ts → error/messages.d.ts} +2 -2
  33. package/dist/lib/error/messages.d.ts.map +1 -0
  34. package/dist/lib/{error-messages.js → error/messages.js} +13 -13
  35. package/dist/lib/{tool-errors.d.ts → error/payload.d.ts} +7 -13
  36. package/dist/lib/error/payload.d.ts.map +1 -0
  37. package/dist/lib/error/payload.js +108 -0
  38. package/dist/lib/mcp-interop.d.ts +1 -0
  39. package/dist/lib/mcp-interop.d.ts.map +1 -1
  40. package/dist/lib/mcp-interop.js +17 -9
  41. package/dist/lib/net/http.d.ts.map +1 -0
  42. package/dist/lib/{http.js → net/http.js} +11 -14
  43. package/dist/lib/net/index.d.ts +4 -0
  44. package/dist/lib/net/index.d.ts.map +1 -0
  45. package/dist/lib/net/index.js +3 -0
  46. package/dist/lib/{fetch-pipeline.d.ts → net/pipeline.d.ts} +3 -3
  47. package/dist/lib/net/pipeline.d.ts.map +1 -0
  48. package/dist/lib/{fetch-pipeline.js → net/pipeline.js} +7 -9
  49. package/dist/lib/{url.d.ts → net/url.d.ts} +2 -2
  50. package/dist/lib/net/url.d.ts.map +1 -0
  51. package/dist/lib/{url.js → net/url.js} +6 -8
  52. package/dist/lib/utils.d.ts +3 -18
  53. package/dist/lib/utils.d.ts.map +1 -1
  54. package/dist/lib/utils.js +33 -105
  55. package/dist/resources/index.d.ts.map +1 -1
  56. package/dist/resources/index.js +6 -3
  57. package/dist/schemas.d.ts +1 -1
  58. package/dist/server.d.ts.map +1 -1
  59. package/dist/server.js +12 -14
  60. package/dist/tasks/index.d.ts +2 -0
  61. package/dist/tasks/index.d.ts.map +1 -0
  62. package/dist/tasks/index.js +1 -0
  63. package/dist/tasks/manager.d.ts +123 -1
  64. package/dist/tasks/manager.d.ts.map +1 -1
  65. package/dist/tasks/manager.js +753 -18
  66. package/dist/tools/{fetch-url.d.ts → index.d.ts} +4 -5
  67. package/dist/tools/index.d.ts.map +1 -0
  68. package/dist/tools/{fetch-url.js → index.js} +14 -31
  69. package/dist/transform/index.d.ts +279 -0
  70. package/dist/transform/index.d.ts.map +1 -0
  71. package/dist/transform/index.js +5234 -0
  72. package/package.json +2 -2
  73. package/dist/cli.d.ts +0 -19
  74. package/dist/cli.d.ts.map +0 -1
  75. package/dist/cli.js +0 -65
  76. package/dist/http/health.d.ts +0 -8
  77. package/dist/http/health.d.ts.map +0 -1
  78. package/dist/http/health.js +0 -152
  79. package/dist/http/helpers.d.ts +0 -68
  80. package/dist/http/helpers.d.ts.map +0 -1
  81. package/dist/http/helpers.js +0 -404
  82. package/dist/lib/error-codes.d.ts +0 -11
  83. package/dist/lib/error-codes.d.ts.map +0 -1
  84. package/dist/lib/error-codes.js +0 -15
  85. package/dist/lib/error-messages.d.ts.map +0 -1
  86. package/dist/lib/fetch-pipeline.d.ts.map +0 -1
  87. package/dist/lib/http.d.ts.map +0 -1
  88. package/dist/lib/logger-names.d.ts +0 -14
  89. package/dist/lib/logger-names.d.ts.map +0 -1
  90. package/dist/lib/logger-names.js +0 -13
  91. package/dist/lib/session.d.ts +0 -44
  92. package/dist/lib/session.d.ts.map +0 -1
  93. package/dist/lib/session.js +0 -137
  94. package/dist/lib/tool-errors.d.ts.map +0 -1
  95. package/dist/lib/tool-errors.js +0 -252
  96. package/dist/lib/url.d.ts.map +0 -1
  97. package/dist/lib/zod.d.ts +0 -3
  98. package/dist/lib/zod.d.ts.map +0 -1
  99. package/dist/lib/zod.js +0 -27
  100. package/dist/tasks/call-contract.d.ts +0 -25
  101. package/dist/tasks/call-contract.d.ts.map +0 -1
  102. package/dist/tasks/call-contract.js +0 -59
  103. package/dist/tasks/execution.d.ts +0 -16
  104. package/dist/tasks/execution.d.ts.map +0 -1
  105. package/dist/tasks/execution.js +0 -241
  106. package/dist/tasks/handlers.d.ts +0 -11
  107. package/dist/tasks/handlers.d.ts.map +0 -1
  108. package/dist/tasks/handlers.js +0 -157
  109. package/dist/tasks/owner.d.ts +0 -43
  110. package/dist/tasks/owner.d.ts.map +0 -1
  111. package/dist/tasks/owner.js +0 -144
  112. package/dist/tasks/registry.d.ts +0 -20
  113. package/dist/tasks/registry.d.ts.map +0 -1
  114. package/dist/tasks/registry.js +0 -40
  115. package/dist/tasks/waiters.d.ts +0 -27
  116. package/dist/tasks/waiters.d.ts.map +0 -1
  117. package/dist/tasks/waiters.js +0 -114
  118. package/dist/tools/fetch-url.d.ts.map +0 -1
  119. package/dist/transform/dom-prep.d.ts +0 -16
  120. package/dist/transform/dom-prep.d.ts.map +0 -1
  121. package/dist/transform/dom-prep.js +0 -1287
  122. package/dist/transform/html-translators.d.ts +0 -5
  123. package/dist/transform/html-translators.d.ts.map +0 -1
  124. package/dist/transform/html-translators.js +0 -697
  125. package/dist/transform/markdown-cleanup.d.ts +0 -10
  126. package/dist/transform/markdown-cleanup.d.ts.map +0 -1
  127. package/dist/transform/markdown-cleanup.js +0 -542
  128. package/dist/transform/metadata.d.ts +0 -18
  129. package/dist/transform/metadata.d.ts.map +0 -1
  130. package/dist/transform/metadata.js +0 -462
  131. package/dist/transform/next-flight.d.ts +0 -2
  132. package/dist/transform/next-flight.d.ts.map +0 -1
  133. package/dist/transform/next-flight.js +0 -374
  134. package/dist/transform/shared.d.ts +0 -8
  135. package/dist/transform/shared.d.ts.map +0 -1
  136. package/dist/transform/shared.js +0 -137
  137. package/dist/transform/transform.d.ts +0 -38
  138. package/dist/transform/transform.d.ts.map +0 -1
  139. package/dist/transform/transform.js +0 -1041
  140. package/dist/transform/types.d.ts +0 -124
  141. package/dist/transform/types.d.ts.map +0 -1
  142. package/dist/transform/types.js +0 -5
  143. package/dist/transform/worker-pool.d.ts +0 -76
  144. package/dist/transform/worker-pool.d.ts.map +0 -1
  145. package/dist/transform/worker-pool.js +0 -725
  146. /package/dist/lib/{http.d.ts → net/http.d.ts} +0 -0
@@ -1,23 +1,567 @@
1
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
2
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
1
3
  import { randomUUID } from 'node:crypto';
2
4
  import { once } from 'node:events';
3
5
  import { readFileSync } from 'node:fs';
4
6
  import { createServer, } from 'node:http';
5
7
  import { createServer as createHttpsServer, } from 'node:https';
6
- import { hostname } from 'node:os';
8
+ import { freemem, hostname, totalmem } from 'node:os';
9
+ import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
7
10
  import process from 'node:process';
8
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
9
- import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
10
- import { composeCloseHandlers, config, createSessionStore, createSlotTracker, enableHttpMode, ensureSessionCapacity, logDebug, logError, logInfo, logWarn, registerMcpSessionOwnerKey, registerMcpSessionServer, reserveSessionSlot, runWithRequestContext, startSessionCleanupLoop, } from '../lib/core.js';
11
- import { LOG_AUTH, LOG_HTTP, LOG_SESSION } from '../lib/logger-names.js';
12
- import { acceptsEventStream, acceptsJsonAndEventStream, isJsonRpcBatchRequest, isMcpMessageBody, isMcpRequestBody, } from '../lib/mcp-interop.js';
13
- import { applyHttpServerTuning, drainConnectionsOnShutdown, isObject, toError, } from '../lib/utils.js';
11
+ import { Writable } from 'node:stream';
12
+ import { pipeline } from 'node:stream/promises';
13
+ import { composeCloseHandlers, config, createSessionStore, createSlotTracker, enableHttpMode, ensureSessionCapacity, logDebug, logError, Loggers, logInfo, logWarn, registerMcpSessionOwnerKey, registerMcpSessionServer, reserveSessionSlot, resolveMcpSessionIdByServer, runWithRequestContext, serverVersion, startSessionCleanupLoop, unregisterMcpSessionServer, unregisterMcpSessionServerByServer, } from '../lib/core.js';
14
+ import { getErrorMessage, toError } from '../lib/error/index.js';
15
+ import { acceptsEventStream, acceptsJsonAndEventStream, isMcpRequestBody, } from '../lib/mcp-interop.js';
16
+ import { createDefaultBlockList, normalizeIpForBlockList, } from '../lib/net/index.js';
17
+ import { applyHttpServerTuning, drainConnectionsOnShutdown, isObject, } from '../lib/utils.js';
14
18
  import { createMcpServerForHttpSession } from '../server.js';
15
- import { buildAuthenticatedOwnerKey } from '../tasks/owner.js';
19
+ import { buildAuthenticatedOwnerKey } from '../tasks/index.js';
20
+ import { getTransformPoolStats } from '../transform/index.js';
16
21
  import { applyInsufficientScopeAuthHeaders, applyUnauthorizedAuthHeaders, assertHttpModeConfiguration, authService, buildAuthFingerprint, buildProtectedResourceMetadataDocument, corsPolicy, DEFAULT_MCP_PROTOCOL_VERSION, ensureMcpProtocolVersion, hostOriginPolicy, isInsufficientScopeError, isOAuthMetadataEnabled, isProtectedResourceMetadataPath, SUPPORTED_MCP_PROTOCOL_VERSIONS, } from './auth.js';
17
- import { disableEventLoopMonitoring, isVerboseHealthRequest, resetEventLoopMonitoring, sendHealthRouteResponse, shouldHandleHealthRoute, } from './health.js';
18
- import { buildRequestContext, createRequestAbortSignal, createTransportAdapter, DEFAULT_BODY_LIMIT_BYTES, drainRequest, findDuplicateSingleValueHeader, getHeaderValue, getMcpSessionId, isJsonBodyError, jsonBodyReader, registerInboundBlockList, sendEmpty, sendError, sendJson, } from './helpers.js';
19
- import { teardownSessionRegistration, teardownSessionResources, teardownUnregisteredSessionResources, } from './helpers.js';
20
22
  import { createRateLimitManagerImpl, } from './rate-limit.js';
23
+ function abortControllerBestEffort(controller) {
24
+ if (!controller.signal.aborted)
25
+ controller.abort();
26
+ }
27
+ function destroyRequestBestEffort(req) {
28
+ try {
29
+ req.destroy();
30
+ }
31
+ catch {
32
+ // Best-effort only.
33
+ }
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // Response helpers
37
+ // ---------------------------------------------------------------------------
38
+ function setNoStoreHeaders(res) {
39
+ res.setHeader('X-Content-Type-Options', 'nosniff');
40
+ res.setHeader('Cache-Control', 'no-store');
41
+ }
42
+ export function sendJson(res, status, body) {
43
+ res.statusCode = status;
44
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
45
+ setNoStoreHeaders(res);
46
+ res.end(JSON.stringify(body));
47
+ }
48
+ export function sendEmpty(res, status) {
49
+ res.statusCode = status;
50
+ res.setHeader('Content-Length', '0');
51
+ res.end();
52
+ }
53
+ export function sendError(res, _code, message, status = 400,
54
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- kept for call-site compat
55
+ _id) {
56
+ sendJson(res, status, { error: message });
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // Request helpers
60
+ // ---------------------------------------------------------------------------
61
+ export function getHeaderValue(req, name) {
62
+ const val = req.headers[name];
63
+ if (!val)
64
+ return null;
65
+ return Array.isArray(val) ? (val[0] ?? null) : val;
66
+ }
67
+ export function getMcpSessionId(req) {
68
+ return (getHeaderValue(req, 'mcp-session-id') ??
69
+ getHeaderValue(req, 'x-mcp-session-id'));
70
+ }
71
+ const SINGLE_VALUE_HEADER_NAMES = [
72
+ 'authorization',
73
+ 'x-api-key',
74
+ 'host',
75
+ 'origin',
76
+ 'content-length',
77
+ 'mcp-protocol-version',
78
+ 'mcp-session-id',
79
+ 'x-mcp-session-id',
80
+ ];
81
+ function hasDuplicateHeader(req, name) {
82
+ const values = req.headersDistinct[name];
83
+ return Array.isArray(values) && values.length > 1;
84
+ }
85
+ export function findDuplicateSingleValueHeader(req) {
86
+ for (const name of SINGLE_VALUE_HEADER_NAMES) {
87
+ if (hasDuplicateHeader(req, name))
88
+ return name;
89
+ }
90
+ return null;
91
+ }
92
+ export function drainRequest(req) {
93
+ if (req.readableEnded)
94
+ return;
95
+ try {
96
+ req.resume();
97
+ }
98
+ catch {
99
+ // Best-effort only.
100
+ }
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // Request abort signal
104
+ // ---------------------------------------------------------------------------
105
+ export function createRequestAbortSignal(req) {
106
+ const controller = new AbortController();
107
+ let cleanedUp = false;
108
+ const abortRequest = () => {
109
+ if (cleanedUp)
110
+ return;
111
+ abortControllerBestEffort(controller);
112
+ };
113
+ if (req.destroyed) {
114
+ abortRequest();
115
+ return {
116
+ signal: controller.signal,
117
+ cleanup: () => {
118
+ cleanedUp = true;
119
+ },
120
+ };
121
+ }
122
+ const onClose = () => {
123
+ // A normal close after a complete body should not be treated as cancellation.
124
+ if (req.complete)
125
+ return;
126
+ abortRequest();
127
+ };
128
+ const onError = () => {
129
+ abortRequest();
130
+ };
131
+ req.once('close', onClose);
132
+ req.once('error', onError);
133
+ return {
134
+ signal: controller.signal,
135
+ cleanup: () => {
136
+ cleanedUp = true;
137
+ req.removeListener('close', onClose);
138
+ req.removeListener('error', onError);
139
+ },
140
+ };
141
+ }
142
+ // ---------------------------------------------------------------------------
143
+ // IP & connection helpers
144
+ // ---------------------------------------------------------------------------
145
+ function normalizeRemoteAddress(address) {
146
+ if (!address)
147
+ return null;
148
+ const trimmed = address.trim();
149
+ if (!trimmed)
150
+ return null;
151
+ const normalized = normalizeIpForBlockList(trimmed);
152
+ if (normalized)
153
+ return normalized.ip;
154
+ return trimmed;
155
+ }
156
+ export function registerInboundBlockList(server) {
157
+ if (!config.server.http.blockPrivateConnections)
158
+ return;
159
+ const blockList = createDefaultBlockList();
160
+ server.on('connection', (socket) => {
161
+ const raw = socket.remoteAddress?.trim();
162
+ if (!raw)
163
+ return;
164
+ const normalized = normalizeIpForBlockList(raw);
165
+ if (!normalized)
166
+ return;
167
+ if (blockList.check(normalized.ip, normalized.family)) {
168
+ logWarn('Blocked inbound connection', {
169
+ remoteAddress: normalized.ip,
170
+ family: normalized.family,
171
+ }, Loggers.LOG_HTTP);
172
+ socket.destroy();
173
+ }
174
+ });
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // Request context builder
178
+ // ---------------------------------------------------------------------------
179
+ export function buildRequestContext(req, res, signal) {
180
+ const url = URL.parse(req.url ?? '', 'http://localhost');
181
+ if (!url) {
182
+ sendJson(res, 400, { error: 'Invalid request URL' });
183
+ return null;
184
+ }
185
+ return {
186
+ req,
187
+ res,
188
+ url,
189
+ method: req.method,
190
+ ip: normalizeRemoteAddress(req.socket.remoteAddress),
191
+ body: undefined,
192
+ ...(signal ? { signal } : {}),
193
+ };
194
+ }
195
+ // ---------------------------------------------------------------------------
196
+ // Transport / MCP helpers
197
+ // ---------------------------------------------------------------------------
198
+ export async function closeTransportBestEffort(transport, context) {
199
+ try {
200
+ await transport.close();
201
+ }
202
+ catch (error) {
203
+ logWarn('Transport close failed', { context, error }, Loggers.LOG_HTTP);
204
+ }
205
+ }
206
+ export async function closeMcpServerBestEffort(server, context) {
207
+ try {
208
+ await server.close();
209
+ }
210
+ catch (error) {
211
+ logWarn('MCP server close failed', { context, error }, Loggers.LOG_HTTP);
212
+ }
213
+ }
214
+ export function createTransportAdapter(transportImpl) {
215
+ const noopOnClose = () => { };
216
+ const noopOnError = () => { };
217
+ const noopOnMessage = () => { };
218
+ const baseOnClose = transportImpl.onclose;
219
+ let oncloseHandler = noopOnClose;
220
+ let onerrorHandler = noopOnError;
221
+ let onmessageHandler = noopOnMessage;
222
+ return {
223
+ start: () => transportImpl.start(),
224
+ send: (message, options) => transportImpl.send(message, options),
225
+ close: () => transportImpl.close(),
226
+ get onclose() {
227
+ return oncloseHandler;
228
+ },
229
+ set onclose(handler) {
230
+ oncloseHandler = handler;
231
+ transportImpl.onclose = composeCloseHandlers(baseOnClose, handler);
232
+ },
233
+ get onerror() {
234
+ return onerrorHandler;
235
+ },
236
+ set onerror(handler) {
237
+ onerrorHandler = handler;
238
+ transportImpl.onerror = handler;
239
+ },
240
+ get onmessage() {
241
+ return onmessageHandler;
242
+ },
243
+ set onmessage(handler) {
244
+ onmessageHandler = handler;
245
+ transportImpl.onmessage = handler;
246
+ },
247
+ };
248
+ }
249
+ export class JsonBodyError extends Error {
250
+ kind;
251
+ constructor(kind, message) {
252
+ super(message);
253
+ this.name = 'JsonBodyError';
254
+ this.kind = kind;
255
+ }
256
+ }
257
+ export function isJsonBodyError(error) {
258
+ return error instanceof JsonBodyError;
259
+ }
260
+ export const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
261
+ function isRequestReadAborted(req) {
262
+ return req.destroyed && !req.complete;
263
+ }
264
+ class JsonBodyReader {
265
+ async read(req, limit = DEFAULT_BODY_LIMIT_BYTES, signal) {
266
+ const contentType = getHeaderValue(req, 'content-type');
267
+ if (!contentType?.includes('application/json'))
268
+ return undefined;
269
+ const contentLengthHeader = getHeaderValue(req, 'content-length');
270
+ if (contentLengthHeader) {
271
+ const contentLength = Number.parseInt(contentLengthHeader, 10);
272
+ if (Number.isFinite(contentLength) && contentLength > limit) {
273
+ const error = new JsonBodyError('payload-too-large', 'Payload too large');
274
+ throw error;
275
+ }
276
+ }
277
+ if (signal?.aborted || isRequestReadAborted(req)) {
278
+ const error = new JsonBodyError('read-failed', 'Request aborted');
279
+ throw error;
280
+ }
281
+ const body = await this.readBody(req, limit, signal);
282
+ if (!body)
283
+ return undefined;
284
+ try {
285
+ return JSON.parse(body);
286
+ }
287
+ catch (err) {
288
+ const error = new JsonBodyError('invalid-json', getErrorMessage(err));
289
+ throw error;
290
+ }
291
+ }
292
+ async readBody(req, limit, signal) {
293
+ const abortListener = signal != null
294
+ ? () => {
295
+ destroyRequestBestEffort(req);
296
+ }
297
+ : null;
298
+ if (signal != null && abortListener) {
299
+ if (signal.aborted) {
300
+ abortListener();
301
+ }
302
+ else {
303
+ signal.addEventListener('abort', abortListener, { once: true });
304
+ }
305
+ }
306
+ try {
307
+ const { chunks, size } = await this.collectChunks(req, limit, signal);
308
+ if (chunks.length === 0)
309
+ return undefined;
310
+ const combined = new Uint8Array(size);
311
+ let offset = 0;
312
+ for (const chunk of chunks) {
313
+ combined.set(chunk, offset);
314
+ offset += chunk.byteLength;
315
+ }
316
+ const text = new TextDecoder().decode(combined);
317
+ return text;
318
+ }
319
+ finally {
320
+ if (signal && abortListener) {
321
+ try {
322
+ signal.removeEventListener('abort', abortListener);
323
+ }
324
+ catch {
325
+ // Best-effort cleanup.
326
+ }
327
+ }
328
+ }
329
+ }
330
+ async collectChunks(req, limit, signal) {
331
+ let size = 0;
332
+ const chunks = [];
333
+ const sink = new Writable({
334
+ write: (chunk, _encoding, callback) => {
335
+ try {
336
+ if (signal?.aborted || isRequestReadAborted(req)) {
337
+ callback(new JsonBodyError('read-failed', 'Request aborted'));
338
+ return;
339
+ }
340
+ const buf = this.normalizeChunk(chunk);
341
+ size += buf.byteLength;
342
+ if (size > limit) {
343
+ callback(new JsonBodyError('payload-too-large', 'Payload too large'));
344
+ return;
345
+ }
346
+ chunks.push(buf);
347
+ callback();
348
+ }
349
+ catch (err) {
350
+ callback(toError(err));
351
+ }
352
+ },
353
+ });
354
+ try {
355
+ if (signal?.aborted || isRequestReadAborted(req)) {
356
+ const error = new JsonBodyError('read-failed', 'Request aborted');
357
+ throw error;
358
+ }
359
+ await pipeline(req, sink, signal ? { signal } : undefined);
360
+ return { chunks, size };
361
+ }
362
+ catch (err) {
363
+ if (err instanceof JsonBodyError)
364
+ throw err;
365
+ if (signal?.aborted || isRequestReadAborted(req)) {
366
+ const error = new JsonBodyError('read-failed', 'Request aborted');
367
+ throw error;
368
+ }
369
+ const error = new JsonBodyError('read-failed', getErrorMessage(err));
370
+ throw error;
371
+ }
372
+ }
373
+ normalizeChunk(chunk) {
374
+ if (typeof chunk === 'string') {
375
+ const encoded = new TextEncoder().encode(chunk);
376
+ return encoded;
377
+ }
378
+ return chunk;
379
+ }
380
+ }
381
+ export const jsonBodyReader = new JsonBodyReader();
382
+ function unregisterSessionTaskScope(server) {
383
+ const sessionId = resolveMcpSessionIdByServer(server);
384
+ if (!sessionId)
385
+ return null;
386
+ unregisterMcpSessionServer(sessionId);
387
+ return sessionId;
388
+ }
389
+ async function closeSessionResources(session, options) {
390
+ const closeTasks = [];
391
+ if (options.closeTransportReason) {
392
+ closeTasks.push(closeTransportBestEffort(session.transport, options.closeTransportReason));
393
+ }
394
+ if (options.closeServerReason) {
395
+ closeTasks.push(closeMcpServerBestEffort(session.server, options.closeServerReason));
396
+ }
397
+ if (options.awaitClose && closeTasks.length > 0) {
398
+ await Promise.all(closeTasks);
399
+ }
400
+ }
401
+ export async function teardownSessionResources(session, options) {
402
+ unregisterSessionTaskScope(session.server);
403
+ if (options.unregisterByServer) {
404
+ unregisterMcpSessionServerByServer(session.server);
405
+ }
406
+ await closeSessionResources(session, options);
407
+ }
408
+ export async function teardownUnregisteredSessionResources(session, context) {
409
+ await closeSessionResources(session, {
410
+ closeTransportReason: context,
411
+ closeServerReason: context,
412
+ awaitClose: true,
413
+ });
414
+ }
415
+ export function teardownSessionRegistration(server) {
416
+ unregisterSessionTaskScope(server);
417
+ }
418
+ // --- health.ts ---
419
+ // ---------------------------------------------------------------------------
420
+ // Event-loop monitoring
421
+ // ---------------------------------------------------------------------------
422
+ const EVENT_LOOP_DELAY_RESOLUTION_MS = 20;
423
+ const eventLoopDelay = monitorEventLoopDelay({
424
+ resolution: EVENT_LOOP_DELAY_RESOLUTION_MS,
425
+ });
426
+ let lastEventLoopUtilization = performance.eventLoopUtilization();
427
+ export function resetEventLoopMonitoring() {
428
+ lastEventLoopUtilization = performance.eventLoopUtilization();
429
+ eventLoopDelay.reset();
430
+ eventLoopDelay.enable();
431
+ }
432
+ export function disableEventLoopMonitoring() {
433
+ eventLoopDelay.disable();
434
+ }
435
+ // ---------------------------------------------------------------------------
436
+ // Stats helpers
437
+ // ---------------------------------------------------------------------------
438
+ function roundTo(value, precision) {
439
+ const factor = 10 ** precision;
440
+ return Math.round(value * factor) / factor;
441
+ }
442
+ function formatEventLoopUtilization(snapshot) {
443
+ return {
444
+ utilization: roundTo(snapshot.utilization, 4),
445
+ activeMs: Math.round(snapshot.active),
446
+ idleMs: Math.round(snapshot.idle),
447
+ };
448
+ }
449
+ function toMs(valueNs) {
450
+ return roundTo(valueNs / 1_000_000, 3);
451
+ }
452
+ function getEventLoopStats() {
453
+ const current = performance.eventLoopUtilization();
454
+ const delta = performance.eventLoopUtilization(current, lastEventLoopUtilization);
455
+ lastEventLoopUtilization = current;
456
+ return {
457
+ utilization: {
458
+ total: formatEventLoopUtilization(current),
459
+ sinceLast: formatEventLoopUtilization(delta),
460
+ },
461
+ delay: {
462
+ minMs: toMs(eventLoopDelay.min),
463
+ maxMs: toMs(eventLoopDelay.max),
464
+ meanMs: toMs(eventLoopDelay.mean),
465
+ stddevMs: toMs(eventLoopDelay.stddev),
466
+ p50Ms: toMs(eventLoopDelay.percentile(50)),
467
+ p95Ms: toMs(eventLoopDelay.percentile(95)),
468
+ p99Ms: toMs(eventLoopDelay.percentile(99)),
469
+ },
470
+ };
471
+ }
472
+ function buildHealthResponse(store, includeDiagnostics) {
473
+ const base = {
474
+ status: 'ok',
475
+ version: serverVersion,
476
+ uptime: Math.floor(process.uptime()),
477
+ timestamp: new Date().toISOString(),
478
+ };
479
+ if (!includeDiagnostics)
480
+ return base;
481
+ const poolStats = getTransformPoolStats();
482
+ return {
483
+ ...base,
484
+ os: {
485
+ hostname: hostname(),
486
+ platform: process.platform,
487
+ arch: process.arch,
488
+ memoryFree: freemem(),
489
+ memoryTotal: totalmem(),
490
+ },
491
+ process: {
492
+ pid: process.pid,
493
+ ppid: process.ppid,
494
+ memory: process.memoryUsage(),
495
+ cpu: process.cpuUsage(),
496
+ resource: process.resourceUsage(),
497
+ ...(typeof process.availableMemory === 'function'
498
+ ? { availableMemory: process.availableMemory() }
499
+ : {}),
500
+ ...(typeof process.constrainedMemory === 'function'
501
+ ? { constrainedMemory: process.constrainedMemory() }
502
+ : {}),
503
+ },
504
+ perf: getEventLoopStats(),
505
+ ...(typeof process.getActiveResourcesInfo === 'function'
506
+ ? { activeResources: process.getActiveResourcesInfo() }
507
+ : {}),
508
+ stats: {
509
+ activeSessions: store.size(),
510
+ workerPool: poolStats ?? {
511
+ queueDepth: 0,
512
+ activeWorkers: 0,
513
+ capacity: 0,
514
+ },
515
+ },
516
+ };
517
+ }
518
+ function sendHealth(store, res, includeDiagnostics) {
519
+ res.setHeader('Cache-Control', 'no-store');
520
+ sendJson(res, 200, buildHealthResponse(store, includeDiagnostics));
521
+ }
522
+ // ---------------------------------------------------------------------------
523
+ // Health route helpers
524
+ // ---------------------------------------------------------------------------
525
+ export function isVerboseHealthRequest(ctx) {
526
+ const value = ctx.url.searchParams.get('verbose');
527
+ if (!value)
528
+ return false;
529
+ const normalized = value.trim().toLowerCase();
530
+ return normalized === '1' || normalized === 'true';
531
+ }
532
+ function isHealthRoute(ctx) {
533
+ return ctx.method === 'GET' && ctx.url.pathname === '/health';
534
+ }
535
+ function isVerboseHealthRoute(ctx) {
536
+ return isHealthRoute(ctx) && isVerboseHealthRequest(ctx);
537
+ }
538
+ function ensureHealthAuthIfNeeded(ctx, authPresent) {
539
+ if (!isVerboseHealthRoute(ctx))
540
+ return true;
541
+ if (!config.security.allowRemote)
542
+ return true;
543
+ if (authPresent)
544
+ return true;
545
+ sendJson(ctx.res, 401, {
546
+ error: 'Authentication required for verbose health metrics',
547
+ });
548
+ return false;
549
+ }
550
+ function resolveHealthDiagnosticsMode(ctx, authPresent) {
551
+ return (isVerboseHealthRoute(ctx) && (authPresent || !config.security.allowRemote));
552
+ }
553
+ export function shouldHandleHealthRoute(ctx) {
554
+ return isHealthRoute(ctx);
555
+ }
556
+ export function sendHealthRouteResponse(store, ctx, authPresent) {
557
+ if (!shouldHandleHealthRoute(ctx))
558
+ return false;
559
+ if (!ensureHealthAuthIfNeeded(ctx, authPresent))
560
+ return true;
561
+ const includeDiagnostics = resolveHealthDiagnosticsMode(ctx, authPresent);
562
+ sendHealth(store, ctx.res, includeDiagnostics);
563
+ return true;
564
+ }
21
565
  function resolveRequestedProtocolVersion(body) {
22
566
  if (!isObject(body))
23
567
  return DEFAULT_MCP_PROTOCOL_VERSION;
@@ -56,7 +600,7 @@ function logGatewayRejection(params) {
56
600
  ...rest,
57
601
  ...(rpcId === null || rpcId === undefined ? {} : { rpcId }),
58
602
  ...(details ?? {}),
59
- }, LOG_HTTP);
603
+ }, Loggers.LOG_HTTP);
60
604
  }
61
605
  function resolveRequestPath(req) {
62
606
  return URL.parse(req.url ?? '', 'http://localhost')?.pathname ?? '/';
@@ -71,14 +615,14 @@ function logRequestCompletion(params) {
71
615
  ...(params.sessionId ? { sessionId: params.sessionId } : {}),
72
616
  };
73
617
  if (params.statusCode >= 500) {
74
- logError('HTTP request failed with server error', meta, LOG_HTTP);
618
+ logError('HTTP request failed with server error', meta, Loggers.LOG_HTTP);
75
619
  return;
76
620
  }
77
621
  if (params.statusCode >= 400) {
78
- logWarn('HTTP client error', meta, LOG_HTTP);
622
+ logWarn('HTTP client error', meta, Loggers.LOG_HTTP);
79
623
  return;
80
624
  }
81
- logDebug('HTTP request completed', meta, LOG_HTTP);
625
+ logDebug('HTTP request completed', meta, Loggers.LOG_HTTP);
82
626
  }
83
627
  function createSessionTeardownOptions(mode, context) {
84
628
  switch (mode) {
@@ -137,7 +681,7 @@ class McpSessionGateway {
137
681
  method: method ?? 'response',
138
682
  rpcId: body.id,
139
683
  sessionId,
140
- }, LOG_HTTP);
684
+ }, Loggers.LOG_HTTP);
141
685
  const transport = await this.getOrCreateTransport(ctx, requestId);
142
686
  if (!transport)
143
687
  return;
@@ -165,7 +709,7 @@ class McpSessionGateway {
165
709
  });
166
710
  return;
167
711
  }
168
- logDebug('MCP GET received', { sessionId }, LOG_HTTP);
712
+ logDebug('MCP GET received', { sessionId }, Loggers.LOG_HTTP);
169
713
  this.store.touch(sessionId);
170
714
  await session.transport.handleRequest(ctx.req, ctx.res);
171
715
  }
@@ -177,7 +721,7 @@ class McpSessionGateway {
177
721
  return;
178
722
  const { sessionId, session } = sessionState;
179
723
  await session.transport.close();
180
- logDebug('MCP DELETE received', { sessionId }, LOG_HTTP);
724
+ logDebug('MCP DELETE received', { sessionId }, Loggers.LOG_HTTP);
181
725
  this.cleanupSessionRecord(sessionId, createSessionTeardownOptions('ended', 'session-delete'));
182
726
  sendJson(ctx.res, 200, { status: 'closed' });
183
727
  }
@@ -196,31 +740,10 @@ class McpSessionGateway {
196
740
  return null;
197
741
  }
198
742
  const { body } = ctx;
199
- if (isJsonRpcBatchRequest(body)) {
200
- logGatewayRejection({
201
- message: 'Rejected MCP POST request',
202
- method: ctx.method,
203
- path: ctx.url.pathname,
204
- reason: 'batch_request_not_supported',
205
- status: 400,
206
- mcpCode: -32600,
207
- });
208
- sendError(ctx.res, -32600, "We don't support batch requests yet. Please send one request at a time.");
209
- return null;
210
- }
211
- if (!isMcpMessageBody(body)) {
212
- logGatewayRejection({
213
- message: 'Rejected MCP POST request',
214
- method: ctx.method,
215
- path: ctx.url.pathname,
216
- reason: 'invalid_request_body',
217
- status: 400,
218
- mcpCode: -32600,
219
- });
220
- sendError(ctx.res, -32600, "The request body isn't quite right. Please check the format and try again.");
221
- return null;
743
+ if (isObject(body) && !Array.isArray(body)) {
744
+ return body;
222
745
  }
223
- return body;
746
+ return { id: undefined, method: undefined };
224
747
  }
225
748
  resolvePostRequestState(ctx, body) {
226
749
  const requestId = body.id ?? null;
@@ -444,7 +967,7 @@ class McpSessionGateway {
444
967
  this.clearSessionInitTimeout(sessionId);
445
968
  if (sessionId)
446
969
  this.store.touch(sessionId);
447
- logDebug('Session initialized', { sessionId }, LOG_SESSION);
970
+ logDebug('Session initialized', { sessionId }, Loggers.LOG_SESSION);
448
971
  }
449
972
  createSessionInitTimeout(sessionId, tracker, unpublishedSession) {
450
973
  const initTimeout = setTimeout(() => {
@@ -454,11 +977,11 @@ class McpSessionGateway {
454
977
  this.clearSessionInitTimeout(sessionId);
455
978
  return;
456
979
  }
457
- logWarn('Session init timeout', { sessionId }, LOG_SESSION);
980
+ logWarn('Session init timeout', { sessionId }, Loggers.LOG_SESSION);
458
981
  this.cleanupSessionRecord(sessionId, createSessionTeardownOptions('init-timeout'));
459
982
  return;
460
983
  }
461
- logWarn('Session init timeout before registration completed', { sessionId }, LOG_SESSION);
984
+ logWarn('Session init timeout before registration completed', { sessionId }, Loggers.LOG_SESSION);
462
985
  tracker.releaseSlot();
463
986
  void teardownUnregisteredSessionResources(unpublishedSession, 'session-init-timeout');
464
987
  }, config.server.sessionInitTimeoutMs);
@@ -481,7 +1004,7 @@ class McpSessionGateway {
481
1004
  logWarn('Session transport connect failed', {
482
1005
  sessionId,
483
1006
  error: toError(err).message,
484
- }, LOG_SESSION);
1007
+ }, Loggers.LOG_SESSION);
485
1008
  clearTimeout(initTimeout);
486
1009
  tracker.releaseSlot();
487
1010
  void teardownUnregisteredSessionResources(unpublishedSession, 'session-connect-failed');
@@ -495,7 +1018,7 @@ class McpSessionGateway {
495
1018
  logError('Session creation failed: missing auth context', {
496
1019
  path: ctx.url.pathname,
497
1020
  method: ctx.method,
498
- }, LOG_SESSION);
1021
+ }, Loggers.LOG_SESSION);
499
1022
  sendError(ctx.res, -32603, "We're missing some authorization details to process this request.", 500, requestId);
500
1023
  return null;
501
1024
  }
@@ -504,7 +1027,7 @@ class McpSessionGateway {
504
1027
  logError('Session creation failed: missing task owner context', {
505
1028
  path: ctx.url.pathname,
506
1029
  method: ctx.method,
507
- }, LOG_SESSION);
1030
+ }, Loggers.LOG_SESSION);
508
1031
  sendError(ctx.res, -32603, "We're missing the owner information needed to authorize this request.", 500, requestId);
509
1032
  return null;
510
1033
  }
@@ -517,7 +1040,7 @@ class McpSessionGateway {
517
1040
  sessionServer = await this.createSessionServer();
518
1041
  }
519
1042
  catch (error) {
520
- logError('Session server creation failed', { sessionId: newSessionId, error: toError(error).message }, LOG_SESSION);
1043
+ logError('Session server creation failed', { sessionId: newSessionId, error: toError(error).message }, Loggers.LOG_SESSION);
521
1044
  tracker.releaseSlot();
522
1045
  throw error;
523
1046
  }
@@ -532,7 +1055,7 @@ class McpSessionGateway {
532
1055
  const isConnected = await this.connectTransport(sessionServer, transportImpl, initTimeout, tracker, unpublishedSession, newSessionId);
533
1056
  tracker.releaseSlot();
534
1057
  if (!isConnected) {
535
- logWarn('Session closed before registration completed', { sessionId: newSessionId }, LOG_SESSION);
1058
+ logWarn('Session closed before registration completed', { sessionId: newSessionId }, Loggers.LOG_SESSION);
536
1059
  void teardownUnregisteredSessionResources(unpublishedSession, 'session-closed-during-connect');
537
1060
  return null;
538
1061
  }
@@ -548,7 +1071,7 @@ class McpSessionGateway {
548
1071
  this.sessionInitTimeouts.set(newSessionId, initTimeout);
549
1072
  registerMcpSessionOwnerKey(newSessionId, ownerKey);
550
1073
  registerMcpSessionServer(newSessionId, sessionServer);
551
- logInfo('Session created', { sessionId: newSessionId, negotiatedProtocolVersion }, LOG_SESSION);
1074
+ logInfo('Session created', { sessionId: newSessionId, negotiatedProtocolVersion }, Loggers.LOG_SESSION);
552
1075
  transportImpl.onclose = composeCloseHandlers(transportImpl.onclose, () => {
553
1076
  this.cleanupSessionRecord(newSessionId, createSessionTeardownOptions('ended', 'session-close'));
554
1077
  });
@@ -558,7 +1081,7 @@ class McpSessionGateway {
558
1081
  const context = teardownOptions.closeTransportReason ??
559
1082
  teardownOptions.closeServerReason ??
560
1083
  'session';
561
- logDebug('Session cleanup', { sessionId, context }, LOG_SESSION);
1084
+ logDebug('Session cleanup', { sessionId, context }, Loggers.LOG_SESSION);
562
1085
  this.clearSessionInitTimeout(sessionId);
563
1086
  const session = this.store.remove(sessionId);
564
1087
  if (!session)
@@ -588,13 +1111,13 @@ class McpSessionGateway {
588
1111
  },
589
1112
  });
590
1113
  if (!allowed) {
591
- logWarn('Session capacity exhausted', { maxSessions: config.server.maxSessions }, LOG_SESSION);
1114
+ logWarn('Session capacity exhausted', { maxSessions: config.server.maxSessions }, Loggers.LOG_SESSION);
592
1115
  sendError(res, -32000, 'The server is currently too busy to handle your request. Please try again in a little while.', 503, requestId);
593
1116
  return false;
594
1117
  }
595
1118
  // Double-check: capacity may have changed during the async eviction window above.
596
1119
  if (!reserveSessionSlot(this.store, config.server.maxSessions)) {
597
- logWarn('Session capacity exhausted (post-eviction)', { maxSessions: config.server.maxSessions }, LOG_SESSION);
1120
+ logWarn('Session capacity exhausted (post-eviction)', { maxSessions: config.server.maxSessions }, Loggers.LOG_SESSION);
598
1121
  sendError(res, -32000, 'The server is currently too busy to handle your request. Please try again in a little while.', 503, requestId);
599
1122
  return false;
600
1123
  }
@@ -662,7 +1185,7 @@ class HttpDispatcher {
662
1185
  }
663
1186
  catch (err) {
664
1187
  const error = toError(err);
665
- logError('Request failed', error, LOG_HTTP);
1188
+ logError('Request failed', error, Loggers.LOG_HTTP);
666
1189
  if (!ctx.res.writableEnded) {
667
1190
  sendJson(ctx.res, 500, {
668
1191
  error: "Something went wrong on our end. We're looking into it!",
@@ -691,7 +1214,7 @@ class HttpDispatcher {
691
1214
  }
692
1215
  catch (err) {
693
1216
  const message = err instanceof Error ? err.message : 'Unauthorized';
694
- logWarn('Authentication failed', { message, method: ctx.method, path: ctx.url.pathname }, LOG_AUTH);
1217
+ logWarn('Authentication failed', { message, method: ctx.method, path: ctx.url.pathname }, Loggers.LOG_AUTH);
695
1218
  if (isInsufficientScopeError(err)) {
696
1219
  applyInsufficientScopeAuthHeaders(ctx.req, ctx.res, err.requiredScopes, message);
697
1220
  sendError(ctx.res, -32000, message, 403);
@@ -839,10 +1362,10 @@ class HttpRequestPipeline {
839
1362
  catch (error) {
840
1363
  const bodyErrorKind = isJsonBodyError(error) ? error.kind : null;
841
1364
  if (bodyErrorKind === 'payload-too-large') {
842
- logWarn('The request body is too large. Please send a smaller payload.', { method: ctx.method, path: ctx.url.pathname }, LOG_HTTP);
1365
+ logWarn('The request body is too large. Please send a smaller payload.', { method: ctx.method, path: ctx.url.pathname }, Loggers.LOG_HTTP);
843
1366
  }
844
1367
  else if (bodyErrorKind === 'read-failed' || bodyErrorKind === null) {
845
- logError('Request body parsing failed', toError(error), LOG_HTTP);
1368
+ logError('Request body parsing failed', toError(error), Loggers.LOG_HTTP);
846
1369
  }
847
1370
  sendBodyParseError(ctx, bodyErrorKind, rawReq);
848
1371
  return false;
@@ -864,7 +1387,7 @@ class HttpRequestPipeline {
864
1387
  // Server bootstrap
865
1388
  // ---------------------------------------------------------------------------
866
1389
  function handlePipelineError(error, res) {
867
- logError('Request pipeline failed', toError(error), LOG_HTTP);
1390
+ logError('Request pipeline failed', toError(error), Loggers.LOG_HTTP);
868
1391
  if (res.writableEnded)
869
1392
  return;
870
1393
  if (!res.headersSent) {
@@ -912,7 +1435,7 @@ function resolveListeningPort(server, fallback) {
912
1435
  function createShutdownHandler(options) {
913
1436
  const closeBatchSize = 10;
914
1437
  return async (signal) => {
915
- logInfo(`Stopping HTTP server (${signal})...`, undefined, LOG_HTTP);
1438
+ logInfo(`Stopping HTTP server (${signal})...`, undefined, Loggers.LOG_HTTP);
916
1439
  options.rateLimiter.stop();
917
1440
  options.sessionCleanup.abort();
918
1441
  drainConnectionsOnShutdown(options.server);
@@ -925,7 +1448,7 @@ function createShutdownHandler(options) {
925
1448
  }));
926
1449
  for (const r of results) {
927
1450
  if (r.status === 'rejected') {
928
- logError('Session teardown failed during shutdown', r.reason instanceof Error ? r.reason : undefined, LOG_HTTP);
1451
+ logError('Session teardown failed during shutdown', r.reason instanceof Error ? r.reason : undefined, Loggers.LOG_HTTP);
929
1452
  }
930
1453
  }
931
1454
  }
@@ -961,7 +1484,7 @@ export async function startHttpServer() {
961
1484
  arch: process.arch,
962
1485
  hostname: hostname(),
963
1486
  nodeVersion: process.version,
964
- }, LOG_HTTP);
1487
+ }, Loggers.LOG_HTTP);
965
1488
  return {
966
1489
  port,
967
1490
  host: config.server.host,