@j0hanz/fetch-url-mcp 1.12.8 → 1.12.10
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.
- package/README.md +2 -0
- package/dist/http/auth.d.ts +2 -2
- package/dist/http/auth.d.ts.map +1 -1
- package/dist/http/auth.js +42 -24
- package/dist/http/index.d.ts +0 -2
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +2 -2
- package/dist/http/native.d.ts +4 -1
- package/dist/http/native.d.ts.map +1 -1
- package/dist/http/native.js +171 -98
- package/dist/http/rate-limit.d.ts +11 -3
- package/dist/http/rate-limit.d.ts.map +1 -1
- package/dist/http/rate-limit.js +19 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +25 -3
- package/dist/lib/core.d.ts +12 -2
- package/dist/lib/core.d.ts.map +1 -1
- package/dist/lib/core.js +166 -87
- package/dist/lib/error/classes.d.ts.map +1 -1
- package/dist/lib/error/classes.js +4 -2
- package/dist/lib/error/classify.d.ts.map +1 -1
- package/dist/lib/error/classify.js +20 -16
- package/dist/lib/error/index.d.ts.map +1 -1
- package/dist/lib/error/index.js +2 -0
- package/dist/lib/mcp-interop.d.ts +12 -0
- package/dist/lib/mcp-interop.d.ts.map +1 -1
- package/dist/lib/mcp-interop.js +20 -2
- package/dist/lib/net/http.js +2 -1
- package/dist/lib/net/index.d.ts.map +1 -1
- package/dist/lib/net/index.js +2 -0
- package/dist/lib/net/pipeline.d.ts.map +1 -1
- package/dist/lib/net/pipeline.js +10 -9
- package/dist/lib/net/url.d.ts +1 -2
- package/dist/lib/net/url.d.ts.map +1 -1
- package/dist/lib/net/url.js +9 -6
- package/dist/lib/utils.d.ts +0 -16
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +3 -51
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +49 -22
- package/dist/schemas.d.ts +9 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -1
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +2 -0
- package/dist/tasks/manager.d.ts +9 -106
- package/dist/tasks/manager.d.ts.map +1 -1
- package/dist/tasks/manager.js +200 -553
- package/dist/tasks/store.d.ts +104 -0
- package/dist/tasks/store.d.ts.map +1 -0
- package/dist/tasks/store.js +480 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -4
- package/dist/transform/index.d.ts +1 -85
- package/dist/transform/index.d.ts.map +1 -1
- package/dist/transform/index.js +312 -942
- package/dist/transform/worker-pool.d.ts +19 -0
- package/dist/transform/worker-pool.d.ts.map +1 -0
- package/dist/transform/worker-pool.js +713 -0
- package/package.json +3 -1
package/dist/http/native.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
2
|
-
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { ErrorCode, isInitializeRequest, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { once } from 'node:events';
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
@@ -10,16 +10,21 @@ import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
|
|
|
10
10
|
import process from 'node:process';
|
|
11
11
|
import { Writable } from 'node:stream';
|
|
12
12
|
import { pipeline } from 'node:stream/promises';
|
|
13
|
-
import {
|
|
13
|
+
import { config, enableHttpMode, serverVersion } from '../lib/config.js';
|
|
14
|
+
import { composeCloseHandlers, createSessionStore, createSlotTracker, ensureSessionCapacity, logDebug, logError, Loggers, logInfo, logWarn, registerMcpSessionOwnerKey, registerMcpSessionServer, reserveSessionSlot, resolveMcpSessionIdByServer, runWithRequestContext, runWithTraceContext, startSessionCleanupLoop, unregisterMcpSessionServer, unregisterMcpSessionServerByServer, } from '../lib/core.js';
|
|
14
15
|
import { getErrorMessage, toError } from '../lib/error/index.js';
|
|
15
|
-
import { acceptsEventStream, acceptsJsonAndEventStream, isMcpRequestBody, } from '../lib/mcp-interop.js';
|
|
16
|
+
import { acceptsEventStream, acceptsJsonAndEventStream, isMcpRequestBody, sendJsonRpcError, } from '../lib/mcp-interop.js';
|
|
16
17
|
import { createDefaultBlockList, normalizeIpForBlockList, } from '../lib/net/index.js';
|
|
17
|
-
import {
|
|
18
|
+
import { isObject } from '../lib/utils.js';
|
|
18
19
|
import { createMcpServerForHttpSession } from '../server.js';
|
|
19
20
|
import { buildAuthenticatedOwnerKey } from '../tasks/index.js';
|
|
20
21
|
import { getTransformPoolStats } from '../transform/index.js';
|
|
21
|
-
import { applyInsufficientScopeAuthHeaders, applyUnauthorizedAuthHeaders, assertHttpModeConfiguration, authService, buildAuthFingerprint, buildProtectedResourceMetadataDocument, corsPolicy,
|
|
22
|
-
import {
|
|
22
|
+
import { applyInsufficientScopeAuthHeaders, applyUnauthorizedAuthHeaders, assertHttpModeConfiguration, authService, buildAuthFingerprint, buildProtectedResourceMetadataDocument, corsPolicy, ensureMcpProtocolVersion, hostOriginPolicy, isInsufficientScopeError, isOAuthMetadataEnabled, isProtectedResourceMetadataPath, SUPPORTED_MCP_PROTOCOL_VERSIONS, } from './auth.js';
|
|
23
|
+
import { RateLimiter } from './rate-limit.js';
|
|
24
|
+
const DROP_LOG_INTERVAL_MS = 10_000;
|
|
25
|
+
const MISSING_SESSION_ID_MESSAGE = "We couldn't find a session ID for your request. Please ensure you have an active session.";
|
|
26
|
+
const SESSION_NOT_INITIALIZED_MESSAGE = "Your session hasn't been initialized yet. Please wait a moment and try again.";
|
|
27
|
+
const SESSION_NOT_FOUND_MESSAGE = "We couldn't find your session. It might have expired or been closed.";
|
|
23
28
|
function abortControllerBestEffort(controller) {
|
|
24
29
|
if (!controller.signal.aborted)
|
|
25
30
|
controller.abort();
|
|
@@ -32,6 +37,47 @@ function destroyRequestBestEffort(req) {
|
|
|
32
37
|
// Best-effort only.
|
|
33
38
|
}
|
|
34
39
|
}
|
|
40
|
+
function applyHttpServerTuning(server) {
|
|
41
|
+
const { headersTimeoutMs, requestTimeoutMs, keepAliveTimeoutMs, keepAliveTimeoutBufferMs, maxHeadersCount, maxConnections, } = config.server.http;
|
|
42
|
+
if (headersTimeoutMs !== undefined)
|
|
43
|
+
server.headersTimeout = headersTimeoutMs;
|
|
44
|
+
if (requestTimeoutMs !== undefined)
|
|
45
|
+
server.requestTimeout = requestTimeoutMs;
|
|
46
|
+
if (keepAliveTimeoutMs !== undefined)
|
|
47
|
+
server.keepAliveTimeout = keepAliveTimeoutMs;
|
|
48
|
+
if (keepAliveTimeoutBufferMs !== undefined)
|
|
49
|
+
server.keepAliveTimeoutBuffer = keepAliveTimeoutBufferMs;
|
|
50
|
+
if (maxHeadersCount !== undefined)
|
|
51
|
+
server.maxHeadersCount = maxHeadersCount;
|
|
52
|
+
if (typeof maxConnections === 'number' && maxConnections > 0) {
|
|
53
|
+
server.maxConnections = maxConnections;
|
|
54
|
+
server.dropMaxConnection = true;
|
|
55
|
+
if (typeof server.on === 'function') {
|
|
56
|
+
let lastLoggedAt = 0;
|
|
57
|
+
let droppedSinceLastLog = 0;
|
|
58
|
+
const onDrop = (data) => {
|
|
59
|
+
droppedSinceLastLog += 1;
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
if (now - lastLoggedAt < DROP_LOG_INTERVAL_MS)
|
|
62
|
+
return;
|
|
63
|
+
logWarn('Incoming connection dropped (maxConnections reached)', {
|
|
64
|
+
maxConnections,
|
|
65
|
+
dropped: droppedSinceLastLog,
|
|
66
|
+
data,
|
|
67
|
+
}, Loggers.LOG_HTTP);
|
|
68
|
+
lastLoggedAt = now;
|
|
69
|
+
droppedSinceLastLog = 0;
|
|
70
|
+
};
|
|
71
|
+
server.on('drop', onDrop);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function drainConnectionsOnShutdown(server) {
|
|
76
|
+
if (typeof server.closeIdleConnections === 'function') {
|
|
77
|
+
server.closeIdleConnections();
|
|
78
|
+
logDebug('Closed idle HTTP connections during shutdown', undefined, Loggers.LOG_HTTP);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
35
81
|
// ---------------------------------------------------------------------------
|
|
36
82
|
// Response helpers
|
|
37
83
|
// ---------------------------------------------------------------------------
|
|
@@ -50,10 +96,8 @@ export function sendEmpty(res, status) {
|
|
|
50
96
|
res.setHeader('Content-Length', '0');
|
|
51
97
|
res.end();
|
|
52
98
|
}
|
|
53
|
-
export function sendError(res,
|
|
54
|
-
|
|
55
|
-
_id) {
|
|
56
|
-
sendJson(res, status, { error: message });
|
|
99
|
+
export function sendError(res, code, message, status = 400, id) {
|
|
100
|
+
sendJsonRpcError(res, status, code, message, id ?? null);
|
|
57
101
|
}
|
|
58
102
|
// ---------------------------------------------------------------------------
|
|
59
103
|
// Request helpers
|
|
@@ -262,18 +306,20 @@ function isRequestReadAborted(req) {
|
|
|
262
306
|
return req.destroyed && !req.complete;
|
|
263
307
|
}
|
|
264
308
|
class JsonBodyReader {
|
|
309
|
+
validateContentLength(req, limit) {
|
|
310
|
+
const contentLengthHeader = getHeaderValue(req, 'content-length');
|
|
311
|
+
if (!contentLengthHeader)
|
|
312
|
+
return;
|
|
313
|
+
const contentLength = Number.parseInt(contentLengthHeader, 10);
|
|
314
|
+
if (Number.isFinite(contentLength) && contentLength > limit) {
|
|
315
|
+
throw new JsonBodyError('payload-too-large', 'Payload too large');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
265
318
|
async read(req, limit = DEFAULT_BODY_LIMIT_BYTES, signal) {
|
|
266
319
|
const contentType = getHeaderValue(req, 'content-type');
|
|
267
320
|
if (!contentType?.includes('application/json'))
|
|
268
321
|
return undefined;
|
|
269
|
-
|
|
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
|
-
}
|
|
322
|
+
this.validateContentLength(req, limit);
|
|
277
323
|
if (signal?.aborted || isRequestReadAborted(req)) {
|
|
278
324
|
const error = new JsonBodyError('read-failed', 'Request aborted');
|
|
279
325
|
throw error;
|
|
@@ -285,24 +331,35 @@ class JsonBodyReader {
|
|
|
285
331
|
return JSON.parse(body);
|
|
286
332
|
}
|
|
287
333
|
catch (err) {
|
|
288
|
-
|
|
289
|
-
throw error;
|
|
334
|
+
throw new JsonBodyError('invalid-json', getErrorMessage(err));
|
|
290
335
|
}
|
|
291
336
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (signal
|
|
299
|
-
|
|
300
|
-
abortListener();
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
signal.addEventListener('abort', abortListener, { once: true });
|
|
304
|
-
}
|
|
337
|
+
setupAbortListener(req, signal) {
|
|
338
|
+
if (signal == null)
|
|
339
|
+
return null;
|
|
340
|
+
const listener = () => {
|
|
341
|
+
destroyRequestBestEffort(req);
|
|
342
|
+
};
|
|
343
|
+
if (signal.aborted) {
|
|
344
|
+
listener();
|
|
305
345
|
}
|
|
346
|
+
else {
|
|
347
|
+
signal.addEventListener('abort', listener, { once: true });
|
|
348
|
+
}
|
|
349
|
+
return listener;
|
|
350
|
+
}
|
|
351
|
+
cleanupAbortListener(signal, listener) {
|
|
352
|
+
if (!signal || !listener)
|
|
353
|
+
return;
|
|
354
|
+
try {
|
|
355
|
+
signal.removeEventListener('abort', listener);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// Best-effort cleanup.
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async readBody(req, limit, signal) {
|
|
362
|
+
const abortListener = this.setupAbortListener(req, signal);
|
|
306
363
|
try {
|
|
307
364
|
const { chunks, size } = await this.collectChunks(req, limit, signal);
|
|
308
365
|
if (chunks.length === 0)
|
|
@@ -317,14 +374,7 @@ class JsonBodyReader {
|
|
|
317
374
|
return text;
|
|
318
375
|
}
|
|
319
376
|
finally {
|
|
320
|
-
|
|
321
|
-
try {
|
|
322
|
-
signal.removeEventListener('abort', abortListener);
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
// Best-effort cleanup.
|
|
326
|
-
}
|
|
327
|
-
}
|
|
377
|
+
this.cleanupAbortListener(signal, abortListener);
|
|
328
378
|
}
|
|
329
379
|
}
|
|
330
380
|
async collectChunks(req, limit, signal) {
|
|
@@ -386,7 +436,7 @@ function unregisterSessionTaskScope(server) {
|
|
|
386
436
|
unregisterMcpSessionServer(sessionId);
|
|
387
437
|
return sessionId;
|
|
388
438
|
}
|
|
389
|
-
|
|
439
|
+
function buildCloseTasks(session, options) {
|
|
390
440
|
const closeTasks = [];
|
|
391
441
|
if (options.closeTransportReason) {
|
|
392
442
|
closeTasks.push(closeTransportBestEffort(session.transport, options.closeTransportReason));
|
|
@@ -394,6 +444,10 @@ async function closeSessionResources(session, options) {
|
|
|
394
444
|
if (options.closeServerReason) {
|
|
395
445
|
closeTasks.push(closeMcpServerBestEffort(session.server, options.closeServerReason));
|
|
396
446
|
}
|
|
447
|
+
return closeTasks;
|
|
448
|
+
}
|
|
449
|
+
async function closeSessionResources(session, options) {
|
|
450
|
+
const closeTasks = buildCloseTasks(session, options);
|
|
397
451
|
if (options.awaitClose && closeTasks.length > 0) {
|
|
398
452
|
await Promise.all(closeTasks);
|
|
399
453
|
}
|
|
@@ -564,19 +618,15 @@ export function sendHealthRouteResponse(store, ctx, authPresent) {
|
|
|
564
618
|
}
|
|
565
619
|
function resolveRequestedProtocolVersion(body) {
|
|
566
620
|
if (!isObject(body))
|
|
567
|
-
return
|
|
621
|
+
return '';
|
|
568
622
|
const { params } = body;
|
|
569
623
|
if (!isObject(params))
|
|
570
|
-
return
|
|
624
|
+
return '';
|
|
571
625
|
const { protocolVersion: value } = params;
|
|
572
626
|
if (typeof value !== 'string')
|
|
573
|
-
return
|
|
627
|
+
return '';
|
|
574
628
|
const normalized = value.trim();
|
|
575
|
-
|
|
576
|
-
return DEFAULT_MCP_PROTOCOL_VERSION;
|
|
577
|
-
return SUPPORTED_MCP_PROTOCOL_VERSIONS.has(normalized)
|
|
578
|
-
? normalized
|
|
579
|
-
: DEFAULT_MCP_PROTOCOL_VERSION;
|
|
629
|
+
return normalized;
|
|
580
630
|
}
|
|
581
631
|
function resolveProtocolVersionHeader(req) {
|
|
582
632
|
const header = getHeaderValue(req, 'mcp-protocol-version');
|
|
@@ -588,6 +638,15 @@ function resolveProtocolVersionHeader(req) {
|
|
|
588
638
|
function isInitializedNotification(method) {
|
|
589
639
|
return method === 'notifications/initialized';
|
|
590
640
|
}
|
|
641
|
+
function getBodyMeta(body) {
|
|
642
|
+
if (!isObject(body))
|
|
643
|
+
return undefined;
|
|
644
|
+
const { params } = body;
|
|
645
|
+
if (!isObject(params))
|
|
646
|
+
return undefined;
|
|
647
|
+
const meta = params['_meta'];
|
|
648
|
+
return isObject(meta) ? meta : undefined;
|
|
649
|
+
}
|
|
591
650
|
function isPingRequest(method) {
|
|
592
651
|
return method === 'ping';
|
|
593
652
|
}
|
|
@@ -704,9 +763,7 @@ class McpSessionGateway {
|
|
|
704
763
|
status: 406,
|
|
705
764
|
sessionId,
|
|
706
765
|
});
|
|
707
|
-
|
|
708
|
-
error: 'We need you to use "text/event-stream" for this connection.',
|
|
709
|
-
});
|
|
766
|
+
sendError(ctx.res, ErrorCode.InvalidRequest, 'We need you to use "text/event-stream" for this connection.', 406);
|
|
710
767
|
return;
|
|
711
768
|
}
|
|
712
769
|
logDebug('MCP GET received', { sessionId }, Loggers.LOG_HTTP);
|
|
@@ -734,9 +791,7 @@ class McpSessionGateway {
|
|
|
734
791
|
reason: 'accept_missing_json_or_event_stream',
|
|
735
792
|
status: 406,
|
|
736
793
|
});
|
|
737
|
-
|
|
738
|
-
error: 'We need the request to accept both "application/json" and "text/event-stream".',
|
|
739
|
-
});
|
|
794
|
+
sendError(ctx.res, ErrorCode.InvalidRequest, 'We need the request to accept both "application/json" and "text/event-stream".', 406);
|
|
740
795
|
return null;
|
|
741
796
|
}
|
|
742
797
|
const { body } = ctx;
|
|
@@ -800,7 +855,7 @@ class McpSessionGateway {
|
|
|
800
855
|
mcpCode: -32600,
|
|
801
856
|
rpcId: requestId,
|
|
802
857
|
});
|
|
803
|
-
sendError(ctx.res, -32600,
|
|
858
|
+
sendError(ctx.res, -32600, MISSING_SESSION_ID_MESSAGE, 400, requestId);
|
|
804
859
|
return false;
|
|
805
860
|
}
|
|
806
861
|
return true;
|
|
@@ -823,7 +878,7 @@ class McpSessionGateway {
|
|
|
823
878
|
sessionId,
|
|
824
879
|
rpcId: requestId,
|
|
825
880
|
});
|
|
826
|
-
sendError(ctx.res, -32600,
|
|
881
|
+
sendError(ctx.res, -32600, SESSION_NOT_INITIALIZED_MESSAGE, 400, requestId);
|
|
827
882
|
return false;
|
|
828
883
|
}
|
|
829
884
|
async getOrCreateTransport(ctx, requestId) {
|
|
@@ -837,40 +892,39 @@ class McpSessionGateway {
|
|
|
837
892
|
return null;
|
|
838
893
|
return this.createNewSession(ctx, requestId, negotiatedProtocolVersion);
|
|
839
894
|
}
|
|
895
|
+
rejectInitializeRequest(ctx, requestId, reason, mcpCode, message) {
|
|
896
|
+
logGatewayRejection({
|
|
897
|
+
message: 'Rejected MCP initialize request',
|
|
898
|
+
method: ctx.method,
|
|
899
|
+
path: ctx.url.pathname,
|
|
900
|
+
reason,
|
|
901
|
+
status: 400,
|
|
902
|
+
mcpCode,
|
|
903
|
+
rpcId: requestId,
|
|
904
|
+
});
|
|
905
|
+
sendError(ctx.res, mcpCode, message, 400, requestId);
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
840
908
|
getInitializeProtocolVersion(ctx, requestId) {
|
|
841
909
|
if (!isMcpRequestBody(ctx.body)) {
|
|
842
|
-
|
|
843
|
-
message: 'Rejected MCP initialize request',
|
|
844
|
-
method: ctx.method,
|
|
845
|
-
path: ctx.url.pathname,
|
|
846
|
-
reason: 'missing_session_id',
|
|
847
|
-
status: 400,
|
|
848
|
-
mcpCode: -32600,
|
|
849
|
-
rpcId: requestId,
|
|
850
|
-
});
|
|
851
|
-
sendError(ctx.res, -32600, "We couldn't find a session ID for your request. Please ensure you have an active session.", 400, requestId);
|
|
852
|
-
return null;
|
|
910
|
+
return this.rejectInitializeRequest(ctx, requestId, 'missing_session_id', -32600, MISSING_SESSION_ID_MESSAGE);
|
|
853
911
|
}
|
|
854
912
|
if (!isInitializeRequest(ctx.body)) {
|
|
855
913
|
const invalidInitialize = ctx.body.method === 'initialize';
|
|
856
|
-
|
|
857
|
-
message: 'Rejected MCP initialize request',
|
|
858
|
-
method: ctx.method,
|
|
859
|
-
path: ctx.url.pathname,
|
|
860
|
-
reason: invalidInitialize
|
|
861
|
-
? 'invalid_initialize_request'
|
|
862
|
-
: 'missing_session_id',
|
|
863
|
-
status: 400,
|
|
864
|
-
mcpCode: invalidInitialize ? -32602 : -32600,
|
|
865
|
-
rpcId: requestId,
|
|
866
|
-
});
|
|
867
|
-
sendError(ctx.res, invalidInitialize ? -32602 : -32600, invalidInitialize
|
|
914
|
+
return this.rejectInitializeRequest(ctx, requestId, invalidInitialize ? 'invalid_initialize_request' : 'missing_session_id', invalidInitialize ? -32602 : -32600, invalidInitialize
|
|
868
915
|
? 'The initialize request format is invalid. Please double-check your parameters.'
|
|
869
|
-
:
|
|
870
|
-
return null;
|
|
916
|
+
: MISSING_SESSION_ID_MESSAGE);
|
|
871
917
|
}
|
|
872
918
|
const negotiatedProtocolVersion = resolveRequestedProtocolVersion(ctx.body);
|
|
919
|
+
if (negotiatedProtocolVersion.length === 0 ||
|
|
920
|
+
!SUPPORTED_MCP_PROTOCOL_VERSIONS.has(negotiatedProtocolVersion)) {
|
|
921
|
+
return this.rejectInitializeRequest(ctx, requestId, 'unsupported_protocol_version', -32600, `The protocol version '${negotiatedProtocolVersion || '(missing)'}' isn't supported right now. Please check and try again.`);
|
|
922
|
+
}
|
|
873
923
|
const headerProtocolVersion = resolveProtocolVersionHeader(ctx.req);
|
|
924
|
+
if (headerProtocolVersion &&
|
|
925
|
+
!SUPPORTED_MCP_PROTOCOL_VERSIONS.has(headerProtocolVersion)) {
|
|
926
|
+
return this.rejectInitializeRequest(ctx, requestId, 'unsupported_protocol_version_header', -32600, `The protocol version '${headerProtocolVersion}' isn't supported right now. Please check and try again.`);
|
|
927
|
+
}
|
|
874
928
|
if (headerProtocolVersion &&
|
|
875
929
|
headerProtocolVersion !== negotiatedProtocolVersion) {
|
|
876
930
|
logGatewayRejection({
|
|
@@ -908,20 +962,30 @@ class McpSessionGateway {
|
|
|
908
962
|
return null;
|
|
909
963
|
return { sessionId, session };
|
|
910
964
|
}
|
|
965
|
+
sendMissingSessionId(res, requestId = null) {
|
|
966
|
+
sendError(res, -32600, MISSING_SESSION_ID_MESSAGE, 400, requestId);
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
sendSessionNotInitialized(res, requestId = null) {
|
|
970
|
+
sendError(res, -32600, SESSION_NOT_INITIALIZED_MESSAGE, 400, requestId);
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
sendSessionUnavailable(res, requestId = null, status = 404) {
|
|
974
|
+
sendError(res, -32600, SESSION_NOT_FOUND_MESSAGE, status, requestId);
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
911
977
|
getRequiredAuthenticatedSession(ctx, requestId = null, options) {
|
|
912
978
|
const state = this.getOptionalAuthenticatedSession(ctx, requestId);
|
|
913
979
|
if (!state)
|
|
914
980
|
return null;
|
|
915
981
|
const { sessionId, session } = state;
|
|
916
982
|
if (!sessionId || !session) {
|
|
917
|
-
|
|
918
|
-
return null;
|
|
983
|
+
return this.sendMissingSessionId(ctx.res, requestId);
|
|
919
984
|
}
|
|
920
985
|
if (!this.ensureSessionProtocolVersion(ctx, session))
|
|
921
986
|
return null;
|
|
922
987
|
if (options?.requireInitialized && !session.protocolInitialized) {
|
|
923
|
-
|
|
924
|
-
return null;
|
|
988
|
+
return this.sendSessionNotInitialized(ctx.res, requestId);
|
|
925
989
|
}
|
|
926
990
|
return { sessionId, session };
|
|
927
991
|
}
|
|
@@ -937,7 +1001,7 @@ class McpSessionGateway {
|
|
|
937
1001
|
sessionId,
|
|
938
1002
|
rpcId: requestId,
|
|
939
1003
|
});
|
|
940
|
-
sendError(res, -32600,
|
|
1004
|
+
sendError(res, -32600, SESSION_NOT_FOUND_MESSAGE, 404, requestId);
|
|
941
1005
|
return null;
|
|
942
1006
|
}
|
|
943
1007
|
if (!authFingerprint || session.authFingerprint !== authFingerprint) {
|
|
@@ -950,8 +1014,7 @@ class McpSessionGateway {
|
|
|
950
1014
|
sessionId,
|
|
951
1015
|
rpcId: requestId,
|
|
952
1016
|
});
|
|
953
|
-
|
|
954
|
-
return null;
|
|
1017
|
+
return this.sendSessionUnavailable(res, requestId);
|
|
955
1018
|
}
|
|
956
1019
|
return session;
|
|
957
1020
|
}
|
|
@@ -1134,6 +1197,13 @@ class HttpDispatcher {
|
|
|
1134
1197
|
this.store = store;
|
|
1135
1198
|
this.mcpGateway = mcpGateway;
|
|
1136
1199
|
}
|
|
1200
|
+
sendAuthFailure(ctx, status, message) {
|
|
1201
|
+
if (isMcpRoute(ctx.url.pathname)) {
|
|
1202
|
+
sendError(ctx.res, -32000, message, status);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
sendJson(ctx.res, status, { error: message });
|
|
1206
|
+
}
|
|
1137
1207
|
async tryHandleHealthRoute(ctx) {
|
|
1138
1208
|
if (!shouldHandleHealthRoute(ctx))
|
|
1139
1209
|
return false;
|
|
@@ -1155,7 +1225,7 @@ class HttpDispatcher {
|
|
|
1155
1225
|
return false;
|
|
1156
1226
|
if (!isProtectedResourceMetadataPath(ctx.url.pathname))
|
|
1157
1227
|
return false;
|
|
1158
|
-
const document = buildProtectedResourceMetadataDocument(
|
|
1228
|
+
const document = buildProtectedResourceMetadataDocument();
|
|
1159
1229
|
sendJson(ctx.res, 200, document);
|
|
1160
1230
|
return true;
|
|
1161
1231
|
}
|
|
@@ -1217,11 +1287,11 @@ class HttpDispatcher {
|
|
|
1217
1287
|
logWarn('Authentication failed', { message, method: ctx.method, path: ctx.url.pathname }, Loggers.LOG_AUTH);
|
|
1218
1288
|
if (isInsufficientScopeError(err)) {
|
|
1219
1289
|
applyInsufficientScopeAuthHeaders(ctx.req, ctx.res, err.requiredScopes, message);
|
|
1220
|
-
|
|
1290
|
+
this.sendAuthFailure(ctx, 403, message);
|
|
1221
1291
|
return null;
|
|
1222
1292
|
}
|
|
1223
1293
|
applyUnauthorizedAuthHeaders(ctx.req, ctx.res);
|
|
1224
|
-
|
|
1294
|
+
this.sendAuthFailure(ctx, 401, message);
|
|
1225
1295
|
return null;
|
|
1226
1296
|
}
|
|
1227
1297
|
}
|
|
@@ -1300,7 +1370,7 @@ class HttpRequestPipeline {
|
|
|
1300
1370
|
return;
|
|
1301
1371
|
if (!(await this.populateRequestBody(ctx, rawReq)))
|
|
1302
1372
|
return;
|
|
1303
|
-
await this.dispatcher.dispatch(ctx);
|
|
1373
|
+
await runWithTraceContext(getBodyMeta(ctx.body), () => this.dispatcher.dispatch(ctx));
|
|
1304
1374
|
});
|
|
1305
1375
|
}
|
|
1306
1376
|
finally {
|
|
@@ -1342,9 +1412,6 @@ class HttpRequestPipeline {
|
|
|
1342
1412
|
return false;
|
|
1343
1413
|
}
|
|
1344
1414
|
if (!this.rateLimiter.check(ctx)) {
|
|
1345
|
-
sendJson(ctx.res, 429, {
|
|
1346
|
-
error: "You're sending requests a bit too quickly. Please slow down and try again.",
|
|
1347
|
-
});
|
|
1348
1415
|
drainRequest(rawReq);
|
|
1349
1416
|
return false;
|
|
1350
1417
|
}
|
|
@@ -1459,7 +1526,7 @@ export async function startHttpServer() {
|
|
|
1459
1526
|
assertHttpModeConfiguration();
|
|
1460
1527
|
enableHttpMode();
|
|
1461
1528
|
resetEventLoopMonitoring();
|
|
1462
|
-
const rateLimiter =
|
|
1529
|
+
const rateLimiter = new RateLimiter(config.rateLimit);
|
|
1463
1530
|
const sessionStore = createSessionStore(config.server.sessionTtlMs);
|
|
1464
1531
|
const sessionCleanup = startSessionCleanupLoop(sessionStore, config.server.sessionTtlMs, {
|
|
1465
1532
|
onEvictSession: (session) => {
|
|
@@ -1479,6 +1546,12 @@ export async function startHttpServer() {
|
|
|
1479
1546
|
await listen(server, config.server.host, config.server.port);
|
|
1480
1547
|
const port = resolveListeningPort(server, config.server.port);
|
|
1481
1548
|
const protocol = config.server.https.enabled ? 'https' : 'http';
|
|
1549
|
+
if (!config.auth.publicBaseUrl) {
|
|
1550
|
+
const resolvedResourceUrl = new URL(config.auth.resourceUrl);
|
|
1551
|
+
resolvedResourceUrl.port = String(port);
|
|
1552
|
+
resolvedResourceUrl.protocol = `${protocol}:`;
|
|
1553
|
+
config.auth.resourceUrl = resolvedResourceUrl;
|
|
1554
|
+
}
|
|
1482
1555
|
logInfo(`${protocol.toUpperCase()} server listening on port ${port}`, {
|
|
1483
1556
|
platform: process.platform,
|
|
1484
1557
|
arch: process.arch,
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { RequestContext } from './native.js';
|
|
2
2
|
interface RateLimitConfig {
|
|
3
3
|
maxRequests: number;
|
|
4
4
|
windowMs: number;
|
|
5
5
|
cleanupIntervalMs: number;
|
|
6
6
|
enabled: boolean;
|
|
7
7
|
}
|
|
8
|
-
export
|
|
8
|
+
export declare class RateLimiter {
|
|
9
|
+
private readonly options;
|
|
10
|
+
private readonly store;
|
|
11
|
+
private readonly cleanup;
|
|
12
|
+
constructor(options: RateLimitConfig);
|
|
13
|
+
private startCleanupLoop;
|
|
14
|
+
private cleanupEntries;
|
|
15
|
+
private resetEntry;
|
|
16
|
+
private incrementEntry;
|
|
17
|
+
private createEntry;
|
|
9
18
|
check(ctx: RequestContext): boolean;
|
|
10
19
|
stop(): void;
|
|
11
20
|
}
|
|
12
|
-
export declare function createRateLimitManagerImpl(options: RateLimitConfig): RateLimitManagerImpl;
|
|
13
21
|
export {};
|
|
14
22
|
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/http/rate-limit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/http/rate-limit.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAYlD,UAAU,eAAe;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;CAClB;AAcD,qBAAa,WAAW;IAIV,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;gBAEpB,OAAO,EAAE,eAAe;IAIrD,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,WAAW;IAQnB,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO;IAyCnC,IAAI,IAAI,IAAI;CAGb"}
|
package/dist/http/rate-limit.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
1
2
|
import { Loggers, logWarn } from '../lib/core.js';
|
|
2
3
|
import { isAbortError } from '../lib/error/index.js';
|
|
4
|
+
import { sendJsonRpcError } from '../lib/mcp-interop.js';
|
|
3
5
|
import { startAbortableIntervalLoop } from '../lib/utils.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
function sendJson(res, status, body) {
|
|
7
|
+
res.statusCode = status;
|
|
8
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
9
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
10
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
11
|
+
res.end(JSON.stringify(body));
|
|
12
|
+
}
|
|
13
|
+
function isMcpEndpoint(pathname) {
|
|
14
|
+
return pathname === '/mcp' || pathname === '/mcp/';
|
|
15
|
+
}
|
|
16
|
+
export class RateLimiter {
|
|
9
17
|
options;
|
|
10
18
|
store = new Map();
|
|
11
19
|
cleanup = new AbortController();
|
|
@@ -74,7 +82,12 @@ class RateLimiter {
|
|
|
74
82
|
logWarn('Rate limit exceeded', { ip: key }, Loggers.LOG_RATE_LIMIT);
|
|
75
83
|
const retryAfter = Math.max(1, Math.ceil((entry.resetTime - now) / 1000));
|
|
76
84
|
ctx.res.setHeader('Retry-After', String(retryAfter));
|
|
77
|
-
|
|
85
|
+
if (isMcpEndpoint(ctx.url.pathname)) {
|
|
86
|
+
sendJsonRpcError(ctx.res, 429, ErrorCode.InvalidRequest, 'Rate limit exceeded', null, { retryAfter });
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
sendJson(ctx.res, 429, { error: 'Rate limit exceeded', retryAfter });
|
|
90
|
+
}
|
|
78
91
|
return false;
|
|
79
92
|
}
|
|
80
93
|
return true;
|
|
@@ -83,7 +96,3 @@ class RateLimiter {
|
|
|
83
96
|
this.cleanup.abort();
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
|
-
export function createRateLimitManagerImpl(options) {
|
|
87
|
-
const limiter = new RateLimiter(options);
|
|
88
|
-
return limiter;
|
|
89
|
-
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAWA,UAAU,SAAS;IACjB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;CAC5B;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,KAAK,cAAc,GAAG,eAAe,GAAG,eAAe,CAAC;AA2CxD,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,CA2BpE"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
|
-
import {
|
|
4
|
+
import { serverVersion } from './lib/config.js';
|
|
5
|
+
import { logError, Loggers } from './lib/core.js';
|
|
5
6
|
import { getErrorMessage, toError } from './lib/error/index.js';
|
|
6
7
|
import { startHttpServer } from './http/index.js';
|
|
7
8
|
import { startStdioServer } from './server.js';
|
package/dist/lib/config.d.ts
CHANGED
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/config.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { findPackageJSON } from 'node:module';
|
|
3
|
+
import { isIP } from 'node:net';
|
|
3
4
|
import process from 'node:process';
|
|
5
|
+
import { domainToASCII } from 'node:url';
|
|
4
6
|
import { z } from 'zod';
|
|
5
|
-
import { getErrorMessage } from './error/
|
|
6
|
-
import { buildIpv4, isIP, normalizeHostname, stripTrailingDots, } from './net/url.js';
|
|
7
|
+
import { getErrorMessage } from './error/classes.js';
|
|
7
8
|
// ── Version ─────────────────────────────────────────────────────────
|
|
8
9
|
function getObjectProperty(value, key) {
|
|
9
10
|
return value[key];
|
|
@@ -87,6 +88,25 @@ const ENV_BOOLEAN_SCHEMA = z.stringbool({
|
|
|
87
88
|
falsy: ['false', '0', 'no', 'off'],
|
|
88
89
|
});
|
|
89
90
|
// ── Host parsing helpers ────────────────────────────────────────────
|
|
91
|
+
function buildIpv4(parts) {
|
|
92
|
+
return parts.join('.');
|
|
93
|
+
}
|
|
94
|
+
function stripTrailingDots(value) {
|
|
95
|
+
let result = value;
|
|
96
|
+
while (result.endsWith('.'))
|
|
97
|
+
result = result.slice(0, -1);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
function normalizeHostname(value) {
|
|
101
|
+
const trimmed = value.trim();
|
|
102
|
+
if (!trimmed)
|
|
103
|
+
return null;
|
|
104
|
+
const lowered = trimmed.toLowerCase();
|
|
105
|
+
if (isIP(lowered))
|
|
106
|
+
return stripTrailingDots(lowered);
|
|
107
|
+
const ascii = domainToASCII(lowered);
|
|
108
|
+
return ascii ? stripTrailingDots(ascii) : null;
|
|
109
|
+
}
|
|
90
110
|
function tryParseUrlHost(raw) {
|
|
91
111
|
if (raw.includes('://')) {
|
|
92
112
|
const hostname = URL.parse(raw)?.hostname;
|
|
@@ -277,7 +297,8 @@ function buildAuthConfig(baseUrl) {
|
|
|
277
297
|
const revocationUrl = parseUrl('OAUTH_REVOCATION_URL');
|
|
278
298
|
const registrationUrl = parseUrl('OAUTH_REGISTRATION_URL');
|
|
279
299
|
const introspectionUrl = parseUrl('OAUTH_INTROSPECTION_URL');
|
|
280
|
-
const
|
|
300
|
+
const publicBaseUrl = parseUrl('PUBLIC_BASE_URL');
|
|
301
|
+
const resourceUrl = new URL('/mcp', publicBaseUrl ?? baseUrl);
|
|
281
302
|
const oauthConfigured = issuerUrl !== undefined ||
|
|
282
303
|
authorizationUrl !== undefined ||
|
|
283
304
|
tokenUrl !== undefined ||
|
|
@@ -293,6 +314,7 @@ function buildAuthConfig(baseUrl) {
|
|
|
293
314
|
revocationUrl,
|
|
294
315
|
registrationUrl,
|
|
295
316
|
introspectionUrl,
|
|
317
|
+
publicBaseUrl,
|
|
296
318
|
resourceUrl,
|
|
297
319
|
requiredScopes: EnvParser.list(env['OAUTH_REQUIRED_SCOPES']),
|
|
298
320
|
clientId: env['OAUTH_CLIENT_ID'],
|