@j0hanz/fetch-url-mcp 1.12.0 → 1.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +34 -17
  2. package/dist/http/auth.d.ts.map +1 -1
  3. package/dist/http/auth.js +61 -20
  4. package/dist/http/helpers.d.ts +1 -1
  5. package/dist/http/helpers.d.ts.map +1 -1
  6. package/dist/http/helpers.js +7 -9
  7. package/dist/http/native.d.ts.map +1 -1
  8. package/dist/http/native.js +271 -54
  9. package/dist/http/rate-limit.d.ts.map +1 -1
  10. package/dist/http/rate-limit.js +2 -1
  11. package/dist/index.js +5 -4
  12. package/dist/lib/config.d.ts +1 -1
  13. package/dist/lib/config.d.ts.map +1 -1
  14. package/dist/lib/config.js +8 -1
  15. package/dist/lib/core.d.ts +8 -4
  16. package/dist/lib/core.d.ts.map +1 -1
  17. package/dist/lib/core.js +240 -73
  18. package/dist/lib/fetch-pipeline.d.ts.map +1 -1
  19. package/dist/lib/fetch-pipeline.js +15 -2
  20. package/dist/lib/http.d.ts.map +1 -1
  21. package/dist/lib/http.js +1 -1
  22. package/dist/lib/mcp-interop.d.ts +15 -3
  23. package/dist/lib/mcp-interop.d.ts.map +1 -1
  24. package/dist/lib/mcp-interop.js +92 -23
  25. package/dist/lib/url.d.ts.map +1 -1
  26. package/dist/lib/url.js +1 -1
  27. package/dist/lib/utils.d.ts.map +1 -1
  28. package/dist/lib/utils.js +2 -2
  29. package/dist/resources/index.d.ts +4 -0
  30. package/dist/resources/index.d.ts.map +1 -1
  31. package/dist/resources/index.js +39 -4
  32. package/dist/schemas.d.ts +5 -5
  33. package/dist/schemas.d.ts.map +1 -1
  34. package/dist/schemas.js +7 -9
  35. package/dist/server.d.ts +3 -1
  36. package/dist/server.d.ts.map +1 -1
  37. package/dist/server.js +20 -11
  38. package/dist/tasks/execution.d.ts +1 -1
  39. package/dist/tasks/execution.d.ts.map +1 -1
  40. package/dist/tasks/execution.js +72 -25
  41. package/dist/tasks/handlers.d.ts.map +1 -1
  42. package/dist/tasks/handlers.js +31 -24
  43. package/dist/tasks/manager.d.ts +5 -2
  44. package/dist/tasks/manager.d.ts.map +1 -1
  45. package/dist/tasks/manager.js +58 -19
  46. package/dist/tasks/owner.d.ts +5 -0
  47. package/dist/tasks/owner.d.ts.map +1 -1
  48. package/dist/tasks/owner.js +15 -7
  49. package/dist/tasks/registry.d.ts +10 -8
  50. package/dist/tasks/registry.d.ts.map +1 -1
  51. package/dist/tasks/registry.js +27 -15
  52. package/dist/tools/fetch-url.d.ts +2 -0
  53. package/dist/tools/fetch-url.d.ts.map +1 -1
  54. package/dist/tools/fetch-url.js +76 -21
  55. package/dist/transform/dom-prep.d.ts.map +1 -1
  56. package/dist/transform/dom-prep.js +6 -6
  57. package/dist/transform/transform.d.ts.map +1 -1
  58. package/dist/transform/transform.js +17 -14
  59. package/dist/transform/worker-pool.d.ts.map +1 -1
  60. package/dist/transform/worker-pool.js +43 -3
  61. package/package.json +2 -2
@@ -7,10 +7,11 @@ import { hostname } from 'node:os';
7
7
  import process from 'node:process';
8
8
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
9
9
  import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
10
- import { composeCloseHandlers, config, createSessionStore, createSlotTracker, enableHttpMode, ensureSessionCapacity, logError, logInfo, registerMcpSessionServer, reserveSessionSlot, runWithRequestContext, startSessionCleanupLoop, } from '../lib/core.js';
10
+ import { composeCloseHandlers, config, createSessionStore, createSlotTracker, enableHttpMode, ensureSessionCapacity, logDebug, logError, logInfo, logWarn, registerMcpSessionOwnerKey, registerMcpSessionServer, reserveSessionSlot, runWithRequestContext, startSessionCleanupLoop, } from '../lib/core.js';
11
11
  import { acceptsEventStream, acceptsJsonAndEventStream, isJsonRpcBatchRequest, isMcpMessageBody, isMcpRequestBody, } from '../lib/mcp-interop.js';
12
12
  import { applyHttpServerTuning, drainConnectionsOnShutdown, isObject, toError, } from '../lib/utils.js';
