@askrjs/node 0.0.8 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -5
- package/dist/index.d.ts +90 -6
- package/dist/index.js +442 -97
- package/dist/mcp.d.ts +26 -0
- package/dist/mcp.js +129 -60
- package/package.json +14 -12
package/dist/mcp.d.ts
CHANGED
|
@@ -2,21 +2,47 @@ import { Readable, Writable } from "node:stream";
|
|
|
2
2
|
import { AuthContext } from "@askrjs/auth";
|
|
3
3
|
import { McpServer } from "@askrjs/server/mcp";
|
|
4
4
|
//#region src/mcp.d.ts
|
|
5
|
+
/** Options for {@link connectMcpStdio}. */
|
|
5
6
|
interface McpStdioOptions<Dependencies = undefined> {
|
|
7
|
+
/** Application dependencies passed through to each MCP request. */
|
|
6
8
|
dependencies: Dependencies;
|
|
9
|
+
/** Stream to read newline-delimited JSON-RPC requests from; defaults to `process.stdin`. */
|
|
7
10
|
input?: Readable;
|
|
11
|
+
/** Stream to write newline-delimited JSON-RPC responses to; defaults to `process.stdout`. */
|
|
8
12
|
output?: Writable;
|
|
13
|
+
/** Stream that non-protocol errors are reported to; defaults to `process.stderr`. */
|
|
9
14
|
diagnostics?: Writable;
|
|
15
|
+
/** Aborting this signal closes the connection. */
|
|
10
16
|
signal?: AbortSignal;
|
|
17
|
+
/** Auth context to use for requests, or a function that derives one from the environment. */
|
|
11
18
|
auth?: AuthContext | ((environment: NodeJS.ProcessEnv) => AuthContext | Promise<AuthContext>);
|
|
19
|
+
/** Environment passed to the `auth` function; defaults to `process.env`. */
|
|
12
20
|
environment?: NodeJS.ProcessEnv;
|
|
21
|
+
/** Maximum size, in bytes, of a single input line before it is rejected; defaults to 1 MiB. */
|
|
13
22
|
maxLineBytes?: number;
|
|
23
|
+
/** Maximum number of requests handled concurrently; defaults to 16. */
|
|
14
24
|
maxConcurrency?: number;
|
|
15
25
|
}
|
|
26
|
+
/** A live MCP stdio connection returned by {@link connectMcpStdio}. */
|
|
16
27
|
interface McpStdioConnection {
|
|
28
|
+
/** Resolves once the connection has fully closed. */
|
|
17
29
|
readonly closed: Promise<void>;
|
|
30
|
+
/** Closes the connection, aborting in-flight requests and terminating the MCP session. */
|
|
18
31
|
close(): Promise<void>;
|
|
19
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Connects an MCP server to newline-delimited JSON-RPC over stdio (or any
|
|
35
|
+
* pair of readable/writable streams).
|
|
36
|
+
*
|
|
37
|
+
* Reads one JSON-RPC message per line, dispatches it to `mcp.handle`, and
|
|
38
|
+
* writes the response back as a line of JSON. Handles request cancellation
|
|
39
|
+
* notifications, enforces `maxConcurrency` and `maxLineBytes`, and cleans up
|
|
40
|
+
* the MCP session when the connection closes.
|
|
41
|
+
*
|
|
42
|
+
* @param mcp - The MCP server to dispatch requests to.
|
|
43
|
+
* @param options - Stdio connection options, including dependencies and stream overrides.
|
|
44
|
+
* @returns A handle exposing `closed` and `close()` for the connection's lifecycle.
|
|
45
|
+
*/
|
|
20
46
|
declare function connectMcpStdio<Dependencies>(mcp: McpServer<Dependencies>, options: McpStdioOptions<Dependencies>): McpStdioConnection;
|
|
21
47
|
//#endregion
|
|
22
48
|
export { McpStdioConnection, McpStdioOptions, connectMcpStdio };
|
package/dist/mcp.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { addAbortListener } from "node:events";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
1
3
|
//#region src/mcp.ts
|
|
2
4
|
const anonymous = Object.freeze({
|
|
3
5
|
authenticated: false,
|
|
@@ -5,12 +7,25 @@ const anonymous = Object.freeze({
|
|
|
5
7
|
session: null,
|
|
6
8
|
tenant: null
|
|
7
9
|
});
|
|
10
|
+
/**
|
|
11
|
+
* Connects an MCP server to newline-delimited JSON-RPC over stdio (or any
|
|
12
|
+
* pair of readable/writable streams).
|
|
13
|
+
*
|
|
14
|
+
* Reads one JSON-RPC message per line, dispatches it to `mcp.handle`, and
|
|
15
|
+
* writes the response back as a line of JSON. Handles request cancellation
|
|
16
|
+
* notifications, enforces `maxConcurrency` and `maxLineBytes`, and cleans up
|
|
17
|
+
* the MCP session when the connection closes.
|
|
18
|
+
*
|
|
19
|
+
* @param mcp - The MCP server to dispatch requests to.
|
|
20
|
+
* @param options - Stdio connection options, including dependencies and stream overrides.
|
|
21
|
+
* @returns A handle exposing `closed` and `close()` for the connection's lifecycle.
|
|
22
|
+
*/
|
|
8
23
|
function connectMcpStdio(mcp, options) {
|
|
9
24
|
const input = options.input ?? process.stdin;
|
|
10
25
|
const output = options.output ?? process.stdout;
|
|
11
26
|
const diagnostics = options.diagnostics ?? process.stderr;
|
|
12
27
|
const controllers = /* @__PURE__ */ new Map();
|
|
13
|
-
const sessionId =
|
|
28
|
+
const sessionId = randomUUID();
|
|
14
29
|
let finish;
|
|
15
30
|
const closed = new Promise((resolve) => {
|
|
16
31
|
finish = resolve;
|
|
@@ -18,31 +33,73 @@ function connectMcpStdio(mcp, options) {
|
|
|
18
33
|
let ended = false;
|
|
19
34
|
const maxLineBytes = options.maxLineBytes ?? 1048576;
|
|
20
35
|
const maxConcurrency = options.maxConcurrency ?? 16;
|
|
21
|
-
if (!Number.
|
|
22
|
-
if (!Number.
|
|
36
|
+
if (!Number.isSafeInteger(maxLineBytes) || maxLineBytes <= 0) throw new TypeError("MCP maxLineBytes must be a positive integer.");
|
|
37
|
+
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency <= 0) throw new TypeError("MCP maxConcurrency must be a positive integer.");
|
|
23
38
|
let active = 0;
|
|
24
|
-
let
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
let abortListener;
|
|
40
|
+
let cleanupTransport = () => void 0;
|
|
41
|
+
const report = (error) => {
|
|
42
|
+
try {
|
|
43
|
+
diagnostics.write(`MCP stdio error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
44
|
+
} catch {}
|
|
45
|
+
};
|
|
46
|
+
const write = (message) => {
|
|
47
|
+
if (ended) return Promise.reject(/* @__PURE__ */ new Error("MCP stdio connection is closed."));
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
output.write(`${JSON.stringify(message)}\n`, (error) => error ? reject(error) : resolve());
|
|
50
|
+
});
|
|
51
|
+
};
|
|
28
52
|
const close = async () => {
|
|
29
53
|
if (ended) return closed;
|
|
30
54
|
ended = true;
|
|
31
|
-
|
|
32
|
-
|
|
55
|
+
cleanupTransport();
|
|
56
|
+
abortListener?.[Symbol.dispose]();
|
|
57
|
+
for (const values of controllers.values()) for (const controller of values) controller.abort();
|
|
33
58
|
controllers.clear();
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
try {
|
|
60
|
+
mcp.terminateSession(sessionId);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
report(error);
|
|
63
|
+
} finally {
|
|
64
|
+
finish();
|
|
65
|
+
}
|
|
36
66
|
return closed;
|
|
37
67
|
};
|
|
38
|
-
options.signal
|
|
68
|
+
if (options.signal) abortListener = addAbortListener(options.signal, () => void close());
|
|
69
|
+
const writeProtocolError = (code, message) => write({
|
|
70
|
+
jsonrpc: "2.0",
|
|
71
|
+
id: null,
|
|
72
|
+
error: {
|
|
73
|
+
code,
|
|
74
|
+
message
|
|
75
|
+
}
|
|
76
|
+
});
|
|
39
77
|
const handleLine = (line) => {
|
|
78
|
+
if (ended) return;
|
|
79
|
+
let message;
|
|
80
|
+
try {
|
|
81
|
+
message = JSON.parse(line);
|
|
82
|
+
} catch {
|
|
83
|
+
writeProtocolError(-32700, "Parse error").catch(() => void close());
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
87
|
+
writeProtocolError(-32600, "Invalid Request").catch(() => void close());
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const value = message;
|
|
91
|
+
if (value.method === "notifications/cancelled" && value.params && typeof value.params === "object") {
|
|
92
|
+
const requestId = value.params.requestId;
|
|
93
|
+
if (typeof requestId === "string" || typeof requestId === "number") for (const controller of controllers.get(requestId) ?? []) controller.abort();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const id = value.id;
|
|
40
97
|
if (active >= maxConcurrency) {
|
|
41
|
-
write({
|
|
98
|
+
if (typeof id === "string" || typeof id === "number") write({
|
|
42
99
|
jsonrpc: "2.0",
|
|
43
|
-
id
|
|
100
|
+
id,
|
|
44
101
|
error: {
|
|
45
|
-
code: -
|
|
102
|
+
code: -32e3,
|
|
46
103
|
message: "Too many concurrent requests"
|
|
47
104
|
}
|
|
48
105
|
}).catch(() => void close());
|
|
@@ -50,51 +107,52 @@ function connectMcpStdio(mcp, options) {
|
|
|
50
107
|
}
|
|
51
108
|
active += 1;
|
|
52
109
|
(async () => {
|
|
53
|
-
|
|
110
|
+
const controller = typeof id === "string" || typeof id === "number" ? new AbortController() : void 0;
|
|
111
|
+
if (controller && (typeof id === "string" || typeof id === "number")) {
|
|
112
|
+
const values = controllers.get(id) ?? /* @__PURE__ */ new Set();
|
|
113
|
+
values.add(controller);
|
|
114
|
+
controllers.set(id, values);
|
|
115
|
+
}
|
|
54
116
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
await
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
117
|
+
const environment = options.environment ?? process.env;
|
|
118
|
+
const auth = typeof options.auth === "function" ? await options.auth(environment) : options.auth ?? anonymous;
|
|
119
|
+
const result = await mcp.handle(message, {
|
|
120
|
+
dependencies: options.dependencies,
|
|
121
|
+
auth,
|
|
122
|
+
transport: "stdio",
|
|
123
|
+
sessionId,
|
|
124
|
+
supportsPush: true,
|
|
125
|
+
signal: controller?.signal ?? options.signal,
|
|
126
|
+
send: write
|
|
64
127
|
});
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const environment = options.environment ?? process.env;
|
|
78
|
-
const auth = typeof options.auth === "function" ? await options.auth(environment) : options.auth ?? anonymous;
|
|
79
|
-
const result = await mcp.handle(message, {
|
|
80
|
-
dependencies: options.dependencies,
|
|
81
|
-
auth,
|
|
82
|
-
transport: "stdio",
|
|
83
|
-
sessionId,
|
|
84
|
-
supportsPush: true,
|
|
85
|
-
signal: controller?.signal ?? options.signal,
|
|
86
|
-
send: write
|
|
128
|
+
if (result !== void 0 && !ended) await write(result);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (!ended) {
|
|
131
|
+
report(error);
|
|
132
|
+
if (typeof id === "string" || typeof id === "number") await write({
|
|
133
|
+
jsonrpc: "2.0",
|
|
134
|
+
id,
|
|
135
|
+
error: {
|
|
136
|
+
code: -32603,
|
|
137
|
+
message: "Internal error"
|
|
138
|
+
}
|
|
87
139
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
if (controller && (typeof id === "string" || typeof id === "number")) {
|
|
143
|
+
const values = controllers.get(id);
|
|
144
|
+
values?.delete(controller);
|
|
145
|
+
if (values?.size === 0) controllers.delete(id);
|
|
91
146
|
}
|
|
92
147
|
}
|
|
93
|
-
})().catch((error) =>
|
|
148
|
+
})().catch((error) => {
|
|
149
|
+
if (!ended) report(error);
|
|
150
|
+
}).finally(() => {
|
|
94
151
|
active -= 1;
|
|
95
152
|
});
|
|
96
153
|
};
|
|
97
154
|
const lineBuffer = Buffer.allocUnsafe(maxLineBytes);
|
|
155
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
98
156
|
let lineBytes = 0;
|
|
99
157
|
let discardingOversizedLine = false;
|
|
100
158
|
const rejectOversizedLine = () => {
|
|
@@ -108,6 +166,7 @@ function connectMcpStdio(mcp, options) {
|
|
|
108
166
|
}).catch(() => void close());
|
|
109
167
|
};
|
|
110
168
|
const onData = (chunk) => {
|
|
169
|
+
if (ended) return;
|
|
111
170
|
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
112
171
|
let offset = 0;
|
|
113
172
|
while (offset < bytes.byteLength) {
|
|
@@ -127,29 +186,39 @@ function connectMcpStdio(mcp, options) {
|
|
|
127
186
|
if (newline < 0) break;
|
|
128
187
|
if (!discardingOversizedLine) {
|
|
129
188
|
const length = lineBytes > 0 && lineBuffer[lineBytes - 1] === 13 ? lineBytes - 1 : lineBytes;
|
|
130
|
-
|
|
189
|
+
try {
|
|
190
|
+
handleLine(decoder.decode(lineBuffer.subarray(0, length)));
|
|
191
|
+
} catch {
|
|
192
|
+
writeProtocolError(-32700, "Parse error").catch(() => void close());
|
|
193
|
+
}
|
|
131
194
|
}
|
|
132
195
|
lineBytes = 0;
|
|
133
196
|
discardingOversizedLine = false;
|
|
134
197
|
offset = newline + 1;
|
|
135
198
|
}
|
|
136
199
|
};
|
|
137
|
-
const finishInput = () =>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
200
|
+
const finishInput = () => void close();
|
|
201
|
+
const inputError = (error) => {
|
|
202
|
+
report(error);
|
|
203
|
+
close();
|
|
204
|
+
};
|
|
205
|
+
const outputError = (error) => {
|
|
206
|
+
report(error);
|
|
207
|
+
close();
|
|
144
208
|
};
|
|
145
209
|
input.on("data", onData);
|
|
146
210
|
input.once("end", finishInput);
|
|
147
211
|
input.once("close", finishInput);
|
|
148
|
-
|
|
212
|
+
input.once("error", inputError);
|
|
213
|
+
output.once("error", outputError);
|
|
214
|
+
cleanupTransport = () => {
|
|
149
215
|
input.off("data", onData);
|
|
150
216
|
input.off("end", finishInput);
|
|
151
217
|
input.off("close", finishInput);
|
|
218
|
+
input.off("error", inputError);
|
|
219
|
+
output.off("error", outputError);
|
|
152
220
|
};
|
|
221
|
+
if (input.readableEnded || input.destroyed || output.destroyed) close();
|
|
153
222
|
return {
|
|
154
223
|
closed,
|
|
155
224
|
close
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askrjs/node",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Node http adapter for @askrjs/server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"askr",
|
|
@@ -43,10 +43,12 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
|
+
"bench": "vitest bench --run",
|
|
47
|
+
"bench:http": "vitest bench --run benches/http.bench.ts",
|
|
46
48
|
"build": "vp pack",
|
|
47
49
|
"fmt": "vp fmt .",
|
|
48
50
|
"lint": "vp lint .",
|
|
49
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
51
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
50
52
|
"test": "vp test run",
|
|
51
53
|
"test:publint": "publint",
|
|
52
54
|
"pack:check": "node tests/package-artifacts.js",
|
|
@@ -55,18 +57,18 @@
|
|
|
55
57
|
"prepublishOnly": "npm run check"
|
|
56
58
|
},
|
|
57
59
|
"dependencies": {
|
|
58
|
-
"@askrjs/auth": ">=0.0.
|
|
59
|
-
"@askrjs/server": ">=0.0.
|
|
60
|
-
"ws": "^8.
|
|
60
|
+
"@askrjs/auth": ">=0.0.8 <0.1.0",
|
|
61
|
+
"@askrjs/server": ">=0.0.11 <0.1.0",
|
|
62
|
+
"@types/ws": "^8.18.1",
|
|
63
|
+
"ws": "^8.21.3"
|
|
61
64
|
},
|
|
62
65
|
"devDependencies": {
|
|
63
|
-
"@askrjs/schema": ">=0.0.
|
|
64
|
-
"@types/node": "^26.
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"vitest": "^4.0.0"
|
|
66
|
+
"@askrjs/schema": ">=0.0.5 <0.1.0",
|
|
67
|
+
"@types/node": "^26.2.0",
|
|
68
|
+
"publint": "^0.3.23",
|
|
69
|
+
"typescript": "^7.0.2",
|
|
70
|
+
"vite-plus": "^0.2.8",
|
|
71
|
+
"vitest": "^4.1.10"
|
|
70
72
|
},
|
|
71
73
|
"engines": {
|
|
72
74
|
"node": ">=24.0.0"
|