@warlock.js/ai-tools 5.30.0 → 5.32.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 (86) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/README.md +25 -13
  3. package/cjs/index.cjs +968 -122
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/contracts/http.type.d.mts +4 -5
  6. package/esm/contracts/http.type.d.mts.map +1 -1
  7. package/esm/contracts/index.d.mts +3 -1
  8. package/esm/contracts/mcp-serve.type.d.mts +285 -0
  9. package/esm/contracts/mcp-serve.type.d.mts.map +1 -0
  10. package/esm/contracts/mcp.type.d.mts +44 -77
  11. package/esm/contracts/mcp.type.d.mts.map +1 -1
  12. package/esm/contracts/utility.type.d.mts +7 -8
  13. package/esm/contracts/utility.type.d.mts.map +1 -1
  14. package/esm/contracts/web.type.d.mts +9 -10
  15. package/esm/contracts/web.type.d.mts.map +1 -1
  16. package/esm/errors.d.mts +62 -22
  17. package/esm/errors.d.mts.map +1 -1
  18. package/esm/errors.mjs +20 -1
  19. package/esm/errors.mjs.map +1 -1
  20. package/esm/http/http-request.d.mts +2 -3
  21. package/esm/http/http-request.d.mts.map +1 -1
  22. package/esm/http/http-request.mjs +7 -5
  23. package/esm/http/http-request.mjs.map +1 -1
  24. package/esm/index.d.mts +10 -4
  25. package/esm/index.mjs +4 -2
  26. package/esm/mcp/client.mjs +37 -6
  27. package/esm/mcp/client.mjs.map +1 -1
  28. package/esm/mcp/index.d.mts +17 -9
  29. package/esm/mcp/index.d.mts.map +1 -1
  30. package/esm/mcp/index.mjs +3 -1
  31. package/esm/mcp/index.mjs.map +1 -1
  32. package/esm/mcp/json-schema-to-standard.d.mts +1 -3
  33. package/esm/mcp/json-schema-to-standard.d.mts.map +1 -1
  34. package/esm/mcp/protocol.d.mts +15 -0
  35. package/esm/mcp/protocol.d.mts.map +1 -0
  36. package/esm/mcp/protocol.mjs +23 -0
  37. package/esm/mcp/protocol.mjs.map +1 -0
  38. package/esm/mcp/result.d.mts +17 -0
  39. package/esm/mcp/result.d.mts.map +1 -0
  40. package/esm/mcp/result.mjs +85 -0
  41. package/esm/mcp/result.mjs.map +1 -0
  42. package/esm/mcp/serve-http.mjs +444 -0
  43. package/esm/mcp/serve-http.mjs.map +1 -0
  44. package/esm/mcp/serve.d.mts +46 -27
  45. package/esm/mcp/serve.d.mts.map +1 -1
  46. package/esm/mcp/serve.mjs +339 -96
  47. package/esm/mcp/serve.mjs.map +1 -1
  48. package/esm/mcp/transport.d.mts +4 -5
  49. package/esm/mcp/transport.d.mts.map +1 -1
  50. package/esm/mcp/transport.mjs +12 -6
  51. package/esm/mcp/transport.mjs.map +1 -1
  52. package/esm/mcp/transport.type.d.mts +2 -2
  53. package/esm/mcp/transport.type.d.mts.map +1 -1
  54. package/esm/register.d.mts +15 -5
  55. package/esm/register.d.mts.map +1 -1
  56. package/esm/register.mjs +3 -2
  57. package/esm/register.mjs.map +1 -1
  58. package/esm/schema.mjs.map +1 -1
  59. package/esm/utility/calculator.d.mts +1 -3
  60. package/esm/utility/calculator.d.mts.map +1 -1
  61. package/esm/utility/calculator.mjs.map +1 -1
  62. package/esm/utility/date-time.d.mts +3 -5
  63. package/esm/utility/date-time.d.mts.map +1 -1
  64. package/esm/utility/date-time.mjs +2 -1
  65. package/esm/utility/date-time.mjs.map +1 -1
  66. package/esm/utility/index.d.mts +2 -1
  67. package/esm/utility/index.mjs +1 -1
  68. package/esm/utility/schema.mjs.map +1 -1
  69. package/esm/web/fetch-url.d.mts +2 -3
  70. package/esm/web/fetch-url.d.mts.map +1 -1
  71. package/esm/web/fetch-url.mjs +2 -1
  72. package/esm/web/fetch-url.mjs.map +1 -1
  73. package/esm/web/index.d.mts +2 -1
  74. package/esm/web/index.mjs +1 -1
  75. package/esm/web/schema.mjs.map +1 -1
  76. package/esm/web/web-search.d.mts +2 -3
  77. package/esm/web/web-search.d.mts.map +1 -1
  78. package/esm/web/web-search.mjs +3 -4
  79. package/esm/web/web-search.mjs.map +1 -1
  80. package/llms-full.txt +245 -56
  81. package/llms.txt +3 -3
  82. package/package.json +2 -6
  83. package/skills/connect-mcp-server/SKILL.md +28 -6
  84. package/skills/expose-as-mcp-server/SKILL.md +168 -35
  85. package/skills/index.md +5 -2
  86. package/skills/use-web-and-http-tools/SKILL.md +49 -15
package/cjs/index.cjs CHANGED
@@ -2,6 +2,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  let _warlock_js_ai = require("@warlock.js/ai");
3
3
  let node_child_process = require("node:child_process");
4
4
  let node_readline = require("node:readline");
5
+ let node_crypto = require("node:crypto");
6
+ let node_http = require("node:http");
7
+ let node_net = require("node:net");
5
8
 
6
9
  //#region ../../packages/ai-tools/src/errors.ts
7
10
  /**
@@ -80,6 +83,25 @@ var McpTransportError = class extends _warlock_js_ai.AIError {
80
83
  }
81
84
  };
82
85
  /**
86
+ * `ai.mcp.serve` could not build or start the server. Thrown by `serve()`
87
+ * itself for an invalid source or a name collision, and by `start()` for
88
+ * transport configuration and bind failures — never for a single bad request, which gets an HTTP / JSON-RPC
89
+ * error response instead. Extends {@link AIError} (code `TOOL_EXEC_FAILED`);
90
+ * branch on `error.type`.
91
+ *
92
+ * @example
93
+ * if (error instanceof McpServeError && error.type === "collision") {
94
+ * // two tools share a name — rename one or use a grouped source + prefixToolNames
95
+ * }
96
+ */
97
+ var McpServeError = class extends _warlock_js_ai.AIError {
98
+ constructor(message, options) {
99
+ super("TOOL_EXEC_FAILED", message, options);
100
+ this.name = "McpServeError";
101
+ this.type = options.type;
102
+ }
103
+ };
104
+ /**
83
105
  * A web tool failed — a missing optional peer, an absent API key, a host
84
106
  * rejected by the `allowHosts` guardrail, an unparseable URL, or a failed
85
107
  * network call.
@@ -276,7 +298,7 @@ const ALL_METHODS = [
276
298
  "DELETE"
277
299
  ];
278
300
  /** Methods that conventionally carry no request body — `body` is dropped for these. */
279
- const BODYLESS_METHODS = new Set(["GET"]);
301
+ const BODYLESS_METHODS = /* @__PURE__ */ new Set(["GET"]);
280
302
  /**
281
303
  * Standard Schema for {@link HttpRequestInput}. `url` is required;
282
304
  * `method` is constrained to the canonical HTTP verb set (further
@@ -355,7 +377,7 @@ async function readCappedBody(response, maxBytes) {
355
377
  * `application/json` and the `+json` structured-suffix convention
356
378
  * (e.g. `application/vnd.api+json`), case-insensitively.
357
379
  */
358
- function isJsonContentType(contentType) {
380
+ function isJsonContentType$1(contentType) {
359
381
  if (!contentType) return false;
360
382
  const value = contentType.toLowerCase();
361
383
  return value.includes("application/json") || value.includes("+json");
@@ -440,10 +462,12 @@ function httpRequestTool(options = {}) {
440
462
  ...input.headers
441
463
  };
442
464
  let body;
443
- if (!BODYLESS_METHODS.has(method) && input.body !== void 0) if (typeof input.body === "string") body = input.body;
444
- else {
445
- body = JSON.stringify(input.body);
446
- if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) headers["content-type"] = "application/json";
465
+ if (!BODYLESS_METHODS.has(method) && input.body !== void 0) {
466
+ if (typeof input.body === "string") body = input.body;
467
+ else {
468
+ body = JSON.stringify(input.body);
469
+ if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) headers["content-type"] = "application/json";
470
+ }
447
471
  }
448
472
  let response;
449
473
  try {
@@ -471,7 +495,7 @@ function httpRequestTool(options = {}) {
471
495
  });
472
496
  const { text, truncated } = await readCappedBody(response, maxBytes);
473
497
  let parsedBody = text;
474
- if (!truncated && isJsonContentType(responseHeaders["content-type"]) && text.length > 0) try {
498
+ if (!truncated && isJsonContentType$1(responseHeaders["content-type"]) && text.length > 0) try {
475
499
  parsedBody = JSON.parse(text);
476
500
  } catch {
477
501
  parsedBody = text;
@@ -695,8 +719,10 @@ function armCall(reject, method, options, onSettle) {
695
719
  }));
696
720
  };
