@j0hanz/fetch-url-mcp 1.11.8 → 1.12.1
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/LICENSE +21 -0
- package/README.md +182 -79
- package/dist/http/auth.d.ts.map +1 -1
- package/dist/http/auth.js +58 -17
- package/dist/http/helpers.d.ts +1 -1
- package/dist/http/helpers.d.ts.map +1 -1
- package/dist/http/helpers.js +7 -9
- package/dist/http/native.d.ts.map +1 -1
- package/dist/http/native.js +232 -27
- package/dist/http/rate-limit.d.ts.map +1 -1
- package/dist/http/rate-limit.js +2 -1
- package/dist/index.js +5 -4
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +8 -1
- package/dist/lib/core.d.ts +8 -4
- package/dist/lib/core.d.ts.map +1 -1
- package/dist/lib/core.js +240 -73
- package/dist/lib/fetch-pipeline.d.ts.map +1 -1
- package/dist/lib/fetch-pipeline.js +15 -2
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +1 -1
- package/dist/lib/mcp-interop.d.ts +15 -3
- package/dist/lib/mcp-interop.d.ts.map +1 -1
- package/dist/lib/mcp-interop.js +94 -25
- package/dist/lib/url.d.ts.map +1 -1
- package/dist/lib/url.js +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +5 -5
- package/dist/resources/index.d.ts +4 -0
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +39 -4
- package/dist/schemas.d.ts +5 -5
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +7 -9
- package/dist/server.d.ts +3 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +20 -11
- package/dist/tasks/execution.d.ts +1 -1
- package/dist/tasks/execution.d.ts.map +1 -1
- package/dist/tasks/execution.js +72 -25
- package/dist/tasks/handlers.d.ts.map +1 -1
- package/dist/tasks/handlers.js +31 -24
- package/dist/tasks/manager.d.ts +5 -2
- package/dist/tasks/manager.d.ts.map +1 -1
- package/dist/tasks/manager.js +58 -19
- package/dist/tasks/owner.d.ts +5 -0
- package/dist/tasks/owner.d.ts.map +1 -1
- package/dist/tasks/owner.js +15 -7
- package/dist/tasks/registry.d.ts +10 -8
- package/dist/tasks/registry.d.ts.map +1 -1
- package/dist/tasks/registry.js +27 -15
- package/dist/tools/fetch-url.d.ts +2 -0
- package/dist/tools/fetch-url.d.ts.map +1 -1
- package/dist/tools/fetch-url.js +76 -21
- package/dist/transform/dom-prep.d.ts.map +1 -1
- package/dist/transform/dom-prep.js +6 -6
- package/dist/transform/transform.d.ts.map +1 -1
- package/dist/transform/transform.js +17 -14
- package/dist/transform/worker-pool.d.ts.map +1 -1
- package/dist/transform/worker-pool.js +43 -3
- package/package.json +14 -5
package/dist/http/helpers.js
CHANGED
|
@@ -4,7 +4,6 @@ import { composeCloseHandlers, config, logWarn } from '../lib/core.js';
|
|
|
4
4
|
import { resolveMcpSessionIdByServer, unregisterMcpSessionServer, unregisterMcpSessionServerByServer, } from '../lib/core.js';
|
|
5
5
|
import { createDefaultBlockList, normalizeIpForBlockList } from '../lib/url.js';
|
|
6
6
|
import { getErrorMessage, toError } from '../lib/utils.js';
|
|
7
|
-
import { cancelTasksForOwner } from '../tasks/handlers.js';
|
|
8
7
|
function abortControllerBestEffort(controller) {
|
|
9
8
|
if (!controller.signal.aborted)
|
|
10
9
|
controller.abort();
|
|
@@ -155,7 +154,7 @@ export function registerInboundBlockList(server) {
|
|
|
155
154
|
logWarn('Blocked inbound connection', {
|
|
156
155
|
remoteAddress: normalized.ip,
|
|
157
156
|
family: normalized.family,
|
|
158
|
-
});
|
|
157
|
+
}, 'http');
|
|
159
158
|
socket.destroy();
|
|
160
159
|
}
|
|
161
160
|
});
|
|
@@ -187,7 +186,7 @@ export async function closeTransportBestEffort(transport, context) {
|
|
|
187
186
|
await transport.close();
|
|
188
187
|
}
|
|
189
188
|
catch (error) {
|
|
190
|
-
logWarn('Transport close failed', { context, error });
|
|
189
|
+
logWarn('Transport close failed', { context, error }, 'http');
|
|
191
190
|
}
|
|
192
191
|
}
|
|
193
192
|
export async function closeMcpServerBestEffort(server, context) {
|
|
@@ -195,7 +194,7 @@ export async function closeMcpServerBestEffort(server, context) {
|
|
|
195
194
|
await server.close();
|
|
196
195
|
}
|
|
197
196
|
catch (error) {
|
|
198
|
-
logWarn('MCP server close failed', { context, error });
|
|
197
|
+
logWarn('MCP server close failed', { context, error }, 'http');
|
|
199
198
|
}
|
|
200
199
|
}
|
|
201
200
|
export function createTransportAdapter(transportImpl) {
|
|
@@ -357,11 +356,10 @@ class JsonBodyReader {
|
|
|
357
356
|
}
|
|
358
357
|
}
|
|
359
358
|
export const jsonBodyReader = new JsonBodyReader();
|
|
360
|
-
function
|
|
359
|
+
function unregisterSessionTaskScope(server) {
|
|
361
360
|
const sessionId = resolveMcpSessionIdByServer(server);
|
|
362
361
|
if (!sessionId)
|
|
363
362
|
return null;
|
|
364
|
-
cancelTasksForOwner(`session:${sessionId}`, message);
|
|
365
363
|
unregisterMcpSessionServer(sessionId);
|
|
366
364
|
return sessionId;
|
|
367
365
|
}
|
|
@@ -378,7 +376,7 @@ async function closeSessionResources(session, options) {
|
|
|
378
376
|
}
|
|
379
377
|
}
|
|
380
378
|
export async function teardownSessionResources(session, options) {
|
|
381
|
-
|
|
379
|
+
unregisterSessionTaskScope(session.server);
|
|
382
380
|
if (options.unregisterByServer) {
|
|
383
381
|
unregisterMcpSessionServerByServer(session.server);
|
|
384
382
|
}
|
|
@@ -391,6 +389,6 @@ export async function teardownUnregisteredSessionResources(session, context) {
|
|
|
391
389
|
awaitClose: true,
|
|
392
390
|
});
|
|
393
391
|
}
|
|
394
|
-
export function teardownSessionRegistration(server
|
|
395
|
-
|
|
392
|
+
export function teardownSessionRegistration(server) {
|
|
393
|
+
unregisterSessionTaskScope(server);
|
|
396
394
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../../src/http/native.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../../src/http/native.ts"],"names":[],"mappings":"AAw4CA,wBAAsB,eAAe,IAAI,OAAO,CAAC;IAC/C,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC,CA2DD"}
|
package/dist/http/native.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
135
|
+
logDebug('MCP POST received', {
|
|
98
136
|
method: method ?? 'response',
|
|
99
|
-
|
|
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
163
|
error: 'Not Acceptable: expected text/event-stream',
|
|
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,11 +176,19 @@ class McpSessionGateway {
|
|
|
129
176
|
return;
|
|
130
177
|
const { sessionId, session } = sessionState;
|
|
131
178
|
await session.transport.close();
|
|
132
|
-
|
|
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
193
|
error: 'Not Acceptable: expected application/json and text/event-stream',
|
|
139
194
|
});
|
|
@@ -141,10 +196,26 @@ class McpSessionGateway {
|
|
|
141
196
|
}
|
|
142
197
|
const { body } = ctx;
|
|
143
198
|
if (isJsonRpcBatchRequest(body)) {
|
|
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
|
+
});
|
|
144
207
|
sendError(ctx.res, -32600, 'Batch requests not supported');
|
|
145
208
|
return null;
|
|
146
209
|
}
|
|
147
210
|
if (!isMcpMessageBody(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
|
+
});
|
|
148
219
|
sendError(ctx.res, -32600, 'Invalid request body');
|
|
149
220
|
return null;
|
|
150
221
|
}
|
|
@@ -156,6 +227,15 @@ class McpSessionGateway {
|
|
|
156
227
|
const isInitializedMethod = method !== null && isInitializedNotification(method);
|
|
157
228
|
const isInitNotification = isInitializedMethod && body.id === undefined;
|
|
158
229
|
if (isInitializedMethod && !isInitNotification) {
|
|
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
|
+
});
|
|
159
239
|
sendError(ctx.res, -32600, 'notifications/initialized must be sent as a notification', 400, requestId);
|
|
160
240
|
return null;
|
|
161
241
|
}
|
|
@@ -187,10 +267,19 @@ class McpSessionGateway {
|
|
|
187
267
|
return false;
|
|
188
268
|
if (!session) {
|
|
189
269
|
if (isInitNotification) {
|
|
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
|
+
});
|
|
190
279
|
sendError(ctx.res, -32600, 'Missing session ID', 400, requestId);
|
|
191
280
|
return false;
|
|
192
281
|
}
|
|
193
|
-
return
|
|
282
|
+
return true;
|
|
194
283
|
}
|
|
195
284
|
if (!this.ensureSessionProtocolVersion(ctx, session))
|
|
196
285
|
return false;
|
|
@@ -200,6 +289,16 @@ class McpSessionGateway {
|
|
|
200
289
|
return true;
|
|
201
290
|
if (method !== null && isPingRequest(method))
|
|
202
291
|
return true;
|
|
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
|
+
});
|
|
203
302
|
sendError(ctx.res, -32600, 'Session not initialized', 400, requestId);
|
|
204
303
|
return false;
|
|
205
304
|
}
|
|
@@ -216,23 +315,51 @@ class McpSessionGateway {
|
|
|
216
315
|
}
|
|
217
316
|
getInitializeProtocolVersion(ctx, requestId) {
|
|
218
317
|
if (!isMcpRequestBody(ctx.body)) {
|
|
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
|
+
});
|
|
219
327
|
sendError(ctx.res, -32600, 'Missing session ID', 400, requestId);
|
|
220
328
|
return null;
|
|
221
329
|
}
|
|
222
330
|
if (!isInitializeRequest(ctx.body)) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
: '
|
|
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 ? 'Invalid initialize request' : 'Missing session ID', 400, requestId);
|
|
226
344
|
return null;
|
|
227
345
|
}
|
|
228
346
|
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
347
|
const headerProtocolVersion = resolveProtocolVersionHeader(ctx.req);
|
|
234
348
|
if (headerProtocolVersion &&
|
|
235
349
|
headerProtocolVersion !== negotiatedProtocolVersion) {
|
|
350
|
+
logGatewayRejection({
|
|
351
|
+
message: 'Rejected MCP initialize request',
|
|
352
|
+
method: ctx.method,
|
|
353
|
+
path: ctx.url.pathname,
|
|
354
|
+
reason: 'protocol_version_mismatch',
|
|
355
|
+
status: 400,
|
|
356
|
+
mcpCode: -32600,
|
|
357
|
+
rpcId: requestId,
|
|
358
|
+
details: {
|
|
359
|
+
headerProtocolVersion,
|
|
360
|
+
negotiatedProtocolVersion,
|
|
361
|
+
},
|
|
362
|
+
});
|
|
236
363
|
sendError(ctx.res, -32600, `initialize protocolVersion mismatch: header=${headerProtocolVersion}, body=${negotiatedProtocolVersion}`, 400, requestId);
|
|
237
364
|
return null;
|
|
238
365
|
}
|
|
@@ -275,10 +402,28 @@ class McpSessionGateway {
|
|
|
275
402
|
getAuthenticatedSessionById(sessionId, authFingerprint, res, requestId = null) {
|
|
276
403
|
const session = this.store.get(sessionId);
|
|
277
404
|
if (!session) {
|
|
405
|
+
logGatewayRejection({
|
|
406
|
+
message: 'Rejected MCP session request',
|
|
407
|
+
path: '/mcp',
|
|
408
|
+
reason: 'session_not_found',
|
|
409
|
+
status: 404,
|
|
410
|
+
mcpCode: -32600,
|
|
411
|
+
sessionId,
|
|
412
|
+
rpcId: requestId,
|
|
413
|
+
});
|
|
278
414
|
sendError(res, -32600, 'Session not found', 404, requestId);
|
|
279
415
|
return null;
|
|
280
416
|
}
|
|
281
417
|
if (!authFingerprint || session.authFingerprint !== authFingerprint) {
|
|
418
|
+
logGatewayRejection({
|
|
419
|
+
message: 'Rejected MCP session request',
|
|
420
|
+
path: '/mcp',
|
|
421
|
+
reason: 'session_auth_mismatch',
|
|
422
|
+
status: 404,
|
|
423
|
+
mcpCode: -32600,
|
|
424
|
+
sessionId,
|
|
425
|
+
rpcId: requestId,
|
|
426
|
+
});
|
|
282
427
|
sendError(res, -32600, 'Session not found', 404, requestId);
|
|
283
428
|
return null;
|
|
284
429
|
}
|
|
@@ -296,6 +441,7 @@ class McpSessionGateway {
|
|
|
296
441
|
this.clearSessionInitTimeout(sessionId);
|
|
297
442
|
if (sessionId)
|
|
298
443
|
this.store.touch(sessionId);
|
|
444
|
+
logDebug('Session initialized', { sessionId }, 'session');
|
|
299
445
|
}
|
|
300
446
|
createSessionInitTimeout(sessionId, tracker, unpublishedSession) {
|
|
301
447
|
const initTimeout = setTimeout(() => {
|
|
@@ -305,9 +451,11 @@ class McpSessionGateway {
|
|
|
305
451
|
this.clearSessionInitTimeout(sessionId);
|
|
306
452
|
return;
|
|
307
453
|
}
|
|
308
|
-
|
|
454
|
+
logWarn('Session init timeout', { sessionId }, 'session');
|
|
455
|
+
this.cleanupSessionRecord(sessionId, createSessionTeardownOptions('init-timeout'));
|
|
309
456
|
return;
|
|
310
457
|
}
|
|
458
|
+
logWarn('Session init timeout before registration completed', { sessionId }, 'session');
|
|
311
459
|
tracker.releaseSlot();
|
|
312
460
|
void teardownUnregisteredSessionResources(unpublishedSession, 'session-init-timeout');
|
|
313
461
|
}, config.server.sessionInitTimeoutMs);
|
|
@@ -327,6 +475,10 @@ class McpSessionGateway {
|
|
|
327
475
|
await sessionServer.connect(transport);
|
|
328
476
|
}
|
|
329
477
|
catch (err) {
|
|
478
|
+
logWarn('Session transport connect failed', {
|
|
479
|
+
sessionId,
|
|
480
|
+
error: toError(err).message,
|
|
481
|
+
}, 'session');
|
|
330
482
|
clearTimeout(initTimeout);
|
|
331
483
|
tracker.releaseSlot();
|
|
332
484
|
void teardownUnregisteredSessionResources(unpublishedSession, 'session-connect-failed');
|
|
@@ -337,9 +489,22 @@ class McpSessionGateway {
|
|
|
337
489
|
async createNewSession(ctx, requestId, negotiatedProtocolVersion) {
|
|
338
490
|
const authFingerprint = buildAuthFingerprint(ctx.auth);
|
|
339
491
|
if (!authFingerprint) {
|
|
492
|
+
logError('Session creation failed: missing auth context', {
|
|
493
|
+
path: ctx.url.pathname,
|
|
494
|
+
method: ctx.method,
|
|
495
|
+
}, 'session');
|
|
340
496
|
sendError(ctx.res, -32603, 'Missing auth context', 500, requestId);
|
|
341
497
|
return null;
|
|
342
498
|
}
|
|
499
|
+
const ownerKey = buildAuthenticatedOwnerKey(ctx.auth);
|
|
500
|
+
if (!ownerKey) {
|
|
501
|
+
logError('Session creation failed: missing task owner context', {
|
|
502
|
+
path: ctx.url.pathname,
|
|
503
|
+
method: ctx.method,
|
|
504
|
+
}, 'session');
|
|
505
|
+
sendError(ctx.res, -32603, 'Missing auth owner context', 500, requestId);
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
343
508
|
if (!this.reserveCapacity(ctx.res, requestId))
|
|
344
509
|
return null;
|
|
345
510
|
const tracker = createSlotTracker(this.store);
|
|
@@ -349,6 +514,7 @@ class McpSessionGateway {
|
|
|
349
514
|
sessionServer = await this.createSessionServer();
|
|
350
515
|
}
|
|
351
516
|
catch (error) {
|
|
517
|
+
logError('Session server creation failed', { sessionId: newSessionId, error: toError(error).message }, 'session');
|
|
352
518
|
tracker.releaseSlot();
|
|
353
519
|
throw error;
|
|
354
520
|
}
|
|
@@ -363,6 +529,7 @@ class McpSessionGateway {
|
|
|
363
529
|
const isConnected = await this.connectTransport(sessionServer, transportImpl, initTimeout, tracker, unpublishedSession, newSessionId);
|
|
364
530
|
tracker.releaseSlot();
|
|
365
531
|
if (!isConnected) {
|
|
532
|
+
logWarn('Session closed before registration completed', { sessionId: newSessionId }, 'session');
|
|
366
533
|
void teardownUnregisteredSessionResources(unpublishedSession, 'session-closed-during-connect');
|
|
367
534
|
return null;
|
|
368
535
|
}
|
|
@@ -376,18 +543,24 @@ class McpSessionGateway {
|
|
|
376
543
|
authFingerprint,
|
|
377
544
|
});
|
|
378
545
|
this.sessionInitTimeouts.set(newSessionId, initTimeout);
|
|
546
|
+
registerMcpSessionOwnerKey(newSessionId, ownerKey);
|
|
379
547
|
registerMcpSessionServer(newSessionId, sessionServer);
|
|
548
|
+
logInfo('Session created', { sessionId: newSessionId, negotiatedProtocolVersion }, 'session');
|
|
380
549
|
transportImpl.onclose = composeCloseHandlers(transportImpl.onclose, () => {
|
|
381
|
-
this.cleanupSessionRecord(newSessionId, 'session-close');
|
|
550
|
+
this.cleanupSessionRecord(newSessionId, createSessionTeardownOptions('ended', 'session-close'));
|
|
382
551
|
});
|
|
383
552
|
return transportImpl;
|
|
384
553
|
}
|
|
385
|
-
cleanupSessionRecord(sessionId,
|
|
554
|
+
cleanupSessionRecord(sessionId, teardownOptions) {
|
|
555
|
+
const context = teardownOptions.closeTransportReason ??
|
|
556
|
+
teardownOptions.closeServerReason ??
|
|
557
|
+
'session';
|
|
558
|
+
logDebug('Session cleanup', { sessionId, context }, 'session');
|
|
386
559
|
this.clearSessionInitTimeout(sessionId);
|
|
387
560
|
const session = this.store.remove(sessionId);
|
|
388
561
|
if (!session)
|
|
389
562
|
return;
|
|
390
|
-
void teardownSessionResources(session,
|
|
563
|
+
void teardownSessionResources(session, teardownOptions);
|
|
391
564
|
}
|
|
392
565
|
clearSessionInitTimeout(sessionId) {
|
|
393
566
|
if (!sessionId)
|
|
@@ -412,11 +585,13 @@ class McpSessionGateway {
|
|
|
412
585
|
},
|
|
413
586
|
});
|
|
414
587
|
if (!allowed) {
|
|
588
|
+
logWarn('Session capacity exhausted', { maxSessions: config.server.maxSessions }, 'session');
|
|
415
589
|
sendError(res, -32000, 'Server busy', 503, requestId);
|
|
416
590
|
return false;
|
|
417
591
|
}
|
|
418
592
|
// Double-check: capacity may have changed during the async eviction window above.
|
|
419
593
|
if (!reserveSessionSlot(this.store, config.server.maxSessions)) {
|
|
594
|
+
logWarn('Session capacity exhausted (post-eviction)', { maxSessions: config.server.maxSessions }, 'session');
|
|
420
595
|
sendError(res, -32000, 'Server busy', 503, requestId);
|
|
421
596
|
return false;
|
|
422
597
|
}
|
|
@@ -472,12 +647,15 @@ class HttpDispatcher {
|
|
|
472
647
|
const handled = await this.handleMcpRoutes(authCtx);
|
|
473
648
|
if (handled)
|
|
474
649
|
return;
|
|
650
|
+
ctx.res.setHeader('Allow', 'DELETE, GET, OPTIONS, POST');
|
|
651
|
+
sendJson(ctx.res, 405, { error: 'Method Not Allowed' });
|
|
652
|
+
return;
|
|
475
653
|
}
|
|
476
654
|
sendJson(ctx.res, 404, { error: 'Not Found' });
|
|
477
655
|
}
|
|
478
656
|
catch (err) {
|
|
479
657
|
const error = toError(err);
|
|
480
|
-
logError('Request failed', error);
|
|
658
|
+
logError('Request failed', error, 'http');
|
|
481
659
|
if (!ctx.res.writableEnded) {
|
|
482
660
|
sendJson(ctx.res, 500, { error: 'Internal Server Error' });
|
|
483
661
|
}
|
|
@@ -504,6 +682,7 @@ class HttpDispatcher {
|
|
|
504
682
|
}
|
|
505
683
|
catch (err) {
|
|
506
684
|
const message = err instanceof Error ? err.message : 'Unauthorized';
|
|
685
|
+
logWarn('Authentication failed', { message, method: ctx.method, path: ctx.url.pathname }, 'auth');
|
|
507
686
|
if (isInsufficientScopeError(err)) {
|
|
508
687
|
applyInsufficientScopeAuthHeaders(ctx.req, ctx.res, err.requiredScopes, message);
|
|
509
688
|
sendError(ctx.res, -32000, message, 403);
|
|
@@ -562,6 +741,18 @@ class HttpRequestPipeline {
|
|
|
562
741
|
const requestId = getHeaderValue(rawReq, 'x-request-id') ?? randomUUID();
|
|
563
742
|
const sessionId = getMcpSessionId(rawReq) ?? undefined;
|
|
564
743
|
const { signal, cleanup } = createRequestAbortSignal(rawReq);
|
|
744
|
+
const path = resolveRequestPath(rawReq);
|
|
745
|
+
const startTime = performance.now();
|
|
746
|
+
rawRes.once('finish', () => {
|
|
747
|
+
logRequestCompletion({
|
|
748
|
+
path,
|
|
749
|
+
statusCode: rawRes.statusCode,
|
|
750
|
+
durationMs: performance.now() - startTime,
|
|
751
|
+
requestId,
|
|
752
|
+
...(rawReq.method ? { method: rawReq.method } : {}),
|
|
753
|
+
...(sessionId ? { sessionId } : {}),
|
|
754
|
+
});
|
|
755
|
+
});
|
|
565
756
|
try {
|
|
566
757
|
await runWithRequestContext({
|
|
567
758
|
requestId,
|
|
@@ -588,6 +779,14 @@ class HttpRequestPipeline {
|
|
|
588
779
|
const duplicateHeader = findDuplicateSingleValueHeader(rawReq);
|
|
589
780
|
if (!duplicateHeader)
|
|
590
781
|
return false;
|
|
782
|
+
logGatewayRejection({
|
|
783
|
+
message: 'Rejected HTTP request',
|
|
784
|
+
method: rawReq.method,
|
|
785
|
+
path: resolveRequestPath(rawReq),
|
|
786
|
+
reason: 'duplicate_single_value_header',
|
|
787
|
+
status: 400,
|
|
788
|
+
details: { header: duplicateHeader },
|
|
789
|
+
});
|
|
591
790
|
sendJson(rawRes, 400, {
|
|
592
791
|
error: `Duplicate ${duplicateHeader} header is not allowed`,
|
|
593
792
|
});
|
|
@@ -628,6 +827,12 @@ class HttpRequestPipeline {
|
|
|
628
827
|
}
|
|
629
828
|
catch (error) {
|
|
630
829
|
const bodyErrorKind = isJsonBodyError(error) ? error.kind : null;
|
|
830
|
+
if (bodyErrorKind === 'payload-too-large') {
|
|
831
|
+
logWarn('Request body too large', { method: ctx.method, path: ctx.url.pathname }, 'http');
|
|
832
|
+
}
|
|
833
|
+
else if (bodyErrorKind === 'read-failed' || bodyErrorKind === null) {
|
|
834
|
+
logError('Request body parsing failed', toError(error), 'http');
|
|
835
|
+
}
|
|
631
836
|
sendBodyParseError(ctx, bodyErrorKind, rawReq);
|
|
632
837
|
return false;
|
|
633
838
|
}
|
|
@@ -648,7 +853,7 @@ class HttpRequestPipeline {
|
|
|
648
853
|
// Server bootstrap
|
|
649
854
|
// ---------------------------------------------------------------------------
|
|
650
855
|
function handlePipelineError(error, res) {
|
|
651
|
-
logError('Request pipeline failed', toError(error));
|
|
856
|
+
logError('Request pipeline failed', toError(error), 'http');
|
|
652
857
|
if (res.writableEnded)
|
|
653
858
|
return;
|
|
654
859
|
if (!res.headersSent) {
|
|
@@ -694,7 +899,7 @@ function resolveListeningPort(server, fallback) {
|
|
|
694
899
|
function createShutdownHandler(options) {
|
|
695
900
|
const closeBatchSize = 10;
|
|
696
901
|
return async (signal) => {
|
|
697
|
-
logInfo(`Stopping HTTP server (${signal})
|
|
902
|
+
logInfo(`Stopping HTTP server (${signal})...`, undefined, 'http');
|
|
698
903
|
options.rateLimiter.stop();
|
|
699
904
|
options.sessionCleanup.abort();
|
|
700
905
|
drainConnectionsOnShutdown(options.server);
|
|
@@ -707,7 +912,7 @@ function createShutdownHandler(options) {
|
|
|
707
912
|
}));
|
|
708
913
|
for (const r of results) {
|
|
709
914
|
if (r.status === 'rejected') {
|
|
710
|
-
logError('Session teardown failed during shutdown', r.reason instanceof Error ? r.reason : undefined);
|
|
915
|
+
logError('Session teardown failed during shutdown', r.reason instanceof Error ? r.reason : undefined, 'http');
|
|
711
916
|
}
|
|
712
917
|
}
|
|
713
918
|
}
|
|
@@ -722,7 +927,7 @@ export async function startHttpServer() {
|
|
|
722
927
|
const sessionStore = createSessionStore(config.server.sessionTtlMs);
|
|
723
928
|
const sessionCleanup = startSessionCleanupLoop(sessionStore, config.server.sessionTtlMs, {
|
|
724
929
|
onEvictSession: (session) => {
|
|
725
|
-
teardownSessionRegistration(session.server
|
|
930
|
+
teardownSessionRegistration(session.server);
|
|
726
931
|
},
|
|
727
932
|
});
|
|
728
933
|
const mcpGateway = new McpSessionGateway(sessionStore, createMcpServerForHttpSession);
|
|
@@ -743,7 +948,7 @@ export async function startHttpServer() {
|
|
|
743
948
|
arch: process.arch,
|
|
744
949
|
hostname: hostname(),
|
|
745
950
|
nodeVersion: process.version,
|
|
746
|
-
});
|
|
951
|
+
}, 'http');
|
|
747
952
|
return {
|
|
748
953
|
port,
|
|
749
954
|
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;
|
|
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"}
|
package/dist/http/rate-limit.js
CHANGED
|
@@ -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 });
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ const isStdioMode = !values.http;
|
|
|
36
36
|
let isShuttingDown = false;
|
|
37
37
|
const shutdownHandlerRef = {};
|
|
38
38
|
function shouldAttemptShutdown() {
|
|
39
|
-
return !isShuttingDown &&
|
|
39
|
+
return !isShuttingDown && Boolean(shutdownHandlerRef.current);
|
|
40
40
|
}
|
|
41
41
|
function attemptShutdown(signal) {
|
|
42
42
|
if (!shutdownHandlerRef.current)
|
|
@@ -60,13 +60,13 @@ function registerHttpSignalHandlers() {
|
|
|
60
60
|
registerSignalHandlers(['SIGINT', 'SIGTERM'], tryShutdown);
|
|
61
61
|
}
|
|
62
62
|
function writeStartupError(error) {
|
|
63
|
-
logError('Failed to start server', error);
|
|
63
|
+
logError('Failed to start server', error, 'server');
|
|
64
64
|
process.stderr.write(`Failed to start server: ${error.message}\n`);
|
|
65
65
|
process.exitCode = 1;
|
|
66
66
|
scheduleForcedExit('Startup failure');
|
|
67
67
|
}
|
|
68
68
|
function handleFatalError(label, error, signal) {
|
|
69
|
-
logError(label, error);
|
|
69
|
+
logError(label, error, 'server');
|
|
70
70
|
process.stderr.write(`${label}: ${error.message}\n`);
|
|
71
71
|
process.exitCode = 1;
|
|
72
72
|
if (shouldAttemptShutdown()) {
|
|
@@ -84,7 +84,8 @@ process.on('unhandledRejection', (reason) => {
|
|
|
84
84
|
});
|
|
85
85
|
try {
|
|
86
86
|
if (isStdioMode) {
|
|
87
|
-
await startStdioServer();
|
|
87
|
+
const { shutdown } = await startStdioServer();
|
|
88
|
+
shutdownHandlerRef.current = shutdown;
|
|
88
89
|
}
|
|
89
90
|
else {
|
|
90
91
|
const { shutdown } = await startHttpServer();
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const serverVersion: string;
|
|
2
|
-
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
2
|
+
export type LogLevel = 'debug' | 'info' | 'notice' | 'warn' | 'error' | 'critical';
|
|
3
3
|
type TransformWorkerMode = 'threads' | 'process';
|
|
4
4
|
type AuthMode = 'oauth' | 'static';
|
|
5
5
|
interface WorkerResourceLimits {
|