polite-crawler-mcp 0.0.0-bootstrap.0 → 0.2.0

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.
@@ -0,0 +1,266 @@
1
+ import { a as INSTRUCTIONS, i as startMcpServer, n as createMcpServer, o as crawlContent, s as crawlTools, t as crawlHostOptions } from "./server-BYGFTvt_.mjs";
2
+ import { n as VERSION, t as SERVER_NAME } from "./version-B9xqi8IR.mjs";
3
+ import { McpTaskStore, McpTaskStore as McpTaskStore$1, createMcpHost } from "@procedures/mcp-server";
4
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
5
+ import { readFileSync, statSync } from "node:fs";
6
+ import { createServer } from "node:http";
7
+ import { isIP } from "node:net";
8
+ //#region src/loopback.ts
9
+ /**
10
+ * This function tells if a host name or an address is a loopback host: `localhost`, an address in
11
+ * `127.0.0.0/8`, or `::1`. The MCP server over HTTP binds to a loopback host unless the operator
12
+ * permits a different host. The function imports only Node.js, so the command line uses it before
13
+ * the store opens.
14
+ */
15
+ const isLoopbackHost = (host) => {
16
+ const name = host.trim().toLowerCase().replace(/^\[(.*)\]$/, "$1");
17
+ if (name === "localhost" || name.endsWith(".localhost")) return true;
18
+ const mapped = name.startsWith("::ffff:") ? name.slice(7) : name;
19
+ if (isIP(mapped) === 4) return mapped.startsWith("127.");
20
+ if (isIP(name) === 6) return /^(?:0{0,4}:){7}0{0,3}1$|^::1$/.test(name);
21
+ return false;
22
+ };
23
+ /** This function gives a host for a URL: an IPv6 address goes in brackets. */
24
+ const urlHost = (host) => isIP(host) === 6 ? `[${host}]` : host;
25
+ //#endregion
26
+ //#region src/http.ts
27
+ /** These are the defaults of the MCP server over HTTP. */
28
+ const MCP_HTTP_DEFAULTS = {
29
+ /** The port of the server. */
30
+ port: 9307,
31
+ /** The host of the server: the IPv4 loopback address. */
32
+ host: "127.0.0.1",
33
+ /** The path of the MCP endpoint. */
34
+ path: "/mcp",
35
+ /** The path of the status of the daemon. */
36
+ statusPath: "/status",
37
+ /** The most sessions at one time. A new session above the limit closes the session that was idle longest. */
38
+ maxSessions: 32,
39
+ /** The largest body of a request that starts a session: 4 MiB. */
40
+ maxBodyBytes: 4194304,
41
+ /** The shortest token: 16 characters. */
42
+ minTokenLength: 16
43
+ };
44
+ const digest = (text) => createHash("sha256").update(text, "utf8").digest();
45
+ /**
46
+ * This function compares a token with the token of the server in constant time. It compares the
47
+ * SHA-256 digests, so the time does not depend on the length or the text of either token.
48
+ */
49
+ const sameToken = (given, token) => timingSafeEqual(digest(given), digest(token));
50
+ /** This function makes a random token: 32 bytes in base64url, 43 characters. */
51
+ const makeToken = () => randomBytes(32).toString("base64url");
52
+ /** A token is printable ASCII with no space, so a header holds it. */
53
+ const TOKEN = /^[\x21-\x7e]+$/;
54
+ /** This function checks a token. A token that is short or that a header cannot hold throws an error. */
55
+ const checkToken = (token, source) => {
56
+ const trimmed = token.trim();
57
+ if (!TOKEN.test(trimmed) || trimmed.length < MCP_HTTP_DEFAULTS.minTokenLength) throw new Error(`${source}: the token must be ${String(MCP_HTTP_DEFAULTS.minTokenLength)} or more printable characters with no space`);
58
+ return trimmed;
59
+ };
60
+ /** This function reads a token file. It gives a warning when other users can read the file. */
61
+ const tokenOf = (options, log) => {
62
+ if (options.token !== void 0) return checkToken(options.token, "token");
63
+ if (options.tokenFile === void 0) return makeToken();
64
+ let text;
65
+ try {
66
+ text = readFileSync(options.tokenFile, "utf8");
67
+ } catch (error) {
68
+ const code = error.code;
69
+ throw new Error(`the token file ${options.tokenFile} does not open (${typeof code === "string" ? code : "error"})`, { cause: error });
70
+ }
71
+ if (process.platform !== "win32" && (statSync(options.tokenFile).mode & 63) !== 0) log(`polite-crawler: other users can read the token file ${options.tokenFile}: chmod 600 it`);
72
+ return checkToken(text, `the token file ${options.tokenFile}`);
73
+ };
74
+ const json = (response, status, body, headers = {}) => {
75
+ if (response.headersSent) return;
76
+ response.writeHead(status, {
77
+ "content-type": "application/json",
78
+ ...headers
79
+ });
80
+ response.end(JSON.stringify(body));
81
+ };
82
+ /** This function gives the answer of a refusal, as a JSON-RPC error with no id. */
83
+ const refuse = (response, status, message, headers = {}) => json(response, status, {
84
+ jsonrpc: "2.0",
85
+ error: {
86
+ code: -32e3,
87
+ message
88
+ },
89
+ id: null
90
+ }, headers);
91
+ /**
92
+ * The number of open sessions of the HTTP face. The state of the face gives it when the shared host
93
+ * has the field. Else the count of its Streamable HTTP endpoint gives it.
94
+ */
95
+ const sessionsOf = (face) => {
96
+ const state = face.getState();
97
+ if (typeof state.sessions === "number") return state.sessions;
98
+ const endpoint = face.http;
99
+ return typeof endpoint?.sessions === "number" ? endpoint.sessions : 0;
100
+ };
101
+ /**
102
+ * This function serves an MCP host over HTTP. `makeHost` makes the host with its HTTP face mounted
103
+ * at `/mcp`, with the task store of the server. Each request needs `Authorization: Bearer <token>`.
104
+ */
105
+ const serveMcpHttp = async (makeHost, options = {}) => {
106
+ const log = options.log ?? (() => void 0);
107
+ const host = options.host ?? MCP_HTTP_DEFAULTS.host;
108
+ const port = options.port ?? MCP_HTTP_DEFAULTS.port;
109
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new RangeError(`the port must be an integer from 0 to 65535, not ${String(port)}`);
110
+ const loopback = isLoopbackHost(host);
111
+ if (!loopback && options.allowRemote !== true) throw new Error(`the host ${host} is not a loopback address: give allowRemote (--allow-remote) to serve other computers`);
112
+ const token = tokenOf(options, log);
113
+ const tasks = options.tasks ?? new McpTaskStore$1();
114
+ const mcp = makeHost({
115
+ token,
116
+ tasks
117
+ });
118
+ const face = mcp.transports[0];
119
+ if (face === void 0) throw new Error("the MCP host has no HTTP face");
120
+ await mcp.start();
121
+ let bound = port;
122
+ let closing;
123
+ /** A loopback server accepts only a loopback name and its port in `Host`. */
124
+ const hostAllowed = (header) => {
125
+ if (!loopback) return true;
126
+ if (header === void 0) return false;
127
+ let parsed;
128
+ try {
129
+ parsed = new URL(`http://${header}`);
130
+ } catch {
131
+ return false;
132
+ }
133
+ return isLoopbackHost(parsed.hostname) && (parsed.port === "" || Number(parsed.port) === bound);
134
+ };
135
+ /** An `Origin` header must name the host of the request: a page of a different site gets 403. */
136
+ const originAllowed = (origin, header) => {
137
+ if (origin === void 0) return true;
138
+ try {
139
+ return new URL(origin).host === header;
140
+ } catch {
141
+ return false;
142
+ }
143
+ };
144
+ const authorized = (header) => {
145
+ const match = /^Bearer\s+(\S+)\s*$/i.exec(header ?? "");
146
+ return match !== null && sameToken(match[1], token);
147
+ };
148
+ const handle = (request, response) => {
149
+ const hostHeader = request.headers.host;
150
+ if (!hostAllowed(hostHeader) || !originAllowed(request.headers.origin, hostHeader)) {
151
+ refuse(response, 403, "the Host or the Origin header is not of this server");
152
+ return;
153
+ }
154
+ if (!authorized(request.headers.authorization)) {
155
+ refuse(response, 401, "the request needs the bearer token of the server", { "www-authenticate": "Bearer realm=\"polite-crawler\"" });
156
+ return;
157
+ }
158
+ if (new URL(request.url ?? "/", "http://localhost").pathname === MCP_HTTP_DEFAULTS.statusPath && options.status !== void 0) {
159
+ if (request.method !== "GET") refuse(response, 405, "the status takes GET");
160
+ else json(response, 200, options.status(), { "cache-control": "no-store" });
161
+ return;
162
+ }
163
+ if (!face.handleHttp(request, response)) refuse(response, 404, `no such path: the MCP endpoint is ${MCP_HTTP_DEFAULTS.path}`);
164
+ };
165
+ const http = createServer((request, response) => {
166
+ try {
167
+ handle(request, response);
168
+ } catch (error) {
169
+ log(`polite-crawler: an MCP request failed: ${error instanceof Error ? error.message : String(error)}`);
170
+ if (!response.headersSent) refuse(response, 500, "the server could not handle the request");
171
+ else response.end();
172
+ }
173
+ });
174
+ try {
175
+ await new Promise((resolve, reject) => {
176
+ const failed = (error) => {
177
+ reject(error.code === "EADDRINUSE" ? new Error(`the port ${String(port)} of ${host} is in use: give a different port`, { cause: error }) : error);
178
+ };
179
+ http.once("error", failed);
180
+ http.listen(port, host, () => {
181
+ http.removeListener("error", failed);
182
+ resolve();
183
+ });
184
+ });
185
+ } catch (error) {
186
+ await mcp.stop();
187
+ await tasks.close();
188
+ throw error;
189
+ }
190
+ bound = http.address().port;
191
+ const url = `http://${urlHost(host)}:${String(bound)}${MCP_HTTP_DEFAULTS.path}`;
192
+ if (!loopback) log(`polite-crawler: the MCP server listens on ${host}, not on a loopback address: other computers can reach it`);
193
+ return {
194
+ url,
195
+ host,
196
+ port: bound,
197
+ token,
198
+ tasks,
199
+ sessions: () => sessionsOf(face),
200
+ close() {
201
+ closing ??= (async () => {
202
+ await tasks.close();
203
+ await mcp.stop();
204
+ await new Promise((resolve) => {
205
+ http.close(() => resolve());
206
+ http.closeAllConnections();
207
+ });
208
+ })();
209
+ return closing;
210
+ }
211
+ };
212
+ };
213
+ /**
214
+ * This function serves the tools of a crawler over the Streamable HTTP transport of MCP. The
215
+ * sessions share one MCP host with the nine tools of the server on the standard input and output.
216
+ * The envelope, the elicitation and the opt-ins are the same. `crawl_site` and `crawl_discover` are
217
+ * MCP tasks. Each request needs the bearer token. The function does not close the crawler.
218
+ */
219
+ const startMcpHttpServer = (crawler, options = {}) => {
220
+ const { onBehalfOf, allowUserProvided, renderOff, ...http } = options;
221
+ return serveMcpHttp(({ tasks }) => createMcpHost({
222
+ ...crawlHostOptions(crawler, {
223
+ onBehalfOf,
224
+ allowUserProvided,
225
+ renderOff,
226
+ tasks
227
+ }),
228
+ faces: { http: {
229
+ mount: true,
230
+ path: MCP_HTTP_DEFAULTS.path,
231
+ allowedHosts: "*",
232
+ maxSessions: MCP_HTTP_DEFAULTS.maxSessions,
233
+ maxBodyBytes: MCP_HTTP_DEFAULTS.maxBodyBytes
234
+ } }
235
+ }), http);
236
+ };
237
+ /**
238
+ * This host serves the tools of the daemon over HTTP. It is the seam between the daemon and its MCP
239
+ * transport: a different MCP host can replace it with `DaemonOptions.mcpHost`.
240
+ */
241
+ const mcpHttpHost = { async start(crawler, options, hooks) {
242
+ const tasks = new McpTaskStore$1({ onChange: hooks.onTasks });
243
+ const served = await startMcpHttpServer(crawler, {
244
+ ...options,
245
+ tasks,
246
+ status: hooks.status,
247
+ log: hooks.log
248
+ });
249
+ const running = () => tasks.running().map((task) => ({
250
+ id: task.id,
251
+ tool: task.tool,
252
+ status: task.status,
253
+ message: task.message,
254
+ created_at: task.createdAt,
255
+ updated_at: task.updatedAt
256
+ }));
257
+ return {
258
+ url: served.url,
259
+ tasks: running,
260
+ close: () => served.close()
261
+ };
262
+ } };
263
+ //#endregion
264
+ export { INSTRUCTIONS, MCP_HTTP_DEFAULTS, McpTaskStore, SERVER_NAME, VERSION, checkToken, crawlContent, crawlHostOptions, crawlTools, createMcpServer, makeToken, mcpHttpHost, sameToken, serveMcpHttp, startMcpHttpServer, startMcpServer };
265
+
266
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["McpTaskStore"],"sources":["../src/loopback.ts","../src/http.ts"],"sourcesContent":["import { isIP } from \"node:net\";\n\n/**\n * This function tells if a host name or an address is a loopback host: `localhost`, an address in\n * `127.0.0.0/8`, or `::1`. The MCP server over HTTP binds to a loopback host unless the operator\n * permits a different host. The function imports only Node.js, so the command line uses it before\n * the store opens.\n */\nexport const isLoopbackHost = (host: string): boolean => {\n const name = host\n .trim()\n .toLowerCase()\n .replace(/^\\[(.*)\\]$/, \"$1\");\n if (name === \"localhost\" || name.endsWith(\".localhost\")) return true;\n const mapped = name.startsWith(\"::ffff:\") ? name.slice(7) : name;\n if (isIP(mapped) === 4) return mapped.startsWith(\"127.\");\n if (isIP(name) === 6) return /^(?:0{0,4}:){7}0{0,3}1$|^::1$/.test(name);\n return false;\n};\n\n/** This function gives a host for a URL: an IPv6 address goes in brackets. */\nexport const urlHost = (host: string): string => (isIP(host) === 6 ? `[${host}]` : host);\n","import { createHash, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { readFileSync, statSync } from \"node:fs\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { createServer } from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport type { McpHost, McpServerTransport } from \"@procedures/mcp-server\";\nimport { McpTaskStore, createMcpHost } from \"@procedures/mcp-server\";\nimport type { DaemonMcpHost, DaemonTask, PoliteCrawler } from \"polite-crawler\";\nimport { isLoopbackHost, urlHost } from \"./loopback.ts\";\nimport type { CrawlToolOptions } from \"./tools.ts\";\nimport { crawlHostOptions } from \"./server.ts\";\n\n/*\n * This file serves the tools over the Streamable HTTP transport of MCP. The listener is the\n * crawler's own: it checks the host, the origin and the token, and it serves the status of the\n * daemon. The MCP endpoint is the HTTP face of the shared MCP host, mounted at /mcp: the face keeps\n * the sessions and the tasks.\n *\n * - Each request needs the bearer token. The check compares the SHA-256 digests of the two tokens\n * in constant time.\n * - The server binds to a loopback host by default. A different host needs `allowRemote`.\n * - A server on a loopback host accepts only a loopback name in the `Host` header, and an `Origin`\n * header must name the same host. Thus a web page cannot reach the server by DNS rebinding.\n */\n\n/** These are the defaults of the MCP server over HTTP. */\nexport const MCP_HTTP_DEFAULTS = {\n /** The port of the server. */\n port: 9307,\n /** The host of the server: the IPv4 loopback address. */\n host: \"127.0.0.1\",\n /** The path of the MCP endpoint. */\n path: \"/mcp\",\n /** The path of the status of the daemon. */\n statusPath: \"/status\",\n /** The most sessions at one time. A new session above the limit closes the session that was idle longest. */\n maxSessions: 32,\n /** The largest body of a request that starts a session: 4 MiB. */\n maxBodyBytes: 4_194_304,\n /** The shortest token: 16 characters. */\n minTokenLength: 16,\n} as const;\n\n/** These options serve MCP over HTTP. */\nexport interface McpHttpOptions {\n /** This value is the port. The default is 9307. The value 0 takes a free port. */\n readonly port?: number | undefined;\n /** This value is the host to bind. The default is `127.0.0.1`. A host that is not loopback needs `allowRemote`. */\n readonly host?: string | undefined;\n /** The value true permits a host that is not loopback. Then other computers can try the token. */\n readonly allowRemote?: boolean | undefined;\n /** This value is the bearer token. The default reads `tokenFile`, or makes a random token. */\n readonly token?: string | undefined;\n /** This file holds the bearer token, when `token` is not given. */\n readonly tokenFile?: string | undefined;\n /** This function gives the JSON of `GET /status`. Without it, the path gives 404. */\n readonly status?: (() => unknown) | undefined;\n /** This store keeps the tasks of each session. The default is a new `McpTaskStore`. */\n readonly tasks?: McpTaskStore | undefined;\n /** This function writes the messages of the server. The default writes nothing. */\n readonly log?: ((line: string) => void) | undefined;\n}\n\n/** This is a server that serves MCP over HTTP. */\nexport interface McpHttpServer {\n /** This URL is the MCP endpoint, for example `http://127.0.0.1:9307/mcp`. */\n readonly url: string;\n readonly host: string;\n readonly port: number;\n /** This value is the bearer token. Give it to the client, and keep it secret. */\n readonly token: string;\n /** This store keeps the tasks of the long tools. */\n readonly tasks: McpTaskStore;\n /** This method counts the open sessions. */\n sessions(): number;\n /** This method closes each session, cancels each task and stops the server. A second close does nothing. */\n close(): Promise<void>;\n}\n\nconst digest = (text: string): Buffer => createHash(\"sha256\").update(text, \"utf8\").digest();\n\n/**\n * This function compares a token with the token of the server in constant time. It compares the\n * SHA-256 digests, so the time does not depend on the length or the text of either token.\n */\nexport const sameToken = (given: string, token: string): boolean =>\n timingSafeEqual(digest(given), digest(token));\n\n/** This function makes a random token: 32 bytes in base64url, 43 characters. */\nexport const makeToken = (): string => randomBytes(32).toString(\"base64url\");\n\n/** A token is printable ASCII with no space, so a header holds it. */\nconst TOKEN = /^[\\x21-\\x7e]+$/;\n\n/** This function checks a token. A token that is short or that a header cannot hold throws an error. */\nexport const checkToken = (token: string, source: string): string => {\n const trimmed = token.trim();\n if (!TOKEN.test(trimmed) || trimmed.length < MCP_HTTP_DEFAULTS.minTokenLength) {\n throw new Error(\n `${source}: the token must be ${String(MCP_HTTP_DEFAULTS.minTokenLength)} or more printable characters with no space`,\n );\n }\n return trimmed;\n};\n\n/** This function reads a token file. It gives a warning when other users can read the file. */\nconst tokenOf = (options: McpHttpOptions, log: (line: string) => void): string => {\n if (options.token !== undefined) return checkToken(options.token, \"token\");\n if (options.tokenFile === undefined) return makeToken();\n let text: string;\n try {\n text = readFileSync(options.tokenFile, \"utf8\");\n } catch (error) {\n const code = (error as { code?: unknown }).code;\n throw new Error(\n `the token file ${options.tokenFile} does not open (${typeof code === \"string\" ? code : \"error\"})`,\n { cause: error },\n );\n }\n if (process.platform !== \"win32\" && (statSync(options.tokenFile).mode & 0o077) !== 0)\n log(`polite-crawler: other users can read the token file ${options.tokenFile}: chmod 600 it`);\n return checkToken(text, `the token file ${options.tokenFile}`);\n};\n\nconst json = (response: ServerResponse, status: number, body: unknown, headers = {}): void => {\n if (response.headersSent) return;\n response.writeHead(status, { \"content-type\": \"application/json\", ...headers });\n response.end(JSON.stringify(body));\n};\n\n/** This function gives the answer of a refusal, as a JSON-RPC error with no id. */\nconst refuse = (response: ServerResponse, status: number, message: string, headers = {}): void =>\n json(response, status, { jsonrpc: \"2.0\", error: { code: -32000, message }, id: null }, headers);\n\n/**\n * The number of open sessions of the HTTP face. The state of the face gives it when the shared host\n * has the field. Else the count of its Streamable HTTP endpoint gives it.\n */\nconst sessionsOf = (face: McpServerTransport): number => {\n const state = face.getState() as { sessions?: unknown };\n if (typeof state.sessions === \"number\") return state.sessions;\n const endpoint = (face as unknown as { http?: { sessions?: unknown } }).http;\n return typeof endpoint?.sessions === \"number\" ? endpoint.sessions : 0;\n};\n\n/**\n * This function serves an MCP host over HTTP. `makeHost` makes the host with its HTTP face mounted\n * at `/mcp`, with the task store of the server. Each request needs `Authorization: Bearer <token>`.\n */\nexport const serveMcpHttp = async (\n makeHost: (face: { readonly token: string; readonly tasks: McpTaskStore }) => McpHost,\n options: McpHttpOptions = {},\n): Promise<McpHttpServer> => {\n const log = options.log ?? (() => undefined);\n const host = options.host ?? MCP_HTTP_DEFAULTS.host;\n const port = options.port ?? MCP_HTTP_DEFAULTS.port;\n if (!Number.isInteger(port) || port < 0 || port > 65_535)\n throw new RangeError(`the port must be an integer from 0 to 65535, not ${String(port)}`);\n const loopback = isLoopbackHost(host);\n if (!loopback && options.allowRemote !== true) {\n throw new Error(\n `the host ${host} is not a loopback address: give allowRemote (--allow-remote) to serve other computers`,\n );\n }\n const token = tokenOf(options, log);\n const tasks = options.tasks ?? new McpTaskStore();\n const mcp = makeHost({ token, tasks });\n const face = mcp.transports[0];\n if (face === undefined) throw new Error(\"the MCP host has no HTTP face\");\n await mcp.start();\n let bound = port;\n let closing: Promise<void> | undefined;\n\n /** A loopback server accepts only a loopback name and its port in `Host`. */\n const hostAllowed = (header: string | undefined): boolean => {\n if (!loopback) return true;\n if (header === undefined) return false;\n let parsed: URL;\n try {\n parsed = new URL(`http://${header}`);\n } catch {\n return false;\n }\n return isLoopbackHost(parsed.hostname) && (parsed.port === \"\" || Number(parsed.port) === bound);\n };\n\n /** An `Origin` header must name the host of the request: a page of a different site gets 403. */\n const originAllowed = (origin: string | undefined, header: string | undefined): boolean => {\n if (origin === undefined) return true;\n try {\n return new URL(origin).host === header;\n } catch {\n return false;\n }\n };\n\n const authorized = (header: string | undefined): boolean => {\n const match = /^Bearer\\s+(\\S+)\\s*$/i.exec(header ?? \"\");\n return match !== null && sameToken(match[1]!, token);\n };\n\n const handle = (request: IncomingMessage, response: ServerResponse): void => {\n const hostHeader = request.headers.host;\n if (!hostAllowed(hostHeader) || !originAllowed(request.headers.origin, hostHeader)) {\n refuse(response, 403, \"the Host or the Origin header is not of this server\");\n return;\n }\n if (!authorized(request.headers.authorization)) {\n refuse(response, 401, \"the request needs the bearer token of the server\", {\n \"www-authenticate\": 'Bearer realm=\"polite-crawler\"',\n });\n return;\n }\n const path = new URL(request.url ?? \"/\", \"http://localhost\").pathname;\n if (path === MCP_HTTP_DEFAULTS.statusPath && options.status !== undefined) {\n if (request.method !== \"GET\") refuse(response, 405, \"the status takes GET\");\n else json(response, 200, options.status(), { \"cache-control\": \"no-store\" });\n return;\n }\n // the face answers its path: the sessions, the initialize rule, the body limit and the tasks\n if (!face.handleHttp(request, response)) {\n refuse(response, 404, `no such path: the MCP endpoint is ${MCP_HTTP_DEFAULTS.path}`);\n }\n };\n\n const http = createServer((request, response) => {\n try {\n handle(request, response);\n } catch (error) {\n log(\n `polite-crawler: an MCP request failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n if (!response.headersSent) refuse(response, 500, \"the server could not handle the request\");\n else response.end();\n }\n });\n try {\n await new Promise<void>((resolve, reject) => {\n const failed = (error: NodeJS.ErrnoException): void => {\n reject(\n error.code === \"EADDRINUSE\"\n ? new Error(`the port ${String(port)} of ${host} is in use: give a different port`, {\n cause: error,\n })\n : error,\n );\n };\n http.once(\"error\", failed);\n http.listen(port, host, () => {\n http.removeListener(\"error\", failed);\n resolve();\n });\n });\n } catch (error) {\n await mcp.stop();\n await tasks.close();\n throw error;\n }\n bound = (http.address() as AddressInfo).port;\n const url = `http://${urlHost(host)}:${String(bound)}${MCP_HTTP_DEFAULTS.path}`;\n if (!loopback)\n log(\n `polite-crawler: the MCP server listens on ${host}, not on a loopback address: other computers can reach it`,\n );\n return {\n url,\n host,\n port: bound,\n token,\n tasks,\n sessions: () => sessionsOf(face),\n close() {\n closing ??= (async () => {\n await tasks.close();\n await mcp.stop();\n await new Promise<void>((resolve) => {\n http.close(() => resolve());\n http.closeAllConnections();\n });\n })();\n return closing;\n },\n };\n};\n\n/** These options serve the tools over HTTP: the options of the tools, and of the HTTP server. */\nexport interface StartMcpHttpOptions extends McpHttpOptions, CrawlToolOptions {}\n\n/**\n * This function serves the tools of a crawler over the Streamable HTTP transport of MCP. The\n * sessions share one MCP host with the nine tools of the server on the standard input and output.\n * The envelope, the elicitation and the opt-ins are the same. `crawl_site` and `crawl_discover` are\n * MCP tasks. Each request needs the bearer token. The function does not close the crawler.\n */\nexport const startMcpHttpServer = (\n crawler: PoliteCrawler,\n options: StartMcpHttpOptions = {},\n): Promise<McpHttpServer> => {\n const { onBehalfOf, allowUserProvided, renderOff, ...http } = options;\n return serveMcpHttp(\n ({ tasks }) =>\n createMcpHost({\n ...crawlHostOptions(crawler, { onBehalfOf, allowUserProvided, renderOff, tasks }),\n faces: {\n // The listener checks the host, the origin and the token of each request, as in 0.1.4.\n // Thus the face checks no token and no host name: the checks have one owner.\n http: {\n mount: true,\n path: MCP_HTTP_DEFAULTS.path,\n allowedHosts: \"*\",\n maxSessions: MCP_HTTP_DEFAULTS.maxSessions,\n maxBodyBytes: MCP_HTTP_DEFAULTS.maxBodyBytes,\n },\n },\n }),\n http,\n );\n};\n\n/**\n * This host serves the tools of the daemon over HTTP. It is the seam between the daemon and its MCP\n * transport: a different MCP host can replace it with `DaemonOptions.mcpHost`.\n */\nexport const mcpHttpHost: DaemonMcpHost = {\n async start(crawler, options, hooks) {\n const tasks = new McpTaskStore({ onChange: hooks.onTasks });\n const served = await startMcpHttpServer(crawler, {\n ...options,\n tasks,\n status: hooks.status,\n log: hooks.log,\n });\n const running = (): DaemonTask[] =>\n tasks.running().map((task) => ({\n id: task.id,\n tool: task.tool,\n status: task.status,\n message: task.message,\n created_at: task.createdAt,\n updated_at: task.updatedAt,\n }));\n return { url: served.url, tasks: running, close: () => served.close() };\n },\n};\n"],"mappings":"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"}