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