@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/cjs/index.cjs +327 -60
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/contracts/http.type.d.mts +4 -5
  5. package/esm/contracts/http.type.d.mts.map +1 -1
  6. package/esm/contracts/index.d.mts +2 -1
  7. package/esm/contracts/mcp-serve.type.d.mts +97 -37
  8. package/esm/contracts/mcp-serve.type.d.mts.map +1 -1
  9. package/esm/contracts/mcp.type.d.mts +29 -19
  10. package/esm/contracts/mcp.type.d.mts.map +1 -1
  11. package/esm/contracts/utility.type.d.mts +7 -8
  12. package/esm/contracts/utility.type.d.mts.map +1 -1
  13. package/esm/contracts/web.type.d.mts +9 -10
  14. package/esm/contracts/web.type.d.mts.map +1 -1
  15. package/esm/errors.d.mts +31 -26
  16. package/esm/errors.d.mts.map +1 -1
  17. package/esm/http/http-request.d.mts +2 -3
  18. package/esm/http/http-request.d.mts.map +1 -1
  19. package/esm/http/http-request.mjs +7 -5
  20. package/esm/http/http-request.mjs.map +1 -1
  21. package/esm/index.d.mts +6 -2
  22. package/esm/index.mjs +2 -1
  23. package/esm/mcp/client.mjs +2 -1
  24. package/esm/mcp/client.mjs.map +1 -1
  25. package/esm/mcp/index.d.mts +5 -4
  26. package/esm/mcp/index.d.mts.map +1 -1
  27. package/esm/mcp/index.mjs +2 -1
  28. package/esm/mcp/index.mjs.map +1 -1
  29. package/esm/mcp/json-schema-to-standard.d.mts +1 -3
  30. package/esm/mcp/json-schema-to-standard.d.mts.map +1 -1
  31. package/esm/mcp/protocol.d.mts +1 -2
  32. package/esm/mcp/protocol.d.mts.map +1 -1
  33. package/esm/mcp/result.d.mts +17 -0
  34. package/esm/mcp/result.d.mts.map +1 -0
  35. package/esm/mcp/result.mjs +85 -0
  36. package/esm/mcp/result.mjs.map +1 -0
  37. package/esm/mcp/serve-http.mjs +64 -25
  38. package/esm/mcp/serve-http.mjs.map +1 -1
  39. package/esm/mcp/serve-requests.mjs +62 -0
  40. package/esm/mcp/serve-requests.mjs.map +1 -0
  41. package/esm/mcp/serve.d.mts +5 -5
  42. package/esm/mcp/serve.d.mts.map +1 -1
  43. package/esm/mcp/serve.mjs +97 -18
  44. package/esm/mcp/serve.mjs.map +1 -1
  45. package/esm/mcp/transport.d.mts +4 -5
  46. package/esm/mcp/transport.d.mts.map +1 -1
  47. package/esm/mcp/transport.mjs +12 -6
  48. package/esm/mcp/transport.mjs.map +1 -1
  49. package/esm/mcp/transport.type.d.mts +2 -2
  50. package/esm/mcp/transport.type.d.mts.map +1 -1
  51. package/esm/register.d.mts +3 -4
  52. package/esm/register.d.mts.map +1 -1
  53. package/esm/utility/calculator.d.mts +1 -3
  54. package/esm/utility/calculator.d.mts.map +1 -1
  55. package/esm/utility/date-time.d.mts +3 -5
  56. package/esm/utility/date-time.d.mts.map +1 -1
  57. package/esm/utility/date-time.mjs +2 -1
  58. package/esm/utility/date-time.mjs.map +1 -1
  59. package/esm/utility/index.d.mts +2 -1
  60. package/esm/utility/index.mjs +1 -1
  61. package/esm/web/fetch-url.d.mts +2 -3
  62. package/esm/web/fetch-url.d.mts.map +1 -1
  63. package/esm/web/fetch-url.mjs +2 -1
  64. package/esm/web/fetch-url.mjs.map +1 -1
  65. package/esm/web/index.d.mts +2 -1
  66. package/esm/web/index.mjs +1 -1
  67. package/esm/web/web-search.d.mts +2 -3
  68. package/esm/web/web-search.d.mts.map +1 -1
  69. package/esm/web/web-search.mjs +3 -4
  70. package/esm/web/web-search.mjs.map +1 -1
  71. package/llms-full.txt +122 -3
  72. package/llms.txt +1 -1
  73. package/package.json +2 -2
  74. package/skills/connect-mcp-server/SKILL.md +15 -0
  75. 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":";AAEA,MAAM,gBAAgB,OAAO,WAAW;;;;;;AAYxC,SAAgB,UAAuC,QAA+B;CACpF,OAAO,GAAG,gBAAgB,OAAO;AACnC;;AAGA,SAAgB,YAAY,OAA0C;CACpE,OAAO,SAAS,KAAK,KAAK,iBAAiB;AAC7C;;AAGA,SAAgB,gBAAgB,OAA2C;CACzE,OAAO,MAAM;AACf;;AAGA,SAAgB,mBAAmB,OAAyB;CAC1D,OACE,SAAS,KAAK,KACd;EAAC;EAAW;EAAW;EAAqB;CAAO,CAAC,CAAC,MAAM,QAAQ,OAAO,KAAK;AAEnF;;AAGA,SAAgB,SAAS,OAAkD;CACzE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,eAAe,OAAyB;CAC/C,OAAO,UAAU,UAAa,OAAO,UAAU;AACjD;AAEA,SAAS,iBAAiB,OAAyB;CACjD,IAAI,UAAU,QAAW,OAAO;CAChC,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO;CAE7B,QACG,MAAM,aAAa,UACjB,MAAM,QAAQ,MAAM,QAAQ,KAC3B,MAAM,SAAS,OAAO,SAAS,SAAS,UAAU,SAAS,WAAW,OACzE,MAAM,aAAa,UACjB,OAAO,MAAM,aAAa,YAAY,MAAM,YAAY,KAAK,MAAM,YAAY,MAClF,eAAe,MAAM,YAAY;AAErC;AAEA,SAAS,cAAc,OAAyC;CAC9D,QACG,MAAM,aAAa,UAAa,SAAS,MAAM,QAAQ,MACxD,iBAAiB,MAAM,WAAW;AAEtC;AAEA,SAAS,UAAU,OAAyB;CAC1C,OACE,SAAS,KAAK,KACd,OAAO,MAAM,QAAQ,YACrB,eAAe,MAAM,QAAQ,MAC5B,MAAM,UAAU,UACd,MAAM,QAAQ,MAAM,KAAK,KAAK,MAAM,MAAM,OAAO,SAAS,OAAO,SAAS,QAAQ,OACpF,MAAM,UAAU,UAAa,MAAM,UAAU,WAAW,MAAM,UAAU;AAE7E;AAEA,SAAS,aAAa,OAAyB;CAC7C,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,cAAc,KAAK,GAAG,OAAO;CAEtD,QAAQ,MAAM,MAAd;EACE,KAAK,QACH,OAAO,OAAO,MAAM,SAAS;EAC/B,KAAK;EACL,KAAK,SACH,OAAO,OAAO,MAAM,SAAS,YAAY,OAAO,MAAM,aAAa;EACrE,KAAK,iBACH,OACE,OAAO,MAAM,QAAQ,YACrB,OAAO,MAAM,SAAS,YACtB,eAAe,MAAM,KAAK,KAC1B,eAAe,MAAM,WAAW,KAChC,eAAe,MAAM,QAAQ,MAC5B,MAAM,SAAS,UAAa,OAAO,MAAM,SAAS,cAClD,MAAM,UAAU,UAAc,MAAM,QAAQ,MAAM,KAAK,KAAK,MAAM,MAAM,MAAM,SAAS;EAE5F,KAAK,YAAY;GACf,MAAM,WAAW,MAAM;GAEvB,OACE,SAAS,QAAQ,KACjB,OAAO,SAAS,QAAQ,YACxB,eAAe,SAAS,QAAQ,KAChC,cAAc,QAAQ,MACrB,OAAO,SAAS,SAAS,YAAY,OAAO,SAAS,SAAS;EAEnE;EACA,SACE,OAAO;CACX;AACF;;AAGA,SAAS,YAAY,OAAgB,4BAAY,IAAI,IAAY,GAAY;CAC3E,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW,OAAO;CACtF,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,SAAS,KAAK;CAC3D,IAAI,OAAO,UAAU,YAAY,UAAU,IAAI,KAAK,GAAG,OAAO;CAE9D,MAAM,YAAqB,OAAO,eAAe,KAAK;CACtD,IAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,cAAc,OAAO,aAAa,cAAc,MAAM,OAAO;CAE1F,UAAU,IAAI,KAAK;CACnB,MAAM,QAAQ,MAAM,QAAQ,KAAK,IAC7B,MAAM,KAAK,KAAK,CAAC,CAAC,OAAO,UAAU,YAAY,OAAO,SAAS,CAAC,IAChE,OAAO,OAAO,KAAK,CAAC,CAAC,OAAO,UAAU,UAAU,UAAa,YAAY,OAAO,SAAS,CAAC;CAC9F,UAAU,OAAO,KAAK;CACtB,OAAO;AACT;;;;;;;;AASA,SAAgB,gBAAgB,OAAoD;CAClF,IACE,CAAC,SAAS,KAAK,KACf,CAAC,MAAM,QAAQ,MAAM,OAAO,KAC5B,CAAC,MAAM,KAAK,MAAM,OAAO,CAAC,CAAC,MAAM,YAAY,KAC5C,MAAM,YAAY,UAAa,OAAO,MAAM,YAAY,aACxD,MAAM,sBAAsB,UAAa,CAAC,SAAS,MAAM,iBAAiB,KAC1E,MAAM,aAAa,UAAa,CAAC,SAAS,MAAM,QAAQ,KACzD,CAAC,YAAY,KAAK,GAElB,MAAM,IAAI,UAAU,oCAAoC;AAE5D"}