13
13
  import { createMcpServerForHttpSession } from '../server.js';
14
+ import { buildAuthenticatedOwnerKey } from '../tasks/owner.js';
14
15
  import { applyInsufficientScopeAuthHeaders, applyUnauthorizedAuthHeaders, assertHttpModeConfiguration, authService, buildAuthFingerprint, buildProtectedResourceMetadataDocument, corsPolicy, DEFAULT_MCP_PROTOCOL_VERSION, ensureMcpProtocolVersion, hostOriginPolicy, isInsufficientScopeError, isOAuthMetadataEnabled, isProtectedResourceMetadataPath, SUPPORTED_MCP_PROTOCOL_VERSIONS, } from './auth.js';
15
16
  import { disableEventLoopMonitoring, isVerboseHealthRequest, resetEventLoopMonitoring, sendHealthRouteResponse, shouldHandleHealthRoute, } from './health.js';
16
17
  import { buildRequestContext, createRequestAbortSignal, createTransportAdapter, DEFAULT_BODY_LIMIT_BYTES, drainRequest, findDuplicateSingleValueHeader, getHeaderValue, getMcpSessionId, isJsonBodyError, jsonBodyReader, registerInboundBlockList, sendEmpty, sendError, sendJson, } from './helpers.js';
@@ -28,10 +29,9 @@ function resolveRequestedProtocolVersion(body) {
28
29
  const normalized = value.trim();
29
30
  if (normalized.length === 0)
30
31
  return DEFAULT_MCP_PROTOCOL_VERSION;
31
- if (!SUPPORTED_MCP_PROTOCOL_VERSIONS.has(normalized)) {
32
- return null;
33
- }
34
- return normalized;
32
+ return SUPPORTED_MCP_PROTOCOL_VERSIONS.has(normalized)
33
+ ? normalized
34
+ : DEFAULT_MCP_PROTOCOL_VERSION;
35
35
  }
