@warlock.js/ai-tools 5.30.0 → 5.31.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 (56) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/README.md +25 -13
  3. package/cjs/index.cjs +843 -101
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/contracts/index.d.mts +2 -1
  6. package/esm/contracts/mcp-serve.type.d.mts +257 -0
  7. package/esm/contracts/mcp-serve.type.d.mts.map +1 -0
  8. package/esm/contracts/mcp.type.d.mts +16 -59
  9. package/esm/contracts/mcp.type.d.mts.map +1 -1
  10. package/esm/errors.d.mts +36 -1
  11. package/esm/errors.d.mts.map +1 -1
  12. package/esm/errors.mjs +20 -1
  13. package/esm/errors.mjs.map +1 -1
  14. package/esm/http/http-request.d.mts.map +1 -1
  15. package/esm/http/http-request.mjs.map +1 -1
  16. package/esm/index.d.mts +6 -4
  17. package/esm/index.mjs +3 -2
  18. package/esm/mcp/client.mjs +35 -5
  19. package/esm/mcp/client.mjs.map +1 -1
  20. package/esm/mcp/index.d.mts +12 -5
  21. package/esm/mcp/index.d.mts.map +1 -1
  22. package/esm/mcp/index.mjs +1 -0
  23. package/esm/mcp/index.mjs.map +1 -1
  24. package/esm/mcp/protocol.d.mts +16 -0
  25. package/esm/mcp/protocol.d.mts.map +1 -0
  26. package/esm/mcp/protocol.mjs +23 -0
  27. package/esm/mcp/protocol.mjs.map +1 -0
  28. package/esm/mcp/serve-http.mjs +443 -0
  29. package/esm/mcp/serve-http.mjs.map +1 -0
  30. package/esm/mcp/serve.d.mts +45 -26
  31. package/esm/mcp/serve.d.mts.map +1 -1
  32. package/esm/mcp/serve.mjs +325 -92
  33. package/esm/mcp/serve.mjs.map +1 -1
  34. package/esm/mcp/transport.d.mts.map +1 -1
  35. package/esm/mcp/transport.mjs.map +1 -1
  36. package/esm/register.d.mts +12 -1
  37. package/esm/register.d.mts.map +1 -1
  38. package/esm/register.mjs +3 -2
  39. package/esm/register.mjs.map +1 -1
  40. package/esm/schema.mjs.map +1 -1
  41. package/esm/utility/calculator.d.mts.map +1 -1
  42. package/esm/utility/calculator.mjs.map +1 -1
  43. package/esm/utility/date-time.d.mts.map +1 -1
  44. package/esm/utility/date-time.mjs.map +1 -1
  45. package/esm/utility/schema.mjs.map +1 -1
  46. package/esm/web/fetch-url.d.mts.map +1 -1
  47. package/esm/web/fetch-url.mjs.map +1 -1
  48. package/esm/web/schema.mjs.map +1 -1
  49. package/esm/web/web-search.mjs.map +1 -1
  50. package/llms-full.txt +194 -57
  51. package/llms.txt +3 -3
  52. package/package.json +2 -6
  53. package/skills/connect-mcp-server/SKILL.md +28 -6
  54. package/skills/expose-as-mcp-server/SKILL.md +117 -36
  55. package/skills/index.md +5 -2
  56. 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.
@@ -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");
@@ -471,7 +493,7 @@ function httpRequestTool(options = {}) {
471
493
  });
472
494
  const { text, truncated } = await readCappedBody(response, maxBytes);
473
495
  let parsedBody = text;