@@ -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 `authorize`.", { type: "config" });
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 { authorize }), or bind 127.0.0.1.`, {
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
- if (!await authorized(info, settings)) {
269
- sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", settings.tokenDigests.length > 0 ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {});
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
- sendJson(response, 200, await dispatch(rpcRequest, {
323
- request: info,
324
- signal
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
- if (settings.authorize) try {
380
- return await settings.authorize(info) === true;
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
- return true;
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":"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"}
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAM,eAAe;;AAGrB,MAAM,eAAe;;AAGrB,MAAM,yBAAyB;;AAG/B,MAAM,2BAA2B;;AAGjC,MAAM,uBAAuB;AAC7B,MAAM,2BAA2B;;AAGjC,MAAM,wBAAwB;;AAG9B,MAAM,iCAAiB,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;;AAsBA,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,aAAa,CAAC,KAAK,cAC1D,MAAM,IAAI,cAAc,oEAAoE,EAC1F,MAAM,SACR,CAAC;CAGH,MAAM,eACJ,OAAO,SAAS,KAAK,MAAM,cAAc,UAAa,MAAM,iBAAiB;CAE/E,IAAI,CAAC,YAAY,CAAC,cAChB,MAAM,IAAI,cACR,yDAAyD,KAAK,yGAE9D;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,cAAc,MAAM;EACpB;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,WAAW,IAAI,iBAAiB;CACtC,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,cACX,SACA,UACA,UACA,UACA,SAAS,QACT,QACF,CAAC,CAAC,YAAY;GACZ,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,QACA,UACe;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,MAAM,iBAAiB,MAAM,WAAW,MAAM,QAAQ;CAEtD,IAAI,CAAC,eAAe,SAAS;EAC3B,MAAM,YACJ,SAAS,aAAa,SAAS,KAAK,SAAS,eACzC,EAAE,oBAAoB,uBAAqB,IAC3C,CAAC;EAEP,YAAY,UAAU,KAAK,uBAAuB,iBAAiB,SAAS;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,SAAS,OAAO,QAAQ,kBAAkB,IAAI,CAAC;GAC/C,UAAU,UAAU,GAAG;GAEvB;EACF;EAEA,MAAM,aAA6B;GAAE,SAAS;GAAO;GAAI,QAAQ,OAAO;EAAO;EAE/E,IAAI,OAAO,WAAW,QACpB,WAAW,SAAS,OAAO;EAG7B,MAAM,eAAe,IAAI,gBAAgB;EACzC,MAAM,gBAAgB,aAAa,MAAM;EACzC,SAAS,KAAK,SAAS,OAAO;EAE9B,IAAI;GACF,MAAM,cAAc,MAAM,SAAS,IACjC,YACA;IACE,SAAS;IACT,WAAW,eAAe;IAC1B,QAAQ,YAAY,IAAI,CAAC,QAAQ,aAAa,MAAM,CAAC;GACvD,IACC,YAAY,SAAS,YAAY,OAAO,GACzC,kBAAkB,IAAI,CACxB;GAEA,IAAI,gBAAgB,QAElB,UAAU,UAAU,GAAG;QAEvB,SAAS,UAAU,KAAK,WAAW;EAEvC,UAAU;GACR,SAAS,IAAI,SAAS,OAAO;EAC/B;EAEA;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;AAUA,eAAe,WACb,MACA,UACyB;CACzB,IAAI,CAAC,SAAS,cACZ,OAAO,EAAE,SAAS,KAAK;CAGzB,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,EAAE,SAAS,MAAM;CAE5B;CAEA,IAAI;EACF,IAAI,SAAS,aAAc,MAAM,SAAS,UAAU,IAAI,MAAO,MAC7D,OAAO,EAAE,SAAS,MAAM;EAG1B,IAAI,SAAS,cAAc;GACzB,MAAM,YAAY,MAAM,SAAS,aAAa,IAAI;GAElD,OAAO,cAAc,SAAY,EAAE,SAAS,MAAM,IAAI;IAAE,SAAS;IAAM;GAAU;EACnF;EAEA,OAAO,EAAE,SAAS,KAAK;CACzB,QAAQ;EACN,OAAO,EAAE,SAAS,MAAM;CAC1B;AACF;;;;;;AAOA,SAAS,kBAAkB,MAAkC;CAC3D,MAAM,mCAAmB,IAAI,IAAI;EAC/B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CACD,MAAM,cAAc,OAAO,QAAQ,KAAK,OAAO,CAAC,CAC7C,QAAQ,CAAC,UAAU,CAAC,iBAAiB,IAAI,IAAI,CAAC,CAAC,CAC/C,MAAM,CAAC,OAAO,CAAC,WAAW,KAAK,cAAc,KAAK,CAAC;CAEtD,OAAO,OAAO,KAAK,UAAU,CAAC,KAAK,eAAe,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK;AACjF;;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"}
@@ -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"}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/serve.ts"],"mappings":";;;;;;UAmnBiB,sBAAA;EAOwD;;;;;;EAAvE,MAAA,CAAO,OAAA,EAAS,cAAA,EAAgB,OAAA,GAAU,mBAAA,GAAsB,OAAA,CAAQ,eAAA;AAAA;;AAAe;AAezF;;;;;;;;;;iBAAgB,kBAAA,CACd,MAAA,EAAQ,cAAA,EACR,OAAA,EAAS,eAAA,GACR,sBAAA;;;;AAAsB;AAuIzB;;;;;;;;;;;;;;;;;;;AAG+B;AAC/B;;;;;;iBAJgB,KAAA,CACd,MAAA,EAAQ,cAAA,EACR,OAAA,EAAS,eAAA;EAAoB,SAAA,EAAW,qBAAA;AAAA,IACvC,SAAA,CAAU,kBAAA;AAAA,iBACG,KAAA,CAAM,MAAA,EAAQ,cAAA,EAAgB,OAAA,EAAS,eAAA,GAAkB,SAAA"}
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"}