36
36
  function resolveProtocolVersionHeader(req) {
37
37
  const header = getHeaderValue(req, 'mcp-protocol-version');
@@ -49,6 +49,36 @@ function isPingRequest(method) {
49
49
  function isMcpRoute(pathname) {
50
50
  return pathname === '/mcp' || pathname === '/mcp/';
51
51
  }
52
+ function logGatewayRejection(params) {
53
+ const { message, details, rpcId, ...rest } = params;
54
+ logWarn(message, {
55
+ ...rest,
56
+ ...(rpcId === null || rpcId === undefined ? {} : { rpcId }),
57
+ ...(details ?? {}),
58
+ }, 'http');
59
+ }
60
+ function resolveRequestPath(req) {
61
+ return URL.parse(req.url ?? '', 'http://localhost')?.pathname ?? '/';
62
+ }
63
+ function logRequestCompletion(params) {
64
+ const meta = {
65
+ method: params.method,
66
+ path: params.path,
67
+ statusCode: params.statusCode,
68
+ durationMs: Math.round(params.durationMs),
69
+ requestId: params.requestId,
70
+ ...(params.sessionId ? { sessionId: params.sessionId } : {}),
71
+ };
72
+ if (params.statusCode >= 500) {
73
+ logError('HTTP request completed with server error', meta, 'http');
74
+ return;
75
+ }
76
+ if (params.statusCode >= 400) {
77
+ logWarn('HTTP request completed with client error', meta, 'http');
78
+ return;
79
+ }
80
+ logDebug('HTTP request completed', meta, 'http');
81
+ }
52
82
  function createSessionTeardownOptions(mode, context) {
53
83
  switch (mode) {
54
84
  case 'ended':
@@ -71,6 +101,14 @@ function createSessionTeardownOptions(mode, context) {
71
101
  unregisterByServer: true,
72
102
  awaitClose: true,
73
103
  };
104
+ case 'init-timeout':
105
+ return {
106
+ cancelMessage: 'The task was cancelled because the MCP session did not finish initialization.',
107
+ closeTransportReason: 'session-init-timeout',
108
+ closeServerReason: 'session-init-timeout',
109
+ unregisterByServer: true,
110
+ awaitClose: true,
111
+ };
74
112
  }
75
113
  }
76
114
  class McpSessionGateway {
@@ -94,11 +132,11 @@ class McpSessionGateway {
94
132
  sendEmpty(ctx.res, 202);
95
133
  return;
96
134
  }
97
- logInfo('[MCP POST]', {
135
+ logDebug('MCP POST received', {
98
136
  method: method ?? 'response',
99
- id: body.id,
137
+ rpcId: body.id,
100
138
  sessionId,
101
- });
139
+ }, 'http');
102
140
  const transport = await this.getOrCreateTransport(ctx, requestId);
103
141
  if (!transport)
104
142
  return;
@@ -113,11 +151,20 @@ class McpSessionGateway {
113
151
  const { sessionId, session } = sessionState;
114
152
  const acceptHeader = getHeaderValue(ctx.req, 'accept');
115
153
  if (!acceptsEventStream(acceptHeader)) {
154
+ logGatewayRejection({
155
+ message: 'Rejected MCP GET request',
156
+ method: ctx.method,
157
+ path: ctx.url.pathname,
158
+ reason: 'accept_missing_event_stream',
159
+ status: 406,
160
+ sessionId,
161
+ });
116
162
  sendJson(ctx.res, 406, {
117
- error: 'Not Acceptable: expected text/event-stream',
163
+ error: 'We need you to use "text/event-stream" for this connection.',
118
164
  });
119
165
  return;
120
166
  }
167
+ logDebug('MCP GET received', { sessionId }, 'http');
121
168
  this.store.touch(sessionId);
122
169
  await session.transport.handleRequest(ctx.req, ctx.res);
123
170
  }
@@ -129,23 +176,47 @@ class McpSessionGateway {
129
176
  return;
130
177
  const { sessionId, session } = sessionState;
131
178
  await session.transport.close();
132
- this.cleanupSessionRecord(sessionId, 'session-delete');
179
+ logDebug('MCP DELETE received', { sessionId }, 'http');
180
+ this.cleanupSessionRecord(sessionId, createSessionTeardownOptions('ended', 'session-delete'));
133
181
  sendJson(ctx.res, 200, { status: 'closed' });
134
182
  }
135
183
  validatePostRequest(ctx) {
136
184
  if (!acceptsJsonAndEventStream(getHeaderValue(ctx.req, 'accept'))) {
185
+ logGatewayRejection({
186
+ message: 'Rejected MCP POST request',
187
+ method: ctx.method,
188
+ path: ctx.url.pathname,
189
+ reason: 'accept_missing_json_or_event_stream',
190
+ status: 406,
191
+ });
137
192
  sendJson(ctx.res, 406, {
138
- error: 'Not Acceptable: expected application/json and text/event-stream',
193
+ error: 'We need the request to accept both "application/json" and "text/event-stream".',
139
194
  });
140
195
  return null;
141
196
  }
142
197
  const { body } = ctx;
143
198
  if (isJsonRpcBatchRequest(body)) {
144
- sendError(ctx.res, -32600, 'Batch requests not supported');
199
+ logGatewayRejection({
200
+ message: 'Rejected MCP POST request',
201
+ method: ctx.method,
202
+ path: ctx.url.pathname,
203
+ reason: 'batch_request_not_supported',
204
+ status: 400,
205
+ mcpCode: -32600,
206
+ });
207
+ sendError(ctx.res, -32600, "We don't support batch requests yet. Please send one request at a time.");
145
208
  return null;
146
209
  }
147
210
  if (!isMcpMessageBody(body)) {
148
- sendError(ctx.res, -32600, 'Invalid request body');
211
+ logGatewayRejection({
212
+ message: 'Rejected MCP POST request',
213
+ method: ctx.method,
214
+ path: ctx.url.pathname,
215
+ reason: 'invalid_request_body',
216
+ status: 400,
217
+ mcpCode: -32600,
218
+ });
219
+ sendError(ctx.res, -32600, "The request body isn't quite right. Please check the format and try again.");
149
220
  return null;
150
221
  }
151
222
  return body;
@@ -156,7 +227,16 @@ class McpSessionGateway {
156
227
  const isInitializedMethod = method !== null && isInitializedNotification(method);
157
228
  const isInitNotification = isInitializedMethod && body.id === undefined;
158
229
  if (isInitializedMethod && !isInitNotification) {
159
- sendError(ctx.res, -32600, 'notifications/initialized must be sent as a notification', 400, requestId);
230
+ logGatewayRejection({
231
+ message: 'Rejected MCP POST request',
232
+ method: ctx.method,
233
+ path: ctx.url.pathname,
234
+ reason: 'initialized_request_must_be_notification',
235
+ status: 400,
236
+ mcpCode: -32600,
237
+ rpcId: requestId,
238
+ });
239
+ sendError(ctx.res, -32600, "The 'notifications/initialized' message must be sent as a notification, without an ID.", 400, requestId);
160
240
  return null;
161
241
  }
162
242
  const sessionState = this.getOptionalAuthenticatedSession(ctx, requestId);
@@ -187,10 +267,19 @@ class McpSessionGateway {
187
267
  return false;
188
268
  if (!session) {
189
269
  if (isInitNotification) {
190
- sendError(ctx.res, -32600, 'Missing session ID', 400, requestId);
270
+ logGatewayRejection({
271
+ message: 'Rejected MCP POST request',
272
+ method: ctx.method,
273
+ path: ctx.url.pathname,
274
+ reason: 'missing_session_id_for_initialized_notification',
275
+ status: 400,
276
+ mcpCode: -32600,
277
+ rpcId: requestId,
278
+ });
279
+ sendError(ctx.res, -32600, "We couldn't find a session ID for your request. Please ensure you have an active session.", 400, requestId);
191
280
  return false;
192
281
  }
193
- return ensureMcpProtocolVersion(ctx.req, ctx.res);
282
+ return true;
194
283
  }
195
284
  if (!this.ensureSessionProtocolVersion(ctx, session))
196
285
  return false;
@@ -200,7 +289,17 @@ class McpSessionGateway {
200
289
  return true;
201
290
  if (method !== null && isPingRequest(method))
202
291
  return true;
203
- sendError(ctx.res, -32600, 'Session not initialized', 400, requestId);
292
+ logGatewayRejection({
293
+ message: 'Rejected MCP request',
294
+ method: ctx.method,
295
+ path: ctx.url.pathname,
296
+ reason: 'session_not_initialized',
297
+ status: 400,
298
+ mcpCode: -32600,
299
+ sessionId,
300
+ rpcId: requestId,
301
+ });
302
+ sendError(ctx.res, -32600, "Your session hasn't been initialized yet. Please wait a moment and try again.", 400, requestId);
204
303
  return false;
205
304
  }
206
305
  async getOrCreateTransport(ctx, requestId) {
@@ -216,24 +315,54 @@ class McpSessionGateway {
216
315
  }
217
316
  getInitializeProtocolVersion(ctx, requestId) {
218
317
  if (!isMcpRequestBody(ctx.body)) {
219
- sendError(ctx.res, -32600, 'Missing session ID', 400, requestId);
318
+ logGatewayRejection({
319
+ message: 'Rejected MCP initialize request',
320
+ method: ctx.method,
321
+ path: ctx.url.pathname,
322
+ reason: 'missing_session_id',
323
+ status: 400,
324
+ mcpCode: -32600,
325
+ rpcId: requestId,
326
+ });
327
+ sendError(ctx.res, -32600, "We couldn't find a session ID for your request. Please ensure you have an active session.", 400, requestId);
220
328
  return null;
221
329
  }
222
330
  if (!isInitializeRequest(ctx.body)) {
223
- sendError(ctx.res, ctx.body.method === 'initialize' ? -32602 : -32600, ctx.body.method === 'initialize'
224
- ? 'Invalid initialize request'
225
- : 'Missing session ID', 400, requestId);
331
+ const invalidInitialize = ctx.body.method === 'initialize';
332
+ logGatewayRejection({
333
+ message: 'Rejected MCP initialize request',
334
+ method: ctx.method,
335
+ path: ctx.url.pathname,
336
+ reason: invalidInitialize
337
+ ? 'invalid_initialize_request'
338
+ : 'missing_session_id',
339
+ status: 400,
340
+ mcpCode: invalidInitialize ? -32602 : -32600,
341
+ rpcId: requestId,
342
+ });
343
+ sendError(ctx.res, invalidInitialize ? -32602 : -32600, invalidInitialize
344
+ ? 'The initialize request format is invalid. Please double-check your parameters.'
345
+ : "We couldn't find a session ID for your request. Please ensure you have an active session.", 400, requestId);
226
346
  return null;
227
347
  }
228
348
  const negotiatedProtocolVersion = resolveRequestedProtocolVersion(ctx.body);
229
- if (!negotiatedProtocolVersion) {
230
- sendError(ctx.res, -32602, `Unsupported protocolVersion; supported versions: ${[...SUPPORTED_MCP_PROTOCOL_VERSIONS].join(', ')}`, 400, requestId);
231
- return null;
232
- }
233
349
  const headerProtocolVersion = resolveProtocolVersionHeader(ctx.req);
234
350
  if (headerProtocolVersion &&
235
351
  headerProtocolVersion !== negotiatedProtocolVersion) {
236
- sendError(ctx.res, -32600, `initialize protocolVersion mismatch: header=${headerProtocolVersion}, body=${negotiatedProtocolVersion}`, 400, requestId);
352
+ logGatewayRejection({
353
+ message: 'Rejected MCP initialize request',
354
+ method: ctx.method,
355
+ path: ctx.url.pathname,
356
+ reason: 'protocol_version_mismatch',
357
+ status: 400,
358
+ mcpCode: -32600,
359
+ rpcId: requestId,
360
+ details: {
361
+ headerProtocolVersion,
362
+ negotiatedProtocolVersion,
363
+ },
364
+ });
365
+ sendError(ctx.res, -32600, `There's a mismatch in the protocol version. The header says '${headerProtocolVersion}' but the body says '${negotiatedProtocolVersion}'.`, 400, requestId);
237
366
  return null;
238
367
  }
239
368
  return negotiatedProtocolVersion;
@@ -261,13 +390,13 @@ class McpSessionGateway {
261
390
  return null;
262
391
  const { sessionId, session } = state;
263
392
  if (!sessionId || !session) {
264
- sendError(ctx.res, -32600, 'Missing session ID', 400, requestId);
393
+ sendError(ctx.res, -32600, "We couldn't find a session ID for your request. Please ensure you have an active session.", 400, requestId);
265
394
  return null;
266
395
  }
267
396
  if (!this.ensureSessionProtocolVersion(ctx, session))
268
397
  return null;
269
398
  if (options?.requireInitialized && !session.protocolInitialized) {
270
- sendError(ctx.res, -32600, 'Session not initialized', 400, requestId);
399
+ sendError(ctx.res, -32600, "Your session hasn't been initialized yet. Please wait a moment and try again.", 400, requestId);
271
400
  return null;
272
401
  }
273
402
  return { sessionId, session };
@@ -275,11 +404,29 @@ class McpSessionGateway {
275
404
  getAuthenticatedSessionById(sessionId, authFingerprint, res, requestId = null) {
276
405
  const session = this.store.get(sessionId);
277
406
  if (!session) {
278
- sendError(res, -32600, 'Session not found', 404, requestId);
407
+ logGatewayRejection({
408
+ message: 'Rejected MCP session request',
409
+ path: '/mcp',
410
+ reason: 'session_not_found',
411
+ status: 404,
412
+ mcpCode: -32600,
413
+ sessionId,
414
+ rpcId: requestId,
415
+ });
416
+ sendError(res, -32600, "We couldn't find your session. It might have expired or been closed.", 404, requestId);
279
417
  return null;
280
418
  }
281
419
  if (!authFingerprint || session.authFingerprint !== authFingerprint) {
282
- sendError(res, -32600, 'Session not found', 404, requestId);
420
+ logGatewayRejection({
421
+ message: 'Rejected MCP session request',
422
+ path: '/mcp',
423
+ reason: 'session_auth_mismatch',
424
+ status: 404,
425
+ mcpCode: -32600,
426
+ sessionId,
427
+ rpcId: requestId,
428
+ });
429
+ sendError(res, -32600, "We couldn't find your session. It might have expired or been closed.", 404, requestId);
283
430
  return null;
284
431
  }
285
432
  return session;
@@ -296,6 +443,7 @@ class McpSessionGateway {
296
443
  this.clearSessionInitTimeout(sessionId);
297
444
  if (sessionId)
298
445
  this.store.touch(sessionId);
446
+ logDebug('Session initialized', { sessionId }, 'session');
299
447
  }
300
448
  createSessionInitTimeout(sessionId, tracker, unpublishedSession) {
301
449
  const initTimeout = setTimeout(() => {
@@ -305,9 +453,11 @@ class McpSessionGateway {
305
453
  this.clearSessionInitTimeout(sessionId);
306
454
  return;
307
455
  }
308
- this.cleanupSessionRecord(sessionId, 'session-init-timeout');
456
+ logWarn('Session init timeout', { sessionId }, 'session');
457
+ this.cleanupSessionRecord(sessionId, createSessionTeardownOptions('init-timeout'));
309
458
  return;
310
459
  }
460
+ logWarn('Session init timeout before registration completed', { sessionId }, 'session');
311
461
  tracker.releaseSlot();
312
462
  void teardownUnregisteredSessionResources(unpublishedSession, 'session-init-timeout');
313
463
  }, config.server.sessionInitTimeoutMs);
@@ -327,6 +477,10 @@ class McpSessionGateway {
327
477
  await sessionServer.connect(transport);
328
478
  }
329
479
  catch (err) {
480
+ logWarn('Session transport connect failed', {
481
+ sessionId,
482
+ error: toError(err).message,
483
+ }, 'session');
330
484
  clearTimeout(initTimeout);
331
485
  tracker.releaseSlot();
332
486
  void teardownUnregisteredSessionResources(unpublishedSession, 'session-connect-failed');
@@ -337,7 +491,20 @@ class McpSessionGateway {
337
491
  async createNewSession(ctx, requestId, negotiatedProtocolVersion) {
338
492
  const authFingerprint = buildAuthFingerprint(ctx.auth);
339
493
  if (!authFingerprint) {
340
- sendError(ctx.res, -32603, 'Missing auth context', 500, requestId);
494
+ logError('Session creation failed: missing auth context', {
495
+ path: ctx.url.pathname,
496
+ method: ctx.method,
497
+ }, 'session');
498
+ sendError(ctx.res, -32603, "We're missing some authorization details to process this request.", 500, requestId);
499
+ return null;
500
+ }
501
+ const ownerKey = buildAuthenticatedOwnerKey(ctx.auth);
502
+ if (!ownerKey) {
503
+ logError('Session creation failed: missing task owner context', {
504
+ path: ctx.url.pathname,
505
+ method: ctx.method,
506
+ }, 'session');
507
+ sendError(ctx.res, -32603, "We're missing the owner information needed to authorize this request.", 500, requestId);
341
508
  return null;
342
509
  }
343
510
  if (!this.reserveCapacity(ctx.res, requestId))
@@ -349,6 +516,7 @@ class McpSessionGateway {
349
516
  sessionServer = await this.createSessionServer();
350
517
  }
351
518
  catch (error) {
519
+ logError('Session server creation failed', { sessionId: newSessionId, error: toError(error).message }, 'session');
352
520
  tracker.releaseSlot();
353
521
  throw error;
354
522
  }
@@ -363,6 +531,7 @@ class McpSessionGateway {
363
531
  const isConnected = await this.connectTransport(sessionServer, transportImpl, initTimeout, tracker, unpublishedSession, newSessionId);
364
532
  tracker.releaseSlot();
365
533
  if (!isConnected) {
534
+ logWarn('Session closed before registration completed', { sessionId: newSessionId }, 'session');
366
535
  void teardownUnregisteredSessionResources(unpublishedSession, 'session-closed-during-connect');
367
536
  return null;
368
537
  }
@@ -376,18 +545,24 @@ class McpSessionGateway {
376
545
  authFingerprint,
377
546
  });
378
547
  this.sessionInitTimeouts.set(newSessionId, initTimeout);
548
+ registerMcpSessionOwnerKey(newSessionId, ownerKey);
379
549
  registerMcpSessionServer(newSessionId, sessionServer);
550
+ logInfo('Session created', { sessionId: newSessionId, negotiatedProtocolVersion }, 'session');
380
551
  transportImpl.onclose = composeCloseHandlers(transportImpl.onclose, () => {
381
- this.cleanupSessionRecord(newSessionId, 'session-close');
552
+ this.cleanupSessionRecord(newSessionId, createSessionTeardownOptions('ended', 'session-close'));
382
553
  });
383
554
  return transportImpl;
384
555
  }
385
- cleanupSessionRecord(sessionId, context) {
556
+ cleanupSessionRecord(sessionId, teardownOptions) {
557
+ const context = teardownOptions.closeTransportReason ??
558
+ teardownOptions.closeServerReason ??
559
+ 'session';
560
+ logDebug('Session cleanup', { sessionId, context }, 'session');
386
561
  this.clearSessionInitTimeout(sessionId);
387
562
  const session = this.store.remove(sessionId);
388
563
  if (!session)
389
564
  return;
390
- void teardownSessionResources(session, createSessionTeardownOptions('ended', context));
565
+ void teardownSessionResources(session, teardownOptions);
391
566
  }
392
567
  clearSessionInitTimeout(sessionId) {
393
568
  if (!sessionId)
@@ -412,12 +587,14 @@ class McpSessionGateway {
412
587
  },
413
588
  });
414
589
  if (!allowed) {
415
- sendError(res, -32000, 'Server busy', 503, requestId);
590
+ logWarn('Session capacity exhausted', { maxSessions: config.server.maxSessions }, 'session');
591
+ sendError(res, -32000, 'The server is currently too busy to handle your request. Please try again in a little while.', 503, requestId);
416
592
  return false;
417
593
  }
418
594
  // Double-check: capacity may have changed during the async eviction window above.
419
595
  if (!reserveSessionSlot(this.store, config.server.maxSessions)) {
420
- sendError(res, -32000, 'Server busy', 503, requestId);
596
+ logWarn('Session capacity exhausted (post-eviction)', { maxSessions: config.server.maxSessions }, 'session');
597
+ sendError(res, -32000, 'The server is currently too busy to handle your request. Please try again in a little while.', 503, requestId);
421
598
  return false;
422
599
  }
423
600
  return true;
@@ -472,14 +649,23 @@ class HttpDispatcher {
472
649
  const handled = await this.handleMcpRoutes(authCtx);
473
650
  if (handled)
474
651
  return;
652
+ ctx.res.setHeader('Allow', 'DELETE, GET, OPTIONS, POST');
653
+ sendJson(ctx.res, 405, {
654
+ error: "Looks like you tried to use a method that isn't allowed here.",
655
+ });
656
+ return;
475
657
  }
476
- sendJson(ctx.res, 404, { error: 'Not Found' });
658
+ sendJson(ctx.res, 404, {
659
+ error: "We couldn't find what you were looking for.",
660
+ });
477
661
  }
478
662
  catch (err) {
479
663
  const error = toError(err);
480
- logError('Request failed', error);
664
+ logError('Request failed', error, 'http');
481
665
  if (!ctx.res.writableEnded) {
482
- sendJson(ctx.res, 500, { error: 'Internal Server Error' });
666
+ sendJson(ctx.res, 500, {
667
+ error: "Something went wrong on our end. We're looking into it!",
668
+ });
483
669
  }
484
670
  }
485
671
  }
@@ -504,6 +690,7 @@ class HttpDispatcher {
504
690
  }
505
691
  catch (err) {
506
692
  const message = err instanceof Error ? err.message : 'Unauthorized';
693
+ logWarn('Authentication failed', { message, method: ctx.method, path: ctx.url.pathname }, 'auth');
507
694
  if (isInsufficientScopeError(err)) {
508
695
  applyInsufficientScopeAuthHeaders(ctx.req, ctx.res, err.requiredScopes, message);
509
696
  sendError(ctx.res, -32000, message, 403);
@@ -521,21 +708,21 @@ class HttpDispatcher {
521
708
  const DEFAULT_BODY_ERROR = {
522
709
  statusCode: 400,
523
710
  mcpCode: -32700,
524
- mcpMsg: 'Parse error',
525
- restMsg: 'Invalid JSON',
711
+ mcpMsg: "We couldn't parse the request body. Please ensure it's valid JSON.",
712
+ restMsg: "The request body doesn't seem to be valid JSON. Please check and try again.",
526
713
  };
527
714
  const BODY_PARSE_ERRORS = {
528
715
  'payload-too-large': {
529
716
  statusCode: 413,
530
717
  mcpCode: -32600,
531
- mcpMsg: 'Request body too large',
532
- restMsg: 'Payload too large',
718
+ mcpMsg: 'The request body is too large. Please send a smaller payload.',
719
+ restMsg: 'That request is a bit too big for us to handle right now.',
533
720
  },
534
721
  'read-failed': {
535
722
  statusCode: 400,
536
723
  mcpCode: -32600,
537
- mcpMsg: 'Request body read failed',
538
- restMsg: 'Invalid JSON',
724
+ mcpMsg: 'We ran into an issue reading the request. Please try sending it again.',
725
+ restMsg: "The request body doesn't seem to be valid JSON. Please check and try again.",
539
726
  },
540
727
  default: DEFAULT_BODY_ERROR,
541
728
  };
@@ -562,6 +749,18 @@ class HttpRequestPipeline {
562
749
  const requestId = getHeaderValue(rawReq, 'x-request-id') ?? randomUUID();
563
750
  const sessionId = getMcpSessionId(rawReq) ?? undefined;
564
751
  const { signal, cleanup } = createRequestAbortSignal(rawReq);
752
+ const path = resolveRequestPath(rawReq);
753
+ const startTime = performance.now();
754
+ rawRes.once('finish', () => {
755
+ logRequestCompletion({
756
+ path,
757
+ statusCode: rawRes.statusCode,
758
+ durationMs: performance.now() - startTime,
759
+ requestId,
760
+ ...(rawReq.method ? { method: rawReq.method } : {}),
761
+ ...(sessionId ? { sessionId } : {}),
762
+ });
763
+ });
565
764
  try {
566
765
  await runWithRequestContext({
567
766
  requestId,
@@ -588,8 +787,16 @@ class HttpRequestPipeline {
588
787
  const duplicateHeader = findDuplicateSingleValueHeader(rawReq);
589
788
  if (!duplicateHeader)
590
789
  return false;
790
+ logGatewayRejection({
791
+ message: 'Rejected HTTP request',
792
+ method: rawReq.method,
793
+ path: resolveRequestPath(rawReq),
794
+ reason: 'duplicate_single_value_header',
795
+ status: 400,
796
+ details: { header: duplicateHeader },
797
+ });
591
798
  sendJson(rawRes, 400, {
592
- error: `Duplicate ${duplicateHeader} header is not allowed`,
799
+ error: `It seems the '${duplicateHeader}' header was sent multiple times when it should only be sent once.`,
593
800
  });
594
801
  drainRequest(rawReq);
595
802
  return true;
@@ -611,7 +818,9 @@ class HttpRequestPipeline {
611
818
  return false;
612
819
  }
613
820
  if (!this.rateLimiter.check(ctx)) {
614
- sendJson(ctx.res, 429, { error: 'Too Many Requests' });
821
+ sendJson(ctx.res, 429, {
822
+ error: "You're sending requests a bit too quickly. Please slow down and try again.",
823
+ });
615
824
  drainRequest(rawReq);
616
825
  return false;
617
826
  }
@@ -628,6 +837,12 @@ class HttpRequestPipeline {
628
837
  }
629
838
  catch (error) {
630
839
  const bodyErrorKind = isJsonBodyError(error) ? error.kind : null;
840
+ if (bodyErrorKind === 'payload-too-large') {
841
+ logWarn('The request body is too large. Please send a smaller payload.', { method: ctx.method, path: ctx.url.pathname }, 'http');
842
+ }
843
+ else if (bodyErrorKind === 'read-failed' || bodyErrorKind === null) {
844
+ logError('Request body parsing failed', toError(error), 'http');
845
+ }
631
846
  sendBodyParseError(ctx, bodyErrorKind, rawReq);
632
847
  return false;
633
848
  }
@@ -648,11 +863,13 @@ class HttpRequestPipeline {
648
863
  // Server bootstrap
649
864
  // ---------------------------------------------------------------------------
650
865
  function handlePipelineError(error, res) {
651
- logError('Request pipeline failed', toError(error));
866
+ logError('Request pipeline failed', toError(error), 'http');
652
867
  if (res.writableEnded)
653
868
  return;
654
869
  if (!res.headersSent) {
655
- sendJson(res, 500, { error: 'Internal Server Error' });
870
+ sendJson(res, 500, {
871
+ error: "Something went wrong on our end. We're looking into it!",
872
+ });
656
873
  return;
657
874
  }
658
875
  res.end();
@@ -694,7 +911,7 @@ function resolveListeningPort(server, fallback) {
694
911
  function createShutdownHandler(options) {
695
912
  const closeBatchSize = 10;
696
913
  return async (signal) => {
697
- logInfo(`Stopping HTTP server (${signal})...`);
914
+ logInfo(`Stopping HTTP server (${signal})...`, undefined, 'http');
698
915
  options.rateLimiter.stop();
699
916
  options.sessionCleanup.abort();
700
917
  drainConnectionsOnShutdown(options.server);
@@ -707,7 +924,7 @@ function createShutdownHandler(options) {
707
924
  }));
708
925
  for (const r of results) {
709
926
  if (r.status === 'rejected') {
710
- logError('Session teardown failed during shutdown', r.reason instanceof Error ? r.reason : undefined);
927
+ logError('Session teardown failed during shutdown', r.reason instanceof Error ? r.reason : undefined, 'http');
711
928
  }
712
929
  }
713
930
  }
@@ -722,7 +939,7 @@ export async function startHttpServer() {
722
939
  const sessionStore = createSessionStore(config.server.sessionTtlMs);
723
940
  const sessionCleanup = startSessionCleanupLoop(sessionStore, config.server.sessionTtlMs, {
724
941
  onEvictSession: (session) => {
725
- teardownSessionRegistration(session.server, 'The task was cancelled because the MCP session expired.');
942
+ teardownSessionRegistration(session.server);
726
943
  },
727
944
  });
728
945
  const mcpGateway = new McpSessionGateway(sessionStore, createMcpServerForHttpSession);
@@ -743,7 +960,7 @@ export async function startHttpServer() {
743
960
  arch: process.arch,
744
961
  hostname: hostname(),
745
962
  nodeVersion: process.version,
746
- });
963
+ }, 'http');
747
964
  return {
748
965
  port,
749
966
  host: config.server.host,
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/http/rate-limit.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,cAAc,EAAY,MAAM,cAAc,CAAC;AAY7D,UAAU,eAAe;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC;IACpC,IAAI,IAAI,IAAI,CAAC;CACd;AA0FD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,GACvB,oBAAoB,CAEtB"}
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/http/rate-limit.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,cAAc,EAAY,MAAM,cAAc,CAAC;AAY7D,UAAU,eAAe;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC;IACpC,IAAI,IAAI,IAAI,CAAC;CACd;AA2FD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,GACvB,oBAAoB,CAEtB"}
@@ -21,7 +21,7 @@ class RateLimiter {
21
21
  },
22
22
  onError: (err) => {
23
23
  if (!isAbortError(err)) {
24
- logWarn('Rate limit cleanup failed', { error: err });
24
+ logWarn('Rate limit cleanup failed', { error: err }, 'rate-limit');
25
25
  }
26
26
  },
27
27
  });
@@ -71,6 +71,7 @@ class RateLimiter {
71
71
  this.store.set(key, entry);
72
72
  }
73
73
  if (entry.count > this.options.maxRequests) {
74
+ logWarn('Rate limit exceeded', { ip: key }, 'rate-limit');
74
75
  const retryAfter = Math.max(1, Math.ceil((entry.resetTime - now) / 1000));
75
76
  ctx.res.setHeader('Retry-After', String(retryAfter));
76
77
  sendJson(ctx.res, 429, { error: 'Rate limit exceeded', retryAfter });