474
- if (!truncated && isJsonContentType(responseHeaders["content-type"]) && text.length > 0) try {
496
+ if (!truncated && isJsonContentType$1(responseHeaders["content-type"]) && text.length > 0) try {
475
497
  parsedBody = JSON.parse(text);
476
498
  } catch {
477
499
  parsedBody = text;
@@ -1046,7 +1068,7 @@ function withIdAllocator(client) {
1046
1068
  /** Default per-call timeout for `tools/call`. */
1047
1069
  const DEFAULT_CALL_TIMEOUT_MS = 3e4;
1048
1070
  /** The MCP protocol version this client advertises in `initialize`. */
1049
- const PROTOCOL_VERSION$1 = "2025-06-18";
1071
+ const PROTOCOL_VERSION = "2025-06-18";
1050
1072
  /**
1051
1073
  * The internal {@link McpClient} implementation. Owns one JSON-RPC client
1052
1074
  * over a transport, runs the `initialize` handshake on first use, lists
@@ -1059,8 +1081,24 @@ const PROTOCOL_VERSION$1 = "2025-06-18";
1059
1081
  var McpClientImpl = class {
1060
1082
  constructor(source, options = {}) {
1061
1083
  this.initialized = false;
1084
+ this.onAbort = () => {
1085
+ this.close().catch(() => void 0);
1086
+ };
1062
1087
  this.rpc = createJsonRpcClient(source);
1063
1088
  this.options = options;
1089
+ const signal = options.signal;
1090
+ if (signal?.aborted) this.onAbort();
1091
+ else signal?.addEventListener("abort", this.onAbort, { once: true });
1092
+ }
1093
+ /**
1094
+ * Reject with a typed `"closed"` error once the client was closed or its
1095
+ * `signal` aborted, so no request is sent on a dead transport.
1096
+ */
1097
+ assertOpen(method) {
1098
+ if (this.closing || this.options.signal?.aborted) throw new McpTransportError(`MCP client is closed; "${method}" was not sent.`, {
1099
+ type: "closed",
1100
+ method
1101
+ });
1064
1102
  }
1065
1103
  /**
1066
1104
  * Run the MCP `initialize` handshake exactly once, then send the
@@ -1070,15 +1108,16 @@ var McpClientImpl = class {
1070
1108
  */
1071
1109
  async handshake() {
1072
1110
  if (this.initialized) return;
1111
+ this.assertOpen("initialize");
1073
1112
  try {
1074
1113
  await this.rpc.call("initialize", {
1075
- protocolVersion: PROTOCOL_VERSION$1,
1114
+ protocolVersion: PROTOCOL_VERSION,
1076
1115
  capabilities: {},
1077
1116
  clientInfo: {
1078
1117
  name: "@warlock.js/ai-tools",
1079
1118
  version: "4.4.0"
1080
1119
  }
1081
- });
1120
+ }, { signal: this.options.signal });
1082
1121
  await this.rpc.notify("notifications/initialized");
1083
1122
  } catch (cause) {
1084
1123
  if (cause instanceof McpTransportError) throw cause;
@@ -1107,7 +1146,8 @@ var McpClientImpl = class {
1107
1146
  */
1108
1147
  async listAndAdapt() {
1109
1148
  await this.handshake();
1110
- const descriptors = (await this.rpc.call("tools/list")).tools ?? [];
1149
+ this.assertOpen("tools/list");
1150
+ const descriptors = (await this.rpc.call("tools/list", void 0, { signal: this.options.signal })).tools ?? [];
1111
1151
  const filter = this.options.filter;
1112
1152
  return (filter ? descriptors.filter((d) => filter(d.name)) : descriptors).map((descriptor) => this.adapt(descriptor));
1113
1153
  }
@@ -1128,11 +1168,12 @@ var McpClientImpl = class {
1128
1168
  description: descriptor.description ?? `Invoke the remote MCP tool "${remoteName}".`,
1129
1169
  input,
1130
1170
  execute: async (args, ctx) => {
1171
+ this.assertOpen("tools/call");
1131
1172
  const result = await this.rpc.call("tools/call", {
1132
1173
  name: remoteName,
1133
1174
  arguments: args ?? {}
1134
1175
  }, {
1135
- signal: ctx?.signal,
1176
+ signal: combineSignals(ctx?.signal, this.options.signal),
1136
1177
  timeoutMs
1137
1178
  });
1138
1179
  if (result.isError) throw new McpTransportError(`MCP tool "${remoteName}" returned an error: ${unwrapContent(result.content)}`, {
@@ -1144,10 +1185,20 @@ var McpClientImpl = class {
1144
1185
  });
1145
1186
  }
1146
1187
  close() {
1147
- return this.rpc.close();
1188
+ this.options.signal?.removeEventListener("abort", this.onAbort);
1189
+ this.closing ??= this.rpc.close();
1190
+ return this.closing;
1148
1191
  }
1149
1192
  };
1150
1193
  /**
1194
+ * The signal a `tools/call` honors: the per-call `ctx.signal`, the
1195
+ * client-level `options.signal`, both (whichever aborts first), or none.
1196
+ */
1197
+ function combineSignals(callSignal, clientSignal) {
1198
+ if (!callSignal || !clientSignal) return callSignal ?? clientSignal;
1199
+ return AbortSignal.any([callSignal, clientSignal]);
1200
+ }
1201
+ /**
1151
1202
  * Flatten an MCP `tools/call` result's content blocks into the value a
1152
1203
  * tool returns. Text blocks are concatenated; a single block whose text is
1153
1204
  * valid JSON is parsed so structured tool output flows back as an object
@@ -1219,7 +1270,8 @@ function tryParseJson(text) {
1219
1270
  * @param server - The transport config (`{ type: "stdio" }` /
1220
1271
  * `{ type: "http" }`). A pre-built transport client may be injected for
1221
1272
  * testing.
1222
- * @param options - Prefix / filter / per-call timeout.
1273
+ * @param options - Prefix / filter / per-call timeout, and a `signal` that
1274
+ * closes the client when it aborts.
1223
1275
  * @returns An {@link McpClient} handle.
1224
1276
  *
1225
1277
  * @example
@@ -1233,6 +1285,462 @@ function mcp$1(server, options) {
1233
1285
  return new McpClientImpl(server, options);
1234
1286
  }
1235
1287
 
1288
+ //#endregion
1289
+ //#region ../../packages/ai-tools/src/mcp/protocol.ts
1290
+ /**
1291
+ * MCP protocol versions the `ai.mcp.serve` server speaks, newest first.
1292
+ * `initialize` echoes the client's requested version when it is listed
1293
+ * here and otherwise answers with the newest one (lifecycle, version
1294
+ * negotiation). The HTTP transport rejects any other value in the
1295
+ * `MCP-Protocol-Version` header with 400.
1296
+ *
1297
+ * Source: https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle
1298
+ * and https://modelcontextprotocol.io/specification/2025-11-25/basic/transports
1299
+ * (checked 2026-10-17).
1300
+ */
1301
+ const SUPPORTED_PROTOCOL_VERSIONS = [
1302
+ "2025-11-25",
1303
+ "2025-06-18",
1304
+ "2025-03-26"
1305
+ ];
1306
+ /** The newest MCP protocol version the server speaks. */
1307
+ const LATEST_PROTOCOL_VERSION = "2025-11-25";
1308
+
1309
+ //#endregion
1310
+ //#region ../../packages/ai-tools/src/mcp/serve-http.ts
1311
+ /**
1312
+ * MCP Streamable HTTP server transport for `ai.mcp.serve`.
1313
+ *
1314
+ * Implements the server side of the Streamable HTTP transport from the MCP
1315
+ * specification (https://modelcontextprotocol.io/specification/2025-11-25/basic/transports,
1316
+ * checked 2026-10-17) on `node:http`, with no SDK dependency:
1317
+ *
1318
+ * - one endpoint path; each POST carries exactly one JSON-RPC message
1319
+ * (batches were removed from the protocol in 2025-06-18 and get 400);
1320
+ * - a request is answered with `Content-Type: application/json` (the spec
1321
+ * lets a server pick JSON or SSE per request); a notification or a
1322
+ * response is acknowledged with `202 Accepted` and no body;
1323
+ * - stateless: no `MCP-Session-Id` is issued (optional in the spec), so
1324
+ * GET (server-initiated SSE stream) and DELETE (session end) answer
1325
+ * `405 Method Not Allowed`, as the spec allows;
1326
+ * - an unsupported `MCP-Protocol-Version` header gets 400;
1327
+ * - security warning items: `Origin` is validated on every request (an
1328
+ * invalid one gets 403), the default bind is `127.0.0.1`, and
1329
+ * authentication is required for any non-loopback bind.
1330
+ */
1331
+ /** Default bind interface — loopback only. */
1332
+ const DEFAULT_HOST = "127.0.0.1";
1333
+ /** Default endpoint path. */
1334
+ const DEFAULT_PATH = "/mcp";
1335
+ /** Default request body cap: 1 MiB. */
1336
+ const DEFAULT_MAX_BODY_BYTES = 1048576;
1337
+ /** Default time `close()` waits for in-flight requests. */
1338
+ const DEFAULT_DRAIN_TIMEOUT_MS = 5e3;
1339
+ /** JSON-RPC codes used for transport-level error bodies. */
1340
+ const JSON_RPC_PARSE_ERROR = -32700;
1341
+ const JSON_RPC_INVALID_REQUEST$1 = -32600;
1342
+ /** Generic server-defined JSON-RPC code for HTTP-level refusals (auth, origin, size). */
1343
+ const JSON_RPC_SERVER_ERROR = -32e3;
1344
+ /** Host names that always mean "this machine". */
1345
+ const LOOPBACK_NAMES = new Set([
1346
+ "localhost",
1347
+ "127.0.0.1",
1348
+ "::1",
1349
+ "[::1]"
1350
+ ]);
1351
+ /**
1352
+ * True when `host` names a loopback interface (`localhost`, `127.0.0.0/8`,
1353
+ * `::1`, or an IPv4-mapped `::ffff:127.x.x.x`).
1354
+ */
1355
+ function isLoopbackHost(host) {
1356
+ const normalized = host.trim().toLowerCase();
1357
+ if (LOOPBACK_NAMES.has(normalized)) return true;
1358
+ const unbracketed = normalized.replace(/^\[|\]$/g, "");
1359
+ const mapped = unbracketed.startsWith("::ffff:") ? unbracketed.slice(7) : unbracketed;
1360
+ return (0, node_net.isIPv4)(mapped) && mapped.startsWith("127.");
1361
+ }
1362
+ /** Strip the port from a `Host` header value (`[::1]:3000` → `[::1]`, `a.b:80` → `a.b`). */
1363
+ function hostnameOf(hostHeader) {
1364
+ const value = hostHeader.trim().toLowerCase();
1365
+ if (value.startsWith("[")) {
1366
+ const end = value.indexOf("]");
1367
+ return end === -1 ? value : value.slice(0, end + 1);
1368
+ }
1369
+ const colon = value.lastIndexOf(":");
1370
+ return colon === -1 ? value : value.slice(0, colon);
1371
+ }
1372
+ /** SHA-256 digest — equal-length input for `timingSafeEqual`. */
1373
+ function digest(value) {
1374
+ return (0, node_crypto.createHash)("sha256").update(value, "utf8").digest();
1375
+ }
1376
+ /** Normalise a configured origin to `scheme://host[:port]`, or throw a config error. */
1377
+ function normalizeOrigin(origin) {
1378
+ try {
1379
+ return new URL(origin).origin;
1380
+ } catch (cause) {
1381
+ throw new McpServeError(`allowedOrigins entry "${origin}" is not a valid origin URL.`, {
1382
+ type: "config",
1383
+ cause
1384
+ });
1385
+ }
1386
+ }
1387
+ /**
1388
+ * Validate and default the transport options. Throws {@link McpServeError}
1389
+ * (`"config"`) for a non-loopback bind without auth, an empty bearer
1390
+ * token, an `auth` with neither check, a bad path / port / limit.
1391
+ */
1392
+ function resolveSettings(transport) {
1393
+ const host = transport.host ?? DEFAULT_HOST;
1394
+ const port = transport.port ?? 0;
1395
+ const path = transport.path ?? DEFAULT_PATH;
1396
+ const loopback = isLoopbackHost(host);
1397
+ const auth = transport.auth;
1398
+ 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" });
1399
+ if (!path.startsWith("/")) throw new McpServeError(`MCP http path must start with "/" (got "${path}").`, { type: "config" });
1400
+ const maxBodyBytes = transport.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
1401
+ if (!Number.isInteger(maxBodyBytes) || maxBodyBytes <= 0) throw new McpServeError("MCP http maxBodyBytes must be a positive integer.", { type: "config" });
1402
+ const drainTimeoutMs = transport.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
1403
+ if (!Number.isFinite(drainTimeoutMs) || drainTimeoutMs < 0) throw new McpServeError("MCP http drainTimeoutMs must be zero or a positive number.", { type: "config" });
1404
+ const tokens = auth?.bearer === void 0 ? [] : typeof auth.bearer === "string" ? [auth.bearer] : [...auth.bearer];
1405
+ if (tokens.some((token) => token.length === 0)) throw new McpServeError("MCP http auth.bearer tokens must be non-empty strings.", { type: "config" });
1406
+ if (auth && auth.bearer !== void 0 && tokens.length === 0) throw new McpServeError("MCP http auth.bearer needs at least one token.", { type: "config" });
1407
+ if (auth && tokens.length === 0 && !auth.authorize) throw new McpServeError("MCP http auth needs `bearer` and/or `authorize`.", { type: "config" });
1408
+ const authRequired = tokens.length > 0 || auth?.authorize !== void 0;
1409
+ 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.`, {
1410
+ type: "config",
1411
+ context: { host }
1412
+ });
1413
+ return {
1414
+ host,
1415
+ port,
1416
+ path,
1417
+ loopback,
1418
+ maxBodyBytes,
1419
+ drainTimeoutMs,
1420
+ tokenDigests: tokens.map(digest),
1421
+ authorize: auth?.authorize,
1422
+ authRequired,
1423
+ allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
1424
+ allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name)))
1425
+ };
1426
+ }
1427
+ /** Flatten Node's header bag into lower-cased `name → value` strings. */
1428
+ function flattenHeaders(headers) {
1429
+ const flat = {};
1430
+ for (const [name, value] of Object.entries(headers)) {
1431
+ if (value === void 0) continue;
1432
+ flat[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
1433
+ }
1434
+ return flat;
1435
+ }
1436
+ /** Write a JSON body with a status code. */
1437
+ function sendJson(response, status, body, headers = {}) {
1438
+ if (response.headersSent || response.destroyed) return;
1439
+ const payload = JSON.stringify(body);
1440
+ response.writeHead(status, {
1441
+ ...headers,
1442
+ "content-type": "application/json",
1443
+ "content-length": Buffer.byteLength(payload).toString()
1444
+ });
1445
+ response.end(payload);
1446
+ }
1447
+ /**
1448
+ * Write an HTTP-level refusal: the status plus a JSON-RPC error body with
1449
+ * no request id (the spec allows such a body on 4xx transport errors).
1450
+ */
1451
+ function sendRefusal(response, status, code, message, headers = {}) {
1452
+ sendJson(response, status, {
1453
+ jsonrpc: "2.0",
1454
+ id: null,
1455
+ error: {
1456
+ code,
1457
+ message
1458
+ }
1459
+ }, headers);
1460
+ }
1461
+ /** Write an empty-bodied status (202 / 405). */
1462
+ function sendEmpty(response, status, headers = {}) {
1463
+ if (response.headersSent || response.destroyed) return;
1464
+ response.writeHead(status, headers);
1465
+ response.end();
1466
+ }
1467
+ /**
1468
+ * Bind an MCP Streamable HTTP endpoint and pump each JSON-RPC request
1469
+ * through `dispatch`. Resolves once the port is bound.
1470
+ *
1471
+ * Throws {@link McpServeError}: `"config"` for invalid / unsafe options
1472
+ * (checked before anything binds), `"listen"` when binding fails.
1473
+ *
1474
+ * @param transport - The http transport options.
1475
+ * @param dispatch - Maps one JSON-RPC request (plus call context) to its response.
1476
+ */
1477
+ async function startHttpServer(transport, dispatch) {
1478
+ const settings = resolveSettings(transport);
1479
+ const shutdown = new AbortController();
1480
+ const inFlight = /* @__PURE__ */ new Set();
1481
+ let closing;
1482
+ const server = (0, node_http.createServer)((request, response) => {
1483
+ if (closing) {
1484
+ sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, "Server is shutting down.", { connection: "close" });
1485
+ return;
1486
+ }
1487
+ const work = handleRequest(request, response, settings, dispatch, shutdown.signal).catch(() => {
1488
+ sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, "Internal server error.");
1489
+ });
1490
+ inFlight.add(work);
1491
+ work.finally(() => {
1492
+ inFlight.delete(work);
1493
+ if (closing) server.closeIdleConnections();
1494
+ });
1495
+ });
1496
+ await new Promise((resolve, reject) => {
1497
+ const onError = (cause) => {
1498
+ reject(new McpServeError(`MCP http server could not listen on ${settings.host}:${settings.port}: ${cause.message}`, {
1499
+ type: "listen",
1500
+ cause,
1501
+ context: {
1502
+ host: settings.host,
1503
+ port: settings.port
1504
+ }
1505
+ }));
1506
+ };
1507
+ server.once("error", onError);
1508
+ server.listen(settings.port, settings.host, () => {
1509
+ server.off("error", onError);
1510
+ resolve();
1511
+ });
1512
+ });
1513
+ const address = server.address();
1514
+ const boundPort = typeof address === "object" && address !== null ? address.port : settings.port;
1515
+ const url = `http://${(0, node_net.isIPv6)(settings.host) ? `[${settings.host}]` : settings.host}:${boundPort}${settings.path}`;
1516
+ const close = () => {
1517
+ if (closing) return closing;
1518
+ closing = (async () => {
1519
+ const closed = new Promise((resolve) => {
1520
+ server.close(() => resolve());
1521
+ });
1522
+ server.closeIdleConnections();
1523
+ if (inFlight.size > 0) {
1524
+ let timer;
1525
+ const timedOut = new Promise((resolve) => {
1526
+ timer = setTimeout(() => resolve("timeout"), settings.drainTimeoutMs);
1527
+ });
1528
+ const outcome = await Promise.race([Promise.allSettled([...inFlight]).then(() => "drained"), timedOut]);
1529
+ clearTimeout(timer);
1530
+ if (outcome === "timeout") {
1531
+ shutdown.abort();
1532
+ server.closeAllConnections();
1533
+ }
1534
+ }
1535
+ server.closeIdleConnections();
1536
+ await closed;
1537
+ })();
1538
+ return closing;
1539
+ };
1540
+ return {
1541
+ url,
1542
+ close
1543
+ };
1544
+ }
1545
+ /**
1546
+ * Validate and answer one HTTP request: path → Host / Origin → auth →
1547
+ * method → headers → body → JSON-RPC dispatch.
1548
+ */
1549
+ async function handleRequest(request, response, settings, dispatch, signal) {
1550
+ const headers = flattenHeaders(request.headers);
1551
+ const method = (request.method ?? "GET").toUpperCase();
1552
+ const path = new URL(request.url ?? "/", "http://localhost").pathname;
1553
+ if (!hostAllowed(headers.host, settings)) {
1554
+ sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Host header not allowed.");
1555
+ return;
1556
+ }
1557
+ if (!originAllowed(headers.origin, settings)) {
1558
+ sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Origin not allowed.");
1559
+ return;
1560
+ }
1561
+ if (path !== settings.path) {
1562
+ sendRefusal(response, 404, JSON_RPC_SERVER_ERROR, "Not found.");
1563
+ return;
1564
+ }
1565
+ const info = {
1566
+ method,
1567
+ path,
1568
+ headers
1569
+ };
1570
+ if (request.socket.remoteAddress !== void 0) info.remoteAddress = request.socket.remoteAddress;
1571
+ if (!await authorized(info, settings)) {
1572
+ sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", settings.tokenDigests.length > 0 ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {});
1573
+ return;
1574
+ }
1575
+ if (method !== "POST") {
1576
+ sendEmpty(response, 405, { allow: "POST" });
1577
+ return;
1578
+ }
1579
+ const protocolVersion = headers["mcp-protocol-version"];
1580
+ if (protocolVersion !== void 0 && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
1581
+ sendRefusal(response, 400, JSON_RPC_SERVER_ERROR, `Unsupported MCP-Protocol-Version "${protocolVersion}" (supported: ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")}).`);
1582
+ return;
1583
+ }
1584
+ if (!isJsonContentType(headers["content-type"])) {
1585
+ sendRefusal(response, 415, JSON_RPC_SERVER_ERROR, "Content-Type must be application/json.");
1586
+ return;
1587
+ }
1588
+ if (!acceptsJson(headers.accept)) {
1589
+ sendRefusal(response, 406, JSON_RPC_SERVER_ERROR, "Accept must allow application/json.");
1590
+ return;
1591
+ }
1592
+ const body = await readBody$1(request, settings.maxBodyBytes);
1593
+ if (body.type === "too-large") {
1594
+ sendRefusal(response, 413, JSON_RPC_SERVER_ERROR, `Request body exceeds ${settings.maxBodyBytes} bytes.`, { connection: "close" });
1595
+ return;
1596
+ }
1597
+ let message;
1598
+ try {
1599
+ message = JSON.parse(body.text);
1600
+ } catch {
1601
+ sendRefusal(response, 400, JSON_RPC_PARSE_ERROR, "Parse error: body is not valid JSON.");
1602
+ return;
1603
+ }
1604
+ if (typeof message !== "object" || message === null || Array.isArray(message)) {
1605
+ sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST$1, "Invalid request: the body must be one JSON-RPC message (batches are not supported).");
1606
+ return;
1607
+ }
1608
+ const record = message;
1609
+ if (record.jsonrpc !== "2.0") {
1610
+ sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST$1, "Invalid request: jsonrpc must be \"2.0\".");
1611
+ return;
1612
+ }
1613
+ const id = record.id;
1614
+ if (typeof record.method === "string") {
1615
+ if (typeof id !== "string" && typeof id !== "number") {
1616
+ sendEmpty(response, 202);
1617
+ return;
1618
+ }
1619
+ const rpcRequest = {
1620
+ jsonrpc: "2.0",
1621
+ id,
1622
+ method: record.method
1623
+ };
1624
+ if (record.params !== void 0) rpcRequest.params = record.params;
1625
+ sendJson(response, 200, await dispatch(rpcRequest, {
1626
+ request: info,
1627
+ signal
1628
+ }));
1629
+ return;
1630
+ }
1631
+ if ((typeof id === "string" || typeof id === "number") && ("result" in record || "error" in record)) {
1632
+ sendEmpty(response, 202);
1633
+ return;
1634
+ }
1635
+ sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST$1, "Invalid request: not a JSON-RPC message.");
1636
+ }
1637
+ /**
1638
+ * Host-header check. While bound to loopback only loopback names (plus
1639
+ * `allowedHosts`) are accepted, so a DNS-rebound name pointing at
1640
+ * 127.0.0.1 is refused. Off loopback the check applies only when
1641
+ * `allowedHosts` is set.
1642
+ */
1643
+ function hostAllowed(hostHeader, settings) {
1644
+ if (!settings.loopback && settings.allowedHosts.size === 0) return true;
1645
+ if (hostHeader === void 0) return false;
1646
+ const name = hostnameOf(hostHeader);
1647
+ if (settings.allowedHosts.has(name)) return true;
1648
+ return settings.loopback && isLoopbackHost(name);
1649
+ }
1650
+ /**
1651
+ * Origin check (MCP Streamable HTTP security warning). No `Origin` header —
1652
+ * a non-browser client — passes; otherwise the origin must be a loopback
1653
+ * `http(s)` origin or listed in `allowedOrigins`. `null` and unparsable
1654
+ * origins are refused.
1655
+ */
1656
+ function originAllowed(origin, settings) {
1657
+ if (origin === void 0) return true;
1658
+ let parsed;
1659
+ try {
1660
+ parsed = new URL(origin);
1661
+ } catch {
1662
+ return false;
1663
+ }
1664
+ if (settings.allowedOrigins.has(parsed.origin)) return true;
1665
+ return (parsed.protocol === "http:" || parsed.protocol === "https:") && isLoopbackHost(parsed.hostname);
1666
+ }
1667
+ /**
1668
+ * Run the configured auth. Bearer tokens are compared in constant time
1669
+ * over SHA-256 digests (equal-length buffers for `timingSafeEqual`), and
1670
+ * every configured token is compared so the match position does not leak.
1671
+ * An `authorize` hook that throws denies the request.
1672
+ */
1673
+ async function authorized(info, settings) {
1674
+ if (!settings.authRequired) return true;
1675
+ if (settings.tokenDigests.length > 0) {
1676
+ const match = /^Bearer[ \t]+(.+)$/i.exec(info.headers.authorization ?? "");
1677
+ const presented = digest(match?.[1]?.trim() ?? "");
1678
+ let matched = false;
1679
+ for (const expected of settings.tokenDigests) matched = (0, node_crypto.timingSafeEqual)(presented, expected) || matched;
1680
+ if (!match || !matched) return false;
1681
+ }
1682
+ if (settings.authorize) try {
1683
+ return await settings.authorize(info) === true;
1684
+ } catch {
1685
+ return false;
1686
+ }
1687
+ return true;
1688
+ }
1689
+ /** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */
1690
+ function isJsonContentType(contentType) {
1691
+ return contentType?.split(";")[0]?.trim().toLowerCase() === "application/json";
1692
+ }
1693
+ /**
1694
+ * True when the `Accept` header allows a JSON reply. Clients are required
1695
+ * to list both `application/json` and `text/event-stream`; this server
1696
+ * always answers JSON, so it only insists on that half. A missing header
1697
+ * is accepted.
1698
+ */
1699
+ function acceptsJson(accept) {
1700
+ if (accept === void 0 || accept.trim() === "") return true;
1701
+ return accept.split(",").map((part) => part.split(";")[0]?.trim().toLowerCase()).some((type) => type === "application/json" || type === "application/*" || type === "*/*");
1702
+ }
1703
+ /**
1704
+ * Read the request body as UTF-8, refusing anything over `limit` bytes —
1705
+ * up front from `Content-Length`, and while streaming for chunked bodies.
1706
+ * A refused body is left unread; the 413 response closes the connection.
1707
+ */
1708
+ function readBody$1(request, limit) {
1709
+ const declared = Number(request.headers["content-length"]);
1710
+ if (Number.isFinite(declared) && declared > limit) return Promise.resolve({ type: "too-large" });
1711
+ return new Promise((resolve, reject) => {
1712
+ const chunks = [];
1713
+ let size = 0;
1714
+ let done = false;
1715
+ const finish = (result) => {
1716
+ if (!done) {
1717
+ done = true;
1718
+ resolve(result);
1719
+ }
1720
+ };
1721
+ request.on("data", (chunk) => {
1722
+ if (done) return;
1723
+ size += chunk.length;
1724
+ if (size > limit) {
1725
+ request.pause();
1726
+ finish({ type: "too-large" });
1727
+ return;
1728
+ }
1729
+ chunks.push(chunk);
1730
+ });
1731
+ request.on("end", () => finish({
1732
+ type: "ok",
1733
+ text: Buffer.concat(chunks).toString("utf8")
1734
+ }));
1735
+ request.on("error", (error) => {
1736
+ if (!done) {
1737
+ done = true;
1738
+ reject(error);
1739
+ }
1740
+ });
1741
+ });
1742
+ }
1743
+
1236
1744
  //#endregion
1237
1745
  //#region ../../packages/ai-tools/src/mcp/serve.ts
1238
1746
  /** JSON-RPC version literal every outbound message carries. */
@@ -1241,59 +1749,159 @@ const JSONRPC_VERSION = "2.0";
1241
1749
  const DEFAULT_SCHEMA_TARGET = "draft-2020-12";
1242
1750
  /** Default advertised server version when the caller omits one. */
1243
1751
  const DEFAULT_VERSION = "4.4.0";
1244
- /** The MCP protocol version this server advertises in `initialize`. */
1245
- const PROTOCOL_VERSION = "2025-06-18";
1246
1752
  /** JSON-RPC standard error codes we emit. */
1247
1753
  const JSON_RPC_METHOD_NOT_FOUND = -32601;
1248
1754
  const JSON_RPC_INVALID_PARAMS = -32602;
1755
+ /** MCP's "resource not found" error code (server/resources, error handling). */
1756
+ const MCP_RESOURCE_NOT_FOUND = -32002;
1249
1757
  /**
1250
- * Resolve the {@link McpServeSource} (either an object exposing `tools()`
1251
- * or a literal `ToolContract[]`) into a flat contract array.
1758
+ * A JSON-RPC failure raised inside a method handler, carrying the exact
1759
+ * code to answer with (unknown resource, missing prompt argument, …).
1252
1760
  */
1253
- function resolveTools(source) {
1254
- if (Array.isArray(source)) return source;
1255
- return source.tools();
1761
+ var RpcFailure = class extends Error {
1762
+ constructor(code, message, data) {
1763
+ super(message);
1764
+ this.code = code;
1765
+ this.data = data;
1766
+ }
1767
+ };
1768
+ /** Enumerate one tool set into a flat contract array. */
1769
+ function toolsOf(source) {
1770
+ return Array.isArray(source) ? source : source.tools();
1771
+ }
1772
+ /** Narrow a serve source to its grouped shape. */
1773
+ function isGroupedSource(source) {
1774
+ return !Array.isArray(source) && "groups" in source;
1775
+ }
1776
+ /**
1777
+ * Resolve the serve source into the exposed tool list, applying
1778
+ * `prefixToolNames` and refusing duplicate names. Throws
1779
+ * {@link McpServeError} — `"config"` for `prefixToolNames` on an ungrouped
1780
+ * source, `"collision"` when two tools would share a name.
1781
+ */
1782
+ function resolveTools(source, options) {
1783
+ const prefix = options.prefixToolNames ?? false;
1784
+ const exposed = [];
1785
+ if (isGroupedSource(source)) for (const [group, groupSource] of Object.entries(source.groups)) for (const contract of toolsOf(groupSource)) {
1786
+ let name = contract.name;
1787
+ if (prefix === true) name = `${group}_${contract.name}`;
1788
+ else if (typeof prefix === "function") name = prefix(group, contract.name);
1789
+ exposed.push({
1790
+ name,
1791
+ contract,
1792
+ origin: `group "${group}"`
1793
+ });
1794
+ }
1795
+ else {
1796
+ if (prefix !== false) throw new McpServeError("prefixToolNames needs a grouped source: serve({ groups: { name: tools } }, …).", { type: "config" });
1797
+ for (const contract of toolsOf(source)) exposed.push({
1798
+ name: contract.name,
1799
+ contract,
1800
+ origin: "the source"
1801
+ });
1802
+ }
1803
+ const seen = /* @__PURE__ */ new Map();
1804
+ for (const tool of exposed) {
1805
+ const previous = seen.get(tool.name);
1806
+ 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.`, {
1807
+ type: "collision",
1808
+ context: { name: tool.name }
1809
+ });
1810
+ seen.set(tool.name, tool.origin);
1811
+ }
1812
+ return exposed.map(({ name, contract }) => ({
1813
+ name,
1814
+ contract
1815
+ }));
1816
+ }
1817
+ /** Throw a collision error when two entries share the same key. */
1818
+ function assertUnique(entries, key, label) {
1819
+ const seen = /* @__PURE__ */ new Set();
1820
+ for (const entry of entries ?? []) {
1821
+ const value = key(entry);
1822
+ if (seen.has(value)) throw new McpServeError(`MCP ${label} "${value}" is declared twice.`, {
1823
+ type: "collision",
1824
+ context: { [label]: value }
1825
+ });
1826
+ seen.add(value);
1827
+ }
1256
1828
  }
1257
1829
  /**
1258
1830
  * 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
1831
+ * response, with no I/O. Both the stdio and HTTP serve-transports pump
1260
1832
  * their inbound requests through this, and specs can drive it directly.
1261
1833
  *
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.
1834
+ * Handles `initialize`, `ping`, `tools/list`, `tools/call`, and — when
1835
+ * configured — `resources/list`, `resources/templates/list`,
1836
+ * `resources/read`, `prompts/list` and `prompts/get`. Any other method
1837
+ * answers with a JSON-RPC `method not found` error.
1265
1838
  *
1266
1839
  * Constructed via {@link createServeHandler}.
1267
1840
  */
1268
1841
  var McpServeHandler = class {
1269
1842
  constructor(source, options) {
1270
- this.tools = resolveTools(source);
1271
- this.byName = new Map(this.tools.map((contract) => [contract.name, contract]));
1843
+ this.tools = resolveTools(source, options);
1844
+ this.byName = new Map(this.tools.map((tool) => [tool.name, tool.contract]));
1845
+ assertUnique(options.resources, (resource) => resource.uri, "resource");
1846
+ assertUnique(options.prompts, (prompt) => prompt.name, "prompt");
1847
+ this.resources = options.resources ? new Map(options.resources.map((resource) => [resource.uri, resource])) : void 0;
1848
+ this.prompts = options.prompts ? new Map(options.prompts.map((prompt) => [prompt.name, prompt])) : void 0;
1272
1849
  this.options = options;
1273
1850
  }
1274
1851
  /**
1275
1852
  * Dispatch one inbound JSON-RPC request to its handler and produce the
1276
1853
  * response. A handler that throws is mapped to a JSON-RPC error response
1277
1854
  * — the serve loop never crashes on a bad request.
1855
+ *
1856
+ * @param request - The JSON-RPC request.
1857
+ * @param context - Per-call context: the inbound HTTP request (headers
1858
+ * reach tools as `ctx.request`) and an abort signal.
1278
1859
  */
1279
- async handle(request) {
1860
+ async handle(request, context = {}) {
1280
1861
  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
- }
1862
+ return this.ok(request.id, await this.dispatch(request, context));
1287
1863
  } catch (cause) {
1864
+ if (cause instanceof RpcFailure) return this.error(request.id, cause.code, cause.message, cause.data);
1288
1865
  const message = cause instanceof Error ? cause.message : String(cause);
1289
1866
  return this.error(request.id, JSON_RPC_INVALID_PARAMS, message);
1290
1867
  }
1291
1868
  }
1292
- /** Build the `initialize` result advertising name / version / capabilities. */
1293
- initializeResult() {
1869
+ /** Route a method to its result builder. */
1870
+ async dispatch(request, context) {
1871
+ switch (request.method) {
1872
+ case "initialize": return this.initializeResult(request.params);
1873
+ case "ping": return {};
1874
+ case "tools/list": return { tools: this.listTools() };
1875
+ case "tools/call": return this.callTool(request.params, context);
1876
+ default: break;
1877
+ }
1878
+ if (this.resources) switch (request.method) {
1879
+ case "resources/list": return { resources: this.listResources(this.resources) };
1880
+ case "resources/templates/list": return { resourceTemplates: [] };
1881
+ case "resources/read": return this.readResource(this.resources, request.params, context);
1882
+ default: break;
1883
+ }
1884
+ if (this.prompts) switch (request.method) {
1885
+ case "prompts/list": return { prompts: this.listPrompts(this.prompts) };
1886
+ case "prompts/get": return this.getPrompt(this.prompts, request.params, context);
1887
+ default: break;
1888
+ }
1889
+ throw new RpcFailure(JSON_RPC_METHOD_NOT_FOUND, `Method "${request.method}" is not supported by this MCP server.`);
1890
+ }
1891
+ /**
1892
+ * Build the `initialize` result advertising name / version /
1893
+ * capabilities, negotiating the protocol version: the client's requested
1894
+ * version when supported, otherwise the newest this server speaks.
1895
+ */
1896
+ initializeResult(params) {
1897
+ const requested = readRecord(params).protocolVersion;
1898
+ const protocolVersion = typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : LATEST_PROTOCOL_VERSION;
1899
+ const capabilities = { tools: {} };
1900
+ if (this.resources) capabilities.resources = {};
1901
+ if (this.prompts) capabilities.prompts = {};
1294
1902
  return {
1295
- protocolVersion: PROTOCOL_VERSION,
1296
- capabilities: { tools: {} },
1903
+ protocolVersion,
1904
+ capabilities,
1297
1905
  serverInfo: {
1298
1906
  name: this.options.name,
1299
1907
  version: this.options.version ?? DEFAULT_VERSION
@@ -1302,19 +1910,19 @@ var McpServeHandler = class {
1302
1910
  }
1303
1911
  /**
1304
1912
  * 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
1913
+ * exposed tool, its `inputSchema` extracted via `extractJsonSchema` at
1914
+ * the configured dialect (default `draft-2020-12` — overriding
1307
1915
  * `extractJsonSchema`'s own `openai-strict` default to a neutral MCP draft).
1308
1916
  */
1309
1917
  listTools() {
1310
1918
  const target = this.options.schemaTarget ?? DEFAULT_SCHEMA_TARGET;
1311
- return this.tools.map((contract) => {
1919
+ return this.tools.map(({ name, contract }) => {
1312
1920
  const inputSchema = (0, _warlock_js_ai.extractJsonSchema)(contract.input, { target }) ?? {
1313
1921
  type: "object",
1314
1922
  properties: {}
1315
1923
  };
1316
1924
  return {
1317
- name: contract.name,
1925
+ name,
1318
1926
  description: contract.description,
1319
1927
  inputSchema
1320
1928
  };
@@ -1325,15 +1933,27 @@ var McpServeHandler = class {
1325
1933
  * never-throwing {@link import("@warlock.js/ai").ToolInvokeResult}: `data`
1326
1934
  * → a text content block, `error` → an `isError: true` result. An unknown
1327
1935
  * tool name throws (mapped to a JSON-RPC error by {@link handle}).
1936
+ *
1937
+ * The contract receives a `ToolContext` carrying the abort signal and,
1938
+ * over HTTP, the inbound request's headers on `ctx.request`.
1328
1939
  */
1329
- async callTool(params) {
1940
+ async callTool(params, context) {
1330
1941
  const { name, args } = readCallParams(params);
1331
1942
  const contract = this.byName.get(name);
1332
1943
  if (!contract) throw new McpTransportError(`Unknown tool "${name}".`, {
1333
1944
  type: "protocol",
1334
1945
  method: "tools/call"
1335
1946
  });
1336
- const result = await contract.invoke(args);
1947
+ const toolContext = { artifacts: {} };
1948
+ if (context.signal) toolContext.signal = context.signal;
1949
+ if (context.request) {
1950
+ const { headers, remoteAddress } = context.request;
1951
+ toolContext.request = remoteAddress === void 0 ? { headers } : {
1952
+ headers,
1953
+ remoteAddress
1954
+ };
1955
+ }
1956
+ const result = await contract.invoke(args, toolContext);
1337
1957
  if (result.error) return {
1338
1958
  content: [{
1339
1959
  type: "text",
@@ -1349,6 +1969,75 @@ var McpServeHandler = class {
1349
1969
  isError: false
1350
1970
  };
1351
1971
  }
1972
+ /** Build the `resources/list` payload (metadata only, no contents). */
1973
+ listResources(resources) {
1974
+ return [...resources.values()].map((resource) => compact({
1975
+ uri: resource.uri,
1976
+ name: resource.name,
1977
+ title: resource.title,
1978
+ description: resource.description,
1979
+ mimeType: resource.mimeType
1980
+ }));
1981
+ }
1982
+ /**
1983
+ * Answer `resources/read`: run the resource's `read()` and wrap the
1984
+ * result as one text or base64 blob content entry.
1985
+ */
1986
+ async readResource(resources, params, context) {
1987
+ const uri = readRecord(params).uri;
1988
+ if (typeof uri !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, "resources/read requires a string `uri`.");
1989
+ const resource = resources.get(uri);
1990
+ if (!resource) throw new RpcFailure(MCP_RESOURCE_NOT_FOUND, "Resource not found", { uri });
1991
+ const body = await resource.read(context);
1992
+ return { contents: [compact(typeof body === "string" ? {
1993
+ uri,
1994
+ mimeType: resource.mimeType,
1995
+ text: body
1996
+ } : {
1997
+ uri,
1998
+ mimeType: resource.mimeType,
1999
+ blob: Buffer.from(body).toString("base64")
2000
+ })] };
2001
+ }
2002
+ /** Build the `prompts/list` payload. */
2003
+ listPrompts(prompts) {
2004
+ return [...prompts.values()].map((prompt) => compact({
2005
+ name: prompt.name,
2006
+ title: prompt.title,
2007
+ description: prompt.description,
2008
+ arguments: prompt.arguments
2009
+ }));
2010
+ }
2011
+ /**
2012
+ * Answer `prompts/get`: validate the arguments (strings only; required
2013
+ * ones present) and build the messages. An unknown prompt or a missing
2014
+ * required argument answers invalid-params, per the MCP prompts spec.
2015
+ */
2016
+ async getPrompt(prompts, params, context) {
2017
+ const record = readRecord(params);
2018
+ const name = record.name;
2019
+ if (typeof name !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, "prompts/get requires a string `name`.");
2020
+ const prompt = prompts.get(name);
2021
+ if (!prompt) throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Unknown prompt "${name}".`);
2022
+ const args = {};
2023
+ for (const [key, value] of Object.entries(readRecord(record.arguments))) {
2024
+ if (typeof value !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Prompt argument "${key}" must be a string.`);
2025
+ args[key] = value;
2026
+ }
2027
+ 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.`);
2028
+ const built = await prompt.get(args, context);
2029
+ const messages = typeof built === "string" ? [{
2030
+ role: "user",
2031
+ content: {
2032
+ type: "text",
2033
+ text: built
2034
+ }
2035
+ }] : built;
2036
+ return compact({
2037
+ description: prompt.description,
2038
+ messages
2039
+ });
2040
+ }
1352
2041
  /** Build a JSON-RPC success response. */
1353
2042
  ok(id, result) {
1354
2043
  return {
@@ -1358,17 +2047,77 @@ var McpServeHandler = class {
1358
2047
  };
1359
2048
  }
1360
2049
  /** Build a JSON-RPC error response. */
1361
- error(id, code, message) {
2050
+ error(id, code, message, data) {
1362
2051
  return {
1363
2052
  jsonrpc: JSONRPC_VERSION,
1364
2053
  id,
1365
- error: {
2054
+ error: data === void 0 ? {
1366
2055
  code,
1367
2056
  message
2057
+ } : {
2058
+ code,
2059
+ message,
2060
+ data
1368
2061
  }
1369
2062
  };
1370
2063
  }
1371
2064
  };
2065
+ /** JSON-RPC "Invalid Request" — the line is JSON but not a request object. */
2066
+ const JSON_RPC_INVALID_REQUEST = -32600;
2067
+ /** JSON-RPC "Internal error" — answering the request failed unexpectedly. */
2068
+ const JSON_RPC_INTERNAL_ERROR = -32603;
2069
+ /**
2070
+ * Turn one stdio line into the line to write back, or `undefined` when
2071
+ * nothing is answered: blank or non-JSON lines and notifications (no `id`).
2072
+ * A JSON value that is not a request object (`null`, a number, an array) is
2073
+ * answered with a JSON-RPC "Invalid Request" error whose `id` is `null`.
2074
+ * Never rejects: a dispatch failure is answered as an internal error.
2075
+ */
2076
+ async function answerStdioLine(line, dispatch) {
2077
+ const trimmed = line.trim();
2078
+ if (!trimmed) return;
2079
+ let message;
2080
+ try {
2081
+ message = JSON.parse(trimmed);
2082
+ } catch {
2083
+ return;
2084
+ }
2085
+ if (typeof message !== "object" || message === null || Array.isArray(message)) return `${JSON.stringify({
2086
+ jsonrpc: JSONRPC_VERSION,
2087
+ id: null,
2088
+ error: {
2089
+ code: JSON_RPC_INVALID_REQUEST,
2090
+ message: "Invalid request: not a JSON-RPC object."
2091
+ }
2092
+ })}\n`;
2093
+ const request = message;
2094
+ const id = request.id;
2095
+ if (id === void 0 || id === null) return;
2096
+ try {
2097
+ const response = await dispatch(request);
2098
+ return `${JSON.stringify(response)}\n`;
2099
+ } catch {
2100
+ return `${JSON.stringify({
2101
+ jsonrpc: JSONRPC_VERSION,
2102
+ id,
2103
+ error: {
2104
+ code: JSON_RPC_INTERNAL_ERROR,
2105
+ message: "Internal error."
2106
+ }
2107
+ })}\n`;
2108
+ }
2109
+ }
2110
+ /** Read an unknown value as a plain record (non-objects read as empty). */
2111
+ function readRecord(value) {
2112
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
2113
+ return value;
2114
+ }
2115
+ /** Drop `undefined` members so optional wire fields are omitted, not `null`. */
2116
+ function compact(value) {
2117
+ const result = {};
2118
+ for (const [key, member] of Object.entries(value)) if (member !== void 0) result[key] = member;
2119
+ return result;
2120
+ }
1372
2121
  /**
1373
2122
  * Read and validate the `tools/call` params into `{ name, args }`. Throws
1374
2123
  * a typed {@link McpTransportError} when `name` is missing — mapped to a
@@ -1404,9 +2153,12 @@ function serializeData(data) {
1404
2153
  * {@link serve}) so callers and tests can drive the MCP protocol without an
1405
2154
  * actual transport — feed it a JSON-RPC request, get the response.
1406
2155
  *
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.
2156
+ * Throws {@link McpServeError} on duplicate tool names / resource URIs /
2157
+ * prompt names, or `prefixToolNames` on an ungrouped source.
2158
+ *
2159
+ * @param source - The tools to expose (one tool set or `{ groups }`).
2160
+ * @param options - Serve options (name / version / schema target / resources / prompts).
2161
+ * @returns An object whose `handle(request, context?)` maps a request to a response.
1410
2162
  */
1411
2163
  function createServeHandler(source, options) {
1412
2164
  return new McpServeHandler(source, options);
@@ -1414,79 +2166,66 @@ function createServeHandler(source, options) {
1414
2166
  /**
1415
2167
  * The internal {@link McpServer} — owns a {@link McpServeHandler} and a
1416
2168
  * 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).
2169
+ * from `process.stdin` and writes responses to `process.stdout`; for
2170
+ * `http` it binds a Streamable HTTP endpoint (see `./serve-http`).
1420
2171
  *
1421
2172
  * Constructed via {@link serve}; the class itself is internal.
1422
2173
  */
1423
2174
  var McpServerImpl = class {
1424
2175
  constructor(source, options) {
1425
- this.running = false;
1426
2176
  this.handler = new McpServeHandler(source, options);
1427
2177
  this.options = options;
1428
2178
  }
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);
2179
+ start() {
2180
+ if (!this.running) {
2181
+ const transport = this.options.transport ?? { type: "stdio" };
2182
+ const starting = transport.type === "http" ? this.startHttp(transport) : Promise.resolve(this.startStdio());
2183
+ this.running = starting.catch((error) => {
2184
+ this.running = void 0;
2185
+ throw error;
2186
+ });
2187
+ }
2188
+ return this.running;
2189
+ }
2190
+ /** Bind the HTTP endpoint; its `close()` also makes this server startable again. */
2191
+ async startHttp(transport) {
2192
+ const http = await startHttpServer(transport, (request, context) => this.handler.handle(request, context));
2193
+ return {
2194
+ url: http.url,
2195
+ close: async () => {
2196
+ await http.close();
2197
+ this.running = void 0;
2198
+ }
2199
+ };
2200
+ }
2201
+ /** Wire the stdin line reader and answer each request on stdout. */
2202
+ startStdio() {
2203
+ const reader = (0, node_readline.createInterface)({ input: process.stdin });
2204
+ reader.on("line", (line) => {
2205
+ this.onLine(line).catch(() => {});
1436
2206
  });
2207
+ return { close: async () => {
2208
+ reader.close();
2209
+ this.running = void 0;
2210
+ } };
1437
2211
  }
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
- */
2212
+ /** Answer one stdin line on stdout (see {@link answerStdioLine}). */
1443
2213
  async onLine(line) {
1444
- const trimmed = line.trim();
1445
- if (!trimmed) return;
1446
- let request;
2214
+ const reply = await answerStdioLine(line, (request) => this.handler.handle(request));
2215
+ if (reply !== void 0) process.stdout.write(reply);
2216
+ }
2217
+ async stop() {
2218
+ const running = this.running;
2219
+ if (!running) return;
2220
+ let handle;
1447
2221
  try {
1448
- request = JSON.parse(trimmed);
2222
+ handle = await running;
1449
2223
  } catch {
1450
2224
  return;
1451
2225
  }
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;
2226
+ await handle.close();
1460
2227
  }
1461
2228
  };
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
2229
  function serve(source, options) {
1491
2230
  return new McpServerImpl(source, options);
1492
2231
  }
@@ -2560,7 +3299,8 @@ _warlock_js_ai.ai.tools = {
2560
3299
  fetchUrl: fetchUrlTool,
2561
3300
  http: httpRequestTool,
2562
3301
  calculator: calculatorTool,
2563
- dateTime: (options) => dateTimeTool(options)
3302
+ dateTime: (options) => dateTimeTool(options),
3303
+ measure: _warlock_js_ai.measureTools
2564
3304
  };
2565
3305
  _warlock_js_ai.ai.mcp = mcp;
2566
3306
 
@@ -2568,7 +3308,9 @@ _warlock_js_ai.ai.mcp = mcp;
2568
3308
  exports.CalculatorError = CalculatorError;
2569
3309
  exports.DateTimeError = DateTimeError;
2570
3310
  exports.HttpPolicyError = HttpPolicyError;
3311
+ exports.McpServeError = McpServeError;
2571
3312
  exports.McpTransportError = McpTransportError;
3313
+ exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;
2572
3314
  exports.WebToolError = WebToolError;
2573
3315
  exports.calculatorTool = calculatorTool;
2574
3316
  exports.createJsonRpcClient = createJsonRpcClient;