@warlock.js/ai-tools 5.31.0 → 5.33.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.
- package/CHANGELOG.md +26 -0
- package/cjs/index.cjs +327 -60
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/http.type.d.mts +4 -5
- package/esm/contracts/http.type.d.mts.map +1 -1
- package/esm/contracts/index.d.mts +2 -1
- package/esm/contracts/mcp-serve.type.d.mts +97 -37
- package/esm/contracts/mcp-serve.type.d.mts.map +1 -1
- package/esm/contracts/mcp.type.d.mts +29 -19
- package/esm/contracts/mcp.type.d.mts.map +1 -1
- package/esm/contracts/utility.type.d.mts +7 -8
- package/esm/contracts/utility.type.d.mts.map +1 -1
- package/esm/contracts/web.type.d.mts +9 -10
- package/esm/contracts/web.type.d.mts.map +1 -1
- package/esm/errors.d.mts +31 -26
- package/esm/errors.d.mts.map +1 -1
- package/esm/http/http-request.d.mts +2 -3
- package/esm/http/http-request.d.mts.map +1 -1
- package/esm/http/http-request.mjs +7 -5
- package/esm/http/http-request.mjs.map +1 -1
- package/esm/index.d.mts +6 -2
- package/esm/index.mjs +2 -1
- package/esm/mcp/client.mjs +2 -1
- package/esm/mcp/client.mjs.map +1 -1
- package/esm/mcp/index.d.mts +5 -4
- package/esm/mcp/index.d.mts.map +1 -1
- package/esm/mcp/index.mjs +2 -1
- package/esm/mcp/index.mjs.map +1 -1
- package/esm/mcp/json-schema-to-standard.d.mts +1 -3
- package/esm/mcp/json-schema-to-standard.d.mts.map +1 -1
- package/esm/mcp/protocol.d.mts +1 -2
- package/esm/mcp/protocol.d.mts.map +1 -1
- package/esm/mcp/result.d.mts +17 -0
- package/esm/mcp/result.d.mts.map +1 -0
- package/esm/mcp/result.mjs +85 -0
- package/esm/mcp/result.mjs.map +1 -0
- package/esm/mcp/serve-http.mjs +64 -25
- package/esm/mcp/serve-http.mjs.map +1 -1
- package/esm/mcp/serve-requests.mjs +62 -0
- package/esm/mcp/serve-requests.mjs.map +1 -0
- package/esm/mcp/serve.d.mts +5 -5
- package/esm/mcp/serve.d.mts.map +1 -1
- package/esm/mcp/serve.mjs +97 -18
- package/esm/mcp/serve.mjs.map +1 -1
- package/esm/mcp/transport.d.mts +4 -5
- package/esm/mcp/transport.d.mts.map +1 -1
- package/esm/mcp/transport.mjs +12 -6
- package/esm/mcp/transport.mjs.map +1 -1
- package/esm/mcp/transport.type.d.mts +2 -2
- package/esm/mcp/transport.type.d.mts.map +1 -1
- package/esm/register.d.mts +3 -4
- package/esm/register.d.mts.map +1 -1
- package/esm/utility/calculator.d.mts +1 -3
- package/esm/utility/calculator.d.mts.map +1 -1
- package/esm/utility/date-time.d.mts +3 -5
- package/esm/utility/date-time.d.mts.map +1 -1
- package/esm/utility/date-time.mjs +2 -1
- package/esm/utility/date-time.mjs.map +1 -1
- package/esm/utility/index.d.mts +2 -1
- package/esm/utility/index.mjs +1 -1
- package/esm/web/fetch-url.d.mts +2 -3
- package/esm/web/fetch-url.d.mts.map +1 -1
- package/esm/web/fetch-url.mjs +2 -1
- package/esm/web/fetch-url.mjs.map +1 -1
- package/esm/web/index.d.mts +2 -1
- package/esm/web/index.mjs +1 -1
- package/esm/web/web-search.d.mts +2 -3
- package/esm/web/web-search.d.mts.map +1 -1
- package/esm/web/web-search.mjs +3 -4
- package/esm/web/web-search.mjs.map +1 -1
- package/llms-full.txt +122 -3
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/connect-mcp-server/SKILL.md +15 -0
- package/skills/expose-as-mcp-server/SKILL.md +107 -3
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//#region ../../packages/ai-tools/src/mcp/result.ts
|
|
2
|
+
const NATIVE_RESULT = Symbol("mcpResult");
|
|
3
|
+
/**
|
|
4
|
+
* Opt in to native MCP content, error status, structured output and metadata.
|
|
5
|
+
* Validation happens after invoke(), so malformed payloads become JSON-RPC
|
|
6
|
+
* errors rather than being caught as tool execution errors.
|
|
7
|
+
*/
|
|
8
|
+
function mcpResult(result) {
|
|
9
|
+
return { [NATIVE_RESULT]: result };
|
|
10
|
+
}
|
|
11
|
+
/** Read a branded value without accepting lookalike application objects. */
|
|
12
|
+
function isMcpResult(value) {
|
|
13
|
+
return isRecord(value) && NATIVE_RESULT in value;
|
|
14
|
+
}
|
|
15
|
+
/** Get the original payload; no cloning, normalization or field stripping. */
|
|
16
|
+
function unwrapMcpResult(value) {
|
|
17
|
+
return value[NATIVE_RESULT];
|
|
18
|
+
}
|
|
19
|
+
/** Objects with result fields are candidates only when native mode is enabled. */
|
|
20
|
+
function looksLikeMcpResult(value) {
|
|
21
|
+
return isRecord(value) && [
|
|
22
|
+
"content",
|
|
23
|
+
"isError",
|
|
24
|
+
"structuredContent",
|
|
25
|
+
"_meta"
|
|
26
|
+
].some((key) => key in value);
|
|
27
|
+
}
|
|
28
|
+
/** Non-array object used by the MCP JSON object fields. */
|
|
29
|
+
function isRecord(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
function optionalString(value) {
|
|
33
|
+
return value === void 0 || typeof value === "string";
|
|
34
|
+
}
|
|
35
|
+
function validAnnotations(value) {
|
|
36
|
+
if (value === void 0) return true;
|
|
37
|
+
if (!isRecord(value)) return false;
|
|
38
|
+
return (value.audience === void 0 || Array.isArray(value.audience) && value.audience.every((role) => role === "user" || role === "assistant")) && (value.priority === void 0 || typeof value.priority === "number" && value.priority >= 0 && value.priority <= 1) && optionalString(value.lastModified);
|
|
39
|
+
}
|
|
40
|
+
function validMetadata(value) {
|
|
41
|
+
return (value["_meta"] === void 0 || isRecord(value["_meta"])) && validAnnotations(value.annotations);
|
|
42
|
+
}
|
|
43
|
+
function validIcon(value) {
|
|
44
|
+
return isRecord(value) && typeof value.src === "string" && optionalString(value.mimeType) && (value.sizes === void 0 || Array.isArray(value.sizes) && value.sizes.every((size) => typeof size === "string")) && (value.theme === void 0 || value.theme === "light" || value.theme === "dark");
|
|
45
|
+
}
|
|
46
|
+
function validContent(value) {
|
|
47
|
+
if (!isRecord(value) || !validMetadata(value)) return false;
|
|
48
|
+
switch (value.type) {
|
|
49
|
+
case "text": return typeof value.text === "string";
|
|
50
|
+
case "image":
|
|
51
|
+
case "audio": return typeof value.data === "string" && typeof value.mimeType === "string";
|
|
52
|
+
case "resource_link": return typeof value.uri === "string" && typeof value.name === "string" && optionalString(value.title) && optionalString(value.description) && optionalString(value.mimeType) && (value.size === void 0 || typeof value.size === "number") && (value.icons === void 0 || Array.isArray(value.icons) && value.icons.every(validIcon));
|
|
53
|
+
case "resource": {
|
|
54
|
+
const resource = value.resource;
|
|
55
|
+
return isRecord(resource) && typeof resource.uri === "string" && optionalString(resource.mimeType) && validMetadata(resource) && (typeof resource.text === "string" || typeof resource.blob === "string");
|
|
56
|
+
}
|
|
57
|
+
default: return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Reject values JSON would drop, coerce or fail to encode (including cycles). */
|
|
61
|
+
function isJsonValue(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
62
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
63
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
64
|
+
if (typeof value !== "object" || ancestors.has(value)) return false;
|
|
65
|
+
const prototype = Object.getPrototypeOf(value);
|
|
66
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
|
|
67
|
+
ancestors.add(value);
|
|
68
|
+
const valid = Array.isArray(value) ? Array.from(value).every((entry) => isJsonValue(entry, ancestors)) : Object.values(value).every((entry) => entry === void 0 || isJsonValue(entry, ancestors));
|
|
69
|
+
ancestors.delete(value);
|
|
70
|
+
return valid;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* MCP 2025-11-25 CallToolResult and ContentBlock fields:
|
|
74
|
+
* https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-result
|
|
75
|
+
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts
|
|
76
|
+
* structuredContent is preserved even when tools/list declares outputSchema.
|
|
77
|
+
* Tool authors remain responsible for conformity to that application schema.
|
|
78
|
+
*/
|
|
79
|
+
function assertMcpResult(value) {
|
|
80
|
+
if (!isRecord(value) || !Array.isArray(value.content) || !Array.from(value.content).every(validContent) || value.isError !== void 0 && typeof value.isError !== "boolean" || value.structuredContent !== void 0 && !isRecord(value.structuredContent) || value["_meta"] !== void 0 && !isRecord(value["_meta"]) || !isJsonValue(value)) throw new TypeError("Invalid native MCP CallToolResult.");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
export { assertMcpResult, isMcpResult, isRecord, looksLikeMcpResult, mcpResult, unwrapMcpResult };
|
|
85
|
+
//# sourceMappingURL=result.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/result.ts"],"sourcesContent":["import type { McpToolCallResult } from \"../contracts\";\n\nconst NATIVE_RESULT = Symbol(\"mcpResult\");\n\n/** An explicit native MCP result, unwrapped only by the serving boundary. */\nexport type McpNativeResult<T extends McpToolCallResult = McpToolCallResult> = {\n readonly [NATIVE_RESULT]: T;\n};\n\n/**\n * Opt in to native MCP content, error status, structured output and metadata.\n * Validation happens after invoke(), so malformed payloads become JSON-RPC\n * errors rather than being caught as tool execution errors.\n */\nexport function mcpResult<T extends McpToolCallResult>(result: T): McpNativeResult<T> {\n return { [NATIVE_RESULT]: result };\n}\n\n/** Read a branded value without accepting lookalike application objects. */\nexport function isMcpResult(value: unknown): value is McpNativeResult {\n return isRecord(value) && NATIVE_RESULT in value;\n}\n\n/** Get the original payload; no cloning, normalization or field stripping. */\nexport function unwrapMcpResult(value: McpNativeResult): McpToolCallResult {\n return value[NATIVE_RESULT];\n}\n\n/** Objects with result fields are candidates only when native mode is enabled. */\nexport function looksLikeMcpResult(value: unknown): boolean {\n return (\n isRecord(value) &&\n [\"content\", \"isError\", \"structuredContent\", \"_meta\"].some((key) => key in value)\n );\n}\n\n/** Non-array object used by the MCP JSON object fields. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction optionalString(value: unknown): boolean {\n return value === undefined || typeof value === \"string\";\n}\n\nfunction validAnnotations(value: unknown): boolean {\n if (value === undefined) return true;\n if (!isRecord(value)) return false;\n\n return (\n (value.audience === undefined ||\n (Array.isArray(value.audience) &&\n value.audience.every((role) => role === \"user\" || role === \"assistant\"))) &&\n (value.priority === undefined ||\n (typeof value.priority === \"number\" && value.priority >= 0 && value.priority <= 1)) &&\n optionalString(value.lastModified)\n );\n}\n\nfunction validMetadata(value: Record<string, unknown>): boolean {\n return (\n (value[\"_meta\"] === undefined || isRecord(value[\"_meta\"])) &&\n validAnnotations(value.annotations)\n );\n}\n\nfunction validIcon(value: unknown): boolean {\n return (\n isRecord(value) &&\n typeof value.src === \"string\" &&\n optionalString(value.mimeType) &&\n (value.sizes === undefined ||\n (Array.isArray(value.sizes) && value.sizes.every((size) => typeof size === \"string\"))) &&\n (value.theme === undefined || value.theme === \"light\" || value.theme === \"dark\")\n );\n}\n\nfunction validContent(value: unknown): boolean {\n if (!isRecord(value) || !validMetadata(value)) return false;\n\n switch (value.type) {\n case \"text\":\n return typeof value.text === \"string\";\n case \"image\":\n case \"audio\":\n return typeof value.data === \"string\" && typeof value.mimeType === \"string\";\n case \"resource_link\":\n return (\n typeof value.uri === \"string\" &&\n typeof value.name === \"string\" &&\n optionalString(value.title) &&\n optionalString(value.description) &&\n optionalString(value.mimeType) &&\n (value.size === undefined || typeof value.size === \"number\") &&\n (value.icons === undefined || (Array.isArray(value.icons) && value.icons.every(validIcon)))\n );\n case \"resource\": {\n const resource = value.resource;\n\n return (\n isRecord(resource) &&\n typeof resource.uri === \"string\" &&\n optionalString(resource.mimeType) &&\n validMetadata(resource) &&\n (typeof resource.text === \"string\" || typeof resource.blob === \"string\")\n );\n }\n default:\n return false;\n }\n}\n\n/** Reject values JSON would drop, coerce or fail to encode (including cycles). */\nfunction isJsonValue(value: unknown, ancestors = new Set<object>()): boolean {\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") return true;\n if (typeof value === \"number\") return Number.isFinite(value);\n if (typeof value !== \"object\" || ancestors.has(value)) return false;\n\n const prototype: unknown = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;\n\n ancestors.add(value);\n const valid = Array.isArray(value)\n ? Array.from(value).every((entry) => isJsonValue(entry, ancestors))\n : Object.values(value).every((entry) => entry === undefined || isJsonValue(entry, ancestors));\n ancestors.delete(value);\n return valid;\n}\n\n/**\n * MCP 2025-11-25 CallToolResult and ContentBlock fields:\n * https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-result\n * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts\n * structuredContent is preserved even when tools/list declares outputSchema.\n * Tool authors remain responsible for conformity to that application schema.\n */\nexport function assertMcpResult(value: unknown): asserts value is McpToolCallResult {\n if (\n !isRecord(value) ||\n !Array.isArray(value.content) ||\n !Array.from(value.content).every(validContent) ||\n (value.isError !== undefined && typeof value.isError !== \"boolean\") ||\n (value.structuredContent !== undefined && !isRecord(value.structuredContent)) ||\n (value[\"_meta\"] !== undefined && !isRecord(value[\"_meta\"])) ||\n !isJsonValue(value)\n ) {\n throw new TypeError(\"Invalid native MCP CallToolResult.\");\n }\n}\n"],"mappings":"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"}
|
package/esm/mcp/serve-http.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { McpServeError } from "../errors.mjs";
|
|
2
2
|
import { SUPPORTED_PROTOCOL_VERSIONS } from "./protocol.mjs";
|
|
3
|
+
import { McpServeRequests } from "./serve-requests.mjs";
|
|
3
4
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
4
5
|
import { createServer } from "node:http";
|
|
5
6
|
import { isIPv4, isIPv6 } from "node:net";
|
|
@@ -39,7 +40,7 @@ const JSON_RPC_INVALID_REQUEST = -32600;
|
|
|
39
40
|
/** Generic server-defined JSON-RPC code for HTTP-level refusals (auth, origin, size). */
|
|
40
41
|
const JSON_RPC_SERVER_ERROR = -32e3;
|
|
41
42
|
/** Host names that always mean "this machine". */
|
|
42
|
-
const LOOPBACK_NAMES = new Set([
|
|
43
|
+
const LOOPBACK_NAMES = /* @__PURE__ */ new Set([
|
|
43
44
|
"localhost",
|
|
44
45
|
"127.0.0.1",
|
|
45
46
|
"::1",
|
|
@@ -101,9 +102,9 @@ function resolveSettings(transport) {
|
|
|
101
102
|
const tokens = auth?.bearer === void 0 ? [] : typeof auth.bearer === "string" ? [auth.bearer] : [...auth.bearer];
|
|
102
103
|
if (tokens.some((token) => token.length === 0)) throw new McpServeError("MCP http auth.bearer tokens must be non-empty strings.", { type: "config" });
|
|
103
104
|
if (auth && auth.bearer !== void 0 && tokens.length === 0) throw new McpServeError("MCP http auth.bearer needs at least one token.", { type: "config" });
|
|
104
|
-
if (auth && tokens.length === 0 && !auth.authorize) throw new McpServeError("MCP http auth needs `bearer` and/or `
|
|
105
|
-
const authRequired = tokens.length > 0 || auth?.authorize !== void 0;
|
|
106
|
-
if (!loopback && !authRequired) throw new McpServeError(`Refusing to serve MCP over http on non-loopback host "${host}" without auth. Set transport.auth ({ bearer } or {
|
|
105
|
+
if (auth && tokens.length === 0 && !auth.authorize && !auth.authenticate) throw new McpServeError("MCP http auth needs `bearer`, `authorize` and/or `authenticate`.", { type: "config" });
|
|
106
|
+
const authRequired = tokens.length > 0 || auth?.authorize !== void 0 || auth?.authenticate !== void 0;
|
|
107
|
+
if (!loopback && !authRequired) throw new McpServeError(`Refusing to serve MCP over http on non-loopback host "${host}" without auth. Set transport.auth ({ bearer }, { authorize } or { authenticate }), or bind 127.0.0.1.`, {
|
|
107
108
|
type: "config",
|
|
108
109
|
context: { host }
|
|
109
110
|
});
|
|
@@ -116,6 +117,7 @@ function resolveSettings(transport) {
|
|
|
116
117
|
drainTimeoutMs,
|
|
117
118
|
tokenDigests: tokens.map(digest),
|
|
118
119
|
authorize: auth?.authorize,
|
|
120
|
+
authenticate: auth?.authenticate,
|
|
119
121
|
authRequired,
|
|
120
122
|
allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
|
|
121
123
|
allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name)))
|
|
@@ -174,6 +176,7 @@ function sendEmpty(response, status, headers = {}) {
|
|
|
174
176
|
async function startHttpServer(transport, dispatch) {
|
|
175
177
|
const settings = resolveSettings(transport);
|
|
176
178
|
const shutdown = new AbortController();
|
|
179
|
+
const requests = new McpServeRequests();
|
|
177
180
|
const inFlight = /* @__PURE__ */ new Set();
|
|
178
181
|
let closing;
|
|
179
182
|
const server = createServer((request, response) => {
|
|
@@ -181,7 +184,7 @@ async function startHttpServer(transport, dispatch) {
|
|
|
181
184
|
sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, "Server is shutting down.", { connection: "close" });
|
|
182
185
|
return;
|
|
183
186
|
}
|
|
184
|
-
const work = handleRequest(request, response, settings, dispatch, shutdown.signal).catch(() => {
|
|
187
|
+
const work = handleRequest(request, response, settings, dispatch, shutdown.signal, requests).catch(() => {
|
|
185
188
|
sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, "Internal server error.");
|
|
186
189
|
});
|
|
187
190
|
inFlight.add(work);
|
|
@@ -243,7 +246,7 @@ async function startHttpServer(transport, dispatch) {
|
|
|
243
246
|
* Validate and answer one HTTP request: path → Host / Origin → auth →
|
|
244
247
|
* method → headers → body → JSON-RPC dispatch.
|
|
245
248
|
*/
|
|
246
|
-
async function handleRequest(request, response, settings, dispatch, signal) {
|
|
249
|
+
async function handleRequest(request, response, settings, dispatch, signal, requests) {
|
|
247
250
|
const headers = flattenHeaders(request.headers);
|
|
248
251
|
const method = (request.method ?? "GET").toUpperCase();
|
|
249
252
|
const path = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
@@ -265,8 +268,10 @@ async function handleRequest(request, response, settings, dispatch, signal) {
|
|
|
265
268
|
headers
|
|
266
269
|
};
|
|
267
270
|
if (request.socket.remoteAddress !== void 0) info.remoteAddress = request.socket.remoteAddress;
|
|
268
|
-
|
|
269
|
-
|
|
271
|
+
const authentication = await authorized(info, settings);
|
|
272
|
+
if (!authentication.allowed) {
|
|
273
|
+
const challenge = settings.tokenDigests.length > 0 || settings.authenticate ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {};
|
|
274
|
+
sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", challenge);
|
|
270
275
|
return;
|
|
271
276
|
}
|
|
272
277
|
if (method !== "POST") {
|
|
@@ -310,6 +315,7 @@ async function handleRequest(request, response, settings, dispatch, signal) {
|
|
|
310
315
|
const id = record.id;
|
|
311
316
|
if (typeof record.method === "string") {
|
|
312
317
|
if (typeof id !== "string" && typeof id !== "number") {
|
|
318
|
+
requests.cancel(record, cancellationScope(info));
|
|
313
319
|
sendEmpty(response, 202);
|
|
314
320
|
return;
|
|
315
321
|
}
|
|
@@ -319,10 +325,20 @@ async function handleRequest(request, response, settings, dispatch, signal) {
|
|
|
319
325
|
method: record.method
|
|
320
326
|
};
|
|
321
327
|
if (record.params !== void 0) rpcRequest.params = record.params;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
328
|
+
const disconnected = new AbortController();
|
|
329
|
+
const onClose = () => disconnected.abort();
|
|
330
|
+
response.once("close", onClose);
|
|
331
|
+
try {
|
|
332
|
+
const rpcResponse = await requests.run(rpcRequest, {
|
|
333
|
+
request: info,
|
|
334
|
+
principal: authentication.principal,
|
|
335
|
+
signal: AbortSignal.any([signal, disconnected.signal])
|
|
336
|
+
}, (context) => dispatch(rpcRequest, context), cancellationScope(info));
|
|
337
|
+
if (rpcResponse === void 0) sendEmpty(response, 202);
|
|
338
|
+
else sendJson(response, 200, rpcResponse);
|
|
339
|
+
} finally {
|
|
340
|
+
response.off("close", onClose);
|
|
341
|
+
}
|
|
326
342
|
return;
|
|
327
343
|
}
|
|
328
344
|
if ((typeof id === "string" || typeof id === "number") && ("result" in record || "error" in record)) {
|
|
@@ -361,27 +377,50 @@ function originAllowed(origin, settings) {
|
|
|
361
377
|
if (settings.allowedOrigins.has(parsed.origin)) return true;
|
|
362
378
|
return (parsed.protocol === "http:" || parsed.protocol === "https:") && isLoopbackHost(parsed.hostname);
|
|
363
379
|
}
|
|
364
|
-
/**
|
|
365
|
-
* Run the configured auth. Bearer tokens are compared in constant time
|
|
366
|
-
* over SHA-256 digests (equal-length buffers for `timingSafeEqual`), and
|
|
367
|
-
* every configured token is compared so the match position does not leak.
|
|
368
|
-
* An `authorize` hook that throws denies the request.
|
|
369
|
-
*/
|
|
370
380
|
async function authorized(info, settings) {
|
|
371
|
-
if (!settings.authRequired) return true;
|
|
381
|
+
if (!settings.authRequired) return { allowed: true };
|
|
372
382
|
if (settings.tokenDigests.length > 0) {
|
|
373
383
|
const match = /^Bearer[ \t]+(.+)$/i.exec(info.headers.authorization ?? "");
|
|
374
384
|
const presented = digest(match?.[1]?.trim() ?? "");
|
|
375
385
|
let matched = false;
|
|
376
386
|
for (const expected of settings.tokenDigests) matched = timingSafeEqual(presented, expected) || matched;
|
|
377
|
-
if (!match || !matched) return false;
|
|
387
|
+
if (!match || !matched) return { allowed: false };
|
|
378
388
|
}
|
|
379
|
-
|
|
380
|
-
|
|
389
|
+
try {
|
|
390
|
+
if (settings.authorize && await settings.authorize(info) !== true) return { allowed: false };
|
|
391
|
+
if (settings.authenticate) {
|
|
392
|
+
const principal = await settings.authenticate(info);
|
|
393
|
+
return principal === void 0 ? { allowed: false } : {
|
|
394
|
+
allowed: true,
|
|
395
|
+
principal
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return { allowed: true };
|
|
381
399
|
} catch {
|
|
382
|
-
return false;
|
|
400
|
+
return { allowed: false };
|
|
383
401
|
}
|
|
384
|
-
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Stateless HTTP has no connection/session id. Repeat the caller's credential
|
|
405
|
+
* and application headers on notifications; per-message transport headers do
|
|
406
|
+
* not identify a caller. Hash the material rather than retaining secrets as keys.
|
|
407
|
+
*/
|
|
408
|
+
function cancellationScope(info) {
|
|
409
|
+
const transportHeaders = /* @__PURE__ */ new Set([
|
|
410
|
+
"host",
|
|
411
|
+
"origin",
|
|
412
|
+
"accept",
|
|
413
|
+
"accept-encoding",
|
|
414
|
+
"accept-language",
|
|
415
|
+
"connection",
|
|
416
|
+
"content-type",
|
|
417
|
+
"content-length",
|
|
418
|
+
"transfer-encoding",
|
|
419
|
+
"mcp-protocol-version",
|
|
420
|
+
"user-agent"
|
|
421
|
+
]);
|
|
422
|
+
const credentials = Object.entries(info.headers).filter(([name]) => !transportHeaders.has(name)).sort(([left], [right]) => left.localeCompare(right));
|
|
423
|
+
return digest(JSON.stringify([info.remoteAddress, credentials])).toString("hex");
|
|
385
424
|
}
|
|
386
425
|
/** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */
|
|
387
426
|
function isJsonContentType(contentType) {
|
|
@@ -439,5 +478,5 @@ function readBody(request, limit) {
|
|
|
439
478
|
}
|
|
440
479
|
|
|
441
480
|
//#endregion
|
|
442
|
-
export { startHttpServer };
|
|
481
|
+
export { isLoopbackHost, startHttpServer };
|
|
443
482
|
//# sourceMappingURL=serve-http.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve-http.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/serve-http.ts"],"sourcesContent":["import { createHash, timingSafeEqual } from \"node:crypto\";\nimport {\n createServer,\n type IncomingHttpHeaders,\n type IncomingMessage,\n type Server,\n type ServerResponse,\n} from \"node:http\";\nimport { isIPv4, isIPv6 } from \"node:net\";\nimport type {\n JsonRpcRequest,\n JsonRpcResponse,\n McpHttpAuth,\n McpHttpRequestInfo,\n McpHttpServeTransport,\n McpServeCallContext,\n} from \"../contracts\";\nimport { McpServeError } from \"../errors\";\nimport { SUPPORTED_PROTOCOL_VERSIONS } from \"./protocol\";\n\n/**\n * MCP Streamable HTTP server transport for `ai.mcp.serve`.\n *\n * Implements the server side of the Streamable HTTP transport from the MCP\n * specification (https://modelcontextprotocol.io/specification/2025-11-25/basic/transports,\n * checked 2026-10-17) on `node:http`, with no SDK dependency:\n *\n * - one endpoint path; each POST carries exactly one JSON-RPC message\n * (batches were removed from the protocol in 2025-06-18 and get 400);\n * - a request is answered with `Content-Type: application/json` (the spec\n * lets a server pick JSON or SSE per request); a notification or a\n * response is acknowledged with `202 Accepted` and no body;\n * - stateless: no `MCP-Session-Id` is issued (optional in the spec), so\n * GET (server-initiated SSE stream) and DELETE (session end) answer\n * `405 Method Not Allowed`, as the spec allows;\n * - an unsupported `MCP-Protocol-Version` header gets 400;\n * - security warning items: `Origin` is validated on every request (an\n * invalid one gets 403), the default bind is `127.0.0.1`, and\n * authentication is required for any non-loopback bind.\n */\n\n/** Default bind interface — loopback only. */\nconst DEFAULT_HOST = \"127.0.0.1\";\n\n/** Default endpoint path. */\nconst DEFAULT_PATH = \"/mcp\";\n\n/** Default request body cap: 1 MiB. */\nconst DEFAULT_MAX_BODY_BYTES = 1_048_576;\n\n/** Default time `close()` waits for in-flight requests. */\nconst DEFAULT_DRAIN_TIMEOUT_MS = 5_000;\n\n/** JSON-RPC codes used for transport-level error bodies. */\nconst JSON_RPC_PARSE_ERROR = -32700;\nconst JSON_RPC_INVALID_REQUEST = -32600;\n\n/** Generic server-defined JSON-RPC code for HTTP-level refusals (auth, origin, size). */\nconst JSON_RPC_SERVER_ERROR = -32000;\n\n/** Host names that always mean \"this machine\". */\nconst LOOPBACK_NAMES = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\"]);\n\n/** The function the transport pumps each JSON-RPC request through. */\nexport type McpHttpDispatch = (\n request: JsonRpcRequest,\n context: McpServeCallContext,\n) => Promise<JsonRpcResponse>;\n\n/** A bound HTTP endpoint. */\nexport type RunningMcpHttpServer = {\n /** Full endpoint URL. */\n url: string;\n /** Drain in-flight requests (up to the drain timeout) and free the port. Idempotent. */\n close(): Promise<void>;\n};\n\n/**\n * True when `host` names a loopback interface (`localhost`, `127.0.0.0/8`,\n * `::1`, or an IPv4-mapped `::ffff:127.x.x.x`).\n */\nexport function isLoopbackHost(host: string): boolean {\n const normalized = host.trim().toLowerCase();\n\n if (LOOPBACK_NAMES.has(normalized)) {\n return true;\n }\n\n const unbracketed = normalized.replace(/^\\[|\\]$/g, \"\");\n const mapped = unbracketed.startsWith(\"::ffff:\")\n ? unbracketed.slice(\"::ffff:\".length)\n : unbracketed;\n\n return isIPv4(mapped) && mapped.startsWith(\"127.\");\n}\n\n/** Strip the port from a `Host` header value (`[::1]:3000` → `[::1]`, `a.b:80` → `a.b`). */\nfunction hostnameOf(hostHeader: string): string {\n const value = hostHeader.trim().toLowerCase();\n\n if (value.startsWith(\"[\")) {\n const end = value.indexOf(\"]\");\n\n return end === -1 ? value : value.slice(0, end + 1);\n }\n\n const colon = value.lastIndexOf(\":\");\n\n return colon === -1 ? value : value.slice(0, colon);\n}\n\n/** SHA-256 digest — equal-length input for `timingSafeEqual`. */\nfunction digest(value: string): Buffer {\n return createHash(\"sha256\").update(value, \"utf8\").digest();\n}\n\n/**\n * Validated, defaulted transport settings plus the precomputed security\n * material (token digests, origin / host allowlists).\n */\ntype ResolvedSettings = {\n host: string;\n port: number;\n path: string;\n loopback: boolean;\n maxBodyBytes: number;\n drainTimeoutMs: number;\n tokenDigests: Buffer[];\n authorize: McpHttpAuth[\"authorize\"];\n authRequired: boolean;\n allowedOrigins: Set<string>;\n allowedHosts: Set<string>;\n};\n\n/** Normalise a configured origin to `scheme://host[:port]`, or throw a config error. */\nfunction normalizeOrigin(origin: string): string {\n try {\n return new URL(origin).origin;\n } catch (cause) {\n throw new McpServeError(`allowedOrigins entry \"${origin}\" is not a valid origin URL.`, {\n type: \"config\",\n cause,\n });\n }\n}\n\n/**\n * Validate and default the transport options. Throws {@link McpServeError}\n * (`\"config\"`) for a non-loopback bind without auth, an empty bearer\n * token, an `auth` with neither check, a bad path / port / limit.\n */\nfunction resolveSettings(transport: McpHttpServeTransport): ResolvedSettings {\n const host = transport.host ?? DEFAULT_HOST;\n const port = transport.port ?? 0;\n const path = transport.path ?? DEFAULT_PATH;\n const loopback = isLoopbackHost(host);\n const auth = transport.auth;\n\n if (!Number.isInteger(port) || port < 0 || port > 65_535) {\n throw new McpServeError(`MCP http port must be an integer in 0–65535 (got ${port}).`, {\n type: \"config\",\n });\n }\n\n if (!path.startsWith(\"/\")) {\n throw new McpServeError(`MCP http path must start with \"/\" (got \"${path}\").`, {\n type: \"config\",\n });\n }\n\n const maxBodyBytes = transport.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n\n if (!Number.isInteger(maxBodyBytes) || maxBodyBytes <= 0) {\n throw new McpServeError(\"MCP http maxBodyBytes must be a positive integer.\", {\n type: \"config\",\n });\n }\n\n const drainTimeoutMs = transport.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;\n\n if (!Number.isFinite(drainTimeoutMs) || drainTimeoutMs < 0) {\n throw new McpServeError(\"MCP http drainTimeoutMs must be zero or a positive number.\", {\n type: \"config\",\n });\n }\n\n const tokens =\n auth?.bearer === undefined\n ? []\n : typeof auth.bearer === \"string\"\n ? [auth.bearer]\n : [...auth.bearer];\n\n if (tokens.some((token) => token.length === 0)) {\n throw new McpServeError(\"MCP http auth.bearer tokens must be non-empty strings.\", {\n type: \"config\",\n });\n }\n\n if (auth && auth.bearer !== undefined && tokens.length === 0) {\n throw new McpServeError(\"MCP http auth.bearer needs at least one token.\", { type: \"config\" });\n }\n\n if (auth && tokens.length === 0 && !auth.authorize) {\n throw new McpServeError(\"MCP http auth needs `bearer` and/or `authorize`.\", {\n type: \"config\",\n });\n }\n\n const authRequired = tokens.length > 0 || auth?.authorize !== undefined;\n\n if (!loopback && !authRequired) {\n throw new McpServeError(\n `Refusing to serve MCP over http on non-loopback host \"${host}\" without auth. ` +\n \"Set transport.auth ({ bearer } or { authorize }), or bind 127.0.0.1.\",\n { type: \"config\", context: { host } },\n );\n }\n\n return {\n host,\n port,\n path,\n loopback,\n maxBodyBytes,\n drainTimeoutMs,\n tokenDigests: tokens.map(digest),\n authorize: auth?.authorize,\n authRequired,\n allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),\n allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name))),\n };\n}\n\n/** Flatten Node's header bag into lower-cased `name → value` strings. */\nfunction flattenHeaders(headers: IncomingHttpHeaders): Record<string, string> {\n const flat: Record<string, string> = {};\n\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined) {\n continue;\n }\n\n flat[name.toLowerCase()] = Array.isArray(value) ? value.join(\", \") : value;\n }\n\n return flat;\n}\n\n/** Write a JSON body with a status code. */\nfunction sendJson(\n response: ServerResponse,\n status: number,\n body: unknown,\n headers: Record<string, string> = {},\n): void {\n if (response.headersSent || response.destroyed) {\n return;\n }\n\n const payload = JSON.stringify(body);\n\n response.writeHead(status, {\n ...headers,\n \"content-type\": \"application/json\",\n \"content-length\": Buffer.byteLength(payload).toString(),\n });\n response.end(payload);\n}\n\n/**\n * Write an HTTP-level refusal: the status plus a JSON-RPC error body with\n * no request id (the spec allows such a body on 4xx transport errors).\n */\nfunction sendRefusal(\n response: ServerResponse,\n status: number,\n code: number,\n message: string,\n headers: Record<string, string> = {},\n): void {\n sendJson(response, status, { jsonrpc: \"2.0\", id: null, error: { code, message } }, headers);\n}\n\n/** Write an empty-bodied status (202 / 405). */\nfunction sendEmpty(\n response: ServerResponse,\n status: number,\n headers: Record<string, string> = {},\n): void {\n if (response.headersSent || response.destroyed) {\n return;\n }\n\n response.writeHead(status, headers);\n response.end();\n}\n\n/**\n * Bind an MCP Streamable HTTP endpoint and pump each JSON-RPC request\n * through `dispatch`. Resolves once the port is bound.\n *\n * Throws {@link McpServeError}: `\"config\"` for invalid / unsafe options\n * (checked before anything binds), `\"listen\"` when binding fails.\n *\n * @param transport - The http transport options.\n * @param dispatch - Maps one JSON-RPC request (plus call context) to its response.\n */\nexport async function startHttpServer(\n transport: McpHttpServeTransport,\n dispatch: McpHttpDispatch,\n): Promise<RunningMcpHttpServer> {\n const settings = resolveSettings(transport);\n const shutdown = new AbortController();\n const inFlight = new Set<Promise<void>>();\n let closing: Promise<void> | undefined;\n\n const server: Server = createServer((request, response) => {\n if (closing) {\n sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, \"Server is shutting down.\", {\n connection: \"close\",\n });\n\n return;\n }\n\n const work = handleRequest(request, response, settings, dispatch, shutdown.signal).catch(() => {\n sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, \"Internal server error.\");\n });\n\n inFlight.add(work);\n void work.finally(() => {\n inFlight.delete(work);\n\n if (closing) {\n server.closeIdleConnections();\n }\n });\n });\n\n await new Promise<void>((resolve, reject) => {\n const onError = (cause: Error) => {\n reject(\n new McpServeError(\n `MCP http server could not listen on ${settings.host}:${settings.port}: ${cause.message}`,\n { type: \"listen\", cause, context: { host: settings.host, port: settings.port } },\n ),\n );\n };\n\n server.once(\"error\", onError);\n server.listen(settings.port, settings.host, () => {\n server.off(\"error\", onError);\n resolve();\n });\n });\n\n const address = server.address();\n const boundPort = typeof address === \"object\" && address !== null ? address.port : settings.port;\n const urlHost = isIPv6(settings.host) ? `[${settings.host}]` : settings.host;\n const url = `http://${urlHost}:${boundPort}${settings.path}`;\n\n const close = (): Promise<void> => {\n if (closing) {\n return closing;\n }\n\n closing = (async () => {\n const closed = new Promise<void>((resolve) => {\n server.close(() => resolve());\n });\n\n server.closeIdleConnections();\n\n if (inFlight.size > 0) {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), settings.drainTimeoutMs);\n });\n\n const outcome = await Promise.race([\n Promise.allSettled([...inFlight]).then(() => \"drained\" as const),\n timedOut,\n ]);\n\n clearTimeout(timer);\n\n if (outcome === \"timeout\") {\n shutdown.abort();\n server.closeAllConnections();\n }\n }\n\n server.closeIdleConnections();\n await closed;\n })();\n\n return closing;\n };\n\n return { url, close };\n}\n\n/**\n * Validate and answer one HTTP request: path → Host / Origin → auth →\n * method → headers → body → JSON-RPC dispatch.\n */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n settings: ResolvedSettings,\n dispatch: McpHttpDispatch,\n signal: AbortSignal,\n): Promise<void> {\n const headers = flattenHeaders(request.headers);\n const method = (request.method ?? \"GET\").toUpperCase();\n const path = new URL(request.url ?? \"/\", \"http://localhost\").pathname;\n\n // DNS-rebinding guards apply to every request on every path.\n if (!hostAllowed(headers.host, settings)) {\n sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, \"Forbidden: Host header not allowed.\");\n\n return;\n }\n\n if (!originAllowed(headers.origin, settings)) {\n sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, \"Forbidden: Origin not allowed.\");\n\n return;\n }\n\n if (path !== settings.path) {\n sendRefusal(response, 404, JSON_RPC_SERVER_ERROR, \"Not found.\");\n\n return;\n }\n\n const info: McpHttpRequestInfo = { method, path, headers };\n\n if (request.socket.remoteAddress !== undefined) {\n info.remoteAddress = request.socket.remoteAddress;\n }\n\n if (!(await authorized(info, settings))) {\n const challenge: Record<string, string> =\n settings.tokenDigests.length > 0 ? { \"www-authenticate\": 'Bearer realm=\"mcp\"' } : {};\n\n sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, \"Unauthorized.\", challenge);\n\n return;\n }\n\n if (method !== \"POST\") {\n // Stateless server: no server-initiated SSE stream (GET) and no\n // session to terminate (DELETE). The spec allows 405 for both.\n sendEmpty(response, 405, { allow: \"POST\" });\n\n return;\n }\n\n const protocolVersion = headers[\"mcp-protocol-version\"];\n\n if (protocolVersion !== undefined && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {\n sendRefusal(\n response,\n 400,\n JSON_RPC_SERVER_ERROR,\n `Unsupported MCP-Protocol-Version \"${protocolVersion}\" (supported: ${SUPPORTED_PROTOCOL_VERSIONS.join(\", \")}).`,\n );\n\n return;\n }\n\n if (!isJsonContentType(headers[\"content-type\"])) {\n sendRefusal(response, 415, JSON_RPC_SERVER_ERROR, \"Content-Type must be application/json.\");\n\n return;\n }\n\n if (!acceptsJson(headers.accept)) {\n sendRefusal(response, 406, JSON_RPC_SERVER_ERROR, \"Accept must allow application/json.\");\n\n return;\n }\n\n const body = await readBody(request, settings.maxBodyBytes);\n\n if (body.type === \"too-large\") {\n sendRefusal(\n response,\n 413,\n JSON_RPC_SERVER_ERROR,\n `Request body exceeds ${settings.maxBodyBytes} bytes.`,\n { connection: \"close\" },\n );\n\n return;\n }\n\n let message: unknown;\n\n try {\n message = JSON.parse(body.text);\n } catch {\n sendRefusal(response, 400, JSON_RPC_PARSE_ERROR, \"Parse error: body is not valid JSON.\");\n\n return;\n }\n\n if (typeof message !== \"object\" || message === null || Array.isArray(message)) {\n sendRefusal(\n response,\n 400,\n JSON_RPC_INVALID_REQUEST,\n \"Invalid request: the body must be one JSON-RPC message (batches are not supported).\",\n );\n\n return;\n }\n\n const record = message as Record<string, unknown>;\n\n if (record.jsonrpc !== \"2.0\") {\n sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST, 'Invalid request: jsonrpc must be \"2.0\".');\n\n return;\n }\n\n const id = record.id;\n\n if (typeof record.method === \"string\") {\n if (typeof id !== \"string\" && typeof id !== \"number\") {\n // A notification (notifications/initialized, notifications/cancelled…).\n sendEmpty(response, 202);\n\n return;\n }\n\n const rpcRequest: JsonRpcRequest = { jsonrpc: \"2.0\", id, method: record.method };\n\n if (record.params !== undefined) {\n rpcRequest.params = record.params;\n }\n\n const rpcResponse = await dispatch(rpcRequest, { request: info, signal });\n\n sendJson(response, 200, rpcResponse);\n\n return;\n }\n\n if (\n (typeof id === \"string\" || typeof id === \"number\") &&\n (\"result\" in record || \"error\" in record)\n ) {\n // A response to a server-initiated request; this server sends none, so acknowledge.\n sendEmpty(response, 202);\n\n return;\n }\n\n sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST, \"Invalid request: not a JSON-RPC message.\");\n}\n\n/**\n * Host-header check. While bound to loopback only loopback names (plus\n * `allowedHosts`) are accepted, so a DNS-rebound name pointing at\n * 127.0.0.1 is refused. Off loopback the check applies only when\n * `allowedHosts` is set.\n */\nfunction hostAllowed(hostHeader: string | undefined, settings: ResolvedSettings): boolean {\n if (!settings.loopback && settings.allowedHosts.size === 0) {\n return true;\n }\n\n if (hostHeader === undefined) {\n return false;\n }\n\n const name = hostnameOf(hostHeader);\n\n if (settings.allowedHosts.has(name)) {\n return true;\n }\n\n return settings.loopback && isLoopbackHost(name);\n}\n\n/**\n * Origin check (MCP Streamable HTTP security warning). No `Origin` header —\n * a non-browser client — passes; otherwise the origin must be a loopback\n * `http(s)` origin or listed in `allowedOrigins`. `null` and unparsable\n * origins are refused.\n */\nfunction originAllowed(origin: string | undefined, settings: ResolvedSettings): boolean {\n if (origin === undefined) {\n return true;\n }\n\n let parsed: URL;\n\n try {\n parsed = new URL(origin);\n } catch {\n return false;\n }\n\n if (settings.allowedOrigins.has(parsed.origin)) {\n return true;\n }\n\n return (\n (parsed.protocol === \"http:\" || parsed.protocol === \"https:\") && isLoopbackHost(parsed.hostname)\n );\n}\n\n/**\n * Run the configured auth. Bearer tokens are compared in constant time\n * over SHA-256 digests (equal-length buffers for `timingSafeEqual`), and\n * every configured token is compared so the match position does not leak.\n * An `authorize` hook that throws denies the request.\n */\nasync function authorized(info: McpHttpRequestInfo, settings: ResolvedSettings): Promise<boolean> {\n if (!settings.authRequired) {\n return true;\n }\n\n if (settings.tokenDigests.length > 0) {\n const match = /^Bearer[ \\t]+(.+)$/i.exec(info.headers.authorization ?? \"\");\n const presented = digest(match?.[1]?.trim() ?? \"\");\n let matched = false;\n\n for (const expected of settings.tokenDigests) {\n matched = timingSafeEqual(presented, expected) || matched;\n }\n\n if (!match || !matched) {\n return false;\n }\n }\n\n if (settings.authorize) {\n try {\n return (await settings.authorize(info)) === true;\n } catch {\n return false;\n }\n }\n\n return true;\n}\n\n/** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */\nfunction isJsonContentType(contentType: string | undefined): boolean {\n return contentType?.split(\";\")[0]?.trim().toLowerCase() === \"application/json\";\n}\n\n/**\n * True when the `Accept` header allows a JSON reply. Clients are required\n * to list both `application/json` and `text/event-stream`; this server\n * always answers JSON, so it only insists on that half. A missing header\n * is accepted.\n */\nfunction acceptsJson(accept: string | undefined): boolean {\n if (accept === undefined || accept.trim() === \"\") {\n return true;\n }\n\n return accept\n .split(\",\")\n .map((part) => part.split(\";\")[0]?.trim().toLowerCase())\n .some((type) => type === \"application/json\" || type === \"application/*\" || type === \"*/*\");\n}\n\n/** The outcome of reading a request body. */\ntype BodyResult = { type: \"ok\"; text: string } | { type: \"too-large\" };\n\n/**\n * Read the request body as UTF-8, refusing anything over `limit` bytes —\n * up front from `Content-Length`, and while streaming for chunked bodies.\n * A refused body is left unread; the 413 response closes the connection.\n */\nfunction readBody(request: IncomingMessage, limit: number): Promise<BodyResult> {\n const declared = Number(request.headers[\"content-length\"]);\n\n if (Number.isFinite(declared) && declared > limit) {\n return Promise.resolve({ type: \"too-large\" });\n }\n\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n let done = false;\n\n const finish = (result: BodyResult) => {\n if (!done) {\n done = true;\n resolve(result);\n }\n };\n\n request.on(\"data\", (chunk: Buffer) => {\n if (done) {\n return;\n }\n\n size += chunk.length;\n\n if (size > limit) {\n request.pause();\n finish({ type: \"too-large\" });\n\n return;\n }\n\n chunks.push(chunk);\n });\n request.on(\"end\", () => finish({ type: \"ok\", text: Buffer.concat(chunks).toString(\"utf8\") }));\n request.on(\"error\", (error) => {\n if (!done) {\n done = true;\n reject(error);\n }\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,eAAe;;AAGrB,MAAM,eAAe;;AAGrB,MAAM,yBAAyB;;AAG/B,MAAM,2BAA2B;;AAGjC,MAAM,uBAAuB;AAC7B,MAAM,2BAA2B;;AAGjC,MAAM,wBAAwB;;AAG9B,MAAM,iBAAiB,IAAI,IAAI;CAAC;CAAa;CAAa;CAAO;AAAO,CAAC;;;;;AAoBzE,SAAgB,eAAe,MAAuB;CACpD,MAAM,aAAa,KAAK,KAAK,CAAC,CAAC,YAAY;CAE3C,IAAI,eAAe,IAAI,UAAU,GAC/B,OAAO;CAGT,MAAM,cAAc,WAAW,QAAQ,YAAY,EAAE;CACrD,MAAM,SAAS,YAAY,WAAW,SAAS,IAC3C,YAAY,MAAM,CAAgB,IAClC;CAEJ,OAAO,OAAO,MAAM,KAAK,OAAO,WAAW,MAAM;AACnD;;AAGA,SAAS,WAAW,YAA4B;CAC9C,MAAM,QAAQ,WAAW,KAAK,CAAC,CAAC,YAAY;CAE5C,IAAI,MAAM,WAAW,GAAG,GAAG;EACzB,MAAM,MAAM,MAAM,QAAQ,GAAG;EAE7B,OAAO,QAAQ,KAAK,QAAQ,MAAM,MAAM,GAAG,MAAM,CAAC;CACpD;CAEA,MAAM,QAAQ,MAAM,YAAY,GAAG;CAEnC,OAAO,UAAU,KAAK,QAAQ,MAAM,MAAM,GAAG,KAAK;AACpD;;AAGA,SAAS,OAAO,OAAuB;CACrC,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,OAAO,MAAM,CAAC,CAAC,OAAO;AAC3D;;AAqBA,SAAS,gBAAgB,QAAwB;CAC/C,IAAI;EACF,OAAO,IAAI,IAAI,MAAM,CAAC,CAAC;CACzB,SAAS,OAAO;EACd,MAAM,IAAI,cAAc,yBAAyB,OAAO,+BAA+B;GACrF,MAAM;GACN;EACF,CAAC;CACH;AACF;;;;;;AAOA,SAAS,gBAAgB,WAAoD;CAC3E,MAAM,OAAO,UAAU,QAAQ;CAC/B,MAAM,OAAO,UAAU,QAAQ;CAC/B,MAAM,OAAO,UAAU,QAAQ;CAC/B,MAAM,WAAW,eAAe,IAAI;CACpC,MAAM,OAAO,UAAU;CAEvB,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,OAChD,MAAM,IAAI,cAAc,oDAAoD,KAAK,KAAK,EACpF,MAAM,SACR,CAAC;CAGH,IAAI,CAAC,KAAK,WAAW,GAAG,GACtB,MAAM,IAAI,cAAc,2CAA2C,KAAK,MAAM,EAC5E,MAAM,SACR,CAAC;CAGH,MAAM,eAAe,UAAU,gBAAgB;CAE/C,IAAI,CAAC,OAAO,UAAU,YAAY,KAAK,gBAAgB,GACrD,MAAM,IAAI,cAAc,qDAAqD,EAC3E,MAAM,SACR,CAAC;CAGH,MAAM,iBAAiB,UAAU,kBAAkB;CAEnD,IAAI,CAAC,OAAO,SAAS,cAAc,KAAK,iBAAiB,GACvD,MAAM,IAAI,cAAc,8DAA8D,EACpF,MAAM,SACR,CAAC;CAGH,MAAM,SACJ,MAAM,WAAW,SACb,CAAC,IACD,OAAO,KAAK,WAAW,WACrB,CAAC,KAAK,MAAM,IACZ,CAAC,GAAG,KAAK,MAAM;CAEvB,IAAI,OAAO,MAAM,UAAU,MAAM,WAAW,CAAC,GAC3C,MAAM,IAAI,cAAc,0DAA0D,EAChF,MAAM,SACR,CAAC;CAGH,IAAI,QAAQ,KAAK,WAAW,UAAa,OAAO,WAAW,GACzD,MAAM,IAAI,cAAc,kDAAkD,EAAE,MAAM,SAAS,CAAC;CAG9F,IAAI,QAAQ,OAAO,WAAW,KAAK,CAAC,KAAK,WACvC,MAAM,IAAI,cAAc,oDAAoD,EAC1E,MAAM,SACR,CAAC;CAGH,MAAM,eAAe,OAAO,SAAS,KAAK,MAAM,cAAc;CAE9D,IAAI,CAAC,YAAY,CAAC,cAChB,MAAM,IAAI,cACR,yDAAyD,KAAK,uFAE9D;EAAE,MAAM;EAAU,SAAS,EAAE,KAAK;CAAE,CACtC;CAGF,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA,cAAc,OAAO,IAAI,MAAM;EAC/B,WAAW,MAAM;EACjB;EACA,gBAAgB,IAAI,KAAK,UAAU,kBAAkB,CAAC,EAAC,CAAE,IAAI,eAAe,CAAC;EAC7E,cAAc,IAAI,KAAK,UAAU,gBAAgB,CAAC,EAAC,CAAE,KAAK,SAAS,WAAW,IAAI,CAAC,CAAC;CACtF;AACF;;AAGA,SAAS,eAAe,SAAsD;CAC5E,MAAM,OAA+B,CAAC;CAEtC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAAG;EACnD,IAAI,UAAU,QACZ;EAGF,KAAK,KAAK,YAAY,KAAK,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI;CACvE;CAEA,OAAO;AACT;;AAGA,SAAS,SACP,UACA,QACA,MACA,UAAkC,CAAC,GAC7B;CACN,IAAI,SAAS,eAAe,SAAS,WACnC;CAGF,MAAM,UAAU,KAAK,UAAU,IAAI;CAEnC,SAAS,UAAU,QAAQ;EACzB,GAAG;EACH,gBAAgB;EAChB,kBAAkB,OAAO,WAAW,OAAO,CAAC,CAAC,SAAS;CACxD,CAAC;CACD,SAAS,IAAI,OAAO;AACtB;;;;;AAMA,SAAS,YACP,UACA,QACA,MACA,SACA,UAAkC,CAAC,GAC7B;CACN,SAAS,UAAU,QAAQ;EAAE,SAAS;EAAO,IAAI;EAAM,OAAO;GAAE;GAAM;EAAQ;CAAE,GAAG,OAAO;AAC5F;;AAGA,SAAS,UACP,UACA,QACA,UAAkC,CAAC,GAC7B;CACN,IAAI,SAAS,eAAe,SAAS,WACnC;CAGF,SAAS,UAAU,QAAQ,OAAO;CAClC,SAAS,IAAI;AACf;;;;;;;;;;;AAYA,eAAsB,gBACpB,WACA,UAC+B;CAC/B,MAAM,WAAW,gBAAgB,SAAS;CAC1C,MAAM,WAAW,IAAI,gBAAgB;CACrC,MAAM,2BAAW,IAAI,IAAmB;CACxC,IAAI;CAEJ,MAAM,SAAiB,cAAc,SAAS,aAAa;EACzD,IAAI,SAAS;GACX,YAAY,UAAU,KAAK,uBAAuB,4BAA4B,EAC5E,YAAY,QACd,CAAC;GAED;EACF;EAEA,MAAM,OAAO,cAAc,SAAS,UAAU,UAAU,UAAU,SAAS,MAAM,CAAC,CAAC,YAAY;GAC7F,YAAY,UAAU,KAAK,uBAAuB,wBAAwB;EAC5E,CAAC;EAED,SAAS,IAAI,IAAI;EACjB,AAAK,KAAK,cAAc;GACtB,SAAS,OAAO,IAAI;GAEpB,IAAI,SACF,OAAO,qBAAqB;EAEhC,CAAC;CACH,CAAC;CAED,MAAM,IAAI,SAAe,SAAS,WAAW;EAC3C,MAAM,WAAW,UAAiB;GAChC,OACE,IAAI,cACF,uCAAuC,SAAS,KAAK,GAAG,SAAS,KAAK,IAAI,MAAM,WAChF;IAAE,MAAM;IAAU;IAAO,SAAS;KAAE,MAAM,SAAS;KAAM,MAAM,SAAS;IAAK;GAAE,CACjF,CACF;EACF;EAEA,OAAO,KAAK,SAAS,OAAO;EAC5B,OAAO,OAAO,SAAS,MAAM,SAAS,YAAY;GAChD,OAAO,IAAI,SAAS,OAAO;GAC3B,QAAQ;EACV,CAAC;CACH,CAAC;CAED,MAAM,UAAU,OAAO,QAAQ;CAC/B,MAAM,YAAY,OAAO,YAAY,YAAY,YAAY,OAAO,QAAQ,OAAO,SAAS;CAE5F,MAAM,MAAM,UADI,OAAO,SAAS,IAAI,IAAI,IAAI,SAAS,KAAK,KAAK,SAAS,KAC1C,GAAG,YAAY,SAAS;CAEtD,MAAM,cAA6B;EACjC,IAAI,SACF,OAAO;EAGT,WAAW,YAAY;GACrB,MAAM,SAAS,IAAI,SAAe,YAAY;IAC5C,OAAO,YAAY,QAAQ,CAAC;GAC9B,CAAC;GAED,OAAO,qBAAqB;GAE5B,IAAI,SAAS,OAAO,GAAG;IACrB,IAAI;IACJ,MAAM,WAAW,IAAI,SAAoB,YAAY;KACnD,QAAQ,iBAAiB,QAAQ,SAAS,GAAG,SAAS,cAAc;IACtE,CAAC;IAED,MAAM,UAAU,MAAM,QAAQ,KAAK,CACjC,QAAQ,WAAW,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,WAAW,SAAkB,GAC/D,QACF,CAAC;IAED,aAAa,KAAK;IAElB,IAAI,YAAY,WAAW;KACzB,SAAS,MAAM;KACf,OAAO,oBAAoB;IAC7B;GACF;GAEA,OAAO,qBAAqB;GAC5B,MAAM;EACR,EAAC,CAAE;EAEH,OAAO;CACT;CAEA,OAAO;EAAE;EAAK;CAAM;AACtB;;;;;AAMA,eAAe,cACb,SACA,UACA,UACA,UACA,QACe;CACf,MAAM,UAAU,eAAe,QAAQ,OAAO;CAC9C,MAAM,UAAU,QAAQ,UAAU,MAAK,CAAE,YAAY;CACrD,MAAM,OAAO,IAAI,IAAI,QAAQ,OAAO,KAAK,kBAAkB,CAAC,CAAC;CAG7D,IAAI,CAAC,YAAY,QAAQ,MAAM,QAAQ,GAAG;EACxC,YAAY,UAAU,KAAK,uBAAuB,qCAAqC;EAEvF;CACF;CAEA,IAAI,CAAC,cAAc,QAAQ,QAAQ,QAAQ,GAAG;EAC5C,YAAY,UAAU,KAAK,uBAAuB,gCAAgC;EAElF;CACF;CAEA,IAAI,SAAS,SAAS,MAAM;EAC1B,YAAY,UAAU,KAAK,uBAAuB,YAAY;EAE9D;CACF;CAEA,MAAM,OAA2B;EAAE;EAAQ;EAAM;CAAQ;CAEzD,IAAI,QAAQ,OAAO,kBAAkB,QACnC,KAAK,gBAAgB,QAAQ,OAAO;CAGtC,IAAI,CAAE,MAAM,WAAW,MAAM,QAAQ,GAAI;EAIvC,YAAY,UAAU,KAAK,uBAAuB,iBAFhD,SAAS,aAAa,SAAS,IAAI,EAAE,oBAAoB,uBAAqB,IAAI,CAAC,CAET;EAE5E;CACF;CAEA,IAAI,WAAW,QAAQ;EAGrB,UAAU,UAAU,KAAK,EAAE,OAAO,OAAO,CAAC;EAE1C;CACF;CAEA,MAAM,kBAAkB,QAAQ;CAEhC,IAAI,oBAAoB,UAAa,CAAC,4BAA4B,SAAS,eAAe,GAAG;EAC3F,YACE,UACA,KACA,uBACA,qCAAqC,gBAAgB,gBAAgB,4BAA4B,KAAK,IAAI,EAAE,GAC9G;EAEA;CACF;CAEA,IAAI,CAAC,kBAAkB,QAAQ,eAAe,GAAG;EAC/C,YAAY,UAAU,KAAK,uBAAuB,wCAAwC;EAE1F;CACF;CAEA,IAAI,CAAC,YAAY,QAAQ,MAAM,GAAG;EAChC,YAAY,UAAU,KAAK,uBAAuB,qCAAqC;EAEvF;CACF;CAEA,MAAM,OAAO,MAAM,SAAS,SAAS,SAAS,YAAY;CAE1D,IAAI,KAAK,SAAS,aAAa;EAC7B,YACE,UACA,KACA,uBACA,wBAAwB,SAAS,aAAa,UAC9C,EAAE,YAAY,QAAQ,CACxB;EAEA;CACF;CAEA,IAAI;CAEJ,IAAI;EACF,UAAU,KAAK,MAAM,KAAK,IAAI;CAChC,QAAQ;EACN,YAAY,UAAU,KAAK,sBAAsB,sCAAsC;EAEvF;CACF;CAEA,IAAI,OAAO,YAAY,YAAY,YAAY,QAAQ,MAAM,QAAQ,OAAO,GAAG;EAC7E,YACE,UACA,KACA,0BACA,qFACF;EAEA;CACF;CAEA,MAAM,SAAS;CAEf,IAAI,OAAO,YAAY,OAAO;EAC5B,YAAY,UAAU,KAAK,0BAA0B,2CAAyC;EAE9F;CACF;CAEA,MAAM,KAAK,OAAO;CAElB,IAAI,OAAO,OAAO,WAAW,UAAU;EACrC,IAAI,OAAO,OAAO,YAAY,OAAO,OAAO,UAAU;GAEpD,UAAU,UAAU,GAAG;GAEvB;EACF;EAEA,MAAM,aAA6B;GAAE,SAAS;GAAO;GAAI,QAAQ,OAAO;EAAO;EAE/E,IAAI,OAAO,WAAW,QACpB,WAAW,SAAS,OAAO;EAK7B,SAAS,UAAU,KAAK,MAFE,SAAS,YAAY;GAAE,SAAS;GAAM;EAAO,CAAC,CAErC;EAEnC;CACF;CAEA,KACG,OAAO,OAAO,YAAY,OAAO,OAAO,cACxC,YAAY,UAAU,WAAW,SAClC;EAEA,UAAU,UAAU,GAAG;EAEvB;CACF;CAEA,YAAY,UAAU,KAAK,0BAA0B,0CAA0C;AACjG;;;;;;;AAQA,SAAS,YAAY,YAAgC,UAAqC;CACxF,IAAI,CAAC,SAAS,YAAY,SAAS,aAAa,SAAS,GACvD,OAAO;CAGT,IAAI,eAAe,QACjB,OAAO;CAGT,MAAM,OAAO,WAAW,UAAU;CAElC,IAAI,SAAS,aAAa,IAAI,IAAI,GAChC,OAAO;CAGT,OAAO,SAAS,YAAY,eAAe,IAAI;AACjD;;;;;;;AAQA,SAAS,cAAc,QAA4B,UAAqC;CACtF,IAAI,WAAW,QACb,OAAO;CAGT,IAAI;CAEJ,IAAI;EACF,SAAS,IAAI,IAAI,MAAM;CACzB,QAAQ;EACN,OAAO;CACT;CAEA,IAAI,SAAS,eAAe,IAAI,OAAO,MAAM,GAC3C,OAAO;CAGT,QACG,OAAO,aAAa,WAAW,OAAO,aAAa,aAAa,eAAe,OAAO,QAAQ;AAEnG;;;;;;;AAQA,eAAe,WAAW,MAA0B,UAA8C;CAChG,IAAI,CAAC,SAAS,cACZ,OAAO;CAGT,IAAI,SAAS,aAAa,SAAS,GAAG;EACpC,MAAM,QAAQ,sBAAsB,KAAK,KAAK,QAAQ,iBAAiB,EAAE;EACzE,MAAM,YAAY,OAAO,QAAQ,EAAE,EAAE,KAAK,KAAK,EAAE;EACjD,IAAI,UAAU;EAEd,KAAK,MAAM,YAAY,SAAS,cAC9B,UAAU,gBAAgB,WAAW,QAAQ,KAAK;EAGpD,IAAI,CAAC,SAAS,CAAC,SACb,OAAO;CAEX;CAEA,IAAI,SAAS,WACX,IAAI;EACF,OAAQ,MAAM,SAAS,UAAU,IAAI,MAAO;CAC9C,QAAQ;EACN,OAAO;CACT;CAGF,OAAO;AACT;;AAGA,SAAS,kBAAkB,aAA0C;CACnE,OAAO,aAAa,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,YAAY,MAAM;AAC9D;;;;;;;AAQA,SAAS,YAAY,QAAqC;CACxD,IAAI,WAAW,UAAa,OAAO,KAAK,MAAM,IAC5C,OAAO;CAGT,OAAO,OACJ,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CACvD,MAAM,SAAS,SAAS,sBAAsB,SAAS,mBAAmB,SAAS,KAAK;AAC7F;;;;;;AAUA,SAAS,SAAS,SAA0B,OAAoC;CAC9E,MAAM,WAAW,OAAO,QAAQ,QAAQ,iBAAiB;CAEzD,IAAI,OAAO,SAAS,QAAQ,KAAK,WAAW,OAC1C,OAAO,QAAQ,QAAQ,EAAE,MAAM,YAAY,CAAC;CAG9C,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAmB,CAAC;EAC1B,IAAI,OAAO;EACX,IAAI,OAAO;EAEX,MAAM,UAAU,WAAuB;GACrC,IAAI,CAAC,MAAM;IACT,OAAO;IACP,QAAQ,MAAM;GAChB;EACF;EAEA,QAAQ,GAAG,SAAS,UAAkB;GACpC,IAAI,MACF;GAGF,QAAQ,MAAM;GAEd,IAAI,OAAO,OAAO;IAChB,QAAQ,MAAM;IACd,OAAO,EAAE,MAAM,YAAY,CAAC;IAE5B;GACF;GAEA,OAAO,KAAK,KAAK;EACnB,CAAC;EACD,QAAQ,GAAG,aAAa,OAAO;GAAE,MAAM;GAAM,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM;EAAE,CAAC,CAAC;EAC5F,QAAQ,GAAG,UAAU,UAAU;GAC7B,IAAI,CAAC,MAAM;IACT,OAAO;IACP,OAAO,KAAK;GACd;EACF,CAAC;CACH,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"serve-http.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/serve-http.ts"],"sourcesContent":["import { createHash, timingSafeEqual } from \"node:crypto\";\nimport {\n createServer,\n type IncomingHttpHeaders,\n type IncomingMessage,\n type Server,\n type ServerResponse,\n} from \"node:http\";\nimport { isIPv4, isIPv6 } from \"node:net\";\nimport type {\n JsonRpcRequest,\n JsonRpcResponse,\n McpHttpAuth,\n McpHttpRequestInfo,\n McpHttpServeTransport,\n McpServeCallContext,\n} from \"../contracts\";\nimport { McpServeError } from \"../errors\";\nimport { SUPPORTED_PROTOCOL_VERSIONS } from \"./protocol\";\nimport { McpServeRequests } from \"./serve-requests\";\n\n/**\n * MCP Streamable HTTP server transport for `ai.mcp.serve`.\n *\n * Implements the server side of the Streamable HTTP transport from the MCP\n * specification (https://modelcontextprotocol.io/specification/2025-11-25/basic/transports,\n * checked 2026-10-17) on `node:http`, with no SDK dependency:\n *\n * - one endpoint path; each POST carries exactly one JSON-RPC message\n * (batches were removed from the protocol in 2025-06-18 and get 400);\n * - a request is answered with `Content-Type: application/json` (the spec\n * lets a server pick JSON or SSE per request); a notification or a\n * response is acknowledged with `202 Accepted` and no body;\n * - stateless: no `MCP-Session-Id` is issued (optional in the spec), so\n * GET (server-initiated SSE stream) and DELETE (session end) answer\n * `405 Method Not Allowed`, as the spec allows;\n * - an unsupported `MCP-Protocol-Version` header gets 400;\n * - security warning items: `Origin` is validated on every request (an\n * invalid one gets 403), the default bind is `127.0.0.1`, and\n * authentication is required for any non-loopback bind.\n */\n\n/** Default bind interface — loopback only. */\nconst DEFAULT_HOST = \"127.0.0.1\";\n\n/** Default endpoint path. */\nconst DEFAULT_PATH = \"/mcp\";\n\n/** Default request body cap: 1 MiB. */\nconst DEFAULT_MAX_BODY_BYTES = 1_048_576;\n\n/** Default time `close()` waits for in-flight requests. */\nconst DEFAULT_DRAIN_TIMEOUT_MS = 5_000;\n\n/** JSON-RPC codes used for transport-level error bodies. */\nconst JSON_RPC_PARSE_ERROR = -32700;\nconst JSON_RPC_INVALID_REQUEST = -32600;\n\n/** Generic server-defined JSON-RPC code for HTTP-level refusals (auth, origin, size). */\nconst JSON_RPC_SERVER_ERROR = -32000;\n\n/** Host names that always mean \"this machine\". */\nconst LOOPBACK_NAMES = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\"]);\n\n/** The function the transport pumps each JSON-RPC request through. */\nexport type McpHttpDispatch = (\n request: JsonRpcRequest,\n context: McpServeCallContext,\n) => Promise<JsonRpcResponse>;\n\n/** A bound HTTP endpoint. */\nexport type RunningMcpHttpServer = {\n /** Full endpoint URL. */\n url: string;\n /** Drain in-flight requests (up to the drain timeout) and free the port. Idempotent. */\n close(): Promise<void>;\n};\n\n/**\n * True when `host` names a loopback interface (`localhost`, `127.0.0.0/8`,\n * `::1`, or an IPv4-mapped `::ffff:127.x.x.x`).\n */\nexport function isLoopbackHost(host: string): boolean {\n const normalized = host.trim().toLowerCase();\n\n if (LOOPBACK_NAMES.has(normalized)) {\n return true;\n }\n\n const unbracketed = normalized.replace(/^\\[|\\]$/g, \"\");\n const mapped = unbracketed.startsWith(\"::ffff:\")\n ? unbracketed.slice(\"::ffff:\".length)\n : unbracketed;\n\n return isIPv4(mapped) && mapped.startsWith(\"127.\");\n}\n\n/** Strip the port from a `Host` header value (`[::1]:3000` → `[::1]`, `a.b:80` → `a.b`). */\nfunction hostnameOf(hostHeader: string): string {\n const value = hostHeader.trim().toLowerCase();\n\n if (value.startsWith(\"[\")) {\n const end = value.indexOf(\"]\");\n\n return end === -1 ? value : value.slice(0, end + 1);\n }\n\n const colon = value.lastIndexOf(\":\");\n\n return colon === -1 ? value : value.slice(0, colon);\n}\n\n/** SHA-256 digest — equal-length input for `timingSafeEqual`. */\nfunction digest(value: string): Buffer {\n return createHash(\"sha256\").update(value, \"utf8\").digest();\n}\n\n/**\n * Validated, defaulted transport settings plus the precomputed security\n * material (token digests, origin / host allowlists).\n */\ntype ResolvedSettings = {\n host: string;\n port: number;\n path: string;\n loopback: boolean;\n maxBodyBytes: number;\n drainTimeoutMs: number;\n tokenDigests: Buffer[];\n authorize: McpHttpAuth[\"authorize\"];\n authenticate: McpHttpAuth[\"authenticate\"];\n authRequired: boolean;\n allowedOrigins: Set<string>;\n allowedHosts: Set<string>;\n};\n\n/** Normalise a configured origin to `scheme://host[:port]`, or throw a config error. */\nfunction normalizeOrigin(origin: string): string {\n try {\n return new URL(origin).origin;\n } catch (cause) {\n throw new McpServeError(`allowedOrigins entry \"${origin}\" is not a valid origin URL.`, {\n type: \"config\",\n cause,\n });\n }\n}\n\n/**\n * Validate and default the transport options. Throws {@link McpServeError}\n * (`\"config\"`) for a non-loopback bind without auth, an empty bearer\n * token, an `auth` with neither check, a bad path / port / limit.\n */\nfunction resolveSettings(transport: McpHttpServeTransport): ResolvedSettings {\n const host = transport.host ?? DEFAULT_HOST;\n const port = transport.port ?? 0;\n const path = transport.path ?? DEFAULT_PATH;\n const loopback = isLoopbackHost(host);\n const auth = transport.auth;\n\n if (!Number.isInteger(port) || port < 0 || port > 65_535) {\n throw new McpServeError(`MCP http port must be an integer in 0–65535 (got ${port}).`, {\n type: \"config\",\n });\n }\n\n if (!path.startsWith(\"/\")) {\n throw new McpServeError(`MCP http path must start with \"/\" (got \"${path}\").`, {\n type: \"config\",\n });\n }\n\n const maxBodyBytes = transport.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n\n if (!Number.isInteger(maxBodyBytes) || maxBodyBytes <= 0) {\n throw new McpServeError(\"MCP http maxBodyBytes must be a positive integer.\", {\n type: \"config\",\n });\n }\n\n const drainTimeoutMs = transport.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;\n\n if (!Number.isFinite(drainTimeoutMs) || drainTimeoutMs < 0) {\n throw new McpServeError(\"MCP http drainTimeoutMs must be zero or a positive number.\", {\n type: \"config\",\n });\n }\n\n const tokens =\n auth?.bearer === undefined\n ? []\n : typeof auth.bearer === \"string\"\n ? [auth.bearer]\n : [...auth.bearer];\n\n if (tokens.some((token) => token.length === 0)) {\n throw new McpServeError(\"MCP http auth.bearer tokens must be non-empty strings.\", {\n type: \"config\",\n });\n }\n\n if (auth && auth.bearer !== undefined && tokens.length === 0) {\n throw new McpServeError(\"MCP http auth.bearer needs at least one token.\", { type: \"config\" });\n }\n\n if (auth && tokens.length === 0 && !auth.authorize && !auth.authenticate) {\n throw new McpServeError(\"MCP http auth needs `bearer`, `authorize` and/or `authenticate`.\", {\n type: \"config\",\n });\n }\n\n const authRequired =\n tokens.length > 0 || auth?.authorize !== undefined || auth?.authenticate !== undefined;\n\n if (!loopback && !authRequired) {\n throw new McpServeError(\n `Refusing to serve MCP over http on non-loopback host \"${host}\" without auth. ` +\n \"Set transport.auth ({ bearer }, { authorize } or { authenticate }), or bind 127.0.0.1.\",\n { type: \"config\", context: { host } },\n );\n }\n\n return {\n host,\n port,\n path,\n loopback,\n maxBodyBytes,\n drainTimeoutMs,\n tokenDigests: tokens.map(digest),\n authorize: auth?.authorize,\n authenticate: auth?.authenticate,\n authRequired,\n allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),\n allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name))),\n };\n}\n\n/** Flatten Node's header bag into lower-cased `name → value` strings. */\nfunction flattenHeaders(headers: IncomingHttpHeaders): Record<string, string> {\n const flat: Record<string, string> = {};\n\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined) {\n continue;\n }\n\n flat[name.toLowerCase()] = Array.isArray(value) ? value.join(\", \") : value;\n }\n\n return flat;\n}\n\n/** Write a JSON body with a status code. */\nfunction sendJson(\n response: ServerResponse,\n status: number,\n body: unknown,\n headers: Record<string, string> = {},\n): void {\n if (response.headersSent || response.destroyed) {\n return;\n }\n\n const payload = JSON.stringify(body);\n\n response.writeHead(status, {\n ...headers,\n \"content-type\": \"application/json\",\n \"content-length\": Buffer.byteLength(payload).toString(),\n });\n response.end(payload);\n}\n\n/**\n * Write an HTTP-level refusal: the status plus a JSON-RPC error body with\n * no request id (the spec allows such a body on 4xx transport errors).\n */\nfunction sendRefusal(\n response: ServerResponse,\n status: number,\n code: number,\n message: string,\n headers: Record<string, string> = {},\n): void {\n sendJson(response, status, { jsonrpc: \"2.0\", id: null, error: { code, message } }, headers);\n}\n\n/** Write an empty-bodied status (202 / 405). */\nfunction sendEmpty(\n response: ServerResponse,\n status: number,\n headers: Record<string, string> = {},\n): void {\n if (response.headersSent || response.destroyed) {\n return;\n }\n\n response.writeHead(status, headers);\n response.end();\n}\n\n/**\n * Bind an MCP Streamable HTTP endpoint and pump each JSON-RPC request\n * through `dispatch`. Resolves once the port is bound.\n *\n * Throws {@link McpServeError}: `\"config\"` for invalid / unsafe options\n * (checked before anything binds), `\"listen\"` when binding fails.\n *\n * @param transport - The http transport options.\n * @param dispatch - Maps one JSON-RPC request (plus call context) to its response.\n */\nexport async function startHttpServer(\n transport: McpHttpServeTransport,\n dispatch: McpHttpDispatch,\n): Promise<RunningMcpHttpServer> {\n const settings = resolveSettings(transport);\n const shutdown = new AbortController();\n const requests = new McpServeRequests();\n const inFlight = new Set<Promise<void>>();\n let closing: Promise<void> | undefined;\n\n const server: Server = createServer((request, response) => {\n if (closing) {\n sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, \"Server is shutting down.\", {\n connection: \"close\",\n });\n\n return;\n }\n\n const work = handleRequest(\n request,\n response,\n settings,\n dispatch,\n shutdown.signal,\n requests,\n ).catch(() => {\n sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, \"Internal server error.\");\n });\n\n inFlight.add(work);\n void work.finally(() => {\n inFlight.delete(work);\n\n if (closing) {\n server.closeIdleConnections();\n }\n });\n });\n\n await new Promise<void>((resolve, reject) => {\n const onError = (cause: Error) => {\n reject(\n new McpServeError(\n `MCP http server could not listen on ${settings.host}:${settings.port}: ${cause.message}`,\n { type: \"listen\", cause, context: { host: settings.host, port: settings.port } },\n ),\n );\n };\n\n server.once(\"error\", onError);\n server.listen(settings.port, settings.host, () => {\n server.off(\"error\", onError);\n resolve();\n });\n });\n\n const address = server.address();\n const boundPort = typeof address === \"object\" && address !== null ? address.port : settings.port;\n const urlHost = isIPv6(settings.host) ? `[${settings.host}]` : settings.host;\n const url = `http://${urlHost}:${boundPort}${settings.path}`;\n\n const close = (): Promise<void> => {\n if (closing) {\n return closing;\n }\n\n closing = (async () => {\n const closed = new Promise<void>((resolve) => {\n server.close(() => resolve());\n });\n\n server.closeIdleConnections();\n\n if (inFlight.size > 0) {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), settings.drainTimeoutMs);\n });\n\n const outcome = await Promise.race([\n Promise.allSettled([...inFlight]).then(() => \"drained\" as const),\n timedOut,\n ]);\n\n clearTimeout(timer);\n\n if (outcome === \"timeout\") {\n shutdown.abort();\n server.closeAllConnections();\n }\n }\n\n server.closeIdleConnections();\n await closed;\n })();\n\n return closing;\n };\n\n return { url, close };\n}\n\n/**\n * Validate and answer one HTTP request: path → Host / Origin → auth →\n * method → headers → body → JSON-RPC dispatch.\n */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n settings: ResolvedSettings,\n dispatch: McpHttpDispatch,\n signal: AbortSignal,\n requests: McpServeRequests,\n): Promise<void> {\n const headers = flattenHeaders(request.headers);\n const method = (request.method ?? \"GET\").toUpperCase();\n const path = new URL(request.url ?? \"/\", \"http://localhost\").pathname;\n\n // DNS-rebinding guards apply to every request on every path.\n if (!hostAllowed(headers.host, settings)) {\n sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, \"Forbidden: Host header not allowed.\");\n\n return;\n }\n\n if (!originAllowed(headers.origin, settings)) {\n sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, \"Forbidden: Origin not allowed.\");\n\n return;\n }\n\n if (path !== settings.path) {\n sendRefusal(response, 404, JSON_RPC_SERVER_ERROR, \"Not found.\");\n\n return;\n }\n\n const info: McpHttpRequestInfo = { method, path, headers };\n\n if (request.socket.remoteAddress !== undefined) {\n info.remoteAddress = request.socket.remoteAddress;\n }\n\n const authentication = await authorized(info, settings);\n\n if (!authentication.allowed) {\n const challenge: Record<string, string> =\n settings.tokenDigests.length > 0 || settings.authenticate\n ? { \"www-authenticate\": 'Bearer realm=\"mcp\"' }\n : {};\n\n sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, \"Unauthorized.\", challenge);\n\n return;\n }\n\n if (method !== \"POST\") {\n // Stateless server: no server-initiated SSE stream (GET) and no\n // session to terminate (DELETE). The spec allows 405 for both.\n sendEmpty(response, 405, { allow: \"POST\" });\n\n return;\n }\n\n const protocolVersion = headers[\"mcp-protocol-version\"];\n\n if (protocolVersion !== undefined && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {\n sendRefusal(\n response,\n 400,\n JSON_RPC_SERVER_ERROR,\n `Unsupported MCP-Protocol-Version \"${protocolVersion}\" (supported: ${SUPPORTED_PROTOCOL_VERSIONS.join(\", \")}).`,\n );\n\n return;\n }\n\n if (!isJsonContentType(headers[\"content-type\"])) {\n sendRefusal(response, 415, JSON_RPC_SERVER_ERROR, \"Content-Type must be application/json.\");\n\n return;\n }\n\n if (!acceptsJson(headers.accept)) {\n sendRefusal(response, 406, JSON_RPC_SERVER_ERROR, \"Accept must allow application/json.\");\n\n return;\n }\n\n const body = await readBody(request, settings.maxBodyBytes);\n\n if (body.type === \"too-large\") {\n sendRefusal(\n response,\n 413,\n JSON_RPC_SERVER_ERROR,\n `Request body exceeds ${settings.maxBodyBytes} bytes.`,\n { connection: \"close\" },\n );\n\n return;\n }\n\n let message: unknown;\n\n try {\n message = JSON.parse(body.text);\n } catch {\n sendRefusal(response, 400, JSON_RPC_PARSE_ERROR, \"Parse error: body is not valid JSON.\");\n\n return;\n }\n\n if (typeof message !== \"object\" || message === null || Array.isArray(message)) {\n sendRefusal(\n response,\n 400,\n JSON_RPC_INVALID_REQUEST,\n \"Invalid request: the body must be one JSON-RPC message (batches are not supported).\",\n );\n\n return;\n }\n\n const record = message as Record<string, unknown>;\n\n if (record.jsonrpc !== \"2.0\") {\n sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST, 'Invalid request: jsonrpc must be \"2.0\".');\n\n return;\n }\n\n const id = record.id;\n\n if (typeof record.method === \"string\") {\n if (typeof id !== \"string\" && typeof id !== \"number\") {\n // Only the same caller credentials can cancel an in-flight request.\n requests.cancel(record, cancellationScope(info));\n sendEmpty(response, 202);\n\n return;\n }\n\n const rpcRequest: JsonRpcRequest = { jsonrpc: \"2.0\", id, method: record.method };\n\n if (record.params !== undefined) {\n rpcRequest.params = record.params;\n }\n\n const disconnected = new AbortController();\n const onClose = () => disconnected.abort();\n response.once(\"close\", onClose);\n\n try {\n const rpcResponse = await requests.run(\n rpcRequest,\n {\n request: info,\n principal: authentication.principal,\n signal: AbortSignal.any([signal, disconnected.signal]),\n },\n (context) => dispatch(rpcRequest, context),\n cancellationScope(info),\n );\n\n if (rpcResponse === undefined) {\n // End the HTTP exchange without a JSON-RPC response for cancelled work.\n sendEmpty(response, 202);\n } else {\n sendJson(response, 200, rpcResponse);\n }\n } finally {\n response.off(\"close\", onClose);\n }\n\n return;\n }\n\n if (\n (typeof id === \"string\" || typeof id === \"number\") &&\n (\"result\" in record || \"error\" in record)\n ) {\n // A response to a server-initiated request; this server sends none, so acknowledge.\n sendEmpty(response, 202);\n\n return;\n }\n\n sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST, \"Invalid request: not a JSON-RPC message.\");\n}\n\n/**\n * Host-header check. While bound to loopback only loopback names (plus\n * `allowedHosts`) are accepted, so a DNS-rebound name pointing at\n * 127.0.0.1 is refused. Off loopback the check applies only when\n * `allowedHosts` is set.\n */\nfunction hostAllowed(hostHeader: string | undefined, settings: ResolvedSettings): boolean {\n if (!settings.loopback && settings.allowedHosts.size === 0) {\n return true;\n }\n\n if (hostHeader === undefined) {\n return false;\n }\n\n const name = hostnameOf(hostHeader);\n\n if (settings.allowedHosts.has(name)) {\n return true;\n }\n\n return settings.loopback && isLoopbackHost(name);\n}\n\n/**\n * Origin check (MCP Streamable HTTP security warning). No `Origin` header —\n * a non-browser client — passes; otherwise the origin must be a loopback\n * `http(s)` origin or listed in `allowedOrigins`. `null` and unparsable\n * origins are refused.\n */\nfunction originAllowed(origin: string | undefined, settings: ResolvedSettings): boolean {\n if (origin === undefined) {\n return true;\n }\n\n let parsed: URL;\n\n try {\n parsed = new URL(origin);\n } catch {\n return false;\n }\n\n if (settings.allowedOrigins.has(parsed.origin)) {\n return true;\n }\n\n return (\n (parsed.protocol === \"http:\" || parsed.protocol === \"https:\") && isLoopbackHost(parsed.hostname)\n );\n}\n\n/**\n * Run the configured auth. Bearer tokens are compared in constant time\n * over SHA-256 digests (equal-length buffers for `timingSafeEqual`), and\n * every configured token is compared so the match position does not leak.\n * An `authorize` hook that throws denies the request.\n */\ntype Authentication = { allowed: false } | { allowed: true; principal?: unknown };\n\nasync function authorized(\n info: McpHttpRequestInfo,\n settings: ResolvedSettings,\n): Promise<Authentication> {\n if (!settings.authRequired) {\n return { allowed: true };\n }\n\n if (settings.tokenDigests.length > 0) {\n const match = /^Bearer[ \\t]+(.+)$/i.exec(info.headers.authorization ?? \"\");\n const presented = digest(match?.[1]?.trim() ?? \"\");\n let matched = false;\n\n for (const expected of settings.tokenDigests) {\n matched = timingSafeEqual(presented, expected) || matched;\n }\n\n if (!match || !matched) {\n return { allowed: false };\n }\n }\n\n try {\n if (settings.authorize && (await settings.authorize(info)) !== true) {\n return { allowed: false };\n }\n\n if (settings.authenticate) {\n const principal = await settings.authenticate(info);\n\n return principal === undefined ? { allowed: false } : { allowed: true, principal };\n }\n\n return { allowed: true };\n } catch {\n return { allowed: false };\n }\n}\n\n/**\n * Stateless HTTP has no connection/session id. Repeat the caller's credential\n * and application headers on notifications; per-message transport headers do\n * not identify a caller. Hash the material rather than retaining secrets as keys.\n */\nfunction cancellationScope(info: McpHttpRequestInfo): string {\n const transportHeaders = new Set([\n \"host\",\n \"origin\",\n \"accept\",\n \"accept-encoding\",\n \"accept-language\",\n \"connection\",\n \"content-type\",\n \"content-length\",\n \"transfer-encoding\",\n \"mcp-protocol-version\",\n \"user-agent\",\n ]);\n const credentials = Object.entries(info.headers)\n .filter(([name]) => !transportHeaders.has(name))\n .sort(([left], [right]) => left.localeCompare(right));\n\n return digest(JSON.stringify([info.remoteAddress, credentials])).toString(\"hex\");\n}\n\n/** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */\nfunction isJsonContentType(contentType: string | undefined): boolean {\n return contentType?.split(\";\")[0]?.trim().toLowerCase() === \"application/json\";\n}\n\n/**\n * True when the `Accept` header allows a JSON reply. Clients are required\n * to list both `application/json` and `text/event-stream`; this server\n * always answers JSON, so it only insists on that half. A missing header\n * is accepted.\n */\nfunction acceptsJson(accept: string | undefined): boolean {\n if (accept === undefined || accept.trim() === \"\") {\n return true;\n }\n\n return accept\n .split(\",\")\n .map((part) => part.split(\";\")[0]?.trim().toLowerCase())\n .some((type) => type === \"application/json\" || type === \"application/*\" || type === \"*/*\");\n}\n\n/** The outcome of reading a request body. */\ntype BodyResult = { type: \"ok\"; text: string } | { type: \"too-large\" };\n\n/**\n * Read the request body as UTF-8, refusing anything over `limit` bytes —\n * up front from `Content-Length`, and while streaming for chunked bodies.\n * A refused body is left unread; the 413 response closes the connection.\n */\nfunction readBody(request: IncomingMessage, limit: number): Promise<BodyResult> {\n const declared = Number(request.headers[\"content-length\"]);\n\n if (Number.isFinite(declared) && declared > limit) {\n return Promise.resolve({ type: \"too-large\" });\n }\n\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n let done = false;\n\n const finish = (result: BodyResult) => {\n if (!done) {\n done = true;\n resolve(result);\n }\n };\n\n request.on(\"data\", (chunk: Buffer) => {\n if (done) {\n return;\n }\n\n size += chunk.length;\n\n if (size > limit) {\n request.pause();\n finish({ type: \"too-large\" });\n\n return;\n }\n\n chunks.push(chunk);\n });\n request.on(\"end\", () => finish({ type: \"ok\", text: Buffer.concat(chunks).toString(\"utf8\") }));\n request.on(\"error\", (error) => {\n if (!done) {\n done = true;\n reject(error);\n }\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isRecord } from "./result.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../../packages/ai-tools/src/mcp/serve-requests.ts
|
|
4
|
+
/**
|
|
5
|
+
* In-flight requests for one transport. HTTP partitions these by credentials;
|
|
6
|
+
* stdio has a single connection. Cancellation never crosses those boundaries.
|
|
7
|
+
* https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation
|
|
8
|
+
* (checked 2026-10-19): ignore unknown/completed ids and never answer cancelled work.
|
|
9
|
+
*/
|
|
10
|
+
var McpServeRequests = class {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.scopes = /* @__PURE__ */ new Map();
|
|
13
|
+
}
|
|
14
|
+
cancel(message, scope = "") {
|
|
15
|
+
if (!isRecord(message) || message.jsonrpc !== "2.0" || message.method !== "notifications/cancelled" || message.id !== void 0 || !isRecord(message.params)) return;
|
|
16
|
+
const { requestId, reason } = message.params;
|
|
17
|
+
if (typeof requestId !== "string" && typeof requestId !== "number" || reason !== void 0 && typeof reason !== "string") return;
|
|
18
|
+
const pending = this.scopes.get(scope)?.get(requestId);
|
|
19
|
+
if (pending?.size === 1) {
|
|
20
|
+
for (const entry of pending) if (entry.cancellable) entry.controller.abort(new DOMException(typeof reason === "string" ? reason : "Request cancelled.", "AbortError"));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async run(request, context, dispatch, scope = "") {
|
|
24
|
+
let pending = this.scopes.get(scope);
|
|
25
|
+
if (!pending) {
|
|
26
|
+
pending = /* @__PURE__ */ new Map();
|
|
27
|
+
this.scopes.set(scope, pending);
|
|
28
|
+
}
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const entry = {
|
|
31
|
+
controller,
|
|
32
|
+
cancellable: request.method !== "initialize"
|
|
33
|
+
};
|
|
34
|
+
const calls = pending.get(request.id) ?? /* @__PURE__ */ new Set();
|
|
35
|
+
calls.add(entry);
|
|
36
|
+
pending.set(request.id, calls);
|
|
37
|
+
const onAbort = () => controller.abort(context.signal?.reason);
|
|
38
|
+
context.signal?.addEventListener("abort", onAbort, { once: true });
|
|
39
|
+
if (context.signal?.aborted) onAbort();
|
|
40
|
+
try {
|
|
41
|
+
if (controller.signal.aborted) return;
|
|
42
|
+
const response = await dispatch({
|
|
43
|
+
...context,
|
|
44
|
+
requestId: request.id,
|
|
45
|
+
signal: controller.signal
|
|
46
|
+
});
|
|
47
|
+
return controller.signal.aborted ? void 0 : response;
|
|
48
|
+
} catch (cause) {
|
|
49
|
+
if (controller.signal.aborted) return;
|
|
50
|
+
throw cause;
|
|
51
|
+
} finally {
|
|
52
|
+
context.signal?.removeEventListener("abort", onAbort);
|
|
53
|
+
calls.delete(entry);
|
|
54
|
+
if (calls.size === 0) pending.delete(request.id);
|
|
55
|
+
if (pending.size === 0) this.scopes.delete(scope);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
export { McpServeRequests };
|
|
62
|
+
//# sourceMappingURL=serve-requests.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve-requests.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/serve-requests.ts"],"sourcesContent":["import type { JsonRpcId, JsonRpcRequest, JsonRpcResponse, McpServeCallContext } from \"../contracts\";\nimport { isRecord } from \"./result\";\n\ntype PendingRequest = { controller: AbortController; cancellable: boolean };\n\n/**\n * In-flight requests for one transport. HTTP partitions these by credentials;\n * stdio has a single connection. Cancellation never crosses those boundaries.\n * https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation\n * (checked 2026-10-19): ignore unknown/completed ids and never answer cancelled work.\n */\nexport class McpServeRequests {\n private readonly scopes = new Map<string, Map<JsonRpcId, Set<PendingRequest>>>();\n\n public cancel(message: unknown, scope = \"\"): void {\n if (\n !isRecord(message) ||\n message.jsonrpc !== \"2.0\" ||\n message.method !== \"notifications/cancelled\" ||\n message.id !== undefined ||\n !isRecord(message.params)\n ) {\n return;\n }\n\n const { requestId, reason } = message.params;\n\n if (\n (typeof requestId !== \"string\" && typeof requestId !== \"number\") ||\n (reason !== undefined && typeof reason !== \"string\")\n ) {\n return;\n }\n\n const pending = this.scopes.get(scope)?.get(requestId);\n\n // Stateless clients may share credentials and reuse an id. Preserve their\n // calls, but never guess which one an ambiguous notification refers to.\n if (pending?.size === 1) {\n for (const entry of pending) {\n if (entry.cancellable) {\n entry.controller.abort(\n new DOMException(\n typeof reason === \"string\" ? reason : \"Request cancelled.\",\n \"AbortError\",\n ),\n );\n }\n }\n }\n }\n\n public async run(\n request: JsonRpcRequest,\n context: McpServeCallContext,\n dispatch: (context: McpServeCallContext) => Promise<JsonRpcResponse>,\n scope = \"\",\n ): Promise<JsonRpcResponse | undefined> {\n let pending = this.scopes.get(scope);\n\n if (!pending) {\n pending = new Map();\n this.scopes.set(scope, pending);\n }\n\n const controller = new AbortController();\n // Clients MUST NOT cancel initialize. Other requests share the same registry.\n const entry = { controller, cancellable: request.method !== \"initialize\" };\n const calls = pending.get(request.id) ?? new Set<PendingRequest>();\n calls.add(entry);\n pending.set(request.id, calls);\n\n const onAbort = () => controller.abort(context.signal?.reason);\n context.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n if (context.signal?.aborted) {\n onAbort();\n }\n\n try {\n if (controller.signal.aborted) {\n return undefined;\n }\n\n const response = await dispatch({\n ...context,\n requestId: request.id,\n signal: controller.signal,\n });\n\n return controller.signal.aborted ? undefined : response;\n } catch (cause) {\n if (controller.signal.aborted) {\n return undefined;\n }\n\n throw cause;\n } finally {\n context.signal?.removeEventListener(\"abort\", onAbort);\n calls.delete(entry);\n\n if (calls.size === 0) {\n pending.delete(request.id);\n }\n\n if (pending.size === 0) {\n this.scopes.delete(scope);\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;AAWA,IAAa,mBAAb,MAA8B;;gCACF,IAAI,IAAiD;;CAE/E,AAAO,OAAO,SAAkB,QAAQ,IAAU;EAChD,IACE,CAAC,SAAS,OAAO,KACjB,QAAQ,YAAY,SACpB,QAAQ,WAAW,6BACnB,QAAQ,OAAO,UACf,CAAC,SAAS,QAAQ,MAAM,GAExB;EAGF,MAAM,EAAE,WAAW,WAAW,QAAQ;EAEtC,IACG,OAAO,cAAc,YAAY,OAAO,cAAc,YACtD,WAAW,UAAa,OAAO,WAAW,UAE3C;EAGF,MAAM,UAAU,KAAK,OAAO,IAAI,KAAK,CAAC,EAAE,IAAI,SAAS;EAIrD,IAAI,SAAS,SAAS,GACpB;QAAK,MAAM,SAAS,SAClB,IAAI,MAAM,aACR,MAAM,WAAW,MACf,IAAI,aACF,OAAO,WAAW,WAAW,SAAS,sBACtC,YACF,CACF;EAEJ;CAEJ;CAEA,MAAa,IACX,SACA,SACA,UACA,QAAQ,IAC8B;EACtC,IAAI,UAAU,KAAK,OAAO,IAAI,KAAK;EAEnC,IAAI,CAAC,SAAS;GACZ,0BAAU,IAAI,IAAI;GAClB,KAAK,OAAO,IAAI,OAAO,OAAO;EAChC;EAEA,MAAM,aAAa,IAAI,gBAAgB;EAEvC,MAAM,QAAQ;GAAE;GAAY,aAAa,QAAQ,WAAW;EAAa;EACzE,MAAM,QAAQ,QAAQ,IAAI,QAAQ,EAAE,qBAAK,IAAI,IAAoB;EACjE,MAAM,IAAI,KAAK;EACf,QAAQ,IAAI,QAAQ,IAAI,KAAK;EAE7B,MAAM,gBAAgB,WAAW,MAAM,QAAQ,QAAQ,MAAM;EAC7D,QAAQ,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAEjE,IAAI,QAAQ,QAAQ,SAClB,QAAQ;EAGV,IAAI;GACF,IAAI,WAAW,OAAO,SACpB;GAGF,MAAM,WAAW,MAAM,SAAS;IAC9B,GAAG;IACH,WAAW,QAAQ;IACnB,QAAQ,WAAW;GACrB,CAAC;GAED,OAAO,WAAW,OAAO,UAAU,SAAY;EACjD,SAAS,OAAO;GACd,IAAI,WAAW,OAAO,SACpB;GAGF,MAAM;EACR,UAAU;GACR,QAAQ,QAAQ,oBAAoB,SAAS,OAAO;GACpD,MAAM,OAAO,KAAK;GAElB,IAAI,MAAM,SAAS,GACjB,QAAQ,OAAO,QAAQ,EAAE;GAG3B,IAAI,QAAQ,SAAS,GACnB,KAAK,OAAO,OAAO,KAAK;EAE5B;CACF;AACF"}
|
package/esm/mcp/serve.d.mts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { JsonRpcRequest, JsonRpcResponse } from "../contracts/mcp.type.mjs";
|
|
2
2
|
import { McpHttpServeHandle, McpHttpServeTransport, McpServeCallContext, McpServeOptions, McpServeSource, McpServer } from "../contracts/mcp-serve.type.mjs";
|
|
3
|
+
import "../contracts/index.mjs";
|
|
3
4
|
//#region ../../packages/ai-tools/src/mcp/serve.d.ts
|
|
4
5
|
/**
|
|
5
6
|
* The pure protocol handler: one JSON-RPC request in, one response out.
|
|
6
7
|
*/
|
|
7
|
-
interface McpServeRequestHandler {
|
|
8
|
+
export interface McpServeRequestHandler {
|
|
8
9
|
/**
|
|
9
10
|
* Map one request to its response.
|
|
10
11
|
*
|
|
@@ -25,7 +26,7 @@ interface McpServeRequestHandler {
|
|
|
25
26
|
* @param options - Serve options (name / version / schema target / resources / prompts).
|
|
26
27
|
* @returns An object whose `handle(request, context?)` maps a request to a response.
|
|
27
28
|
*/
|
|
28
|
-
declare function createServeHandler(source: McpServeSource, options: McpServeOptions): McpServeRequestHandler;
|
|
29
|
+
export declare function createServeHandler(source: McpServeSource, options: McpServeOptions): McpServeRequestHandler;
|
|
29
30
|
/**
|
|
30
31
|
* Expose a built agent / supervisor / orchestrator (or a raw
|
|
31
32
|
* `ToolContract[]`, or several named groups) AS an MCP server (Direction B:
|
|
@@ -56,10 +57,9 @@ declare function createServeHandler(source: McpServeSource, options: McpServeOpt
|
|
|
56
57
|
* transport: { type: "http", auth: { bearer: sessionToken } },
|
|
57
58
|
* }).start();
|
|
58
59
|
*/
|
|
59
|
-
declare function serve(source: McpServeSource, options: McpServeOptions & {
|
|
60
|
+
export declare function serve(source: McpServeSource, options: McpServeOptions & {
|
|
60
61
|
transport: McpHttpServeTransport;
|
|
61
62
|
}): McpServer<McpHttpServeHandle>;
|
|
62
|
-
declare function serve(source: McpServeSource, options: McpServeOptions): McpServer;
|
|
63
|
+
export declare function serve(source: McpServeSource, options: McpServeOptions): McpServer;
|
|
63
64
|
//#endregion
|
|
64
|
-
export { McpServeRequestHandler, createServeHandler, serve };
|
|
65
65
|
//# sourceMappingURL=serve.d.mts.map
|
package/esm/mcp/serve.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/serve.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"serve.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/serve.ts"],"mappings":";;;;;;;iBA6tBiB;;;;;;;EAOf,OAAO,SAAS,gBAAgB,UAAU,sBAAsB,QAAQ;;;;;;;;;;;;;;wBAe1D,mBACd,QAAQ,gBACR,SAAS,kBACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBA6Ja,MACd,QAAQ,gBACR,SAAS;EAAoB,WAAW;IACvC,UAAU;wBACG,MAAM,QAAQ,gBAAgB,SAAS,kBAAkB"}
|