@askrjs/node 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +102 -25
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +76 -12
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ const app = createServerApp({
|
|
|
23
23
|
routes: [{ path: "/health", handler: () => json({ status: "ok" }) }],
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
createServer(createNodeHandler(app)).listen(3000);
|
|
26
|
+
createServer(createNodeHandler(app, { baseUrl: "http://localhost:3000" })).listen(3000);
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
`createNodeHandler` also works as Connect middleware because it accepts an optional `next`
|
|
@@ -48,7 +48,8 @@ Pass an `AbortSignal` to integrate shutdown with your process lifecycle.
|
|
|
48
48
|
|
|
49
49
|
Enable the built-in `ws` transport with `websocket: true`. It defaults to a
|
|
50
50
|
1 MiB maximum message payload with compression disabled; pass
|
|
51
|
-
`websocket: { maxPayload, perMessageDeflate }` to override
|
|
51
|
+
`websocket: { maxPayload, maxRejectionBodyBytes, perMessageDeflate }` to override
|
|
52
|
+
those settings. Rejected upgrade bodies are capped at 64 KiB by default.
|
|
52
53
|
|
|
53
54
|
```ts
|
|
54
55
|
router.ws("/echo", (socket) => {
|
|
@@ -76,6 +77,8 @@ await running.close();
|
|
|
76
77
|
```
|
|
77
78
|
|
|
78
79
|
`serve` handles static assets and closes both the HTTP server and the application during shutdown.
|
|
80
|
+
Both `listen` and `serve` bind to `127.0.0.1` by default. A non-loopback
|
|
81
|
+
`host` also requires `allowPublicBind: true` so public exposure is explicit.
|
|
79
82
|
|
|
80
83
|
## MCP over stdio
|
|
81
84
|
|
package/dist/index.d.ts
CHANGED
|
@@ -5,14 +5,18 @@ import { AddressInfo } from "node:net";
|
|
|
5
5
|
//#region src/contracts.d.ts
|
|
6
6
|
interface NodeWebSocketOptions {
|
|
7
7
|
readonly maxPayload?: number;
|
|
8
|
+
readonly maxRejectionBodyBytes?: number;
|
|
8
9
|
readonly perMessageDeflate?: boolean | PerMessageDeflateOptions;
|
|
10
|
+
readonly allowedOrigins?: readonly string[];
|
|
9
11
|
}
|
|
10
12
|
interface NodeHandlerOptions {
|
|
11
13
|
baseUrl?: string;
|
|
14
|
+
allowedHosts?: readonly string[];
|
|
12
15
|
}
|
|
13
16
|
interface ListenOptions {
|
|
14
17
|
port?: number;
|
|
15
18
|
host?: string;
|
|
19
|
+
allowPublicBind?: boolean;
|
|
16
20
|
backlog?: number;
|
|
17
21
|
signal?: AbortSignal;
|
|
18
22
|
requestTimeout?: number;
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,10 @@ import { Readable } from "node:stream";
|
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { WebSocketServer } from "ws";
|
|
4
4
|
import { createReadStream } from "node:fs";
|
|
5
|
-
import { stat } from "node:fs/promises";
|
|
5
|
+
import { realpath, stat } from "node:fs/promises";
|
|
6
6
|
import { extname, resolve, sep } from "node:path";
|
|
7
7
|
//#region src/request.ts
|
|
8
|
+
var NodeRequestError = class extends TypeError {};
|
|
8
9
|
function requestHeaders(request) {
|
|
9
10
|
const headers = new Headers();
|
|
10
11
|
for (const [key, value] of Object.entries(request.headers)) {
|
|
@@ -13,11 +14,22 @@ function requestHeaders(request) {
|
|
|
13
14
|
}
|
|
14
15
|
return headers;
|
|
15
16
|
}
|
|
17
|
+
function resolveNodeRequestUrl(request, options) {
|
|
18
|
+
const host = request.headers.host;
|
|
19
|
+
if (!options.baseUrl && !options.allowedHosts?.length) throw new NodeRequestError("Node handling requires baseUrl or an allowedHosts allowlist.");
|
|
20
|
+
if (!host) throw new NodeRequestError("Request Host header is required.");
|
|
21
|
+
const hostName = host.startsWith("[") ? host.slice(1, host.indexOf("]")) : host.split(":", 1)[0];
|
|
22
|
+
if (options.allowedHosts?.length && !options.allowedHosts.some((allowed) => allowed === host || allowed === hostName)) throw new NodeRequestError("Request Host header is not allowed.");
|
|
23
|
+
const base = options.baseUrl ?? `http://${host}`;
|
|
24
|
+
const target = new URL(request.url ?? "/", base);
|
|
25
|
+
if (/^https?:\/\//i.test(request.url ?? "") && target.origin !== new URL(base).origin) throw new NodeRequestError("Absolute-form request target origin is not allowed.");
|
|
26
|
+
return target;
|
|
27
|
+
}
|
|
16
28
|
function requestFromNode(request, options, signal) {
|
|
17
|
-
const
|
|
29
|
+
const target = resolveNodeRequestUrl(request, options);
|
|
18
30
|
const method = request.method ?? "GET";
|
|
19
31
|
const body = method === "GET" || method === "HEAD" ? void 0 : Readable.toWeb(request);
|
|
20
|
-
return new Request(
|
|
32
|
+
return new Request(target, {
|
|
21
33
|
method,
|
|
22
34
|
headers: requestHeaders(request),
|
|
23
35
|
body,
|
|
@@ -101,10 +113,11 @@ function minimalError(response, error) {
|
|
|
101
113
|
response.destroy(error instanceof Error ? error : void 0);
|
|
102
114
|
return;
|
|
103
115
|
}
|
|
104
|
-
|
|
105
|
-
response.
|
|
116
|
+
const clientError = error instanceof NodeRequestError;
|
|
117
|
+
response.statusCode = clientError ? 400 : 500;
|
|
118
|
+
response.statusMessage = clientError ? "Bad Request" : "Internal Server Error";
|
|
106
119
|
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
107
|
-
response.end("Internal Server Error");
|
|
120
|
+
response.end(clientError ? "Bad Request" : "Internal Server Error");
|
|
108
121
|
}
|
|
109
122
|
function attachAbort(request, response, controller) {
|
|
110
123
|
const abort = () => controller.abort();
|
|
@@ -130,6 +143,16 @@ function createNodeHandler(app, options = {}) {
|
|
|
130
143
|
};
|
|
131
144
|
}
|
|
132
145
|
//#endregion
|
|
146
|
+
//#region src/bind.ts
|
|
147
|
+
function resolveBindHost(options) {
|
|
148
|
+
const host = options.host ?? "127.0.0.1";
|
|
149
|
+
if (!(host === "localhost" || host === "::1" || /^127(?:\.\d{1,3}){3}$/.test(host)) && options.allowPublicBind !== true) throw new TypeError(`Refusing to bind non-loopback host ${host} without allowPublicBind: true.`);
|
|
150
|
+
return host;
|
|
151
|
+
}
|
|
152
|
+
function formatHostForUrl(host) {
|
|
153
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
133
156
|
//#region src/websocket.ts
|
|
134
157
|
function subscribe(socket, event, listener) {
|
|
135
158
|
socket.on(event, listener);
|
|
@@ -154,15 +177,43 @@ function socketLike(socket) {
|
|
|
154
177
|
onError: (listener) => subscribe(socket, "error", listener)
|
|
155
178
|
};
|
|
156
179
|
}
|
|
157
|
-
async function
|
|
158
|
-
const
|
|
180
|
+
async function readRejectionBody(response, maxBytes) {
|
|
181
|
+
const lengthHeader = response.headers.get("content-length");
|
|
182
|
+
const declaredLength = lengthHeader === null ? void 0 : Number(lengthHeader);
|
|
183
|
+
if (declaredLength !== void 0 && Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
184
|
+
await response.body?.cancel("WebSocket rejection body exceeded maxRejectionBodyBytes");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (!response.body) return Buffer.alloc(0);
|
|
188
|
+
const reader = response.body.getReader();
|
|
189
|
+
const chunks = [];
|
|
190
|
+
let length = 0;
|
|
191
|
+
while (true) {
|
|
192
|
+
const part = await reader.read();
|
|
193
|
+
if (part.done) return Buffer.concat(chunks, length);
|
|
194
|
+
length += part.value.byteLength;
|
|
195
|
+
if (length > maxBytes) {
|
|
196
|
+
await reader.cancel("WebSocket rejection body exceeded maxRejectionBodyBytes");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
chunks.push(Buffer.from(part.value));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function rejectUpgrade(socket, response, maxBodyBytes) {
|
|
203
|
+
const buffered = await readRejectionBody(response, maxBodyBytes);
|
|
204
|
+
if (!buffered) response = new Response("WebSocket rejection body exceeded configured limit", { status: 500 });
|
|
205
|
+
const body = buffered ?? Buffer.from(await response.arrayBuffer());
|
|
159
206
|
const lines = [`HTTP/1.1 ${response.status} ${response.statusText || "Rejected"}`];
|
|
160
|
-
response.headers.forEach((value, name) =>
|
|
161
|
-
|
|
207
|
+
response.headers.forEach((value, name) => {
|
|
208
|
+
if (name !== "content-length" && name !== "transfer-encoding") lines.push(`${name}: ${value}`);
|
|
209
|
+
});
|
|
210
|
+
lines.push(`content-length: ${body.byteLength}`);
|
|
162
211
|
lines.push("connection: close", "", "");
|
|
163
212
|
socket.end(Buffer.concat([Buffer.from(lines.join("\r\n")), body]));
|
|
164
213
|
}
|
|
165
214
|
function installWebSockets(server, app, options = {}, handlerOptions = {}) {
|
|
215
|
+
const maxRejectionBodyBytes = options.maxRejectionBodyBytes ?? 65536;
|
|
216
|
+
if (!Number.isInteger(maxRejectionBodyBytes) || maxRejectionBodyBytes <= 0) throw new TypeError("WebSocket maxRejectionBodyBytes must be a positive integer.");
|
|
166
217
|
const webSockets = new WebSocketServer({
|
|
167
218
|
noServer: true,
|
|
168
219
|
maxPayload: options.maxPayload ?? 1048576,
|
|
@@ -172,6 +223,19 @@ function installWebSockets(server, app, options = {}, handlerOptions = {}) {
|
|
|
172
223
|
const controller = new AbortController();
|
|
173
224
|
let accepted;
|
|
174
225
|
Promise.resolve().then(() => requestFromNode(request, handlerOptions, controller.signal)).then(async (webRequest) => {
|
|
226
|
+
const originHeader = request.headers.origin;
|
|
227
|
+
const origin = Array.isArray(originHeader) ? void 0 : originHeader;
|
|
228
|
+
const allowedOrigins = options.allowedOrigins ?? [new URL(webRequest.url).origin];
|
|
229
|
+
let normalizedOrigin;
|
|
230
|
+
try {
|
|
231
|
+
normalizedOrigin = origin ? new URL(origin).origin : void 0;
|
|
232
|
+
} catch {
|
|
233
|
+
normalizedOrigin = void 0;
|
|
234
|
+
}
|
|
235
|
+
if (!normalizedOrigin || !allowedOrigins.includes(normalizedOrigin)) {
|
|
236
|
+
await rejectUpgrade(socket, new Response("Forbidden", { status: 403 }), maxRejectionBodyBytes);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
175
239
|
let marker;
|
|
176
240
|
const response = await app.fetch(webRequest, { websocket: { upgrade: (_request, handler, context) => {
|
|
177
241
|
marker = new Response(null, { status: 200 });
|
|
@@ -183,7 +247,7 @@ function installWebSockets(server, app, options = {}, handlerOptions = {}) {
|
|
|
183
247
|
return marker;
|
|
184
248
|
} } });
|
|
185
249
|
if (!accepted || response !== accepted.response) {
|
|
186
|
-
await rejectUpgrade(socket, response);
|
|
250
|
+
await rejectUpgrade(socket, response, maxRejectionBodyBytes);
|
|
187
251
|
return;
|
|
188
252
|
}
|
|
189
253
|
webSockets.handleUpgrade(request, socket, head, (webSocket) => {
|
|
@@ -200,11 +264,13 @@ function installWebSockets(server, app, options = {}, handlerOptions = {}) {
|
|
|
200
264
|
//#endregion
|
|
201
265
|
//#region src/listen.ts
|
|
202
266
|
function listen(app, options = {}) {
|
|
203
|
-
const
|
|
267
|
+
const host = resolveBindHost(options);
|
|
268
|
+
const handlerOptions = { allowedHosts: [host, "localhost"] };
|
|
269
|
+
const server = createServer(createNodeHandler(app, handlerOptions));
|
|
204
270
|
if (options.requestTimeout !== void 0) server.requestTimeout = options.requestTimeout;
|
|
205
271
|
if (options.headersTimeout !== void 0) server.headersTimeout = options.headersTimeout;
|
|
206
272
|
if (options.keepAliveTimeout !== void 0) server.keepAliveTimeout = options.keepAliveTimeout;
|
|
207
|
-
const webSockets = options.websocket ? installWebSockets(server, app, options.websocket === true ? {} : options.websocket) : void 0;
|
|
273
|
+
const webSockets = options.websocket ? installWebSockets(server, app, options.websocket === true ? {} : options.websocket, handlerOptions) : void 0;
|
|
208
274
|
if (webSockets) {
|
|
209
275
|
const nativeClose = server.close.bind(server);
|
|
210
276
|
server.close = ((callback) => {
|
|
@@ -221,7 +287,7 @@ function listen(app, options = {}) {
|
|
|
221
287
|
reject(error);
|
|
222
288
|
};
|
|
223
289
|
server.once("error", onError);
|
|
224
|
-
server.listen(options.port ?? 0,
|
|
290
|
+
server.listen(options.port ?? 0, host, options.backlog, () => {
|
|
225
291
|
server.off("error", onError);
|
|
226
292
|
resolve(server);
|
|
227
293
|
});
|
|
@@ -247,8 +313,14 @@ const mimeTypes = {
|
|
|
247
313
|
function isAssetPath(pathname) {
|
|
248
314
|
return extname(pathname) !== "";
|
|
249
315
|
}
|
|
316
|
+
function isWithinRoot(root, candidate) {
|
|
317
|
+
const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
|
|
318
|
+
return candidate === root || candidate.startsWith(prefix);
|
|
319
|
+
}
|
|
250
320
|
async function serve(app, options = {}) {
|
|
251
|
-
const
|
|
321
|
+
const host = resolveBindHost(options);
|
|
322
|
+
const root = options.assets ? await realpath(resolve(options.assets.root)) : void 0;
|
|
323
|
+
const handlerOptions = { allowedHosts: [host, "localhost"] };
|
|
252
324
|
const applicationHandler = createNodeHandler({ async fetch(request, dispatchOptions) {
|
|
253
325
|
const result = await app.fetch(request, dispatchOptions);
|
|
254
326
|
if (!result.headers.has("cache-control") && result.headers.get("content-type")?.includes("text/html")) {
|
|
@@ -261,11 +333,11 @@ async function serve(app, options = {}) {
|
|
|
261
333
|
});
|
|
262
334
|
}
|
|
263
335
|
return result;
|
|
264
|
-
} });
|
|
336
|
+
} }, handlerOptions);
|
|
265
337
|
const server = createServer(async (request, response) => {
|
|
266
338
|
let pathname;
|
|
267
339
|
try {
|
|
268
|
-
pathname = decodeURIComponent(
|
|
340
|
+
pathname = decodeURIComponent(resolveNodeRequestUrl(request, handlerOptions).pathname);
|
|
269
341
|
} catch {
|
|
270
342
|
response.writeHead(400, { "content-type": "text/plain; charset=utf-8" }).end("Bad Request");
|
|
271
343
|
return;
|
|
@@ -273,15 +345,21 @@ async function serve(app, options = {}) {
|
|
|
273
345
|
const method = request.method ?? "GET";
|
|
274
346
|
if (root && (method === "GET" || method === "HEAD") && isAssetPath(pathname)) {
|
|
275
347
|
const extension = extname(pathname).toLowerCase();
|
|
276
|
-
const
|
|
277
|
-
const inside =
|
|
348
|
+
const unresolvedCandidate = resolve(root, `.${pathname}`);
|
|
349
|
+
const inside = isWithinRoot(root, unresolvedCandidate);
|
|
350
|
+
let candidate;
|
|
278
351
|
let file;
|
|
279
352
|
if (inside && extension !== ".map") try {
|
|
280
|
-
|
|
353
|
+
const resolvedCandidate = await realpath(unresolvedCandidate);
|
|
354
|
+
if (isWithinRoot(root, resolvedCandidate)) {
|
|
355
|
+
candidate = resolvedCandidate;
|
|
356
|
+
file = await stat(candidate);
|
|
357
|
+
}
|
|
281
358
|
} catch {
|
|
359
|
+
candidate = void 0;
|
|
282
360
|
file = void 0;
|
|
283
361
|
}
|
|
284
|
-
if (!file?.isFile()) {
|
|
362
|
+
if (!candidate || !file?.isFile()) {
|
|
285
363
|
response.writeHead(404, {
|
|
286
364
|
"content-type": "text/plain; charset=utf-8",
|
|
287
365
|
"x-content-type-options": "nosniff"
|
|
@@ -303,7 +381,7 @@ async function serve(app, options = {}) {
|
|
|
303
381
|
if (options.requestTimeout !== void 0) server.requestTimeout = options.requestTimeout;
|
|
304
382
|
if (options.headersTimeout !== void 0) server.headersTimeout = options.headersTimeout;
|
|
305
383
|
if (options.keepAliveTimeout !== void 0) server.keepAliveTimeout = options.keepAliveTimeout;
|
|
306
|
-
const webSockets = options.websocket ? installWebSockets(server, app, options.websocket === true ? {} : options.websocket) : void 0;
|
|
384
|
+
const webSockets = options.websocket ? installWebSockets(server, app, options.websocket === true ? {} : options.websocket, handlerOptions) : void 0;
|
|
307
385
|
let closing;
|
|
308
386
|
const signals = options.signals === false ? [] : options.signals ?? ["SIGINT", "SIGTERM"];
|
|
309
387
|
const removeListeners = () => {
|
|
@@ -330,7 +408,7 @@ async function serve(app, options = {}) {
|
|
|
330
408
|
for (const signal of signals) process.once(signal, shutdown);
|
|
331
409
|
await new Promise((resolveListen, rejectListen) => {
|
|
332
410
|
server.once("error", rejectListen);
|
|
333
|
-
server.listen(options.port ?? 0,
|
|
411
|
+
server.listen(options.port ?? 0, host, options.backlog, () => {
|
|
334
412
|
server.removeListener("error", rejectListen);
|
|
335
413
|
resolveListen();
|
|
336
414
|
});
|
|
@@ -343,10 +421,9 @@ async function serve(app, options = {}) {
|
|
|
343
421
|
await close();
|
|
344
422
|
throw new Error("serve requires a TCP address.");
|
|
345
423
|
}
|
|
346
|
-
const host = options.host ?? "127.0.0.1";
|
|
347
424
|
return Object.freeze({
|
|
348
425
|
server,
|
|
349
|
-
url: `http://${host}:${address.port}`,
|
|
426
|
+
url: `http://${formatHostForUrl(host)}:${address.port}`,
|
|
350
427
|
close
|
|
351
428
|
});
|
|
352
429
|
}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ interface McpStdioOptions<Dependencies = undefined> {
|
|
|
10
10
|
signal?: AbortSignal;
|
|
11
11
|
auth?: AuthContext | ((environment: NodeJS.ProcessEnv) => AuthContext | Promise<AuthContext>);
|
|
12
12
|
environment?: NodeJS.ProcessEnv;
|
|
13
|
+
maxLineBytes?: number;
|
|
14
|
+
maxConcurrency?: number;
|
|
13
15
|
}
|
|
14
16
|
interface McpStdioConnection {
|
|
15
17
|
readonly closed: Promise<void>;
|
package/dist/mcp.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createInterface } from "node:readline";
|
|
2
1
|
//#region src/mcp.ts
|
|
3
2
|
const anonymous = Object.freeze({
|
|
4
3
|
authenticated: false,
|
|
@@ -10,11 +9,6 @@ function connectMcpStdio(mcp, options) {
|
|
|
10
9
|
const input = options.input ?? process.stdin;
|
|
11
10
|
const output = options.output ?? process.stdout;
|
|
12
11
|
const diagnostics = options.diagnostics ?? process.stderr;
|
|
13
|
-
const lines = createInterface({
|
|
14
|
-
input,
|
|
15
|
-
crlfDelay: Infinity,
|
|
16
|
-
terminal: false
|
|
17
|
-
});
|
|
18
12
|
const controllers = /* @__PURE__ */ new Map();
|
|
19
13
|
const sessionId = crypto.randomUUID();
|
|
20
14
|
let finish;
|
|
@@ -22,13 +16,19 @@ function connectMcpStdio(mcp, options) {
|
|
|
22
16
|
finish = resolve;
|
|
23
17
|
});
|
|
24
18
|
let ended = false;
|
|
19
|
+
const maxLineBytes = options.maxLineBytes ?? 1048576;
|
|
20
|
+
const maxConcurrency = options.maxConcurrency ?? 16;
|
|
21
|
+
if (!Number.isInteger(maxLineBytes) || maxLineBytes <= 0) throw new TypeError("MCP maxLineBytes must be a positive integer.");
|
|
22
|
+
if (!Number.isInteger(maxConcurrency) || maxConcurrency <= 0) throw new TypeError("MCP maxConcurrency must be a positive integer.");
|
|
23
|
+
let active = 0;
|
|
24
|
+
let cleanupInput = () => void 0;
|
|
25
25
|
const write = (message) => new Promise((resolve, reject) => {
|
|
26
26
|
output.write(`${JSON.stringify(message)}\n`, (error) => error ? reject(error) : resolve());
|
|
27
27
|
});
|
|
28
28
|
const close = async () => {
|
|
29
29
|
if (ended) return closed;
|
|
30
30
|
ended = true;
|
|
31
|
-
|
|
31
|
+
cleanupInput();
|
|
32
32
|
for (const controller of controllers.values()) controller.abort();
|
|
33
33
|
controllers.clear();
|
|
34
34
|
mcp.terminateSession(sessionId);
|
|
@@ -36,7 +36,19 @@ function connectMcpStdio(mcp, options) {
|
|
|
36
36
|
return closed;
|
|
37
37
|
};
|
|
38
38
|
options.signal?.addEventListener("abort", () => void close(), { once: true });
|
|
39
|
-
|
|
39
|
+
const handleLine = (line) => {
|
|
40
|
+
if (active >= maxConcurrency) {
|
|
41
|
+
write({
|
|
42
|
+
jsonrpc: "2.0",
|
|
43
|
+
id: null,
|
|
44
|
+
error: {
|
|
45
|
+
code: -32600,
|
|
46
|
+
message: "Too many concurrent requests"
|
|
47
|
+
}
|
|
48
|
+
}).catch(() => void close());
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
active += 1;
|
|
40
52
|
(async () => {
|
|
41
53
|
let message;
|
|
42
54
|
try {
|
|
@@ -78,14 +90,66 @@ function connectMcpStdio(mcp, options) {
|
|
|
78
90
|
if (controller && id !== void 0) controllers.delete(id);
|
|
79
91
|
}
|
|
80
92
|
}
|
|
81
|
-
})().catch((error) => diagnostics.write(`MCP stdio error: ${error instanceof Error ? error.message : String(error)}\n`))
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
})().catch((error) => diagnostics.write(`MCP stdio error: ${error instanceof Error ? error.message : String(error)}\n`)).finally(() => {
|
|
94
|
+
active -= 1;
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
const lineBuffer = Buffer.allocUnsafe(maxLineBytes);
|
|
98
|
+
let lineBytes = 0;
|
|
99
|
+
let discardingOversizedLine = false;
|
|
100
|
+
const rejectOversizedLine = () => {
|
|
101
|
+
write({
|
|
102
|
+
jsonrpc: "2.0",
|
|
103
|
+
id: null,
|
|
104
|
+
error: {
|
|
105
|
+
code: -32600,
|
|
106
|
+
message: "Request line exceeds the configured limit"
|
|
107
|
+
}
|
|
108
|
+
}).catch(() => void close());
|
|
109
|
+
};
|
|
110
|
+
const onData = (chunk) => {
|
|
111
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
112
|
+
let offset = 0;
|
|
113
|
+
while (offset < bytes.byteLength) {
|
|
114
|
+
const newline = bytes.indexOf(10, offset);
|
|
115
|
+
const end = newline < 0 ? bytes.byteLength : newline;
|
|
116
|
+
const segmentBytes = end - offset;
|
|
117
|
+
if (!discardingOversizedLine) {
|
|
118
|
+
if (lineBytes + segmentBytes > maxLineBytes) {
|
|
119
|
+
discardingOversizedLine = true;
|
|
120
|
+
lineBytes = 0;
|
|
121
|
+
rejectOversizedLine();
|
|
122
|
+
} else if (segmentBytes > 0) {
|
|
123
|
+
bytes.copy(lineBuffer, lineBytes, offset, end);
|
|
124
|
+
lineBytes += segmentBytes;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (newline < 0) break;
|
|
128
|
+
if (!discardingOversizedLine) {
|
|
129
|
+
const length = lineBytes > 0 && lineBuffer[lineBytes - 1] === 13 ? lineBytes - 1 : lineBytes;
|
|
130
|
+
handleLine(lineBuffer.subarray(0, length).toString("utf8"));
|
|
131
|
+
}
|
|
132
|
+
lineBytes = 0;
|
|
133
|
+
discardingOversizedLine = false;
|
|
134
|
+
offset = newline + 1;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const finishInput = () => {
|
|
84
138
|
if (!ended) {
|
|
139
|
+
if (!discardingOversizedLine && lineBytes > 0) handleLine(lineBuffer.subarray(0, lineBytes).toString("utf8"));
|
|
85
140
|
ended = true;
|
|
141
|
+
cleanupInput();
|
|
86
142
|
finish();
|
|
87
143
|
}
|
|
88
|
-
}
|
|
144
|
+
};
|
|
145
|
+
input.on("data", onData);
|
|
146
|
+
input.once("end", finishInput);
|
|
147
|
+
input.once("close", finishInput);
|
|
148
|
+
cleanupInput = () => {
|
|
149
|
+
input.off("data", onData);
|
|
150
|
+
input.off("end", finishInput);
|
|
151
|
+
input.off("close", finishInput);
|
|
152
|
+
};
|
|
89
153
|
return {
|
|
90
154
|
closed,
|
|
91
155
|
close
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askrjs/node",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Node http adapter for @askrjs/server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"askr",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"ws": "^8.21.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
+
"@askrjs/schema": ">=0.0.2 <0.1.0",
|
|
63
64
|
"@types/node": "^26.0.0",
|
|
64
65
|
"@types/ws": "^8.18.1",
|
|
65
66
|
"publint": "^0.3.21",
|