@askrjs/node 0.0.1 → 0.0.2
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/index.d.ts +17 -2
- package/dist/index.js +122 -1
- package/dist/mcp.d.ts +21 -0
- package/dist/mcp.js +97 -0
- package/package.json +47 -11
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { IncomingMessage, Server, ServerResponse } from "node:http";
|
|
2
2
|
import { ServerApp } from "@askrjs/server";
|
|
3
3
|
import { AddressInfo } from "node:net";
|
|
4
|
-
|
|
5
4
|
//#region src/contracts.d.ts
|
|
6
5
|
interface NodeHandlerOptions {
|
|
7
6
|
baseUrl?: string;
|
|
@@ -12,6 +11,17 @@ interface ListenOptions {
|
|
|
12
11
|
backlog?: number;
|
|
13
12
|
signal?: AbortSignal;
|
|
14
13
|
}
|
|
14
|
+
interface ServeOptions extends ListenOptions {
|
|
15
|
+
readonly assets?: {
|
|
16
|
+
readonly root: string;
|
|
17
|
+
};
|
|
18
|
+
readonly signals?: false | readonly NodeJS.Signals[];
|
|
19
|
+
}
|
|
20
|
+
interface ServedApplication {
|
|
21
|
+
readonly server: import("node:http").Server;
|
|
22
|
+
readonly url: string;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
}
|
|
15
25
|
type ConnectNext = (error?: unknown) => void;
|
|
16
26
|
type NodeHandler = (request: IncomingMessage, response: ServerResponse, next?: ConnectNext) => void;
|
|
17
27
|
//#endregion
|
|
@@ -24,5 +34,10 @@ type ListeningServer = Server & {
|
|
|
24
34
|
};
|
|
25
35
|
declare function listen(app: ServerApp, options?: ListenOptions): Promise<ListeningServer>;
|
|
26
36
|
//#endregion
|
|
27
|
-
|
|
37
|
+
//#region src/serve.d.ts
|
|
38
|
+
declare function serve(app: ServerApp & {
|
|
39
|
+
close?: () => void | Promise<void>;
|
|
40
|
+
}, options?: ServeOptions): Promise<ServedApplication>;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { ConnectNext, ListenOptions, ListeningServer, NodeHandler, NodeHandlerOptions, ServeOptions, ServedApplication, createNodeHandler, listen, serve };
|
|
28
43
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
+
import { createReadStream } from "node:fs";
|
|
4
|
+
import { stat } from "node:fs/promises";
|
|
5
|
+
import { extname, resolve, sep } from "node:path";
|
|
3
6
|
//#region src/request.ts
|
|
4
7
|
function requestHeaders(request) {
|
|
5
8
|
const headers = new Headers();
|
|
@@ -145,6 +148,124 @@ function listen(app, options = {}) {
|
|
|
145
148
|
});
|
|
146
149
|
}
|
|
147
150
|
//#endregion
|
|
148
|
-
|
|
151
|
+
//#region src/serve.ts
|
|
152
|
+
const mimeTypes = {
|
|
153
|
+
".css": "text/css; charset=utf-8",
|
|
154
|
+
".html": "text/html; charset=utf-8",
|
|
155
|
+
".ico": "image/x-icon",
|
|
156
|
+
".jpeg": "image/jpeg",
|
|
157
|
+
".jpg": "image/jpeg",
|
|
158
|
+
".js": "text/javascript; charset=utf-8",
|
|
159
|
+
".json": "application/json; charset=utf-8",
|
|
160
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
161
|
+
".png": "image/png",
|
|
162
|
+
".svg": "image/svg+xml",
|
|
163
|
+
".webp": "image/webp",
|
|
164
|
+
".woff": "font/woff",
|
|
165
|
+
".woff2": "font/woff2"
|
|
166
|
+
};
|
|
167
|
+
function isAssetPath(pathname) {
|
|
168
|
+
return extname(pathname) !== "";
|
|
169
|
+
}
|
|
170
|
+
async function serve(app, options = {}) {
|
|
171
|
+
const root = options.assets ? resolve(options.assets.root) : void 0;
|
|
172
|
+
const applicationHandler = createNodeHandler({ async fetch(request) {
|
|
173
|
+
const result = await app.fetch(request);
|
|
174
|
+
if (!result.headers.has("cache-control") && result.headers.get("content-type")?.includes("text/html")) {
|
|
175
|
+
const headers = new Headers(result.headers);
|
|
176
|
+
headers.set("cache-control", "no-cache");
|
|
177
|
+
return new Response(result.body, {
|
|
178
|
+
status: result.status,
|
|
179
|
+
statusText: result.statusText,
|
|
180
|
+
headers
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
} });
|
|
185
|
+
const server = createServer(async (request, response) => {
|
|
186
|
+
let pathname;
|
|
187
|
+
try {
|
|
188
|
+
pathname = decodeURIComponent(new URL(request.url ?? "/", "http://askr.local").pathname);
|
|
189
|
+
} catch {
|
|
190
|
+
response.writeHead(400, { "content-type": "text/plain; charset=utf-8" }).end("Bad Request");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const method = request.method ?? "GET";
|
|
194
|
+
if (root && (method === "GET" || method === "HEAD") && isAssetPath(pathname)) {
|
|
195
|
+
const extension = extname(pathname).toLowerCase();
|
|
196
|
+
const candidate = resolve(root, `.${pathname}`);
|
|
197
|
+
const inside = candidate.startsWith(`${root}${sep}`);
|
|
198
|
+
let file;
|
|
199
|
+
if (inside && extension !== ".map") try {
|
|
200
|
+
file = await stat(candidate);
|
|
201
|
+
} catch {
|
|
202
|
+
file = void 0;
|
|
203
|
+
}
|
|
204
|
+
if (!file?.isFile()) {
|
|
205
|
+
response.writeHead(404, {
|
|
206
|
+
"content-type": "text/plain; charset=utf-8",
|
|
207
|
+
"x-content-type-options": "nosniff"
|
|
208
|
+
}).end("Not Found");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
response.writeHead(200, {
|
|
212
|
+
"content-type": mimeTypes[extension] ?? "application/octet-stream",
|
|
213
|
+
"content-length": Number(file.size),
|
|
214
|
+
"cache-control": pathname.startsWith("/assets/") && /-[A-Za-z0-9_-]{8,}\.[^.]+$/.test(pathname) ? "public, max-age=31536000, immutable" : "no-cache",
|
|
215
|
+
"x-content-type-options": "nosniff"
|
|
216
|
+
});
|
|
217
|
+
if (method === "HEAD") response.end();
|
|
218
|
+
else createReadStream(candidate).pipe(response);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
applicationHandler(request, response);
|
|
222
|
+
});
|
|
223
|
+
let closing;
|
|
224
|
+
const signals = options.signals === false ? [] : options.signals ?? ["SIGINT", "SIGTERM"];
|
|
225
|
+
const removeListeners = () => {
|
|
226
|
+
options.signal?.removeEventListener("abort", shutdown);
|
|
227
|
+
for (const signal of signals) process.removeListener(signal, shutdown);
|
|
228
|
+
};
|
|
229
|
+
const close = () => closing ??= new Promise((resolveClose, rejectClose) => {
|
|
230
|
+
removeListeners();
|
|
231
|
+
const finish = async (serverError) => {
|
|
232
|
+
try {
|
|
233
|
+
await app.close?.();
|
|
234
|
+
if (serverError) rejectClose(serverError);
|
|
235
|
+
else resolveClose();
|
|
236
|
+
} catch (error) {
|
|
237
|
+
rejectClose(error);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
if (!server.listening) finish();
|
|
241
|
+
else server.close((error) => void finish(error));
|
|
242
|
+
});
|
|
243
|
+
const shutdown = () => void close().catch(() => void 0);
|
|
244
|
+
options.signal?.addEventListener("abort", shutdown, { once: true });
|
|
245
|
+
for (const signal of signals) process.once(signal, shutdown);
|
|
246
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
247
|
+
server.once("error", rejectListen);
|
|
248
|
+
server.listen(options.port ?? 0, options.host ?? "127.0.0.1", options.backlog, () => {
|
|
249
|
+
server.removeListener("error", rejectListen);
|
|
250
|
+
resolveListen();
|
|
251
|
+
});
|
|
252
|
+
}).catch((error) => {
|
|
253
|
+
removeListeners();
|
|
254
|
+
throw error;
|
|
255
|
+
});
|
|
256
|
+
const address = server.address();
|
|
257
|
+
if (!address || typeof address === "string") {
|
|
258
|
+
await close();
|
|
259
|
+
throw new Error("serve requires a TCP address.");
|
|
260
|
+
}
|
|
261
|
+
const host = options.host ?? "127.0.0.1";
|
|
262
|
+
return Object.freeze({
|
|
263
|
+
server,
|
|
264
|
+
url: `http://${host}:${address.port}`,
|
|
265
|
+
close
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
export { createNodeHandler, listen, serve };
|
|
149
270
|
|
|
150
271
|
//# sourceMappingURL=index.js.map
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Readable, Writable } from "node:stream";
|
|
2
|
+
import { AuthContext } from "@askrjs/auth";
|
|
3
|
+
import { McpServer } from "@askrjs/server/mcp";
|
|
4
|
+
//#region src/mcp.d.ts
|
|
5
|
+
interface McpStdioOptions<Dependencies = undefined> {
|
|
6
|
+
dependencies: Dependencies;
|
|
7
|
+
input?: Readable;
|
|
8
|
+
output?: Writable;
|
|
9
|
+
diagnostics?: Writable;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
auth?: AuthContext | ((environment: NodeJS.ProcessEnv) => AuthContext | Promise<AuthContext>);
|
|
12
|
+
environment?: NodeJS.ProcessEnv;
|
|
13
|
+
}
|
|
14
|
+
interface McpStdioConnection {
|
|
15
|
+
readonly closed: Promise<void>;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
declare function connectMcpStdio<Dependencies>(mcp: McpServer<Dependencies>, options: McpStdioOptions<Dependencies>): McpStdioConnection;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { McpStdioConnection, McpStdioOptions, connectMcpStdio };
|
|
21
|
+
//# sourceMappingURL=mcp.d.ts.map
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
//#region src/mcp.ts
|
|
3
|
+
const anonymous = Object.freeze({
|
|
4
|
+
authenticated: false,
|
|
5
|
+
principal: null,
|
|
6
|
+
session: null,
|
|
7
|
+
tenant: null
|
|
8
|
+
});
|
|
9
|
+
function connectMcpStdio(mcp, options) {
|
|
10
|
+
const input = options.input ?? process.stdin;
|
|
11
|
+
const output = options.output ?? process.stdout;
|
|
12
|
+
const diagnostics = options.diagnostics ?? process.stderr;
|
|
13
|
+
const lines = createInterface({
|
|
14
|
+
input,
|
|
15
|
+
crlfDelay: Infinity,
|
|
16
|
+
terminal: false
|
|
17
|
+
});
|
|
18
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
19
|
+
const sessionId = crypto.randomUUID();
|
|
20
|
+
let finish;
|
|
21
|
+
const closed = new Promise((resolve) => {
|
|
22
|
+
finish = resolve;
|
|
23
|
+
});
|
|
24
|
+
let ended = false;
|
|
25
|
+
const write = (message) => new Promise((resolve, reject) => {
|
|
26
|
+
output.write(`${JSON.stringify(message)}\n`, (error) => error ? reject(error) : resolve());
|
|
27
|
+
});
|
|
28
|
+
const close = async () => {
|
|
29
|
+
if (ended) return closed;
|
|
30
|
+
ended = true;
|
|
31
|
+
lines.close();
|
|
32
|
+
for (const controller of controllers.values()) controller.abort();
|
|
33
|
+
controllers.clear();
|
|
34
|
+
mcp.terminateSession(sessionId);
|
|
35
|
+
finish();
|
|
36
|
+
return closed;
|
|
37
|
+
};
|
|
38
|
+
options.signal?.addEventListener("abort", () => void close(), { once: true });
|
|
39
|
+
lines.on("line", (line) => {
|
|
40
|
+
(async () => {
|
|
41
|
+
let message;
|
|
42
|
+
try {
|
|
43
|
+
message = JSON.parse(line);
|
|
44
|
+
} catch {
|
|
45
|
+
await write({
|
|
46
|
+
jsonrpc: "2.0",
|
|
47
|
+
id: null,
|
|
48
|
+
error: {
|
|
49
|
+
code: -32700,
|
|
50
|
+
message: "Parse error"
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (message && typeof message === "object") {
|
|
56
|
+
const value = message;
|
|
57
|
+
if (value.method === "notifications/cancelled" && value.params && typeof value.params === "object") {
|
|
58
|
+
const requestId = value.params.requestId;
|
|
59
|
+
if (typeof requestId === "string" || typeof requestId === "number") controllers.get(requestId)?.abort();
|
|
60
|
+
}
|
|
61
|
+
const id = value.id;
|
|
62
|
+
const controller = typeof id === "string" || typeof id === "number" ? new AbortController() : void 0;
|
|
63
|
+
if (controller && (typeof id === "string" || typeof id === "number")) controllers.set(id, controller);
|
|
64
|
+
try {
|
|
65
|
+
const environment = options.environment ?? process.env;
|
|
66
|
+
const auth = typeof options.auth === "function" ? await options.auth(environment) : options.auth ?? anonymous;
|
|
67
|
+
const result = await mcp.handle(message, {
|
|
68
|
+
dependencies: options.dependencies,
|
|
69
|
+
auth,
|
|
70
|
+
transport: "stdio",
|
|
71
|
+
sessionId,
|
|
72
|
+
supportsPush: true,
|
|
73
|
+
signal: controller?.signal ?? options.signal,
|
|
74
|
+
send: write
|
|
75
|
+
});
|
|
76
|
+
if (result !== void 0) await write(result);
|
|
77
|
+
} finally {
|
|
78
|
+
if (controller && id !== void 0) controllers.delete(id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
})().catch((error) => diagnostics.write(`MCP stdio error: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
82
|
+
});
|
|
83
|
+
lines.once("close", () => {
|
|
84
|
+
if (!ended) {
|
|
85
|
+
ended = true;
|
|
86
|
+
finish();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
closed,
|
|
91
|
+
close
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { connectMcpStdio };
|
|
96
|
+
|
|
97
|
+
//# sourceMappingURL=mcp.js.map
|
package/package.json
CHANGED
|
@@ -1,22 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askrjs/node",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Node http adapter for @askrjs/server",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"askr",
|
|
7
|
+
"http",
|
|
8
|
+
"node",
|
|
9
|
+
"server"
|
|
10
|
+
],
|
|
6
11
|
"homepage": "https://github.com/askrjs/askr-node#readme",
|
|
7
|
-
"bugs": {
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/askrjs/askr-node/issues"
|
|
14
|
+
},
|
|
10
15
|
"license": "Apache-2.0",
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "askrjs",
|
|
18
|
+
"url": "https://github.com/askrjs"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/askrjs/askr-node.git"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/**/*.js",
|
|
26
|
+
"dist/**/*.d.ts"
|
|
27
|
+
],
|
|
11
28
|
"type": "module",
|
|
12
29
|
"main": "./dist/index.js",
|
|
13
30
|
"types": "./dist/index.d.ts",
|
|
14
31
|
"exports": {
|
|
15
|
-
".": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./mcp": {
|
|
37
|
+
"types": "./dist/mcp.d.ts",
|
|
38
|
+
"import": "./dist/mcp.js"
|
|
39
|
+
},
|
|
16
40
|
"./package.json": "./package.json"
|
|
17
41
|
},
|
|
18
|
-
"
|
|
19
|
-
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
20
45
|
"scripts": {
|
|
21
46
|
"build": "vp pack",
|
|
22
47
|
"fmt": "vp fmt .",
|
|
@@ -29,7 +54,18 @@
|
|
|
29
54
|
"prepack": "npm run build",
|
|
30
55
|
"prepublishOnly": "npm run check"
|
|
31
56
|
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
|
|
34
|
-
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@askrjs/auth": ">=0.0.1 <0.1.0",
|
|
59
|
+
"@askrjs/server": ">=0.0.1 <0.1.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/node": "^26.0.0",
|
|
63
|
+
"publint": "^0.3.21",
|
|
64
|
+
"typescript": "^6.0.0",
|
|
65
|
+
"vite-plus": "^0.2.4",
|
|
66
|
+
"vitest": "^4.0.0"
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=18"
|
|
70
|
+
}
|
|
35
71
|
}
|