holon-sh 0.0.1-dev.14 → 0.0.1-dev.15
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 +12 -0
- package/dist/serve.d.ts +22 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +74 -0
- package/dist/serve.js.map +1 -0
- package/package.json +9 -1
- package/src/serve.ts +76 -0
package/README.md
CHANGED
|
@@ -122,6 +122,18 @@ A refused subscription is answered Forbidden, and access is checked again at eac
|
|
|
122
122
|
|
|
123
123
|
Poll and push delivery are here, with targeted emits, subscription hooks, authorization, ending subscriptions, limits, paged listing and removing event types. Webhook delivery and check functions, which read a source that keeps its own history, are not, and neither are typed event names. Clients are not told when the event types change: they list them again.
|
|
124
124
|
|
|
125
|
+
## Run an app without Holon
|
|
126
|
+
|
|
127
|
+
`holon-sh/serve` serves an app's module at the paths Holon gives it, so the app also runs wherever Node does:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { serve } from "holon-sh/serve";
|
|
131
|
+
|
|
132
|
+
await serve(await import("./server.ts"));
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The MCP endpoint is at `/mcp` and `/mcp/<app>`, and the files of `frontend` are at every other path. `PORT` and `HOST` change where it listens, 127.0.0.1:3000 unless they are set. Holon answers clients of the 2025 MCP versions in front of an app, and here the app's handler answers them, so create it without `legacy: "reject"`.
|
|
136
|
+
|
|
125
137
|
## Community
|
|
126
138
|
|
|
127
139
|
Ask questions and share what you're building on the [Holon Discord](https://discord.gg/jXExyBdy).
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
/** An app's module: the MCP handler it exports by default, and the directory of its page. */
|
|
3
|
+
export interface App {
|
|
4
|
+
default: {
|
|
5
|
+
fetch(request: Request): Promise<Response>;
|
|
6
|
+
};
|
|
7
|
+
/** The directory of the page's files, as a file URL. */
|
|
8
|
+
frontend?: URL;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Runs an app without Holon, at the paths Holon gives it: the MCP endpoint at `/mcp` and `/mcp/<app>`, and the page's
|
|
12
|
+
* files everywhere else. Listens on `PORT` and `HOST`, or else on 127.0.0.1:3000.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* await serve(await import("./server.ts"));
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Holon answers clients of the 2025 MCP versions in front of an app. Here the handler answers them, so create it
|
|
19
|
+
* without `legacy: "reject"`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function serve({ default: handler, frontend }: App): Promise<Server>;
|
|
22
|
+
//# sourceMappingURL=serve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAMjG,6FAA6F;AAC7F,MAAM,WAAW,GAAG;IAClB,OAAO,EAAE;QAAE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAC;IACxD,wDAAwD;IACxD,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,KAAK,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBhF"}
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import mime from "mime";
|
|
7
|
+
/**
|
|
8
|
+
* Runs an app without Holon, at the paths Holon gives it: the MCP endpoint at `/mcp` and `/mcp/<app>`, and the page's
|
|
9
|
+
* files everywhere else. Listens on `PORT` and `HOST`, or else on 127.0.0.1:3000.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* await serve(await import("./server.ts"));
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Holon answers clients of the 2025 MCP versions in front of an app. Here the handler answers them, so create it
|
|
16
|
+
* without `legacy: "reject"`.
|
|
17
|
+
*/
|
|
18
|
+
export async function serve({ default: handler, frontend }) {
|
|
19
|
+
const { PORT = "3000", HOST = "127.0.0.1" } = process.env;
|
|
20
|
+
// A directory's URL ends in `/`. Without it, a name would resolve beside the directory instead of in it.
|
|
21
|
+
const directory = frontend && new URL(frontend.href.replace(/\/?$/, "/"));
|
|
22
|
+
const server = createServer((req, res) => respond(handler, directory, req, res).catch((error) => {
|
|
23
|
+
console.error(error);
|
|
24
|
+
if (!res.headersSent)
|
|
25
|
+
res.writeHead(500);
|
|
26
|
+
res.end();
|
|
27
|
+
}));
|
|
28
|
+
server.listen(Number(PORT), HOST);
|
|
29
|
+
await once(server, "listening");
|
|
30
|
+
const { port } = server.address();
|
|
31
|
+
const origin = `http://${HOST.includes(":") ? `[${HOST}]` : HOST}:${port}`;
|
|
32
|
+
console.log(`Serving ${origin}/ with the MCP endpoint at ${origin}/mcp`);
|
|
33
|
+
return server;
|
|
34
|
+
}
|
|
35
|
+
async function respond(handler, directory, req, res) {
|
|
36
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
37
|
+
if (/^\/mcp(\/[^/]+)?$/.test(url.pathname)) {
|
|
38
|
+
const gone = new AbortController();
|
|
39
|
+
res.on("close", () => gone.abort());
|
|
40
|
+
const headers = new Headers();
|
|
41
|
+
for (const [name, values = []] of Object.entries(req.headersDistinct))
|
|
42
|
+
for (const value of values)
|
|
43
|
+
headers.append(name, value);
|
|
44
|
+
// Holon forwards the origin it serves the app at. Here that is this server, unless a proxy in front forwards its own.
|
|
45
|
+
if (!headers.has("x-forwarded-host"))
|
|
46
|
+
headers.set("x-forwarded-host", url.host);
|
|
47
|
+
if (!headers.has("x-forwarded-proto"))
|
|
48
|
+
headers.set("x-forwarded-proto", "http");
|
|
49
|
+
const body = req.method === "GET" || req.method === "HEAD" ? undefined : req;
|
|
50
|
+
const response = await handler.fetch(new Request(url, { method: req.method, headers, body, duplex: "half", signal: gone.signal }));
|
|
51
|
+
res.setHeaders(response.headers).writeHead(response.status);
|
|
52
|
+
// When the client leaves, pipeline cancels the body, so a stream between events ends then and not at its next
|
|
53
|
+
// write. Either side failing ends the response, which is all there is to do.
|
|
54
|
+
if (response.body)
|
|
55
|
+
await pipeline(Readable.fromWeb(response.body), res).catch(() => { });
|
|
56
|
+
else
|
|
57
|
+
res.end();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!directory || (req.method !== "GET" && req.method !== "HEAD"))
|
|
61
|
+
return void res.writeHead(404).end();
|
|
62
|
+
const name = url.pathname.endsWith("/") ? `${url.pathname}index.html` : url.pathname;
|
|
63
|
+
// The URL parser has resolved every `..`, and a file URL may not encode `/`, so no name leads out of the directory.
|
|
64
|
+
// Like Holon, serve nothing hidden, however its dot is spelled, and treat what cannot be read, such as a directory, as
|
|
65
|
+
// not found.
|
|
66
|
+
const contents = /\/(\.|%2e)/i.test(name) ? undefined : await readFile(new URL(`.${name}`, directory)).catch(() => undefined);
|
|
67
|
+
if (!contents)
|
|
68
|
+
return void res.writeHead(404).end();
|
|
69
|
+
// Like Holon, take the type from the MIME database, and call text UTF-8, which a built page is.
|
|
70
|
+
const type = mime.getType(name) ?? "application/octet-stream";
|
|
71
|
+
const contentType = type.startsWith("text/") ? `${type}; charset=utf-8` : type;
|
|
72
|
+
res.writeHead(200, { "content-type": contentType, "x-content-type-options": "nosniff" }).end(contents);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=serve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "holon-sh",
|
|
3
|
-
"version": "0.0.1-dev.
|
|
3
|
+
"version": "0.0.1-dev.15",
|
|
4
4
|
"description": "Typed MCP clients, MCP Events, and the caller of an MCP request, for Node.js.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"./ext/files": {
|
|
36
36
|
"types": "./dist/ext/files.d.ts",
|
|
37
37
|
"import": "./dist/ext/files.js"
|
|
38
|
+
},
|
|
39
|
+
"./serve": {
|
|
40
|
+
"types": "./dist/serve.d.ts",
|
|
41
|
+
"import": "./dist/serve.js"
|
|
38
42
|
}
|
|
39
43
|
},
|
|
40
44
|
"typesVersions": {
|
|
@@ -53,6 +57,9 @@
|
|
|
53
57
|
],
|
|
54
58
|
"ext/files": [
|
|
55
59
|
"./dist/ext/files.d.ts"
|
|
60
|
+
],
|
|
61
|
+
"serve": [
|
|
62
|
+
"./dist/serve.d.ts"
|
|
56
63
|
]
|
|
57
64
|
}
|
|
58
65
|
},
|
|
@@ -73,6 +80,7 @@
|
|
|
73
80
|
"dependencies": {
|
|
74
81
|
"@modelcontextprotocol/client": "^2.3.1",
|
|
75
82
|
"ipaddr.js": "^2.5.0",
|
|
83
|
+
"mime": "^4.1.2",
|
|
76
84
|
"undici": "^7.16.0"
|
|
77
85
|
},
|
|
78
86
|
"peerDependencies": {
|
package/src/serve.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
4
|
+
import type { AddressInfo } from "node:net";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import mime from "mime";
|
|
8
|
+
|
|
9
|
+
/** An app's module: the MCP handler it exports by default, and the directory of its page. */
|
|
10
|
+
export interface App {
|
|
11
|
+
default: { fetch(request: Request): Promise<Response> };
|
|
12
|
+
/** The directory of the page's files, as a file URL. */
|
|
13
|
+
frontend?: URL;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs an app without Holon, at the paths Holon gives it: the MCP endpoint at `/mcp` and `/mcp/<app>`, and the page's
|
|
18
|
+
* files everywhere else. Listens on `PORT` and `HOST`, or else on 127.0.0.1:3000.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* await serve(await import("./server.ts"));
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Holon answers clients of the 2025 MCP versions in front of an app. Here the handler answers them, so create it
|
|
25
|
+
* without `legacy: "reject"`.
|
|
26
|
+
*/
|
|
27
|
+
export async function serve({ default: handler, frontend }: App): Promise<Server> {
|
|
28
|
+
const { PORT = "3000", HOST = "127.0.0.1" } = process.env;
|
|
29
|
+
// A directory's URL ends in `/`. Without it, a name would resolve beside the directory instead of in it.
|
|
30
|
+
const directory = frontend && new URL(frontend.href.replace(/\/?$/, "/"));
|
|
31
|
+
const server = createServer((req, res) =>
|
|
32
|
+
respond(handler, directory, req, res).catch((error: unknown) => {
|
|
33
|
+
console.error(error);
|
|
34
|
+
if (!res.headersSent) res.writeHead(500);
|
|
35
|
+
res.end();
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
server.listen(Number(PORT), HOST);
|
|
39
|
+
await once(server, "listening");
|
|
40
|
+
const { port } = server.address() as AddressInfo;
|
|
41
|
+
const origin = `http://${HOST.includes(":") ? `[${HOST}]` : HOST}:${port}`;
|
|
42
|
+
console.log(`Serving ${origin}/ with the MCP endpoint at ${origin}/mcp`);
|
|
43
|
+
return server;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function respond(handler: App["default"], directory: URL | undefined, req: IncomingMessage, res: ServerResponse) {
|
|
47
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
48
|
+
if (/^\/mcp(\/[^/]+)?$/.test(url.pathname)) {
|
|
49
|
+
const gone = new AbortController();
|
|
50
|
+
res.on("close", () => gone.abort());
|
|
51
|
+
const headers = new Headers();
|
|
52
|
+
for (const [name, values = []] of Object.entries(req.headersDistinct)) for (const value of values) headers.append(name, value);
|
|
53
|
+
// Holon forwards the origin it serves the app at. Here that is this server, unless a proxy in front forwards its own.
|
|
54
|
+
if (!headers.has("x-forwarded-host")) headers.set("x-forwarded-host", url.host);
|
|
55
|
+
if (!headers.has("x-forwarded-proto")) headers.set("x-forwarded-proto", "http");
|
|
56
|
+
const body = req.method === "GET" || req.method === "HEAD" ? undefined : req;
|
|
57
|
+
const response = await handler.fetch(new Request(url, { method: req.method, headers, body, duplex: "half", signal: gone.signal }));
|
|
58
|
+
res.setHeaders(response.headers).writeHead(response.status);
|
|
59
|
+
// When the client leaves, pipeline cancels the body, so a stream between events ends then and not at its next
|
|
60
|
+
// write. Either side failing ends the response, which is all there is to do.
|
|
61
|
+
if (response.body) await pipeline(Readable.fromWeb(response.body), res).catch(() => {});
|
|
62
|
+
else res.end();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!directory || (req.method !== "GET" && req.method !== "HEAD")) return void res.writeHead(404).end();
|
|
66
|
+
const name = url.pathname.endsWith("/") ? `${url.pathname}index.html` : url.pathname;
|
|
67
|
+
// The URL parser has resolved every `..`, and a file URL may not encode `/`, so no name leads out of the directory.
|
|
68
|
+
// Like Holon, serve nothing hidden, however its dot is spelled, and treat what cannot be read, such as a directory, as
|
|
69
|
+
// not found.
|
|
70
|
+
const contents = /\/(\.|%2e)/i.test(name) ? undefined : await readFile(new URL(`.${name}`, directory)).catch(() => undefined);
|
|
71
|
+
if (!contents) return void res.writeHead(404).end();
|
|
72
|
+
// Like Holon, take the type from the MIME database, and call text UTF-8, which a built page is.
|
|
73
|
+
const type = mime.getType(name) ?? "application/octet-stream";
|
|
74
|
+
const contentType = type.startsWith("text/") ? `${type}; charset=utf-8` : type;
|
|
75
|
+
res.writeHead(200, { "content-type": contentType, "x-content-type-options": "nosniff" }).end(contents);
|
|
76
|
+
}
|