@j0hanz/fetch-url-mcp 1.12.9 → 1.12.11
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 +4 -9
- package/dist/http/auth.d.ts.map +1 -1
- package/dist/http/auth.js +18 -26
- package/dist/http/native.d.ts +4 -1
- package/dist/http/native.d.ts.map +1 -1
- package/dist/http/native.js +127 -97
- package/dist/http/rate-limit.d.ts +12 -9
- package/dist/http/rate-limit.d.ts.map +1 -1
- package/dist/http/rate-limit.js +12 -9
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +4 -2
- package/dist/lib/core.d.ts +11 -1
- package/dist/lib/core.d.ts.map +1 -1
- package/dist/lib/core.js +166 -86
- 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/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/pipeline.d.ts.map +1 -1
- package/dist/lib/net/pipeline.js +8 -8
- package/dist/lib/net/url.d.ts +0 -1
- package/dist/lib/net/url.d.ts.map +1 -1
- package/dist/lib/net/url.js +7 -5
- package/dist/lib/utils.d.ts +0 -2
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +3 -8
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +46 -21
- package/dist/schemas.d.ts +9 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/tasks/manager.d.ts +9 -106
- package/dist/tasks/manager.d.ts.map +1 -1
- package/dist/tasks/manager.js +189 -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.js +3 -3
- package/dist/transform/index.d.ts +1 -85
- package/dist/transform/index.d.ts.map +1 -1
- package/dist/transform/index.js +310 -941
- 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 +716 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -540,6 +540,8 @@ You get text content back by default. If output validation passes, the response
|
|
|
540
540
|
|
|
541
541
|
To opt into progress updates, include `_meta.progressToken` in the tool call. The token may be a string or number. The server may then emit monotonic `notifications/progress` updates, and task mode reuses the same token until the task reaches a terminal state.
|
|
542
542
|
|
|
543
|
+
To run the tool in task mode, include `_meta["modelcontextprotocol.io/task"] = { "taskId": "<client-id>", "keepAlive": <ms> }`. `tasks/result` returns output only after the task reaches `completed`. Task-linked progress notifications, task summaries, and final results include `_meta["modelcontextprotocol.io/related-task"] = { "taskId": "<client-id>" }`.
|
|
544
|
+
|
|
543
545
|
```json
|
|
544
546
|
{
|
|
545
547
|
"method": "tools/call",
|
package/dist/http/auth.d.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
|
|
2
2
|
import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
|
3
3
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
|
-
|
|
5
|
-
readonly req: IncomingMessage;
|
|
6
|
-
readonly res: ServerResponse;
|
|
7
|
-
readonly url: URL;
|
|
8
|
-
readonly method: string | undefined;
|
|
9
|
-
}
|
|
4
|
+
import type { RequestContext } from './native.js';
|
|
10
5
|
declare class CorsPolicy {
|
|
11
|
-
handle(ctx:
|
|
6
|
+
handle(ctx: RequestContext): boolean;
|
|
12
7
|
}
|
|
13
8
|
export declare const corsPolicy: CorsPolicy;
|
|
14
9
|
declare class InsufficientScopeError extends InvalidTokenError {
|
|
@@ -17,7 +12,7 @@ declare class InsufficientScopeError extends InvalidTokenError {
|
|
|
17
12
|
}
|
|
18
13
|
export declare function isInsufficientScopeError(error: unknown): error is InsufficientScopeError;
|
|
19
14
|
declare class HostOriginPolicy {
|
|
20
|
-
validate(ctx:
|
|
15
|
+
validate(ctx: RequestContext): boolean;
|
|
21
16
|
private resolveHostHeader;
|
|
22
17
|
private resolveRequestOrigin;
|
|
23
18
|
private resolveOrigin;
|
|
@@ -57,7 +52,7 @@ declare class AuthService {
|
|
|
57
52
|
}
|
|
58
53
|
export declare function applyUnauthorizedAuthHeaders(req: IncomingMessage, res: ServerResponse): void;
|
|
59
54
|
export declare function applyInsufficientScopeAuthHeaders(req: IncomingMessage, res: ServerResponse, requiredScopes: readonly string[], message?: string): void;
|
|
60
|
-
export declare function buildProtectedResourceMetadataDocument(
|
|
55
|
+
export declare function buildProtectedResourceMetadataDocument(): {
|
|
61
56
|
resource: string;
|
|
62
57
|
resource_metadata: string;
|
|
63
58
|
authorization_servers: string[];
|
package/dist/http/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/http/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EAElB,MAAM,iDAAiD,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gDAAgD,CAAC;AAG/E,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAajE,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/http/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/http/auth.js
CHANGED
|
@@ -2,8 +2,9 @@ import { InvalidTokenError, ServerError, } from '@modelcontextprotocol/sdk/serve
|
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { config } from '../lib/config.js';
|
|
4
4
|
import { logDebug, Loggers, logWarn } from '../lib/core.js';
|
|
5
|
+
import { sendJsonRpcError } from '../lib/mcp-interop.js';
|
|
5
6
|
import { normalizeHost } from '../lib/net/index.js';
|
|
6
|
-
import { composeAbortSignal, hmacSha256Hex, isObject,
|
|
7
|
+
import { composeAbortSignal, hmacSha256Hex, isObject, timingSafeEqualUtf8, } from '../lib/utils.js';
|
|
7
8
|
function setNoStoreHeaders(res) {
|
|
8
9
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
9
10
|
res.setHeader('Cache-Control', 'no-store');
|
|
@@ -19,10 +20,8 @@ function sendEmpty(res, status) {
|
|
|
19
20
|
res.setHeader('Content-Length', '0');
|
|
20
21
|
res.end();
|
|
21
22
|
}
|
|
22
|
-
function sendError(res,
|
|
23
|
-
|
|
24
|
-
_id) {
|
|
25
|
-
sendJson(res, status, { error: message });
|
|
23
|
+
function sendError(res, code, message, status = 400, id) {
|
|
24
|
+
sendJsonRpcError(res, status, code, message, id ?? null);
|
|
26
25
|
}
|
|
27
26
|
function getHeaderValue(req, name) {
|
|
28
27
|
const value = req.headers[name];
|
|
@@ -136,7 +135,7 @@ class HostOriginPolicy {
|
|
|
136
135
|
return null;
|
|
137
136
|
const isEncrypted = Reflect.get(req.socket, 'encrypted') === true;
|
|
138
137
|
const scheme = isEncrypted ? 'https' : 'http';
|
|
139
|
-
const parsed =
|
|
138
|
+
const parsed = URL.parse(`${scheme}://${hostHeader}`);
|
|
140
139
|
if (!parsed)
|
|
141
140
|
return null;
|
|
142
141
|
const normalizedHost = normalizeHost(parsed.host);
|
|
@@ -151,7 +150,7 @@ class HostOriginPolicy {
|
|
|
151
150
|
resolveOrigin(origin) {
|
|
152
151
|
if (origin === 'null')
|
|
153
152
|
return null;
|
|
154
|
-
const parsed =
|
|
153
|
+
const parsed = URL.parse(origin);
|
|
155
154
|
if (!parsed)
|
|
156
155
|
return null;
|
|
157
156
|
const scheme = parsed.protocol === 'https:' ? 'https' : 'http';
|
|
@@ -200,6 +199,9 @@ export function assertHttpModeConfiguration() {
|
|
|
200
199
|
if (config.auth.mode === 'oauth' && !config.auth.issuerUrl) {
|
|
201
200
|
throw Error('OAuth mode requires OAUTH_ISSUER_URL to serve RFC9728 metadata');
|
|
202
201
|
}
|
|
202
|
+
if (config.auth.mode === 'oauth' && !config.auth.introspectionUrl) {
|
|
203
|
+
throw Error('OAuth mode requires OAUTH_INTROSPECTION_URL');
|
|
204
|
+
}
|
|
203
205
|
if (config.auth.mode === 'static' && config.auth.staticTokens.length === 0) {
|
|
204
206
|
throw Error('Static auth requires ACCESS_TOKENS or API_KEY to be configured');
|
|
205
207
|
}
|
|
@@ -222,11 +224,6 @@ export function ensureMcpProtocolVersion(req, res, options) {
|
|
|
222
224
|
const version = resolveMcpProtocolVersion(req);
|
|
223
225
|
const path = URL.parse(req.url ?? '', 'http://localhost')?.pathname;
|
|
224
226
|
if (!version) {
|
|
225
|
-
// Tolerate missing header on sessioned requests (expectedVersion set)
|
|
226
|
-
// to avoid breaking older clients that don't send it yet.
|
|
227
|
-
if (options?.expectedVersion) {
|
|
228
|
-
return true;
|
|
229
|
-
}
|
|
230
227
|
logWarn('MCP protocol version rejected', { reason: 'missing_header', path }, 'http');
|
|
231
228
|
sendError(res, -32600, 'Please include the MCP-Protocol-Version header in your request.');
|
|
232
229
|
return false;
|
|
@@ -507,16 +504,11 @@ class AuthService {
|
|
|
507
504
|
}
|
|
508
505
|
}
|
|
509
506
|
}
|
|
510
|
-
function resolvePublicOrigin(
|
|
511
|
-
const host = getHeaderValue(req, 'host');
|
|
512
|
-
if (host) {
|
|
513
|
-
const protocol = config.server.https.enabled ? 'https' : 'http';
|
|
514
|
-
return `${protocol}://${host}`;
|
|
515
|
-
}
|
|
507
|
+
function resolvePublicOrigin() {
|
|
516
508
|
return config.auth.resourceUrl.origin;
|
|
517
509
|
}
|
|
518
|
-
function buildRequestScopedProtectedResourceUrls(
|
|
519
|
-
const origin = resolvePublicOrigin(
|
|
510
|
+
function buildRequestScopedProtectedResourceUrls() {
|
|
511
|
+
const origin = resolvePublicOrigin();
|
|
520
512
|
return {
|
|
521
513
|
resource: new URL('/mcp', `${origin}/`).href,
|
|
522
514
|
resourceMetadata: new URL(resolveResourceMetadataPath(), `${origin}/`).href,
|
|
@@ -525,13 +517,13 @@ function buildRequestScopedProtectedResourceUrls(req) {
|
|
|
525
517
|
function resolveResourceMetadataPath() {
|
|
526
518
|
return '/.well-known/oauth-protected-resource/mcp';
|
|
527
519
|
}
|
|
528
|
-
function buildResourceMetadataUrl(
|
|
529
|
-
return buildRequestScopedProtectedResourceUrls(
|
|
520
|
+
function buildResourceMetadataUrl() {
|
|
521
|
+
return buildRequestScopedProtectedResourceUrls().resourceMetadata;
|
|
530
522
|
}
|
|
531
523
|
export function applyUnauthorizedAuthHeaders(req, res) {
|
|
532
524
|
if (!isOAuthMetadataEnabled())
|
|
533
525
|
return;
|
|
534
|
-
const resourceMetadata = buildResourceMetadataUrl(
|
|
526
|
+
const resourceMetadata = buildResourceMetadataUrl();
|
|
535
527
|
const challengeParts = [`resource_metadata="${resourceMetadata}"`];
|
|
536
528
|
if (config.auth.requiredScopes.length > 0) {
|
|
537
529
|
challengeParts.push(`scope="${config.auth.requiredScopes.join(' ')}"`);
|
|
@@ -541,7 +533,7 @@ export function applyUnauthorizedAuthHeaders(req, res) {
|
|
|
541
533
|
export function applyInsufficientScopeAuthHeaders(req, res, requiredScopes, message = 'Additional authorization scope is required') {
|
|
542
534
|
if (!isOAuthMetadataEnabled())
|
|
543
535
|
return;
|
|
544
|
-
const resourceMetadata = buildResourceMetadataUrl(
|
|
536
|
+
const resourceMetadata = buildResourceMetadataUrl();
|
|
545
537
|
const challengeParts = [
|
|
546
538
|
'error="insufficient_scope"',
|
|
547
539
|
`scope="${requiredScopes.join(' ')}"`,
|
|
@@ -550,8 +542,8 @@ export function applyInsufficientScopeAuthHeaders(req, res, requiredScopes, mess
|
|
|
550
542
|
];
|
|
551
543
|
res.setHeader('WWW-Authenticate', `Bearer ${challengeParts.join(', ')}`);
|
|
552
544
|
}
|
|
553
|
-
export function buildProtectedResourceMetadataDocument(
|
|
554
|
-
const urls = buildRequestScopedProtectedResourceUrls(
|
|
545
|
+
export function buildProtectedResourceMetadataDocument() {
|
|
546
|
+
const urls = buildRequestScopedProtectedResourceUrls();
|
|
555
547
|
if (!config.auth.issuerUrl) {
|
|
556
548
|
const error = new ServerError('OAuth issuer URL is required for protected resource metadata');
|
|
557
549
|
throw error;
|
package/dist/http/native.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface AuthenticatedContext extends RequestContext {
|
|
|
21
21
|
}
|
|
22
22
|
export declare function sendJson(res: ServerResponse, status: number, body: unknown): void;
|
|
23
23
|
export declare function sendEmpty(res: ServerResponse, status: number): void;
|
|
24
|
-
export declare function sendError(res: ServerResponse,
|
|
24
|
+
export declare function sendError(res: ServerResponse, code: number, message: string, status?: number, id?: JsonRpcId | null): void;
|
|
25
25
|
export declare function getHeaderValue(req: IncomingMessage, name: string): string | null;
|
|
26
26
|
export declare function getMcpSessionId(req: IncomingMessage): string | null;
|
|
27
27
|
export declare function findDuplicateSingleValueHeader(req: IncomingMessage): string | null;
|
|
@@ -45,7 +45,10 @@ export declare class JsonBodyError extends Error {
|
|
|
45
45
|
export declare function isJsonBodyError(error: unknown): error is JsonBodyError;
|
|
46
46
|
export declare const DEFAULT_BODY_LIMIT_BYTES: number;
|
|
47
47
|
declare class JsonBodyReader {
|
|
48
|
+
private validateContentLength;
|
|
48
49
|
read(req: IncomingMessage, limit?: number, signal?: AbortSignal): Promise<unknown>;
|
|
50
|
+
private setupAbortListener;
|
|
51
|
+
private cleanupAbortListener;
|
|
49
52
|
private readBody;
|
|
50
53
|
private collectChunks;
|
|
51
54
|
private normalizeChunk;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../../src/http/native.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gDAAgD,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AACnG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;
|
|
1
|
+
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../../src/http/native.ts"],"names":[],"mappings":"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"}
|
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';
|
|
@@ -11,17 +11,20 @@ import process from 'node:process';
|
|
|
11
11
|
import { Writable } from 'node:stream';
|
|
12
12
|
import { pipeline } from 'node:stream/promises';
|
|
13
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, startSessionCleanupLoop, unregisterMcpSessionServer, unregisterMcpSessionServerByServer, } from '../lib/core.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';
|
|
15
15
|
import { getErrorMessage, toError } from '../lib/error/index.js';
|
|
16
|
-
import { acceptsEventStream, acceptsJsonAndEventStream, isMcpRequestBody, } from '../lib/mcp-interop.js';
|
|
16
|
+
import { acceptsEventStream, acceptsJsonAndEventStream, isMcpRequestBody, sendJsonRpcError, } from '../lib/mcp-interop.js';
|
|
17
17
|
import { createDefaultBlockList, normalizeIpForBlockList, } from '../lib/net/index.js';
|
|
18
18
|
import { isObject } from '../lib/utils.js';
|
|
19
19
|
import { createMcpServerForHttpSession } from '../server.js';
|
|
20
20
|
import { buildAuthenticatedOwnerKey } from '../tasks/index.js';
|
|
21
21
|
import { getTransformPoolStats } from '../transform/index.js';
|
|
22
|
-
import { applyInsufficientScopeAuthHeaders, applyUnauthorizedAuthHeaders, assertHttpModeConfiguration, authService, buildAuthFingerprint, buildProtectedResourceMetadataDocument, corsPolicy,
|
|
23
|
-
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
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.";
|
|
25
28
|
function abortControllerBestEffort(controller) {
|
|
26
29
|
if (!controller.signal.aborted)
|
|
27
30
|
controller.abort();
|
|
@@ -93,10 +96,8 @@ export function sendEmpty(res, status) {
|
|
|
93
96
|
res.setHeader('Content-Length', '0');
|
|
94
97
|
res.end();
|
|
95
98
|
}
|
|
96
|
-
export function sendError(res,
|
|
97
|
-
|
|
98
|
-
_id) {
|
|
99
|
-
sendJson(res, status, { error: message });
|
|
99
|
+
export function sendError(res, code, message, status = 400, id) {
|
|
100
|
+
sendJsonRpcError(res, status, code, message, id ?? null);
|
|
100
101
|
}
|
|
101
102
|
// ---------------------------------------------------------------------------
|
|
102
103
|
// Request helpers
|
|
@@ -305,18 +306,20 @@ function isRequestReadAborted(req) {
|
|
|
305
306
|
return req.destroyed && !req.complete;
|
|
306
307
|
}
|
|
307
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
|
+
}
|
|
308
318
|
async read(req, limit = DEFAULT_BODY_LIMIT_BYTES, signal) {
|
|
309
319
|
const contentType = getHeaderValue(req, 'content-type');
|
|
310
320
|
if (!contentType?.includes('application/json'))
|
|
311
321
|
return undefined;
|
|
312
|
-
|
|
313
|
-
if (contentLengthHeader) {
|
|
314
|
-
const contentLength = Number.parseInt(contentLengthHeader, 10);
|
|
315
|
-
if (Number.isFinite(contentLength) && contentLength > limit) {
|
|
316
|
-
const error = new JsonBodyError('payload-too-large', 'Payload too large');
|
|
317
|
-
throw error;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
322
|
+
this.validateContentLength(req, limit);
|
|
320
323
|
if (signal?.aborted || isRequestReadAborted(req)) {
|
|
321
324
|
const error = new JsonBodyError('read-failed', 'Request aborted');
|
|
322
325
|
throw error;
|
|
@@ -328,24 +331,35 @@ class JsonBodyReader {
|
|
|
328
331
|
return JSON.parse(body);
|
|
329
332
|
}
|
|
330
333
|
catch (err) {
|
|
331
|
-
|
|
332
|
-
throw error;
|
|
334
|
+
throw new JsonBodyError('invalid-json', getErrorMessage(err));
|
|
333
335
|
}
|
|
334
336
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (signal
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
signal.addEventListener('abort', abortListener, { once: true });
|
|
347
|
-
}
|
|
337
|
+
setupAbortListener(req, signal) {
|
|
338
|
+
if (signal == null)
|
|
339
|
+
return null;
|
|
340
|
+
const listener = () => {
|
|
341
|
+
destroyRequestBestEffort(req);
|
|
342
|
+
};
|
|
343
|
+
if (signal.aborted) {
|
|
344
|
+
listener();
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
signal.addEventListener('abort', listener, { once: true });
|
|
348
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);
|
|
349
363
|
try {
|
|
350
364
|
const { chunks, size } = await this.collectChunks(req, limit, signal);
|
|
351
365
|
if (chunks.length === 0)
|
|
@@ -360,14 +374,7 @@ class JsonBodyReader {
|
|
|
360
374
|
return text;
|
|
361
375
|
}
|
|
362
376
|
finally {
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
signal.removeEventListener('abort', abortListener);
|
|
366
|
-
}
|
|
367
|
-
catch {
|
|
368
|
-
// Best-effort cleanup.
|
|
369
|
-
}
|
|
370
|
-
}
|
|
377
|
+
this.cleanupAbortListener(signal, abortListener);
|
|
371
378
|
}
|
|
372
379
|
}
|
|
373
380
|
async collectChunks(req, limit, signal) {
|
|
@@ -429,7 +436,7 @@ function unregisterSessionTaskScope(server) {
|
|
|
429
436
|
unregisterMcpSessionServer(sessionId);
|
|
430
437
|
return sessionId;
|
|
431
438
|
}
|
|
432
|
-
|
|
439
|
+
function buildCloseTasks(session, options) {
|
|
433
440
|
const closeTasks = [];
|
|
434
441
|
if (options.closeTransportReason) {
|
|
435
442
|
closeTasks.push(closeTransportBestEffort(session.transport, options.closeTransportReason));
|
|
@@ -437,6 +444,10 @@ async function closeSessionResources(session, options) {
|
|
|
437
444
|
if (options.closeServerReason) {
|
|
438
445
|
closeTasks.push(closeMcpServerBestEffort(session.server, options.closeServerReason));
|
|
439
446
|
}
|
|
447
|
+
return closeTasks;
|
|
448
|
+
}
|
|
449
|
+
async function closeSessionResources(session, options) {
|
|
450
|
+
const closeTasks = buildCloseTasks(session, options);
|
|
440
451
|
if (options.awaitClose && closeTasks.length > 0) {
|
|
441
452
|
await Promise.all(closeTasks);
|
|
442
453
|
}
|
|
@@ -607,19 +618,15 @@ export function sendHealthRouteResponse(store, ctx, authPresent) {
|
|
|
607
618
|
}
|
|
608
619
|
function resolveRequestedProtocolVersion(body) {
|
|
609
620
|
if (!isObject(body))
|
|
610
|
-
return
|
|
621
|
+
return '';
|
|
611
622
|
const { params } = body;
|
|
612
623
|
if (!isObject(params))
|
|
613
|
-
return
|
|
624
|
+
return '';
|
|
614
625
|
const { protocolVersion: value } = params;
|
|
615
626
|
if (typeof value !== 'string')
|
|
616
|
-
return
|
|
627
|
+
return '';
|
|
617
628
|
const normalized = value.trim();
|
|
618
|
-
|
|
619
|
-
return DEFAULT_MCP_PROTOCOL_VERSION;
|
|
620
|
-
return SUPPORTED_MCP_PROTOCOL_VERSIONS.has(normalized)
|
|
621
|
-
? normalized
|
|
622
|
-
: DEFAULT_MCP_PROTOCOL_VERSION;
|
|
629
|
+
return normalized;
|
|
623
630
|
}
|
|
624
631
|
function resolveProtocolVersionHeader(req) {
|
|
625
632
|
const header = getHeaderValue(req, 'mcp-protocol-version');
|
|
@@ -631,6 +638,15 @@ function resolveProtocolVersionHeader(req) {
|
|
|
631
638
|
function isInitializedNotification(method) {
|
|
632
639
|
return method === 'notifications/initialized';
|
|
633
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
|
+
}
|
|
634
650
|
function isPingRequest(method) {
|
|
635
651
|
return method === 'ping';
|
|
636
652
|
}
|
|
@@ -747,9 +763,7 @@ class McpSessionGateway {
|
|
|
747
763
|
status: 406,
|
|
748
764
|
sessionId,
|
|
749
765
|
});
|
|
750
|
-
|
|
751
|
-
error: 'We need you to use "text/event-stream" for this connection.',
|
|
752
|
-
});
|
|
766
|
+
sendError(ctx.res, ErrorCode.InvalidRequest, 'We need you to use "text/event-stream" for this connection.', 406);
|
|
753
767
|
return;
|
|
754
768
|
}
|
|
755
769
|
logDebug('MCP GET received', { sessionId }, Loggers.LOG_HTTP);
|
|
@@ -777,9 +791,7 @@ class McpSessionGateway {
|
|
|
777
791
|
reason: 'accept_missing_json_or_event_stream',
|
|
778
792
|
status: 406,
|
|
779
793
|
});
|
|
780
|
-
|
|
781
|
-
error: 'We need the request to accept both "application/json" and "text/event-stream".',
|
|
782
|
-
});
|
|
794
|
+
sendError(ctx.res, ErrorCode.InvalidRequest, 'We need the request to accept both "application/json" and "text/event-stream".', 406);
|
|
783
795
|
return null;
|
|
784
796
|
}
|
|
785
797
|
const { body } = ctx;
|
|
@@ -843,7 +855,7 @@ class McpSessionGateway {
|
|
|
843
855
|
mcpCode: -32600,
|
|
844
856
|
rpcId: requestId,
|
|
845
857
|
});
|
|
846
|
-
sendError(ctx.res, -32600,
|
|
858
|
+
sendError(ctx.res, -32600, MISSING_SESSION_ID_MESSAGE, 400, requestId);
|
|
847
859
|
return false;
|
|
848
860
|
}
|
|
849
861
|
return true;
|
|
@@ -866,7 +878,7 @@ class McpSessionGateway {
|
|
|
866
878
|
sessionId,
|
|
867
879
|
rpcId: requestId,
|
|
868
880
|
});
|
|
869
|
-
sendError(ctx.res, -32600,
|
|
881
|
+
sendError(ctx.res, -32600, SESSION_NOT_INITIALIZED_MESSAGE, 400, requestId);
|
|
870
882
|
return false;
|
|
871
883
|
}
|
|
872
884
|
async getOrCreateTransport(ctx, requestId) {
|
|
@@ -880,40 +892,39 @@ class McpSessionGateway {
|
|
|
880
892
|
return null;
|
|
881
893
|
return this.createNewSession(ctx, requestId, negotiatedProtocolVersion);
|
|
882
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
|
+
}
|
|
883
908
|
getInitializeProtocolVersion(ctx, requestId) {
|
|
884
909
|
if (!isMcpRequestBody(ctx.body)) {
|
|
885
|
-
|
|
886
|
-
message: 'Rejected MCP initialize request',
|
|
887
|
-
method: ctx.method,
|
|
888
|
-
path: ctx.url.pathname,
|
|
889
|
-
reason: 'missing_session_id',
|
|
890
|
-
status: 400,
|
|
891
|
-
mcpCode: -32600,
|
|
892
|
-
rpcId: requestId,
|
|
893
|
-
});
|
|
894
|
-
sendError(ctx.res, -32600, "We couldn't find a session ID for your request. Please ensure you have an active session.", 400, requestId);
|
|
895
|
-
return null;
|
|
910
|
+
return this.rejectInitializeRequest(ctx, requestId, 'missing_session_id', -32600, MISSING_SESSION_ID_MESSAGE);
|
|
896
911
|
}
|
|
897
912
|
if (!isInitializeRequest(ctx.body)) {
|
|
898
913
|
const invalidInitialize = ctx.body.method === 'initialize';
|
|
899
|
-
|
|
900
|
-
message: 'Rejected MCP initialize request',
|
|
901
|
-
method: ctx.method,
|
|
902
|
-
path: ctx.url.pathname,
|
|
903
|
-
reason: invalidInitialize
|
|
904
|
-
? 'invalid_initialize_request'
|
|
905
|
-
: 'missing_session_id',
|
|
906
|
-
status: 400,
|
|
907
|
-
mcpCode: invalidInitialize ? -32602 : -32600,
|
|
908
|
-
rpcId: requestId,
|
|
909
|
-
});
|
|
910
|
-
sendError(ctx.res, invalidInitialize ? -32602 : -32600, invalidInitialize
|
|
914
|
+
return this.rejectInitializeRequest(ctx, requestId, invalidInitialize ? 'invalid_initialize_request' : 'missing_session_id', invalidInitialize ? -32602 : -32600, invalidInitialize
|
|
911
915
|
? 'The initialize request format is invalid. Please double-check your parameters.'
|
|
912
|
-
:
|
|
913
|
-
return null;
|
|
916
|
+
: MISSING_SESSION_ID_MESSAGE);
|
|
914
917
|
}
|
|
915
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
|
+
}
|
|
916
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
|
+
}
|
|
917
928
|
if (headerProtocolVersion &&
|
|
918
929
|
headerProtocolVersion !== negotiatedProtocolVersion) {
|
|
919
930
|
logGatewayRejection({
|
|
@@ -951,20 +962,30 @@ class McpSessionGateway {
|
|
|
951
962
|
return null;
|
|
952
963
|
return { sessionId, session };
|
|
953
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
|
+
}
|
|
954
977
|
getRequiredAuthenticatedSession(ctx, requestId = null, options) {
|
|
955
978
|
const state = this.getOptionalAuthenticatedSession(ctx, requestId);
|
|
956
979
|
if (!state)
|
|
957
980
|
return null;
|
|
958
981
|
const { sessionId, session } = state;
|
|
959
982
|
if (!sessionId || !session) {
|
|
960
|
-
|
|
961
|
-
return null;
|
|
983
|
+
return this.sendMissingSessionId(ctx.res, requestId);
|
|
962
984
|
}
|
|
963
985
|
if (!this.ensureSessionProtocolVersion(ctx, session))
|
|
964
986
|
return null;
|
|
965
987
|
if (options?.requireInitialized && !session.protocolInitialized) {
|
|
966
|
-
|
|
967
|
-
return null;
|
|
988
|
+
return this.sendSessionNotInitialized(ctx.res, requestId);
|
|
968
989
|
}
|
|
969
990
|
return { sessionId, session };
|
|
970
991
|
}
|
|
@@ -980,7 +1001,7 @@ class McpSessionGateway {
|
|
|
980
1001
|
sessionId,
|
|
981
1002
|
rpcId: requestId,
|
|
982
1003
|
});
|
|
983
|
-
sendError(res, -32600,
|
|
1004
|
+
sendError(res, -32600, SESSION_NOT_FOUND_MESSAGE, 404, requestId);
|
|
984
1005
|
return null;
|
|
985
1006
|
}
|
|
986
1007
|
if (!authFingerprint || session.authFingerprint !== authFingerprint) {
|
|
@@ -993,8 +1014,7 @@ class McpSessionGateway {
|
|
|
993
1014
|
sessionId,
|
|
994
1015
|
rpcId: requestId,
|
|
995
1016
|
});
|
|
996
|
-
|
|
997
|
-
return null;
|
|
1017
|
+
return this.sendSessionUnavailable(res, requestId);
|
|
998
1018
|
}
|
|
999
1019
|
return session;
|
|
1000
1020
|
}
|
|
@@ -1177,6 +1197,13 @@ class HttpDispatcher {
|
|
|
1177
1197
|
this.store = store;
|
|
1178
1198
|
this.mcpGateway = mcpGateway;
|
|
1179
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
|
+
}
|
|
1180
1207
|
async tryHandleHealthRoute(ctx) {
|
|
1181
1208
|
if (!shouldHandleHealthRoute(ctx))
|
|
1182
1209
|
return false;
|
|
@@ -1198,7 +1225,7 @@ class HttpDispatcher {
|
|
|
1198
1225
|
return false;
|
|
1199
1226
|
if (!isProtectedResourceMetadataPath(ctx.url.pathname))
|
|
1200
1227
|
return false;
|
|
1201
|
-
const document = buildProtectedResourceMetadataDocument(
|
|
1228
|
+
const document = buildProtectedResourceMetadataDocument();
|
|
1202
1229
|
sendJson(ctx.res, 200, document);
|
|
1203
1230
|
return true;
|
|
1204
1231
|
}
|
|
@@ -1260,11 +1287,11 @@ class HttpDispatcher {
|
|
|
1260
1287
|
logWarn('Authentication failed', { message, method: ctx.method, path: ctx.url.pathname }, Loggers.LOG_AUTH);
|
|
1261
1288
|
if (isInsufficientScopeError(err)) {
|
|
1262
1289
|
applyInsufficientScopeAuthHeaders(ctx.req, ctx.res, err.requiredScopes, message);
|
|
1263
|
-
|
|
1290
|
+
this.sendAuthFailure(ctx, 403, message);
|
|
1264
1291
|
return null;
|
|
1265
1292
|
}
|
|
1266
1293
|
applyUnauthorizedAuthHeaders(ctx.req, ctx.res);
|
|
1267
|
-
|
|
1294
|
+
this.sendAuthFailure(ctx, 401, message);
|
|
1268
1295
|
return null;
|
|
1269
1296
|
}
|
|
1270
1297
|
}
|
|
@@ -1343,7 +1370,7 @@ class HttpRequestPipeline {
|
|
|
1343
1370
|
return;
|
|
1344
1371
|
if (!(await this.populateRequestBody(ctx, rawReq)))
|
|
1345
1372
|
return;
|
|
1346
|
-
await this.dispatcher.dispatch(ctx);
|
|
1373
|
+
await runWithTraceContext(getBodyMeta(ctx.body), () => this.dispatcher.dispatch(ctx));
|
|
1347
1374
|
});
|
|
1348
1375
|
}
|
|
1349
1376
|
finally {
|
|
@@ -1385,9 +1412,6 @@ class HttpRequestPipeline {
|
|
|
1385
1412
|
return false;
|
|
1386
1413
|
}
|
|
1387
1414
|
if (!this.rateLimiter.check(ctx)) {
|
|
1388
|
-
sendJson(ctx.res, 429, {
|
|
1389
|
-
error: "You're sending requests a bit too quickly. Please slow down and try again.",
|
|
1390
|
-
});
|
|
1391
1415
|
drainRequest(rawReq);
|
|
1392
1416
|
return false;
|
|
1393
1417
|
}
|
|
@@ -1502,7 +1526,7 @@ export async function startHttpServer() {
|
|
|
1502
1526
|
assertHttpModeConfiguration();
|
|
1503
1527
|
enableHttpMode();
|
|
1504
1528
|
resetEventLoopMonitoring();
|
|
1505
|
-
const rateLimiter =
|
|
1529
|
+
const rateLimiter = new RateLimiter(config.rateLimit);
|
|
1506
1530
|
const sessionStore = createSessionStore(config.server.sessionTtlMs);
|
|
1507
1531
|
const sessionCleanup = startSessionCleanupLoop(sessionStore, config.server.sessionTtlMs, {
|
|
1508
1532
|
onEvictSession: (session) => {
|
|
@@ -1522,6 +1546,12 @@ export async function startHttpServer() {
|
|
|
1522
1546
|
await listen(server, config.server.host, config.server.port);
|
|
1523
1547
|
const port = resolveListeningPort(server, config.server.port);
|
|
1524
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
|
+
}
|
|
1525
1555
|
logInfo(`${protocol.toUpperCase()} server listening on port ${port}`, {
|
|
1526
1556
|
platform: process.platform,
|
|
1527
1557
|
arch: process.arch,
|