697
721
  const signal = options?.signal;
698
- if (signal) if (signal.aborted) queueMicrotask(onAbort);
699
- else signal.addEventListener("abort", onAbort, { once: true });
722
+ if (signal) {
723
+ if (signal.aborted) queueMicrotask(onAbort);
724
+ else signal.addEventListener("abort", onAbort, { once: true });
725
+ }
700
726
  function cleanup() {
701
727
  clearTimeout(timer);
702
728
  if (signal) signal.removeEventListener("abort", onAbort);
@@ -849,8 +875,10 @@ var HttpTransport = class {
849
875
  const timer = setTimeout(() => controller.abort(), timeoutMs);
850
876
  const onAbort = () => controller.abort();
851
877
  const signal = options?.signal;
852
- if (signal) if (signal.aborted) controller.abort();
853
- else signal.addEventListener("abort", onAbort, { once: true });
878
+ if (signal) {
879
+ if (signal.aborted) controller.abort();
880
+ else signal.addEventListener("abort", onAbort, { once: true });
881
+ }
854
882
  let response;
855
883
  try {
856
884
  response = await fetch(this.url, {
@@ -915,7 +943,8 @@ var HttpTransport = class {
915
943
  body: JSON.stringify(notification)
916
944
  });
917
945
  } catch (cause) {
918
- throw new McpTransportError(`MCP http transport notification "${method}" failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
946
+ const message = cause instanceof Error ? cause.message : String(cause);
947
+ throw new McpTransportError(`MCP http transport notification "${method}" failed: ${message}`, {
919
948
  type: "connect",
920
949
  method,
921
950
  cause
@@ -1018,7 +1047,8 @@ var JsonRpcClient = class {
1018
1047
  * @returns A JSON-RPC client handle.
1019
1048
  */
1020
1049
  function createJsonRpcClient(source) {
1021
- return new JsonRpcClient(isTransportConfig(source) ? createTransport(source) : withIdAllocator(source));
1050
+ const transport = isTransportConfig(source) ? createTransport(source) : withIdAllocator(source);
1051
+ return new JsonRpcClient(transport);
1022
1052
  }
1023
1053
  /**
1024
1054
  * Distinguish an {@link McpTransport} config (a plain object with a `type`
@@ -1046,7 +1076,7 @@ function withIdAllocator(client) {
1046
1076
  /** Default per-call timeout for `tools/call`. */
1047
1077
  const DEFAULT_CALL_TIMEOUT_MS = 3e4;
1048
1078
  /** The MCP protocol version this client advertises in `initialize`. */
1049
- const PROTOCOL_VERSION$1 = "2025-06-18";
1079
+ const PROTOCOL_VERSION = "2025-06-18";
1050
1080
  /**
1051
1081
  * The internal {@link McpClient} implementation. Owns one JSON-RPC client
1052
1082
  * over a transport, runs the `initialize` handshake on first use, lists
@@ -1059,8 +1089,24 @@ const PROTOCOL_VERSION$1 = "2025-06-18";
1059
1089
  var McpClientImpl = class {
1060
1090
  constructor(source, options = {}) {
1061
1091
  this.initialized = false;
1092
+ this.onAbort = () => {
1093
+ this.close().catch(() => void 0);
1094
+ };
1062
1095
  this.rpc = createJsonRpcClient(source);
1063
1096
  this.options = options;
1097
+ const signal = options.signal;
1098
+ if (signal?.aborted) this.onAbort();
1099
+ else signal?.addEventListener("abort", this.onAbort, { once: true });
1100
+ }
1101
+ /**
1102
+ * Reject with a typed `"closed"` error once the client was closed or its
1103
+ * `signal` aborted, so no request is sent on a dead transport.
1104
+ */
1105
+ assertOpen(method) {
1106
+ if (this.closing || this.options.signal?.aborted) throw new McpTransportError(`MCP client is closed; "${method}" was not sent.`, {
1107
+ type: "closed",
1108
+ method
1109
+ });
1064
1110
  }
1065
1111
  /**
1066
1112
  * Run the MCP `initialize` handshake exactly once, then send the
@@ -1070,19 +1116,21 @@ var McpClientImpl = class {
1070
1116
  */
1071
1117
  async handshake() {
1072
1118
  if (this.initialized) return;
1119
+ this.assertOpen("initialize");
1073
1120
  try {
1074
1121
  await this.rpc.call("initialize", {
1075
- protocolVersion: PROTOCOL_VERSION$1,
1122
+ protocolVersion: PROTOCOL_VERSION,
1076
1123
  capabilities: {},
1077
1124
  clientInfo: {
1078
1125
  name: "@warlock.js/ai-tools",
1079
1126
  version: "4.4.0"
1080
1127
  }
1081
- });
1128
+ }, { signal: this.options.signal });
1082
1129
  await this.rpc.notify("notifications/initialized");
1083
1130
  } catch (cause) {
1084
1131
  if (cause instanceof McpTransportError) throw cause;
1085
- throw new McpTransportError(`MCP initialize handshake failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
1132
+ const message = cause instanceof Error ? cause.message : String(cause);
1133
+ throw new McpTransportError(`MCP initialize handshake failed: ${message}`, {
1086
1134
  type: "connect",
1087
1135
  method: "initialize",
1088
1136
  cause
@@ -1107,7 +1155,8 @@ var McpClientImpl = class {
1107
1155
  */
1108
1156
  async listAndAdapt() {
1109
1157
  await this.handshake();
1110
- const descriptors = (await this.rpc.call("tools/list")).tools ?? [];
1158
+ this.assertOpen("tools/list");
1159
+ const descriptors = (await this.rpc.call("tools/list", void 0, { signal: this.options.signal })).tools ?? [];
1111
1160
  const filter = this.options.filter;
1112
1161
  return (filter ? descriptors.filter((d) => filter(d.name)) : descriptors).map((descriptor) => this.adapt(descriptor));
1113
1162
  }
@@ -1128,11 +1177,12 @@ var McpClientImpl = class {
1128
1177
  description: descriptor.description ?? `Invoke the remote MCP tool "${remoteName}".`,
1129
1178
  input,
1130
1179
  execute: async (args, ctx) => {
1180
+ this.assertOpen("tools/call");
1131
1181
  const result = await this.rpc.call("tools/call", {
1132
1182
  name: remoteName,
1133
1183
  arguments: args ?? {}
1134
1184
  }, {
1135
- signal: ctx?.signal,
1185
+ signal: combineSignals(ctx?.signal, this.options.signal),
1136
1186
  timeoutMs
1137
1187
  });
1138
1188
  if (result.isError) throw new McpTransportError(`MCP tool "${remoteName}" returned an error: ${unwrapContent(result.content)}`, {
@@ -1144,10 +1194,20 @@ var McpClientImpl = class {
1144
1194
  });
1145
1195
  }
1146
1196
  close() {
1147
- return this.rpc.close();
1197
+ this.options.signal?.removeEventListener("abort", this.onAbort);
1198
+ this.closing ??= this.rpc.close();
1199
+ return this.closing;
1148
1200
  }
1149
1201
  };
1150
1202
  /**
1203
+ * The signal a `tools/call` honors: the per-call `ctx.signal`, the
1204
+ * client-level `options.signal`, both (whichever aborts first), or none.
1205
+ */
1206
+ function combineSignals(callSignal, clientSignal) {
1207
+ if (!callSignal || !clientSignal) return callSignal ?? clientSignal;
1208
+ return AbortSignal.any([callSignal, clientSignal]);
1209
+ }
1210
+ /**
1151
1211
  * Flatten an MCP `tools/call` result's content blocks into the value a
1152
1212
  * tool returns. Text blocks are concatenated; a single block whose text is
1153
1213
  * valid JSON is parsed so structured tool output flows back as an object
@@ -1219,7 +1279,8 @@ function tryParseJson(text) {
1219
1279
  * @param server - The transport config (`{ type: "stdio" }` /
1220
1280
  * `{ type: "http" }`). A pre-built transport client may be injected for
1221
1281
  * testing.
1222
- * @param options - Prefix / filter / per-call timeout.
1282
+ * @param options - Prefix / filter / per-call timeout, and a `signal` that
1283
+ * closes the client when it aborts.
1223
1284
  * @returns An {@link McpClient} handle.
1224
1285
  *
1225
1286
  * @example
@@ -1233,6 +1294,546 @@ function mcp$1(server, options) {
1233
1294
  return new McpClientImpl(server, options);
1234
1295
  }
1235
1296
 
1297
+ //#endregion
1298
+ //#region ../../packages/ai-tools/src/mcp/protocol.ts
1299
+ /**
1300
+ * MCP protocol versions the `ai.mcp.serve` server speaks, newest first.
1301
+ * `initialize` echoes the client's requested version when it is listed
1302
+ * here and otherwise answers with the newest one (lifecycle, version
1303
+ * negotiation). The HTTP transport rejects any other value in the
1304
+ * `MCP-Protocol-Version` header with 400.
1305
+ *
1306
+ * Source: https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle
1307
+ * and https://modelcontextprotocol.io/specification/2025-11-25/basic/transports
1308
+ * (checked 2026-10-17).
1309
+ */
1310
+ const SUPPORTED_PROTOCOL_VERSIONS = [
1311
+ "2025-11-25",
1312
+ "2025-06-18",
1313
+ "2025-03-26"
1314
+ ];
1315
+ /** The newest MCP protocol version the server speaks. */
1316
+ const LATEST_PROTOCOL_VERSION = "2025-11-25";
1317
+
1318
+ //#endregion
1319
+ //#region ../../packages/ai-tools/src/mcp/serve-http.ts
1320
+ /**
1321
+ * MCP Streamable HTTP server transport for `ai.mcp.serve`.
1322
+ *
1323
+ * Implements the server side of the Streamable HTTP transport from the MCP
1324
+ * specification (https://modelcontextprotocol.io/specification/2025-11-25/basic/transports,
1325
+ * checked 2026-10-17) on `node:http`, with no SDK dependency:
1326
+ *
1327
+ * - one endpoint path; each POST carries exactly one JSON-RPC message
1328
+ * (batches were removed from the protocol in 2025-06-18 and get 400);
1329
+ * - a request is answered with `Content-Type: application/json` (the spec
1330
+ * lets a server pick JSON or SSE per request); a notification or a
1331
+ * response is acknowledged with `202 Accepted` and no body;
1332
+ * - stateless: no `MCP-Session-Id` is issued (optional in the spec), so
1333
+ * GET (server-initiated SSE stream) and DELETE (session end) answer
1334
+ * `405 Method Not Allowed`, as the spec allows;
1335
+ * - an unsupported `MCP-Protocol-Version` header gets 400;
1336
+ * - security warning items: `Origin` is validated on every request (an
1337
+ * invalid one gets 403), the default bind is `127.0.0.1`, and
1338
+ * authentication is required for any non-loopback bind.
1339
+ */
1340
+ /** Default bind interface — loopback only. */
1341
+ const DEFAULT_HOST = "127.0.0.1";
1342
+ /** Default endpoint path. */
1343
+ const DEFAULT_PATH = "/mcp";
1344
+ /** Default request body cap: 1 MiB. */
1345
+ const DEFAULT_MAX_BODY_BYTES = 1048576;
1346
+ /** Default time `close()` waits for in-flight requests. */
1347
+ const DEFAULT_DRAIN_TIMEOUT_MS = 5e3;
1348
+ /** JSON-RPC codes used for transport-level error bodies. */
1349
+ const JSON_RPC_PARSE_ERROR = -32700;
1350
+ const JSON_RPC_INVALID_REQUEST$1 = -32600;
1351
+ /** Generic server-defined JSON-RPC code for HTTP-level refusals (auth, origin, size). */
1352
+ const JSON_RPC_SERVER_ERROR = -32e3;
1353
+ /** Host names that always mean "this machine". */
1354
+ const LOOPBACK_NAMES = /* @__PURE__ */ new Set([
1355
+ "localhost",
1356
+ "127.0.0.1",
1357
+ "::1",
1358
+ "[::1]"
1359
+ ]);
1360
+ /**
1361
+ * True when `host` names a loopback interface (`localhost`, `127.0.0.0/8`,
1362
+ * `::1`, or an IPv4-mapped `::ffff:127.x.x.x`).
1363
+ */
1364
+ function isLoopbackHost(host) {
1365
+ const normalized = host.trim().toLowerCase();
1366
+ if (LOOPBACK_NAMES.has(normalized)) return true;
1367
+ const unbracketed = normalized.replace(/^\[|\]$/g, "");
1368
+ const mapped = unbracketed.startsWith("::ffff:") ? unbracketed.slice(7) : unbracketed;
1369
+ return (0, node_net.isIPv4)(mapped) && mapped.startsWith("127.");
1370
+ }
1371
+ /** Strip the port from a `Host` header value (`[::1]:3000` → `[::1]`, `a.b:80` → `a.b`). */
1372
+ function hostnameOf(hostHeader) {
1373
+ const value = hostHeader.trim().toLowerCase();
1374
+ if (value.startsWith("[")) {
1375
+ const end = value.indexOf("]");
1376
+ return end === -1 ? value : value.slice(0, end + 1);
1377
+ }
1378
+ const colon = value.lastIndexOf(":");
1379
+ return colon === -1 ? value : value.slice(0, colon);
1380
+ }
1381
+ /** SHA-256 digest — equal-length input for `timingSafeEqual`. */
1382
+ function digest(value) {
1383
+ return (0, node_crypto.createHash)("sha256").update(value, "utf8").digest();
1384
+ }
1385
+ /** Normalise a configured origin to `scheme://host[:port]`, or throw a config error. */
1386
+ function normalizeOrigin(origin) {
1387
+ try {
1388
+ return new URL(origin).origin;
1389
+ } catch (cause) {
1390
+ throw new McpServeError(`allowedOrigins entry "${origin}" is not a valid origin URL.`, {
1391
+ type: "config",
1392
+ cause
1393
+ });
1394
+ }
1395
+ }
1396
+ /**
1397
+ * Validate and default the transport options. Throws {@link McpServeError}
1398
+ * (`"config"`) for a non-loopback bind without auth, an empty bearer
1399
+ * token, an `auth` with neither check, a bad path / port / limit.
1400
+ */
1401
+ function resolveSettings(transport) {
1402
+ const host = transport.host ?? DEFAULT_HOST;
1403
+ const port = transport.port ?? 0;
1404
+ const path = transport.path ?? DEFAULT_PATH;
1405
+ const loopback = isLoopbackHost(host);
1406
+ const auth = transport.auth;
1407
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new McpServeError(`MCP http port must be an integer in 0–65535 (got ${port}).`, { type: "config" });
1408
+ if (!path.startsWith("/")) throw new McpServeError(`MCP http path must start with "/" (got "${path}").`, { type: "config" });
1409
+ const maxBodyBytes = transport.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
1410
+ if (!Number.isInteger(maxBodyBytes) || maxBodyBytes <= 0) throw new McpServeError("MCP http maxBodyBytes must be a positive integer.", { type: "config" });
1411
+ const drainTimeoutMs = transport.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
1412
+ if (!Number.isFinite(drainTimeoutMs) || drainTimeoutMs < 0) throw new McpServeError("MCP http drainTimeoutMs must be zero or a positive number.", { type: "config" });
1413
+ const tokens = auth?.bearer === void 0 ? [] : typeof auth.bearer === "string" ? [auth.bearer] : [...auth.bearer];
1414
+ if (tokens.some((token) => token.length === 0)) throw new McpServeError("MCP http auth.bearer tokens must be non-empty strings.", { type: "config" });
1415
+ if (auth && auth.bearer !== void 0 && tokens.length === 0) throw new McpServeError("MCP http auth.bearer needs at least one token.", { type: "config" });
1416
+ if (auth && tokens.length === 0 && !auth.authorize) throw new McpServeError("MCP http auth needs `bearer` and/or `authorize`.", { type: "config" });
1417
+ const authRequired = tokens.length > 0 || auth?.authorize !== void 0;
1418
+ 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.`, {
1419
+ type: "config",
1420
+ context: { host }
1421
+ });
1422
+ return {
1423
+ host,
1424
+ port,
1425
+ path,
1426
+ loopback,
1427
+ maxBodyBytes,
1428
+ drainTimeoutMs,
1429
+ tokenDigests: tokens.map(digest),
1430
+ authorize: auth?.authorize,
1431
+ authRequired,
1432
+ allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
1433
+ allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name)))
1434
+ };
1435
+ }
1436
+ /** Flatten Node's header bag into lower-cased `name → value` strings. */
1437
+ function flattenHeaders(headers) {
1438
+ const flat = {};
1439
+ for (const [name, value] of Object.entries(headers)) {
1440
+ if (value === void 0) continue;
1441
+ flat[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
1442
+ }
1443
+ return flat;
1444
+ }
1445
+ /** Write a JSON body with a status code. */
1446
+ function sendJson(response, status, body, headers = {}) {
1447
+ if (response.headersSent || response.destroyed) return;
1448
+ const payload = JSON.stringify(body);
1449
+ response.writeHead(status, {
1450
+ ...headers,
1451
+ "content-type": "application/json",
1452
+ "content-length": Buffer.byteLength(payload).toString()
1453
+ });
1454
+ response.end(payload);
1455
+ }
1456
+ /**
1457
+ * Write an HTTP-level refusal: the status plus a JSON-RPC error body with
1458
+ * no request id (the spec allows such a body on 4xx transport errors).
1459
+ */
1460
+ function sendRefusal(response, status, code, message, headers = {}) {
1461
+ sendJson(response, status, {
1462
+ jsonrpc: "2.0",
1463
+ id: null,
1464
+ error: {
1465
+ code,
1466
+ message
1467
+ }
1468
+ }, headers);
1469
+ }
1470
+ /** Write an empty-bodied status (202 / 405). */
1471
+ function sendEmpty(response, status, headers = {}) {
1472
+ if (response.headersSent || response.destroyed) return;
1473
+ response.writeHead(status, headers);
1474
+ response.end();
1475
+ }
1476
+ /**
1477
+ * Bind an MCP Streamable HTTP endpoint and pump each JSON-RPC request
1478
+ * through `dispatch`. Resolves once the port is bound.
1479
+ *
1480
+ * Throws {@link McpServeError}: `"config"` for invalid / unsafe options
1481
+ * (checked before anything binds), `"listen"` when binding fails.
1482
+ *
1483
+ * @param transport - The http transport options.
1484
+ * @param dispatch - Maps one JSON-RPC request (plus call context) to its response.
1485
+ */
1486
+ async function startHttpServer(transport, dispatch) {
1487
+ const settings = resolveSettings(transport);
1488
+ const shutdown = new AbortController();
1489
+ const inFlight = /* @__PURE__ */ new Set();
1490
+ let closing;
1491
+ const server = (0, node_http.createServer)((request, response) => {
1492
+ if (closing) {
1493
+ sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, "Server is shutting down.", { connection: "close" });
1494
+ return;
1495
+ }
1496
+ const work = handleRequest(request, response, settings, dispatch, shutdown.signal).catch(() => {
1497
+ sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, "Internal server error.");
1498
+ });
1499
+ inFlight.add(work);
1500
+ work.finally(() => {
1501
+ inFlight.delete(work);
1502
+ if (closing) server.closeIdleConnections();
1503
+ });
1504
+ });
1505
+ await new Promise((resolve, reject) => {
1506
+ const onError = (cause) => {
1507
+ reject(new McpServeError(`MCP http server could not listen on ${settings.host}:${settings.port}: ${cause.message}`, {
1508
+ type: "listen",
1509
+ cause,
1510
+ context: {
1511
+ host: settings.host,
1512
+ port: settings.port
1513
+ }
1514
+ }));
1515
+ };
1516
+ server.once("error", onError);
1517
+ server.listen(settings.port, settings.host, () => {
1518
+ server.off("error", onError);
1519
+ resolve();
1520
+ });
1521
+ });
1522
+ const address = server.address();
1523
+ const boundPort = typeof address === "object" && address !== null ? address.port : settings.port;
1524
+ const url = `http://${(0, node_net.isIPv6)(settings.host) ? `[${settings.host}]` : settings.host}:${boundPort}${settings.path}`;
1525
+ const close = () => {
1526
+ if (closing) return closing;
1527
+ closing = (async () => {
1528
+ const closed = new Promise((resolve) => {
1529
+ server.close(() => resolve());
1530
+ });
1531
+ server.closeIdleConnections();
1532
+ if (inFlight.size > 0) {
1533
+ let timer;
1534
+ const timedOut = new Promise((resolve) => {
1535
+ timer = setTimeout(() => resolve("timeout"), settings.drainTimeoutMs);
1536
+ });
1537
+ const outcome = await Promise.race([Promise.allSettled([...inFlight]).then(() => "drained"), timedOut]);
1538
+ clearTimeout(timer);
1539
+ if (outcome === "timeout") {
1540
+ shutdown.abort();
1541
+ server.closeAllConnections();
1542
+ }
1543
+ }
1544
+ server.closeIdleConnections();
1545
+ await closed;
1546
+ })();
1547
+ return closing;
1548
+ };
1549
+ return {
1550
+ url,
1551
+ close
1552
+ };
1553
+ }
1554
+ /**
1555
+ * Validate and answer one HTTP request: path → Host / Origin → auth →
1556
+ * method → headers → body → JSON-RPC dispatch.
1557
+ */
1558
+ async function handleRequest(request, response, settings, dispatch, signal) {
1559
+ const headers = flattenHeaders(request.headers);
1560
+ const method = (request.method ?? "GET").toUpperCase();
1561
+ const path = new URL(request.url ?? "/", "http://localhost").pathname;
1562
+ if (!hostAllowed(headers.host, settings)) {
1563
+ sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Host header not allowed.");
1564
+ return;
1565
+ }
1566
+ if (!originAllowed(headers.origin, settings)) {
1567
+ sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Origin not allowed.");
1568
+ return;
1569
+ }
1570
+ if (path !== settings.path) {
1571
+ sendRefusal(response, 404, JSON_RPC_SERVER_ERROR, "Not found.");
1572
+ return;
1573
+ }
1574
+ const info = {
1575
+ method,
1576
+ path,
1577
+ headers
1578
+ };
1579
+ if (request.socket.remoteAddress !== void 0) info.remoteAddress = request.socket.remoteAddress;
1580
+ if (!await authorized(info, settings)) {
1581
+ const challenge = settings.tokenDigests.length > 0 ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {};
1582
+ sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", challenge);
1583
+ return;
1584
+ }
1585
+ if (method !== "POST") {
1586
+ sendEmpty(response, 405, { allow: "POST" });
1587
+ return;
1588
+ }
1589
+ const protocolVersion = headers["mcp-protocol-version"];
1590
+ if (protocolVersion !== void 0 && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
1591
+ sendRefusal(response, 400, JSON_RPC_SERVER_ERROR, `Unsupported MCP-Protocol-Version "${protocolVersion}" (supported: ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")}).`);
1592
+ return;
1593
+ }
1594
+ if (!isJsonContentType(headers["content-type"])) {
1595
+ sendRefusal(response, 415, JSON_RPC_SERVER_ERROR, "Content-Type must be application/json.");
1596
+ return;
1597
+ }
1598
+ if (!acceptsJson(headers.accept)) {
1599
+ sendRefusal(response, 406, JSON_RPC_SERVER_ERROR, "Accept must allow application/json.");
1600
+ return;
1601
+ }
1602
+ const body = await readBody$1(request, settings.maxBodyBytes);
1603
+ if (body.type === "too-large") {
1604
+ sendRefusal(response, 413, JSON_RPC_SERVER_ERROR, `Request body exceeds ${settings.maxBodyBytes} bytes.`, { connection: "close" });
1605
+ return;
1606
+ }
1607
+ let message;
1608
+ try {
1609
+ message = JSON.parse(body.text);
1610
+ } catch {
1611
+ sendRefusal(response, 400, JSON_RPC_PARSE_ERROR, "Parse error: body is not valid JSON.");
1612
+ return;
1613
+ }
1614
+ if (typeof message !== "object" || message === null || Array.isArray(message)) {
1615
+ sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST$1, "Invalid request: the body must be one JSON-RPC message (batches are not supported).");
1616
+ return;
1617
+ }
1618
+ const record = message;
1619
+ if (record.jsonrpc !== "2.0") {
1620
+ sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST$1, "Invalid request: jsonrpc must be \"2.0\".");
1621
+ return;
1622
+ }
1623
+ const id = record.id;
1624
+ if (typeof record.method === "string") {
1625
+ if (typeof id !== "string" && typeof id !== "number") {
1626
+ sendEmpty(response, 202);
1627
+ return;
1628
+ }
1629
+ const rpcRequest = {
1630
+ jsonrpc: "2.0",
1631
+ id,
1632
+ method: record.method
1633
+ };
1634
+ if (record.params !== void 0) rpcRequest.params = record.params;
1635
+ sendJson(response, 200, await dispatch(rpcRequest, {
1636
+ request: info,
1637
+ signal
1638
+ }));
1639
+ return;
1640
+ }
1641
+ if ((typeof id === "string" || typeof id === "number") && ("result" in record || "error" in record)) {
1642
+ sendEmpty(response, 202);
1643
+ return;
1644
+ }
1645
+ sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST$1, "Invalid request: not a JSON-RPC message.");
1646
+ }
1647
+ /**
1648
+ * Host-header check. While bound to loopback only loopback names (plus
1649
+ * `allowedHosts`) are accepted, so a DNS-rebound name pointing at
1650
+ * 127.0.0.1 is refused. Off loopback the check applies only when
1651
+ * `allowedHosts` is set.
1652
+ */
1653
+ function hostAllowed(hostHeader, settings) {
1654
+ if (!settings.loopback && settings.allowedHosts.size === 0) return true;
1655
+ if (hostHeader === void 0) return false;
1656
+ const name = hostnameOf(hostHeader);
1657
+ if (settings.allowedHosts.has(name)) return true;
1658
+ return settings.loopback && isLoopbackHost(name);
1659
+ }
1660
+ /**
1661
+ * Origin check (MCP Streamable HTTP security warning). No `Origin` header —
1662
+ * a non-browser client — passes; otherwise the origin must be a loopback
1663
+ * `http(s)` origin or listed in `allowedOrigins`. `null` and unparsable
1664
+ * origins are refused.
1665
+ */
1666
+ function originAllowed(origin, settings) {
1667
+ if (origin === void 0) return true;
1668
+ let parsed;
1669
+ try {
1670
+ parsed = new URL(origin);
1671
+ } catch {
1672
+ return false;
1673
+ }
1674
+ if (settings.allowedOrigins.has(parsed.origin)) return true;
1675
+ return (parsed.protocol === "http:" || parsed.protocol === "https:") && isLoopbackHost(parsed.hostname);
1676
+ }
1677
+ /**
1678
+ * Run the configured auth. Bearer tokens are compared in constant time
1679
+ * over SHA-256 digests (equal-length buffers for `timingSafeEqual`), and
1680
+ * every configured token is compared so the match position does not leak.
1681
+ * An `authorize` hook that throws denies the request.
1682
+ */
1683
+ async function authorized(info, settings) {
1684
+ if (!settings.authRequired) return true;
1685
+ if (settings.tokenDigests.length > 0) {
1686
+ const match = /^Bearer[ \t]+(.+)$/i.exec(info.headers.authorization ?? "");
1687
+ const presented = digest(match?.[1]?.trim() ?? "");
1688
+ let matched = false;
1689
+ for (const expected of settings.tokenDigests) matched = (0, node_crypto.timingSafeEqual)(presented, expected) || matched;
1690
+ if (!match || !matched) return false;
1691
+ }
1692
+ if (settings.authorize) try {
1693
+ return await settings.authorize(info) === true;
1694
+ } catch {
1695
+ return false;
1696
+ }
1697
+ return true;
1698
+ }
1699
+ /** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */
1700
+ function isJsonContentType(contentType) {
1701
+ return contentType?.split(";")[0]?.trim().toLowerCase() === "application/json";
1702
+ }
1703
+ /**
1704
+ * True when the `Accept` header allows a JSON reply. Clients are required
1705
+ * to list both `application/json` and `text/event-stream`; this server
1706
+ * always answers JSON, so it only insists on that half. A missing header
1707
+ * is accepted.
1708
+ */
1709
+ function acceptsJson(accept) {
1710
+ if (accept === void 0 || accept.trim() === "") return true;
1711
+ return accept.split(",").map((part) => part.split(";")[0]?.trim().toLowerCase()).some((type) => type === "application/json" || type === "application/*" || type === "*/*");
1712
+ }
1713
+ /**
1714
+ * Read the request body as UTF-8, refusing anything over `limit` bytes —
1715
+ * up front from `Content-Length`, and while streaming for chunked bodies.
1716
+ * A refused body is left unread; the 413 response closes the connection.
1717
+ */
1718
+ function readBody$1(request, limit) {
1719
+ const declared = Number(request.headers["content-length"]);
1720
+ if (Number.isFinite(declared) && declared > limit) return Promise.resolve({ type: "too-large" });
1721
+ return new Promise((resolve, reject) => {
1722
+ const chunks = [];
1723
+ let size = 0;
1724
+ let done = false;
1725
+ const finish = (result) => {
1726
+ if (!done) {
1727
+ done = true;
1728
+ resolve(result);
1729
+ }
1730
+ };
1731
+ request.on("data", (chunk) => {
1732
+ if (done) return;
1733
+ size += chunk.length;
1734
+ if (size > limit) {
1735
+ request.pause();
1736
+ finish({ type: "too-large" });
1737
+ return;
1738
+ }
1739
+ chunks.push(chunk);
1740
+ });
1741
+ request.on("end", () => finish({
1742
+ type: "ok",
1743
+ text: Buffer.concat(chunks).toString("utf8")
1744
+ }));
1745
+ request.on("error", (error) => {
1746
+ if (!done) {
1747
+ done = true;
1748
+ reject(error);
1749
+ }
1750
+ });
1751
+ });
1752
+ }
1753
+
1754
+ //#endregion
1755
+ //#region ../../packages/ai-tools/src/mcp/result.ts
1756
+ const NATIVE_RESULT = Symbol("mcpResult");
1757
+ /**
1758
+ * Opt in to native MCP content, error status, structured output and metadata.
1759
+ * Validation happens after invoke(), so malformed payloads become JSON-RPC
1760
+ * errors rather than being caught as tool execution errors.
1761
+ */
1762
+ function mcpResult(result) {
1763
+ return { [NATIVE_RESULT]: result };
1764
+ }
1765
+ /** Read a branded value without accepting lookalike application objects. */
1766
+ function isMcpResult(value) {
1767
+ return isRecord(value) && NATIVE_RESULT in value;
1768
+ }
1769
+ /** Get the original payload; no cloning, normalization or field stripping. */
1770
+ function unwrapMcpResult(value) {
1771
+ return value[NATIVE_RESULT];
1772
+ }
1773
+ /** Objects with result fields are candidates only when native mode is enabled. */
1774
+ function looksLikeMcpResult(value) {
1775
+ return isRecord(value) && [
1776
+ "content",
1777
+ "isError",
1778
+ "structuredContent",
1779
+ "_meta"
1780
+ ].some((key) => key in value);
1781
+ }
1782
+ /** Non-array object used by the MCP JSON object fields. */
1783
+ function isRecord(value) {
1784
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1785
+ }
1786
+ function optionalString(value) {
1787
+ return value === void 0 || typeof value === "string";
1788
+ }
1789
+ function validAnnotations(value) {
1790
+ if (value === void 0) return true;
1791
+ if (!isRecord(value)) return false;
1792
+ 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);
1793
+ }
1794
+ function validMetadata(value) {
1795
+ return (value["_meta"] === void 0 || isRecord(value["_meta"])) && validAnnotations(value.annotations);
1796
+ }
1797
+ function validIcon(value) {
1798
+ 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");
1799
+ }
1800
+ function validContent(value) {
1801
+ if (!isRecord(value) || !validMetadata(value)) return false;
1802
+ switch (value.type) {
1803
+ case "text": return typeof value.text === "string";
1804
+ case "image":
1805
+ case "audio": return typeof value.data === "string" && typeof value.mimeType === "string";
1806
+ 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));
1807
+ case "resource": {
1808
+ const resource = value.resource;
1809
+ return isRecord(resource) && typeof resource.uri === "string" && optionalString(resource.mimeType) && validMetadata(resource) && (typeof resource.text === "string" || typeof resource.blob === "string");
1810
+ }
1811
+ default: return false;
1812
+ }
1813
+ }
1814
+ /** Reject values JSON would drop, coerce or fail to encode (including cycles). */
1815
+ function isJsonValue(value, ancestors = /* @__PURE__ */ new Set()) {
1816
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1817
+ if (typeof value === "number") return Number.isFinite(value);
1818
+ if (typeof value !== "object" || ancestors.has(value)) return false;
1819
+ const prototype = Object.getPrototypeOf(value);
1820
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
1821
+ ancestors.add(value);
1822
+ const valid = Array.isArray(value) ? Array.from(value).every((entry) => isJsonValue(entry, ancestors)) : Object.values(value).every((entry) => entry === void 0 || isJsonValue(entry, ancestors));
1823
+ ancestors.delete(value);
1824
+ return valid;
1825
+ }
1826
+ /**
1827
+ * MCP 2025-11-25 CallToolResult and ContentBlock fields:
1828
+ * https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-result
1829
+ * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts
1830
+ * structuredContent is preserved even when tools/list declares outputSchema.
1831
+ * Tool authors remain responsible for conformity to that application schema.
1832
+ */
1833
+ function assertMcpResult(value) {
1834
+ 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.");
1835
+ }
1836
+
1236
1837
  //#endregion
1237
1838
  //#region ../../packages/ai-tools/src/mcp/serve.ts
1238
1839
  /** JSON-RPC version literal every outbound message carries. */
@@ -1241,59 +1842,156 @@ const JSONRPC_VERSION = "2.0";
1241
1842
  const DEFAULT_SCHEMA_TARGET = "draft-2020-12";
1242
1843
  /** Default advertised server version when the caller omits one. */
1243
1844
  const DEFAULT_VERSION = "4.4.0";
1244
- /** The MCP protocol version this server advertises in `initialize`. */
1245
- const PROTOCOL_VERSION = "2025-06-18";
1246
1845
  /** JSON-RPC standard error codes we emit. */
1247
1846
  const JSON_RPC_METHOD_NOT_FOUND = -32601;
1248
1847
  const JSON_RPC_INVALID_PARAMS = -32602;
1848
+ /** MCP's "resource not found" error code (server/resources, error handling). */
1849
+ const MCP_RESOURCE_NOT_FOUND = -32002;
1249
1850
  /**
1250
- * Resolve the {@link McpServeSource} (either an object exposing `tools()`
1251
- * or a literal `ToolContract[]`) into a flat contract array.
1851
+ * A JSON-RPC failure raised inside a method handler, carrying the exact
1852
+ * code to answer with (unknown resource, missing prompt argument, …).
1252
1853
  */
1253
- function resolveTools(source) {
1254
- if (Array.isArray(source)) return source;
1255
- return source.tools();
1854
+ var RpcFailure = class extends Error {
1855
+ constructor(code, message, data) {
1856
+ super(message);
1857
+ this.code = code;
1858
+ this.data = data;
1859
+ }
1860
+ };
1861
+ /** Enumerate one tool set into a flat contract array. */
1862
+ function toolsOf(source) {
1863
+ return Array.isArray(source) ? source : source.tools();
1864
+ }
1865
+ /** Narrow a serve source to its grouped shape. */
1866
+ function isGroupedSource(source) {
1867
+ return !Array.isArray(source) && "groups" in source;
1868
+ }
1869
+ /**
1870
+ * Resolve the serve source into the exposed tool list, applying
1871
+ * `prefixToolNames` and refusing duplicate names. Throws
1872
+ * {@link McpServeError} — `"config"` for `prefixToolNames` on an ungrouped
1873
+ * source, `"collision"` when two tools would share a name.
1874
+ */
1875
+ function resolveTools(source, options) {
1876
+ const prefix = options.prefixToolNames ?? false;
1877
+ const exposed = [];
1878
+ if (isGroupedSource(source)) for (const [group, groupSource] of Object.entries(source.groups)) for (const contract of toolsOf(groupSource)) {
1879
+ let name = contract.name;
1880
+ if (prefix === true) name = `${group}_${contract.name}`;
1881
+ else if (typeof prefix === "function") name = prefix(group, contract.name);
1882
+ exposed.push({
1883
+ name,
1884
+ contract,
1885
+ origin: `group "${group}"`
1886
+ });
1887
+ }
1888
+ else {
1889
+ if (prefix !== false) throw new McpServeError("prefixToolNames needs a grouped source: serve({ groups: { name: tools } }, …).", { type: "config" });
1890
+ for (const contract of toolsOf(source)) exposed.push({
1891
+ name: contract.name,
1892
+ contract,
1893
+ origin: "the source"
1894
+ });
1895
+ }
1896
+ const seen = /* @__PURE__ */ new Map();
1897
+ for (const tool of exposed) {
1898
+ const previous = seen.get(tool.name);
1899
+ if (previous !== void 0) throw new McpServeError(`MCP tool name "${tool.name}" is exposed twice (from ${previous} and ${tool.origin}). Rename one tool, or serve a grouped source with prefixToolNames.`, {
1900
+ type: "collision",
1901
+ context: { name: tool.name }
1902
+ });
1903
+ seen.set(tool.name, tool.origin);
1904
+ }
1905
+ return exposed.map(({ name, contract }) => ({
1906
+ name,
1907
+ contract
1908
+ }));
1909
+ }
1910
+ /** Throw a collision error when two entries share the same key. */
1911
+ function assertUnique(entries, key, label) {
1912
+ const seen = /* @__PURE__ */ new Set();
1913
+ for (const entry of entries ?? []) {
1914
+ const value = key(entry);
1915
+ if (seen.has(value)) throw new McpServeError(`MCP ${label} "${value}" is declared twice.`, {
1916
+ type: "collision",
1917
+ context: { [label]: value }
1918
+ });
1919
+ seen.add(value);
1920
+ }
1256
1921
  }
1257
1922
  /**
1258
1923
  * The pure protocol core of `serve` — maps one JSON-RPC request to its
1259
- * response, with no I/O. Both the stdio and http serve-transports pump
1924
+ * response, with no I/O. Both the stdio and HTTP serve-transports pump
1260
1925
  * their inbound requests through this, and specs can drive it directly.
1261
1926
  *
1262
- * Handles exactly the MCP slice this package serves: `initialize`,
1263
- * `tools/list`, and `tools/call`. Any other method answers with a
1264
- * JSON-RPC `method not found` error.
1927
+ * Handles `initialize`, `ping`, `tools/list`, `tools/call`, and — when
1928
+ * configured — `resources/list`, `resources/templates/list`,
1929
+ * `resources/read`, `prompts/list` and `prompts/get`. Any other method
1930
+ * answers with a JSON-RPC `method not found` error.
1265
1931
  *
1266
1932
  * Constructed via {@link createServeHandler}.
1267
1933
  */
1268
1934
  var McpServeHandler = class {
1269
1935
  constructor(source, options) {
1270
- this.tools = resolveTools(source);
1271
- this.byName = new Map(this.tools.map((contract) => [contract.name, contract]));
1936
+ this.tools = resolveTools(source, options);
1937
+ this.byName = new Map(this.tools.map((tool) => [tool.name, tool.contract]));
1938
+ assertUnique(options.resources, (resource) => resource.uri, "resource");
1939
+ assertUnique(options.prompts, (prompt) => prompt.name, "prompt");
1940
+ this.resources = options.resources ? new Map(options.resources.map((resource) => [resource.uri, resource])) : void 0;
1941
+ this.prompts = options.prompts ? new Map(options.prompts.map((prompt) => [prompt.name, prompt])) : void 0;
1272
1942
  this.options = options;
1273
1943
  }
1274
1944
  /**
1275
1945
  * Dispatch one inbound JSON-RPC request to its handler and produce the
1276
1946
  * response. A handler that throws is mapped to a JSON-RPC error response
1277
1947
  * — the serve loop never crashes on a bad request.
1948
+ *
1949
+ * @param request - The JSON-RPC request.
1950
+ * @param context - Per-call context: the inbound HTTP request (headers
1951
+ * reach tools as `ctx.request`) and an abort signal.
1278
1952
  */
1279
- async handle(request) {
1953
+ async handle(request, context = {}) {
1280
1954
  try {
1281
- switch (request.method) {
1282
- case "initialize": return this.ok(request.id, this.initializeResult());
1283
- case "tools/list": return this.ok(request.id, { tools: this.listTools() });
1284
- case "tools/call": return this.ok(request.id, await this.callTool(request.params));
1285
- default: return this.error(request.id, JSON_RPC_METHOD_NOT_FOUND, `Method "${request.method}" is not supported by this MCP server.`);
1286
- }
1955
+ return this.ok(request.id, await this.dispatch(request, context));
1287
1956
  } catch (cause) {
1957
+ if (cause instanceof RpcFailure) return this.error(request.id, cause.code, cause.message, cause.data);
1288
1958
  const message = cause instanceof Error ? cause.message : String(cause);
1289
1959
  return this.error(request.id, JSON_RPC_INVALID_PARAMS, message);
1290
1960
  }
1291
1961
  }
1292
- /** Build the `initialize` result advertising name / version / capabilities. */
1293
- initializeResult() {
1962
+ /** Route a method to its result builder. */
1963
+ async dispatch(request, context) {
1964
+ switch (request.method) {
1965
+ case "initialize": return this.initializeResult(request.params);
1966
+ case "ping": return {};
1967
+ case "tools/list": return { tools: this.listTools() };
1968
+ case "tools/call": return this.callTool(request.params, context);
1969
+ }
1970
+ if (this.resources) switch (request.method) {
1971
+ case "resources/list": return { resources: this.listResources(this.resources) };
1972
+ case "resources/templates/list": return { resourceTemplates: [] };
1973
+ case "resources/read": return this.readResource(this.resources, request.params, context);
1974
+ }
1975
+ if (this.prompts) switch (request.method) {
1976
+ case "prompts/list": return { prompts: this.listPrompts(this.prompts) };
1977
+ case "prompts/get": return this.getPrompt(this.prompts, request.params, context);
1978
+ }
1979
+ throw new RpcFailure(JSON_RPC_METHOD_NOT_FOUND, `Method "${request.method}" is not supported by this MCP server.`);
1980
+ }
1981
+ /**
1982
+ * Build the `initialize` result advertising name / version /
1983
+ * capabilities, negotiating the protocol version: the client's requested
1984
+ * version when supported, otherwise the newest this server speaks.
1985
+ */
1986
+ initializeResult(params) {
1987
+ const requested = readRecord(params).protocolVersion;
1988
+ const protocolVersion = typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : LATEST_PROTOCOL_VERSION;
1989
+ const capabilities = { tools: {} };
1990
+ if (this.resources) capabilities.resources = {};
1991
+ if (this.prompts) capabilities.prompts = {};
1294
1992
  return {
1295
- protocolVersion: PROTOCOL_VERSION,
1296
- capabilities: { tools: {} },
1993
+ protocolVersion,
1994
+ capabilities,
1297
1995
  serverInfo: {
1298
1996
  name: this.options.name,
1299
1997
  version: this.options.version ?? DEFAULT_VERSION
@@ -1302,38 +2000,52 @@ var McpServeHandler = class {
1302
2000
  }
1303
2001
  /**
1304
2002
  * Build the `tools/list` payload: one {@link McpToolDescriptor} per
1305
- * contract, its `inputSchema` extracted via `extractJsonSchema` at the
1306
- * configured dialect (default `draft-2020-12` — overriding
2003
+ * exposed tool, its `inputSchema` extracted via `extractJsonSchema` at
2004
+ * the configured dialect (default `draft-2020-12` — overriding
1307
2005
  * `extractJsonSchema`'s own `openai-strict` default to a neutral MCP draft).
1308
2006
  */
1309
2007
  listTools() {
1310
2008
  const target = this.options.schemaTarget ?? DEFAULT_SCHEMA_TARGET;
1311
- return this.tools.map((contract) => {
2009
+ return this.tools.map(({ name, contract }) => {
1312
2010
  const inputSchema = (0, _warlock_js_ai.extractJsonSchema)(contract.input, { target }) ?? {
1313
2011
  type: "object",
1314
2012
  properties: {}
1315
2013
  };
1316
2014
  return {
1317
- name: contract.name,
2015
+ name,
1318
2016
  description: contract.description,
1319
- inputSchema
2017
+ inputSchema,
2018
+ ...isRecord(contract.meta?.outputSchema) ? { outputSchema: contract.meta.outputSchema } : {}
1320
2019
  };
1321
2020
  });
1322
2021
  }
1323
2022
  /**
1324
2023
  * Route a `tools/call` to the named contract's `invoke()` and map the
1325
2024
  * never-throwing {@link import("@warlock.js/ai").ToolInvokeResult}: `data`
1326
- * → a text content block, `error` → an `isError: true` result. An unknown
1327
- * tool name throws (mapped to a JSON-RPC error by {@link handle}).
2025
+ * → a text content block (or an opted-in native result), `error` → an
2026
+ * `isError: true` result. An unknown tool name throws (mapped to a
2027
+ * JSON-RPC error by {@link handle}).
2028
+ *
2029
+ * The contract receives a `ToolContext` carrying the abort signal and,
2030
+ * over HTTP, the inbound request's headers on `ctx.request`.
1328
2031
  */
1329
- async callTool(params) {
2032
+ async callTool(params, context) {
1330
2033
  const { name, args } = readCallParams(params);
1331
2034
  const contract = this.byName.get(name);
1332
2035
  if (!contract) throw new McpTransportError(`Unknown tool "${name}".`, {
1333
2036
  type: "protocol",
1334
2037
  method: "tools/call"
1335
2038
  });
1336
- const result = await contract.invoke(args);
2039
+ const toolContext = { artifacts: {} };
2040
+ if (context.signal) toolContext.signal = context.signal;
2041
+ if (context.request) {
2042
+ const { headers, remoteAddress } = context.request;
2043
+ toolContext.request = remoteAddress === void 0 ? { headers } : {
2044
+ headers,
2045
+ remoteAddress
2046
+ };
2047
+ }
2048
+ const result = await contract.invoke(args, toolContext);
1337
2049
  if (result.error) return {
1338
2050
  content: [{
1339
2051
  type: "text",
@@ -1341,6 +2053,16 @@ var McpServeHandler = class {
1341
2053
  }],
1342
2054
  isError: true
1343
2055
  };
2056
+ const mode = contract.meta?.mcpResults ?? this.options.results ?? "text";
2057
+ if (isMcpResult(result.data) || mode === "native" && looksLikeMcpResult(result.data)) {
2058
+ const native = isMcpResult(result.data) ? unwrapMcpResult(result.data) : result.data;
2059
+ try {
2060
+ assertMcpResult(native);
2061
+ } catch {
2062
+ throw new RpcFailure(JSON_RPC_INTERNAL_ERROR, "Invalid native MCP CallToolResult.");
2063
+ }
2064
+ return native;
2065
+ }
1344
2066
  return {
1345
2067
  content: [{
1346
2068
  type: "text",
@@ -1349,6 +2071,75 @@ var McpServeHandler = class {
1349
2071
  isError: false
1350
2072
  };
1351
2073
  }
2074
+ /** Build the `resources/list` payload (metadata only, no contents). */
2075
+ listResources(resources) {
2076
+ return [...resources.values()].map((resource) => compact({
2077
+ uri: resource.uri,
2078
+ name: resource.name,
2079
+ title: resource.title,
2080
+ description: resource.description,
2081
+ mimeType: resource.mimeType
2082
+ }));
2083
+ }
2084
+ /**
2085
+ * Answer `resources/read`: run the resource's `read()` and wrap the
2086
+ * result as one text or base64 blob content entry.
2087
+ */
2088
+ async readResource(resources, params, context) {
2089
+ const uri = readRecord(params).uri;
2090
+ if (typeof uri !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, "resources/read requires a string `uri`.");
2091
+ const resource = resources.get(uri);
2092
+ if (!resource) throw new RpcFailure(MCP_RESOURCE_NOT_FOUND, "Resource not found", { uri });
2093
+ const body = await resource.read(context);
2094
+ return { contents: [compact(typeof body === "string" ? {
2095
+ uri,
2096
+ mimeType: resource.mimeType,
2097
+ text: body
2098
+ } : {
2099
+ uri,
2100
+ mimeType: resource.mimeType,
2101
+ blob: Buffer.from(body).toString("base64")
2102
+ })] };
2103
+ }
2104
+ /** Build the `prompts/list` payload. */
2105
+ listPrompts(prompts) {
2106
+ return [...prompts.values()].map((prompt) => compact({
2107
+ name: prompt.name,
2108
+ title: prompt.title,
2109
+ description: prompt.description,
2110
+ arguments: prompt.arguments
2111
+ }));
2112
+ }
2113
+ /**
2114
+ * Answer `prompts/get`: validate the arguments (strings only; required
2115
+ * ones present) and build the messages. An unknown prompt or a missing
2116
+ * required argument answers invalid-params, per the MCP prompts spec.
2117
+ */
2118
+ async getPrompt(prompts, params, context) {
2119
+ const record = readRecord(params);
2120
+ const name = record.name;
2121
+ if (typeof name !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, "prompts/get requires a string `name`.");
2122
+ const prompt = prompts.get(name);
2123
+ if (!prompt) throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Unknown prompt "${name}".`);
2124
+ const args = {};
2125
+ for (const [key, value] of Object.entries(readRecord(record.arguments))) {
2126
+ if (typeof value !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Prompt argument "${key}" must be a string.`);
2127
+ args[key] = value;
2128
+ }
2129
+ for (const argument of prompt.arguments ?? []) if (argument.required && args[argument.name] === void 0) throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Prompt "${name}" requires the "${argument.name}" argument.`);
2130
+ const built = await prompt.get(args, context);
2131
+ const messages = typeof built === "string" ? [{
2132
+ role: "user",
2133
+ content: {
2134
+ type: "text",
2135
+ text: built
2136
+ }
2137
+ }] : built;
2138
+ return compact({
2139
+ description: prompt.description,
2140
+ messages
2141
+ });
2142
+ }
1352
2143
  /** Build a JSON-RPC success response. */
1353
2144
  ok(id, result) {
1354
2145
  return {
@@ -1358,17 +2149,77 @@ var McpServeHandler = class {
1358
2149
  };
1359
2150
  }
1360
2151
  /** Build a JSON-RPC error response. */
1361
- error(id, code, message) {
2152
+ error(id, code, message, data) {
1362
2153
  return {
1363
2154
  jsonrpc: JSONRPC_VERSION,
1364
2155
  id,
1365
- error: {
2156
+ error: data === void 0 ? {
1366
2157
  code,
1367
2158
  message
2159
+ } : {
2160
+ code,
2161
+ message,
2162
+ data
1368
2163
  }
1369
2164
  };
1370
2165
  }
1371
2166
  };
2167
+ /** JSON-RPC "Invalid Request" — the line is JSON but not a request object. */
2168
+ const JSON_RPC_INVALID_REQUEST = -32600;
2169
+ /** JSON-RPC "Internal error" — answering the request failed unexpectedly. */
2170
+ const JSON_RPC_INTERNAL_ERROR = -32603;
2171
+ /**
2172
+ * Turn one stdio line into the line to write back, or `undefined` when
2173
+ * nothing is answered: blank or non-JSON lines and notifications (no `id`).
2174
+ * A JSON value that is not a request object (`null`, a number, an array) is
2175
+ * answered with a JSON-RPC "Invalid Request" error whose `id` is `null`.
2176
+ * Never rejects: a dispatch failure is answered as an internal error.
2177
+ */
2178
+ async function answerStdioLine(line, dispatch) {
2179
+ const trimmed = line.trim();
2180
+ if (!trimmed) return;
2181
+ let message;
2182
+ try {
2183
+ message = JSON.parse(trimmed);
2184
+ } catch {
2185
+ return;
2186
+ }
2187
+ if (typeof message !== "object" || message === null || Array.isArray(message)) return `${JSON.stringify({
2188
+ jsonrpc: JSONRPC_VERSION,
2189
+ id: null,
2190
+ error: {
2191
+ code: JSON_RPC_INVALID_REQUEST,
2192
+ message: "Invalid request: not a JSON-RPC object."
2193
+ }
2194
+ })}\n`;
2195
+ const request = message;
2196
+ const id = request.id;
2197
+ if (id === void 0 || id === null) return;
2198
+ try {
2199
+ const response = await dispatch(request);
2200
+ return `${JSON.stringify(response)}\n`;
2201
+ } catch {
2202
+ return `${JSON.stringify({
2203
+ jsonrpc: JSONRPC_VERSION,
2204
+ id,
2205
+ error: {
2206
+ code: JSON_RPC_INTERNAL_ERROR,
2207
+ message: "Internal error."
2208
+ }
2209
+ })}\n`;
2210
+ }
2211
+ }
2212
+ /** Read an unknown value as a plain record (non-objects read as empty). */
2213
+ function readRecord(value) {
2214
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
2215
+ return value;
2216
+ }
2217
+ /** Drop `undefined` members so optional wire fields are omitted, not `null`. */
2218
+ function compact(value) {
2219
+ const result = {};
2220
+ for (const [key, member] of Object.entries(value)) if (member !== void 0) result[key] = member;
2221
+ return result;
2222
+ }
1372
2223
  /**
1373
2224
  * Read and validate the `tools/call` params into `{ name, args }`. Throws
1374
2225
  * a typed {@link McpTransportError} when `name` is missing — mapped to a
@@ -1404,9 +2255,12 @@ function serializeData(data) {
1404
2255
  * {@link serve}) so callers and tests can drive the MCP protocol without an
1405
2256
  * actual transport — feed it a JSON-RPC request, get the response.
1406
2257
  *
1407
- * @param source - The tools to expose (an object with `tools()` or a literal array).
1408
- * @param options - Serve options (name / version / schema target).
1409
- * @returns An object whose `handle(request)` maps a request to a response.
2258
+ * Throws {@link McpServeError} on duplicate tool names / resource URIs /
2259
+ * prompt names, or `prefixToolNames` on an ungrouped source.
2260
+ *
2261
+ * @param source - The tools to expose (one tool set or `{ groups }`).
2262
+ * @param options - Serve options (name / version / schema target / resources / prompts).
2263
+ * @returns An object whose `handle(request, context?)` maps a request to a response.
1410
2264
  */
1411
2265
  function createServeHandler(source, options) {
1412
2266
  return new McpServeHandler(source, options);
@@ -1414,79 +2268,66 @@ function createServeHandler(source, options) {
1414
2268
  /**
1415
2269
  * The internal {@link McpServer} — owns a {@link McpServeHandler} and a
1416
2270
  * transport pump. For `stdio` it reads newline-delimited JSON-RPC requests
1417
- * from `process.stdin` and writes responses to `process.stdout`; the
1418
- * `http` transport is accepted but listening is deferred to the host
1419
- * (a serve-over-HTTP needs a server the caller owns).
2271
+ * from `process.stdin` and writes responses to `process.stdout`; for
2272
+ * `http` it binds a Streamable HTTP endpoint (see `./serve-http`).
1420
2273
  *
1421
2274
  * Constructed via {@link serve}; the class itself is internal.
1422
2275
  */
1423
2276
  var McpServerImpl = class {
1424
2277
  constructor(source, options) {
1425
- this.running = false;
1426
2278
  this.handler = new McpServeHandler(source, options);
1427
2279
  this.options = options;
1428
2280
  }
1429
- async start() {
1430
- if (this.running) return;
1431
- if ((this.options.transport ?? { type: "stdio" }).type !== "stdio") throw new McpTransportError("serve() over http requires a host-provided server; only stdio is auto-pumped.", { type: "connect" });
1432
- this.running = true;
1433
- this.reader = (0, node_readline.createInterface)({ input: process.stdin });
1434
- this.reader.on("line", (line) => {
1435
- this.onLine(line);
2281
+ start() {
2282
+ if (!this.running) {
2283
+ const transport = this.options.transport ?? { type: "stdio" };
2284
+ const starting = transport.type === "http" ? this.startHttp(transport) : Promise.resolve(this.startStdio());
2285
+ this.running = starting.catch((error) => {
2286
+ this.running = void 0;
2287
+ throw error;
2288
+ });
2289
+ }
2290
+ return this.running;
2291
+ }
2292
+ /** Bind the HTTP endpoint; its `close()` also makes this server startable again. */
2293
+ async startHttp(transport) {
2294
+ const http = await startHttpServer(transport, (request, context) => this.handler.handle(request, context));
2295
+ return {
2296
+ url: http.url,
2297
+ close: async () => {
2298
+ await http.close();
2299
+ this.running = void 0;
2300
+ }
2301
+ };
2302
+ }
2303
+ /** Wire the stdin line reader and answer each request on stdout. */
2304
+ startStdio() {
2305
+ const reader = (0, node_readline.createInterface)({ input: process.stdin });
2306
+ reader.on("line", (line) => {
2307
+ this.onLine(line).catch(() => {});
1436
2308
  });
2309
+ return { close: async () => {
2310
+ reader.close();
2311
+ this.running = void 0;
2312
+ } };
1437
2313
  }
1438
- /**
1439
- * Parse one stdin line as a JSON-RPC request, dispatch it through the
1440
- * handler, and write the response as a single line to stdout. Non-JSON
1441
- * lines and notifications (no `id`) are ignored.
1442
- */
2314
+ /** Answer one stdin line on stdout (see {@link answerStdioLine}). */
1443
2315
  async onLine(line) {
1444
- const trimmed = line.trim();
1445
- if (!trimmed) return;
1446
- let request;
2316
+ const reply = await answerStdioLine(line, (request) => this.handler.handle(request));
2317
+ if (reply !== void 0) process.stdout.write(reply);
2318
+ }
2319
+ async stop() {
2320
+ const running = this.running;
2321
+ if (!running) return;
2322
+ let handle;
1447
2323
  try {
1448
- request = JSON.parse(trimmed);
2324
+ handle = await running;
1449
2325
  } catch {
1450
2326
  return;
1451
2327
  }
1452
- if (request.id === void 0 || request.id === null) return;
1453
- const response = await this.handler.handle(request);
1454
- process.stdout.write(`${JSON.stringify(response)}\n`);
1455
- }
1456
- async stop() {
1457
- this.running = false;
1458
- this.reader?.close();
1459
- this.reader = void 0;
2328
+ await handle.close();
1460
2329
  }
1461
2330
  };
1462
- /**
1463
- * Expose a built agent / supervisor / orchestrator (or a raw
1464
- * `ToolContract[]`) AS an MCP server (Direction B: local primitive → MCP
1465
- * server other clients consume).
1466
- *
1467
- * Enumerates `source.tools()` (or the literal array) once at construction.
1468
- * `tools/list` answers with each tool's `inputSchema` extracted via
1469
- * `extractJsonSchema` at the configured `schemaTarget` (default
1470
- * `draft-2020-12`). `tools/call` routes to the named contract's
1471
- * `invoke()` and maps the never-throwing result — `data` becomes a text
1472
- * content block, `error` becomes an `isError: true` result — so a failing
1473
- * tool surfaces as a normal MCP tool error rather than crashing the server.
1474
- *
1475
- * The default transport is `stdio`, pumped over `process.stdin` /
1476
- * `process.stdout`. Serving over HTTP is left to a host-owned server;
1477
- * `start()` rejects an `http` transport (the protocol core is available
1478
- * via {@link createServeHandler} for a caller's own HTTP wiring).
1479
- *
1480
- * @param source - The tools to expose.
1481
- * @param options - Server name, version, transport, and schema dialect.
1482
- * @returns An {@link McpServer} with `start()` / `stop()`.
1483
- *
1484
- * @example
1485
- * serve(
1486
- * { tools: () => ws.allTools() },
1487
- * { name: "warlock-workspace", transport: { type: "stdio" } },
1488
- * ).start();
1489
- */
1490
2331
  function serve(source, options) {
1491
2332
  return new McpServerImpl(source, options);
1492
2333
  }
@@ -1935,7 +2776,8 @@ const UNIT_ALIASES = {
1935
2776
  };
1936
2777
  /** Resolve a (possibly aliased / singular) unit token to its ms length. */
1937
2778
  function unitToMs(unit) {
1938
- const ms = UNIT_MS[UNIT_ALIASES[unit] ?? unit];
2779
+ const canonical = UNIT_ALIASES[unit] ?? unit;
2780
+ const ms = UNIT_MS[canonical];
1939
2781
  if (ms === void 0) throw new DateTimeError(`Unknown unit "${unit}". Supported units: ${Object.keys(UNIT_MS).join(", ")}.`, { type: "invalid-unit" });
1940
2782
  return ms;
1941
2783
  }
@@ -2375,7 +3217,8 @@ function fetchUrlTool(options) {
2375
3217
  type: "denied-host",
2376
3218
  cause
2377
3219
  });
2378
- throw new WebToolError(`fetch_url request failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
3220
+ const message = cause instanceof Error ? cause.message : String(cause);
3221
+ throw new WebToolError(`fetch_url request failed: ${message}`, {
2379
3222
  type: "request-failed",
2380
3223
  cause
2381
3224
  });
@@ -2439,7 +3282,8 @@ async function fetchJson(url, init, provider) {
2439
3282
  try {
2440
3283
  response = await fetch(url, init);
2441
3284
  } catch (cause) {
2442
- throw new WebToolError(`web_search request to the "${provider}" provider failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
3285
+ const message = cause instanceof Error ? cause.message : String(cause);
3286
+ throw new WebToolError(`web_search request to the "${provider}" provider failed: ${message}`, {
2443
3287
  type: "request-failed",
2444
3288
  cause
2445
3289
  });
@@ -2544,9 +3388,7 @@ function webSearchTool(options) {
2544
3388
  case "brave":
2545
3389
  results = await searchBrave(input.query, limit, apiKey);
2546
3390
  break;
2547
- case "serpapi":
2548
- results = await searchSerpApi(input.query, limit, apiKey);
2549
- break;
3391
+ case "serpapi": results = await searchSerpApi(input.query, limit, apiKey);
2550
3392
  }
2551
3393
  return { results };
2552
3394
  }
@@ -2560,7 +3402,8 @@ _warlock_js_ai.ai.tools = {
2560
3402
  fetchUrl: fetchUrlTool,
2561
3403
  http: httpRequestTool,
2562
3404
  calculator: calculatorTool,
2563
- dateTime: (options) => dateTimeTool(options)
3405
+ dateTime: (options) => dateTimeTool(options),
3406
+ measure: _warlock_js_ai.measureTools
2564
3407
  };
2565
3408
  _warlock_js_ai.ai.mcp = mcp;
2566
3409
 
@@ -2568,7 +3411,9 @@ _warlock_js_ai.ai.mcp = mcp;
2568
3411
  exports.CalculatorError = CalculatorError;
2569
3412
  exports.DateTimeError = DateTimeError;
2570
3413
  exports.HttpPolicyError = HttpPolicyError;
3414
+ exports.McpServeError = McpServeError;
2571
3415
  exports.McpTransportError = McpTransportError;
3416
+ exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;
2572
3417
  exports.WebToolError = WebToolError;
2573
3418
  exports.calculatorTool = calculatorTool;
2574
3419
  exports.createJsonRpcClient = createJsonRpcClient;
@@ -2579,6 +3424,7 @@ exports.fetchUrlTool = fetchUrlTool;
2579
3424
  exports.httpRequestTool = httpRequestTool;
2580
3425
  exports.jsonSchemaToStandard = jsonSchemaToStandard;
2581
3426
  exports.mcp = mcp;
3427
+ exports.mcpResult = mcpResult;
2582
3428
  exports.serve = serve;
2583
3429
  exports.webSearchTool = webSearchTool;
2584
3430
  //# sourceMappingURL=index.cjs.map