@j0hanz/fetch-url-mcp 1.12.7 → 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 +4 -5
- 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 +554 -10
- 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 +3 -4
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +67 -6
- 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 +155 -4
- 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} +2 -2
- 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.map +1 -1
- package/dist/lib/mcp-interop.js +4 -6
- package/dist/lib/net/http.d.ts.map +1 -0
- package/dist/lib/{http.js → net/http.js} +4 -7
- 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} +3 -5
- package/dist/lib/{url.d.ts → net/url.d.ts} +1 -1
- package/dist/lib/net/url.d.ts.map +1 -0
- package/dist/lib/{url.js → net/url.js} +3 -5
- package/dist/lib/utils.d.ts +2 -18
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +29 -104
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +8 -5
- package/dist/schemas.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +7 -9
- 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 +745 -10
- 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} +6 -8
- 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 -402
- package/dist/lib/error-codes.d.ts +0 -13
- package/dist/lib/error-codes.d.ts.map +0 -1
- package/dist/lib/error-codes.js +0 -12
- 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 -16
- package/dist/lib/logger-names.d.ts.map +0 -1
- package/dist/lib/logger-names.js +0 -15
- 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 -253
- 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 -1042
- 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 {
|
|
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';
|
|
12
15
|
import { acceptsEventStream, acceptsJsonAndEventStream, isMcpRequestBody, } from '../lib/mcp-interop.js';
|
|
13
|
-
import {
|
|
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;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/http/rate-limit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/http/rate-limit.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,cAAc,EAAY,MAAM,aAAa,CAAC;AAY5D,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;AA+FD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,GACvB,oBAAoB,CAGtB"}
|
package/dist/http/rate-limit.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { logWarn } from '../lib/core.js';
|
|
2
|
-
import {
|
|
3
|
-
import { isAbortError } from '../lib/utils.js';
|
|
1
|
+
import { Loggers, logWarn } from '../lib/core.js';
|
|
2
|
+
import { isAbortError } from '../lib/error/index.js';
|
|
4
3
|
import { startAbortableIntervalLoop } from '../lib/utils.js';
|
|
5
|
-
import { sendJson } from './
|
|
4
|
+
import { sendJson } from './native.js';
|
|
6
5
|
// ---------------------------------------------------------------------------
|
|
7
6
|
// Rate limiter
|
|
8
7
|
// ---------------------------------------------------------------------------
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
interface CliValues {
|
|
3
|
+
readonly stdio: boolean;
|
|
4
|
+
readonly http: boolean;
|
|
5
|
+
readonly help: boolean;
|
|
6
|
+
readonly version: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface CliParseSuccess {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly values: CliValues;
|
|
11
|
+
}
|
|
12
|
+
interface CliParseFailure {
|
|
13
|
+
readonly ok: false;
|
|
14
|
+
readonly message: string;
|
|
15
|
+
}
|
|
16
|
+
type CliParseResult = CliParseSuccess | CliParseFailure;
|
|
17
|
+
export declare function renderCliUsage(): string;
|
|
18
|
+
export declare function parseCliArgs(args: readonly string[]): CliParseResult;
|
|
2
19
|
export {};
|
|
3
20
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAUA,UAAU,SAAS;IACjB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;CAC5B;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,KAAK,cAAc,GAAG,eAAe,GAAG,eAAe,CAAC;AA2CxD,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,CA2BpE"}
|