@warlock.js/ai-tools 5.31.0 → 5.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/cjs/index.cjs +327 -60
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/contracts/http.type.d.mts +4 -5
  5. package/esm/contracts/http.type.d.mts.map +1 -1
  6. package/esm/contracts/index.d.mts +2 -1
  7. package/esm/contracts/mcp-serve.type.d.mts +97 -37
  8. package/esm/contracts/mcp-serve.type.d.mts.map +1 -1
  9. package/esm/contracts/mcp.type.d.mts +29 -19
  10. package/esm/contracts/mcp.type.d.mts.map +1 -1
  11. package/esm/contracts/utility.type.d.mts +7 -8
  12. package/esm/contracts/utility.type.d.mts.map +1 -1
  13. package/esm/contracts/web.type.d.mts +9 -10
  14. package/esm/contracts/web.type.d.mts.map +1 -1
  15. package/esm/errors.d.mts +31 -26
  16. package/esm/errors.d.mts.map +1 -1
  17. package/esm/http/http-request.d.mts +2 -3
  18. package/esm/http/http-request.d.mts.map +1 -1
  19. package/esm/http/http-request.mjs +7 -5
  20. package/esm/http/http-request.mjs.map +1 -1
  21. package/esm/index.d.mts +6 -2
  22. package/esm/index.mjs +2 -1
  23. package/esm/mcp/client.mjs +2 -1
  24. package/esm/mcp/client.mjs.map +1 -1
  25. package/esm/mcp/index.d.mts +5 -4
  26. package/esm/mcp/index.d.mts.map +1 -1
  27. package/esm/mcp/index.mjs +2 -1
  28. package/esm/mcp/index.mjs.map +1 -1
  29. package/esm/mcp/json-schema-to-standard.d.mts +1 -3
  30. package/esm/mcp/json-schema-to-standard.d.mts.map +1 -1
  31. package/esm/mcp/protocol.d.mts +1 -2
  32. package/esm/mcp/protocol.d.mts.map +1 -1
  33. package/esm/mcp/result.d.mts +17 -0
  34. package/esm/mcp/result.d.mts.map +1 -0
  35. package/esm/mcp/result.mjs +85 -0
  36. package/esm/mcp/result.mjs.map +1 -0
  37. package/esm/mcp/serve-http.mjs +64 -25
  38. package/esm/mcp/serve-http.mjs.map +1 -1
  39. package/esm/mcp/serve-requests.mjs +62 -0
  40. package/esm/mcp/serve-requests.mjs.map +1 -0
  41. package/esm/mcp/serve.d.mts +5 -5
  42. package/esm/mcp/serve.d.mts.map +1 -1
  43. package/esm/mcp/serve.mjs +97 -18
  44. package/esm/mcp/serve.mjs.map +1 -1
  45. package/esm/mcp/transport.d.mts +4 -5
  46. package/esm/mcp/transport.d.mts.map +1 -1
  47. package/esm/mcp/transport.mjs +12 -6
  48. package/esm/mcp/transport.mjs.map +1 -1
  49. package/esm/mcp/transport.type.d.mts +2 -2
  50. package/esm/mcp/transport.type.d.mts.map +1 -1
  51. package/esm/register.d.mts +3 -4
  52. package/esm/register.d.mts.map +1 -1
  53. package/esm/utility/calculator.d.mts +1 -3
  54. package/esm/utility/calculator.d.mts.map +1 -1
  55. package/esm/utility/date-time.d.mts +3 -5
  56. package/esm/utility/date-time.d.mts.map +1 -1
  57. package/esm/utility/date-time.mjs +2 -1
  58. package/esm/utility/date-time.mjs.map +1 -1
  59. package/esm/utility/index.d.mts +2 -1
  60. package/esm/utility/index.mjs +1 -1
  61. package/esm/web/fetch-url.d.mts +2 -3
  62. package/esm/web/fetch-url.d.mts.map +1 -1
  63. package/esm/web/fetch-url.mjs +2 -1
  64. package/esm/web/fetch-url.mjs.map +1 -1
  65. package/esm/web/index.d.mts +2 -1
  66. package/esm/web/index.mjs +1 -1
  67. package/esm/web/web-search.d.mts +2 -3
  68. package/esm/web/web-search.d.mts.map +1 -1
  69. package/esm/web/web-search.mjs +3 -4
  70. package/esm/web/web-search.mjs.map +1 -1
  71. package/llms-full.txt +122 -3
  72. package/llms.txt +1 -1
  73. package/package.json +2 -2
  74. package/skills/connect-mcp-server/SKILL.md +15 -0
  75. package/skills/expose-as-mcp-server/SKILL.md +107 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ All notable changes to `@warlock.js/ai-tools` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 5.33.0 - 2026-10-19
8
+
9
+ ### Fixed
10
+
11
+ - Keep HTTP and MCP fixtures valid under strict indexed access, accept string JSON-RPC request IDs in serving tests, and replace stdin through its descriptor without pretending a stream is a terminal.
12
+
13
+ ### Added
14
+
15
+ - MCP serving now exposes per-call `requestId`, authenticated `principal` and cancellation-aware signals over HTTP and stdio. HTTP `auth.authenticate` resolves principals alongside bearer/authorize checks; stdio accepts static or provider principals. `beforeCall` validates policy after input validation and can return a native error refusal or throw a JSON-RPC error. `notifications/cancelled` aborts only the matching caller/request and suppresses its response. New hook/context/refusal and stdio transport types are exported.
16
+
17
+ ## 5.32.0 - 2026-10-18
18
+
19
+ ### Changed
20
+
21
+ - Pin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
22
+
23
+ ### Fixed
24
+
25
+ - Opted-in native MCP tool results preserve content blocks, `isError`, `structuredContent` and `_meta` over stdio and HTTP, including tools with an advertised output schema. Per-request headers and abort signals remain available to tools; default result serialization and thrown-tool error handling are unchanged.
26
+ - Malformed native results produce a JSON-RPC internal error instead of sending invalid MCP payloads.
27
+
28
+ ### Added
29
+
30
+ - `mcpResult(...)` and its `McpNativeResult` type explicitly mark native tool results. `serve(..., { results: "native" })` enables detection for existing native-returning tools; `meta.mcpResults: "native" | "text"` overrides detection per tool.
31
+ - `meta.outputSchema` advertises structured tool output in `tools/list`; `McpToolCallResult` now includes `structuredContent` and `_meta`.
32
+
7
33
  ## 5.31.0 - 2026-10-18
8
34
 
9
35
  ### Added
package/cjs/index.cjs CHANGED
@@ -298,7 +298,7 @@ const ALL_METHODS = [
298
298
  "DELETE"
299
299
  ];
300
300
  /** Methods that conventionally carry no request body — `body` is dropped for these. */
301
- const BODYLESS_METHODS = new Set(["GET"]);
301
+ const BODYLESS_METHODS = /* @__PURE__ */ new Set(["GET"]);
302
302
  /**
303
303
  * Standard Schema for {@link HttpRequestInput}. `url` is required;
304
304
  * `method` is constrained to the canonical HTTP verb set (further
@@ -462,10 +462,12 @@ function httpRequestTool(options = {}) {
462
462
  ...input.headers
463
463
  };
464
464
  let body;
465
- if (!BODYLESS_METHODS.has(method) && input.body !== void 0) if (typeof input.body === "string") body = input.body;
466
- else {
467
- body = JSON.stringify(input.body);
468
- 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
+ }
469
471
  }
470
472
  let response;
471
473
  try {
@@ -717,8 +719,10 @@ function armCall(reject, method, options, onSettle) {
717
719
  }));
718
720
  };
719
721
  const signal = options?.signal;
720
- if (signal) if (signal.aborted) queueMicrotask(onAbort);
721
- 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
+ }
722
726
  function cleanup() {
723
727
  clearTimeout(timer);
724
728
  if (signal) signal.removeEventListener("abort", onAbort);
@@ -871,8 +875,10 @@ var HttpTransport = class {
871
875
  const timer = setTimeout(() => controller.abort(), timeoutMs);
872
876
  const onAbort = () => controller.abort();
873
877
  const signal = options?.signal;
874
- if (signal) if (signal.aborted) controller.abort();
875
- 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
+ }
876
882
  let response;
877
883
  try {
878
884
  response = await fetch(this.url, {
@@ -937,7 +943,8 @@ var HttpTransport = class {
937
943
  body: JSON.stringify(notification)
938
944
  });
939
945
  } catch (cause) {
940
- 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}`, {
941
948
  type: "connect",
942
949
  method,
943
950
  cause
@@ -1040,7 +1047,8 @@ var JsonRpcClient = class {
1040
1047
  * @returns A JSON-RPC client handle.
1041
1048
  */
1042
1049
  function createJsonRpcClient(source) {
1043
- return new JsonRpcClient(isTransportConfig(source) ? createTransport(source) : withIdAllocator(source));
1050
+ const transport = isTransportConfig(source) ? createTransport(source) : withIdAllocator(source);
1051
+ return new JsonRpcClient(transport);
1044
1052
  }
1045
1053
  /**
1046
1054
  * Distinguish an {@link McpTransport} config (a plain object with a `type`
@@ -1121,7 +1129,8 @@ var McpClientImpl = class {
1121
1129
  await this.rpc.notify("notifications/initialized");
1122
1130
  } catch (cause) {
1123
1131
  if (cause instanceof McpTransportError) throw cause;
1124
- 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}`, {
1125
1134
  type: "connect",
1126
1135
  method: "initialize",
1127
1136
  cause
@@ -1306,6 +1315,147 @@ const SUPPORTED_PROTOCOL_VERSIONS = [
1306
1315
  /** The newest MCP protocol version the server speaks. */
1307
1316
  const LATEST_PROTOCOL_VERSION = "2025-11-25";
1308
1317
 
1318
+ //#endregion
1319
+ //#region ../../packages/ai-tools/src/mcp/result.ts
1320
+ const NATIVE_RESULT = Symbol("mcpResult");
1321
+ /**
1322
+ * Opt in to native MCP content, error status, structured output and metadata.
1323
+ * Validation happens after invoke(), so malformed payloads become JSON-RPC
1324
+ * errors rather than being caught as tool execution errors.
1325
+ */
1326
+ function mcpResult(result) {
1327
+ return { [NATIVE_RESULT]: result };
1328
+ }
1329
+ /** Read a branded value without accepting lookalike application objects. */
1330
+ function isMcpResult(value) {
1331
+ return isRecord(value) && NATIVE_RESULT in value;
1332
+ }
1333
+ /** Get the original payload; no cloning, normalization or field stripping. */
1334
+ function unwrapMcpResult(value) {
1335
+ return value[NATIVE_RESULT];
1336
+ }
1337
+ /** Objects with result fields are candidates only when native mode is enabled. */
1338
+ function looksLikeMcpResult(value) {
1339
+ return isRecord(value) && [
1340
+ "content",
1341
+ "isError",
1342
+ "structuredContent",
1343
+ "_meta"
1344
+ ].some((key) => key in value);
1345
+ }
1346
+ /** Non-array object used by the MCP JSON object fields. */
1347
+ function isRecord(value) {
1348
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1349
+ }
1350
+ function optionalString(value) {
1351
+ return value === void 0 || typeof value === "string";
1352
+ }
1353
+ function validAnnotations(value) {
1354
+ if (value === void 0) return true;
1355
+ if (!isRecord(value)) return false;
1356
+ 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);
1357
+ }
1358
+ function validMetadata(value) {
1359
+ return (value["_meta"] === void 0 || isRecord(value["_meta"])) && validAnnotations(value.annotations);
1360
+ }
1361
+ function validIcon(value) {
1362
+ 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");
1363
+ }
1364
+ function validContent(value) {
1365
+ if (!isRecord(value) || !validMetadata(value)) return false;
1366
+ switch (value.type) {
1367
+ case "text": return typeof value.text === "string";
1368
+ case "image":
1369
+ case "audio": return typeof value.data === "string" && typeof value.mimeType === "string";
1370
+ 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));
1371
+ case "resource": {
1372
+ const resource = value.resource;
1373
+ return isRecord(resource) && typeof resource.uri === "string" && optionalString(resource.mimeType) && validMetadata(resource) && (typeof resource.text === "string" || typeof resource.blob === "string");
1374
+ }
1375
+ default: return false;
1376
+ }
1377
+ }
1378
+ /** Reject values JSON would drop, coerce or fail to encode (including cycles). */
1379
+ function isJsonValue(value, ancestors = /* @__PURE__ */ new Set()) {
1380
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1381
+ if (typeof value === "number") return Number.isFinite(value);
1382
+ if (typeof value !== "object" || ancestors.has(value)) return false;
1383
+ const prototype = Object.getPrototypeOf(value);
1384
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
1385
+ ancestors.add(value);
1386
+ const valid = Array.isArray(value) ? Array.from(value).every((entry) => isJsonValue(entry, ancestors)) : Object.values(value).every((entry) => entry === void 0 || isJsonValue(entry, ancestors));
1387
+ ancestors.delete(value);
1388
+ return valid;
1389
+ }
1390
+ /**
1391
+ * MCP 2025-11-25 CallToolResult and ContentBlock fields:
1392
+ * https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-result
1393
+ * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts
1394
+ * structuredContent is preserved even when tools/list declares outputSchema.
1395
+ * Tool authors remain responsible for conformity to that application schema.
1396
+ */
1397
+ function assertMcpResult(value) {
1398
+ 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.");
1399
+ }
1400
+
1401
+ //#endregion
1402
+ //#region ../../packages/ai-tools/src/mcp/serve-requests.ts
1403
+ /**
1404
+ * In-flight requests for one transport. HTTP partitions these by credentials;
1405
+ * stdio has a single connection. Cancellation never crosses those boundaries.
1406
+ * https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation
1407
+ * (checked 2026-10-19): ignore unknown/completed ids and never answer cancelled work.
1408
+ */
1409
+ var McpServeRequests = class {
1410
+ constructor() {
1411
+ this.scopes = /* @__PURE__ */ new Map();
1412
+ }
1413
+ cancel(message, scope = "") {
1414
+ if (!isRecord(message) || message.jsonrpc !== "2.0" || message.method !== "notifications/cancelled" || message.id !== void 0 || !isRecord(message.params)) return;
1415
+ const { requestId, reason } = message.params;
1416
+ if (typeof requestId !== "string" && typeof requestId !== "number" || reason !== void 0 && typeof reason !== "string") return;
1417
+ const pending = this.scopes.get(scope)?.get(requestId);
1418
+ if (pending?.size === 1) {
1419
+ for (const entry of pending) if (entry.cancellable) entry.controller.abort(new DOMException(typeof reason === "string" ? reason : "Request cancelled.", "AbortError"));
1420
+ }
1421
+ }
1422
+ async run(request, context, dispatch, scope = "") {
1423
+ let pending = this.scopes.get(scope);
1424
+ if (!pending) {
1425
+ pending = /* @__PURE__ */ new Map();
1426
+ this.scopes.set(scope, pending);
1427
+ }
1428
+ const controller = new AbortController();
1429
+ const entry = {
1430
+ controller,
1431
+ cancellable: request.method !== "initialize"
1432
+ };
1433
+ const calls = pending.get(request.id) ?? /* @__PURE__ */ new Set();
1434
+ calls.add(entry);
1435
+ pending.set(request.id, calls);
1436
+ const onAbort = () => controller.abort(context.signal?.reason);
1437
+ context.signal?.addEventListener("abort", onAbort, { once: true });
1438
+ if (context.signal?.aborted) onAbort();
1439
+ try {
1440
+ if (controller.signal.aborted) return;
1441
+ const response = await dispatch({
1442
+ ...context,
1443
+ requestId: request.id,
1444
+ signal: controller.signal
1445
+ });
1446
+ return controller.signal.aborted ? void 0 : response;
1447
+ } catch (cause) {
1448
+ if (controller.signal.aborted) return;
1449
+ throw cause;
1450
+ } finally {
1451
+ context.signal?.removeEventListener("abort", onAbort);
1452
+ calls.delete(entry);
1453
+ if (calls.size === 0) pending.delete(request.id);
1454
+ if (pending.size === 0) this.scopes.delete(scope);
1455
+ }
1456
+ }
1457
+ };
1458
+
1309
1459
  //#endregion
1310
1460
  //#region ../../packages/ai-tools/src/mcp/serve-http.ts
1311
1461
  /**
@@ -1342,7 +1492,7 @@ const JSON_RPC_INVALID_REQUEST$1 = -32600;
1342
1492
  /** Generic server-defined JSON-RPC code for HTTP-level refusals (auth, origin, size). */
1343
1493
  const JSON_RPC_SERVER_ERROR = -32e3;
1344
1494
  /** Host names that always mean "this machine". */
1345
- const LOOPBACK_NAMES = new Set([
1495
+ const LOOPBACK_NAMES = /* @__PURE__ */ new Set([
1346
1496
  "localhost",
1347
1497
  "127.0.0.1",
1348
1498
  "::1",
@@ -1404,9 +1554,9 @@ function resolveSettings(transport) {
1404
1554
  const tokens = auth?.bearer === void 0 ? [] : typeof auth.bearer === "string" ? [auth.bearer] : [...auth.bearer];
1405
1555
  if (tokens.some((token) => token.length === 0)) throw new McpServeError("MCP http auth.bearer tokens must be non-empty strings.", { type: "config" });
1406
1556
  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.`, {
1557
+ if (auth && tokens.length === 0 && !auth.authorize && !auth.authenticate) throw new McpServeError("MCP http auth needs `bearer`, `authorize` and/or `authenticate`.", { type: "config" });
1558
+ const authRequired = tokens.length > 0 || auth?.authorize !== void 0 || auth?.authenticate !== void 0;
1559
+ if (!loopback && !authRequired) throw new McpServeError(`Refusing to serve MCP over http on non-loopback host "${host}" without auth. Set transport.auth ({ bearer }, { authorize } or { authenticate }), or bind 127.0.0.1.`, {
1410
1560
  type: "config",
1411
1561
  context: { host }
1412
1562
  });
@@ -1419,6 +1569,7 @@ function resolveSettings(transport) {
1419
1569
  drainTimeoutMs,
1420
1570
  tokenDigests: tokens.map(digest),
1421
1571
  authorize: auth?.authorize,
1572
+ authenticate: auth?.authenticate,
1422
1573
  authRequired,
1423
1574
  allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
1424
1575
  allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name)))
@@ -1477,6 +1628,7 @@ function sendEmpty(response, status, headers = {}) {
1477
1628
  async function startHttpServer(transport, dispatch) {
1478
1629
  const settings = resolveSettings(transport);
1479
1630
  const shutdown = new AbortController();
1631
+ const requests = new McpServeRequests();
1480
1632
  const inFlight = /* @__PURE__ */ new Set();
1481
1633
  let closing;
1482
1634
  const server = (0, node_http.createServer)((request, response) => {
@@ -1484,7 +1636,7 @@ async function startHttpServer(transport, dispatch) {
1484
1636
  sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, "Server is shutting down.", { connection: "close" });
1485
1637
  return;
1486
1638
  }
1487
- const work = handleRequest(request, response, settings, dispatch, shutdown.signal).catch(() => {
1639
+ const work = handleRequest(request, response, settings, dispatch, shutdown.signal, requests).catch(() => {
1488
1640
  sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, "Internal server error.");
1489
1641
  });
1490
1642
  inFlight.add(work);
@@ -1546,7 +1698,7 @@ async function startHttpServer(transport, dispatch) {
1546
1698
  * Validate and answer one HTTP request: path → Host / Origin → auth →
1547
1699
  * method → headers → body → JSON-RPC dispatch.
1548
1700
  */
1549
- async function handleRequest(request, response, settings, dispatch, signal) {
1701
+ async function handleRequest(request, response, settings, dispatch, signal, requests) {
1550
1702
  const headers = flattenHeaders(request.headers);
1551
1703
  const method = (request.method ?? "GET").toUpperCase();
1552
1704
  const path = new URL(request.url ?? "/", "http://localhost").pathname;
@@ -1568,8 +1720,10 @@ async function handleRequest(request, response, settings, dispatch, signal) {
1568
1720
  headers
1569
1721
  };
1570
1722
  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\"" } : {});
1723
+ const authentication = await authorized(info, settings);
1724
+ if (!authentication.allowed) {
1725
+ const challenge = settings.tokenDigests.length > 0 || settings.authenticate ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {};
1726
+ sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", challenge);
1573
1727
  return;
1574
1728
  }
1575
1729
  if (method !== "POST") {
@@ -1613,6 +1767,7 @@ async function handleRequest(request, response, settings, dispatch, signal) {
1613
1767
  const id = record.id;
1614
1768
  if (typeof record.method === "string") {
1615
1769
  if (typeof id !== "string" && typeof id !== "number") {
1770
+ requests.cancel(record, cancellationScope(info));
1616
1771
  sendEmpty(response, 202);
1617
1772
  return;
1618
1773
  }
@@ -1622,10 +1777,20 @@ async function handleRequest(request, response, settings, dispatch, signal) {
1622
1777
  method: record.method
1623
1778
  };
1624
1779
  if (record.params !== void 0) rpcRequest.params = record.params;
1625
- sendJson(response, 200, await dispatch(rpcRequest, {
1626
- request: info,
1627
- signal
1628
- }));
1780
+ const disconnected = new AbortController();
1781
+ const onClose = () => disconnected.abort();
1782
+ response.once("close", onClose);
1783
+ try {
1784
+ const rpcResponse = await requests.run(rpcRequest, {
1785
+ request: info,
1786
+ principal: authentication.principal,
1787
+ signal: AbortSignal.any([signal, disconnected.signal])
1788
+ }, (context) => dispatch(rpcRequest, context), cancellationScope(info));
1789
+ if (rpcResponse === void 0) sendEmpty(response, 202);
1790
+ else sendJson(response, 200, rpcResponse);
1791
+ } finally {
1792
+ response.off("close", onClose);
1793
+ }
1629
1794
  return;
1630
1795
  }
1631
1796
  if ((typeof id === "string" || typeof id === "number") && ("result" in record || "error" in record)) {
@@ -1664,27 +1829,50 @@ function originAllowed(origin, settings) {
1664
1829
  if (settings.allowedOrigins.has(parsed.origin)) return true;
1665
1830
  return (parsed.protocol === "http:" || parsed.protocol === "https:") && isLoopbackHost(parsed.hostname);
1666
1831
  }
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
1832
  async function authorized(info, settings) {
1674
- if (!settings.authRequired) return true;
1833
+ if (!settings.authRequired) return { allowed: true };
1675
1834
  if (settings.tokenDigests.length > 0) {
1676
1835
  const match = /^Bearer[ \t]+(.+)$/i.exec(info.headers.authorization ?? "");
1677
1836
  const presented = digest(match?.[1]?.trim() ?? "");
1678
1837
  let matched = false;
1679
1838
  for (const expected of settings.tokenDigests) matched = (0, node_crypto.timingSafeEqual)(presented, expected) || matched;
1680
- if (!match || !matched) return false;
1839
+ if (!match || !matched) return { allowed: false };
1681
1840
  }
1682
- if (settings.authorize) try {
1683
- return await settings.authorize(info) === true;
1841
+ try {
1842
+ if (settings.authorize && await settings.authorize(info) !== true) return { allowed: false };
1843
+ if (settings.authenticate) {
1844
+ const principal = await settings.authenticate(info);
1845
+ return principal === void 0 ? { allowed: false } : {
1846
+ allowed: true,
1847
+ principal
1848
+ };
1849
+ }
1850
+ return { allowed: true };
1684
1851
  } catch {
1685
- return false;
1686
- }
1687
- return true;
1852
+ return { allowed: false };
1853
+ }
1854
+ }
1855
+ /**
1856
+ * Stateless HTTP has no connection/session id. Repeat the caller's credential
1857
+ * and application headers on notifications; per-message transport headers do
1858
+ * not identify a caller. Hash the material rather than retaining secrets as keys.
1859
+ */
1860
+ function cancellationScope(info) {
1861
+ const transportHeaders = /* @__PURE__ */ new Set([
1862
+ "host",
1863
+ "origin",
1864
+ "accept",
1865
+ "accept-encoding",
1866
+ "accept-language",
1867
+ "connection",
1868
+ "content-type",
1869
+ "content-length",
1870
+ "transfer-encoding",
1871
+ "mcp-protocol-version",
1872
+ "user-agent"
1873
+ ]);
1874
+ const credentials = Object.entries(info.headers).filter(([name]) => !transportHeaders.has(name)).sort(([left], [right]) => left.localeCompare(right));
1875
+ return digest(JSON.stringify([info.remoteAddress, credentials])).toString("hex");
1688
1876
  }
1689
1877
  /** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */
1690
1878
  function isJsonContentType(contentType) {
@@ -1859,7 +2047,10 @@ var McpServeHandler = class {
1859
2047
  */
1860
2048
  async handle(request, context = {}) {
1861
2049
  try {
1862
- return this.ok(request.id, await this.dispatch(request, context));
2050
+ return this.ok(request.id, await this.dispatch(request, {
2051
+ ...context,
2052
+ requestId: request.id
2053
+ }));
1863
2054
  } catch (cause) {
1864
2055
  if (cause instanceof RpcFailure) return this.error(request.id, cause.code, cause.message, cause.data);
1865
2056
  const message = cause instanceof Error ? cause.message : String(cause);
@@ -1872,19 +2063,19 @@ var McpServeHandler = class {
1872
2063
  case "initialize": return this.initializeResult(request.params);
1873
2064
  case "ping": return {};
1874
2065
  case "tools/list": return { tools: this.listTools() };
1875
- case "tools/call": return this.callTool(request.params, context);
1876
- default: break;
2066
+ case "tools/call": return this.callTool(request.params, {
2067
+ ...context,
2068
+ requestId: request.id
2069
+ });
1877
2070
  }
1878
2071
  if (this.resources) switch (request.method) {
1879
2072
  case "resources/list": return { resources: this.listResources(this.resources) };
1880
2073
  case "resources/templates/list": return { resourceTemplates: [] };
1881
2074
  case "resources/read": return this.readResource(this.resources, request.params, context);
1882
- default: break;
1883
2075
  }
1884
2076
  if (this.prompts) switch (request.method) {
1885
2077
  case "prompts/list": return { prompts: this.listPrompts(this.prompts) };
1886
2078
  case "prompts/get": return this.getPrompt(this.prompts, request.params, context);
1887
- default: break;
1888
2079
  }
1889
2080
  throw new RpcFailure(JSON_RPC_METHOD_NOT_FOUND, `Method "${request.method}" is not supported by this MCP server.`);
1890
2081
  }
@@ -1924,15 +2115,17 @@ var McpServeHandler = class {
1924
2115
  return {
1925
2116
  name,
1926
2117
  description: contract.description,
1927
- inputSchema
2118
+ inputSchema,
2119
+ ...isRecord(contract.meta?.outputSchema) ? { outputSchema: contract.meta.outputSchema } : {}
1928
2120
  };
1929
2121
  });
1930
2122
  }
1931
2123
  /**
1932
2124
  * Route a `tools/call` to the named contract's `invoke()` and map the
1933
2125
  * never-throwing {@link import("@warlock.js/ai").ToolInvokeResult}: `data`
1934
- * → a text content block, `error` → an `isError: true` result. An unknown
1935
- * tool name throws (mapped to a JSON-RPC error by {@link handle}).
2126
+ * → a text content block (or an opted-in native result), `error` → an
2127
+ * `isError: true` result. An unknown tool name throws (mapped to a
2128
+ * JSON-RPC error by {@link handle}).
1936
2129
  *
1937
2130
  * The contract receives a `ToolContext` carrying the abort signal and,
1938
2131
  * over HTTP, the inbound request's headers on `ctx.request`.
@@ -1944,7 +2137,11 @@ var McpServeHandler = class {
1944
2137
  type: "protocol",
1945
2138
  method: "tools/call"
1946
2139
  });
1947
- const toolContext = { artifacts: {} };
2140
+ const toolContext = {
2141
+ artifacts: {},
2142
+ requestId: context.requestId,
2143
+ principal: context.principal
2144
+ };
1948
2145
  if (context.signal) toolContext.signal = context.signal;
1949
2146
  if (context.request) {
1950
2147
  const { headers, remoteAddress } = context.request;
@@ -1953,6 +2150,43 @@ var McpServeHandler = class {
1953
2150
  remoteAddress
1954
2151
  };
1955
2152
  }
2153
+ if (this.options.beforeCall) {
2154
+ let input = args;
2155
+ if (contract.input) {
2156
+ let validation;
2157
+ try {
2158
+ validation = await contract.input["~standard"].validate(args);
2159
+ } catch (cause) {
2160
+ return {
2161
+ content: [{
2162
+ type: "text",
2163
+ text: `Schema validation threw for tool "${name}": ${cause instanceof Error ? cause.message : String(cause)}`
2164
+ }],
2165
+ isError: true
2166
+ };
2167
+ }
2168
+ if (validation.issues) return {
2169
+ content: [{
2170
+ type: "text",
2171
+ text: `Validation failed: ${validation.issues.map((issue) => issue.message).join("; ")}`
2172
+ }],
2173
+ isError: true
2174
+ };
2175
+ input = validation.value;
2176
+ }
2177
+ context.signal?.throwIfAborted();
2178
+ const refusal = await this.options.beforeCall(context, contract, input);
2179
+ if (refusal !== void 0) {
2180
+ try {
2181
+ assertMcpResult(refusal);
2182
+ if (refusal.isError !== true) throw new Error("A refusal must be an error result.");
2183
+ } catch {
2184
+ throw new RpcFailure(JSON_RPC_INTERNAL_ERROR, "Invalid MCP beforeCall refusal.");
2185
+ }
2186
+ return refusal;
2187
+ }
2188
+ }
2189
+ context.signal?.throwIfAborted();
1956
2190
  const result = await contract.invoke(args, toolContext);
1957
2191
  if (result.error) return {
1958
2192
  content: [{
@@ -1961,6 +2195,16 @@ var McpServeHandler = class {
1961
2195
  }],
1962
2196
  isError: true
1963
2197
  };
2198
+ const mode = contract.meta?.mcpResults ?? this.options.results ?? "text";
2199
+ if (isMcpResult(result.data) || mode === "native" && looksLikeMcpResult(result.data)) {
2200
+ const native = isMcpResult(result.data) ? unwrapMcpResult(result.data) : result.data;
2201
+ try {
2202
+ assertMcpResult(native);
2203
+ } catch {
2204
+ throw new RpcFailure(JSON_RPC_INTERNAL_ERROR, "Invalid native MCP CallToolResult.");
2205
+ }
2206
+ return native;
2207
+ }
1964
2208
  return {
1965
2209
  content: [{
1966
2210
  type: "text",
@@ -2073,7 +2317,7 @@ const JSON_RPC_INTERNAL_ERROR = -32603;
2073
2317
  * answered with a JSON-RPC "Invalid Request" error whose `id` is `null`.
2074
2318
  * Never rejects: a dispatch failure is answered as an internal error.
2075
2319
  */
2076
- async function answerStdioLine(line, dispatch) {
2320
+ async function answerStdioLine(line, dispatch, notify) {
2077
2321
  const trimmed = line.trim();
2078
2322
  if (!trimmed) return;
2079
2323
  let message;
@@ -2092,10 +2336,21 @@ async function answerStdioLine(line, dispatch) {
2092
2336
  })}\n`;
2093
2337
  const request = message;
2094
2338
  const id = request.id;
2095
- if (id === void 0 || id === null) return;
2339
+ if (id === void 0 || id === null) {
2340
+ notify?.(message);
2341
+ return;
2342
+ }
2343
+ if (typeof id !== "string" && typeof id !== "number") return `${JSON.stringify({
2344
+ jsonrpc: JSONRPC_VERSION,
2345
+ id: null,
2346
+ error: {
2347
+ code: JSON_RPC_INVALID_REQUEST,
2348
+ message: "Invalid request: id must be a string or number."
2349
+ }
2350
+ })}\n`;
2096
2351
  try {
2097
2352
  const response = await dispatch(request);
2098
- return `${JSON.stringify(response)}\n`;
2353
+ return response === void 0 ? void 0 : `${JSON.stringify(response)}\n`;
2099
2354
  } catch {
2100
2355
  return `${JSON.stringify({
2101
2356
  jsonrpc: JSONRPC_VERSION,
@@ -2179,7 +2434,7 @@ var McpServerImpl = class {
2179
2434
  start() {
2180
2435
  if (!this.running) {
2181
2436
  const transport = this.options.transport ?? { type: "stdio" };
2182
- const starting = transport.type === "http" ? this.startHttp(transport) : Promise.resolve(this.startStdio());
2437
+ const starting = transport.type === "http" ? this.startHttp(transport) : Promise.resolve(this.startStdio(transport));
2183
2438
  this.running = starting.catch((error) => {
2184
2439
  this.running = void 0;
2185
2440
  throw error;
@@ -2199,19 +2454,29 @@ var McpServerImpl = class {
2199
2454
  };
2200
2455
  }
2201
2456
  /** Wire the stdin line reader and answer each request on stdout. */
2202
- startStdio() {
2457
+ startStdio(transport) {
2458
+ const shutdown = new AbortController();
2459
+ const requests = new McpServeRequests();
2203
2460
  const reader = (0, node_readline.createInterface)({ input: process.stdin });
2461
+ reader.once("close", () => shutdown.abort());
2204
2462
  reader.on("line", (line) => {
2205
- this.onLine(line).catch(() => {});
2463
+ this.onLine(line, transport, requests, shutdown.signal).catch(() => {});
2206
2464
  });
2207
2465
  return { close: async () => {
2466
+ shutdown.abort();
2208
2467
  reader.close();
2209
2468
  this.running = void 0;
2210
2469
  } };
2211
2470
  }
2212
2471
  /** Answer one stdin line on stdout (see {@link answerStdioLine}). */
2213
- async onLine(line) {
2214
- const reply = await answerStdioLine(line, (request) => this.handler.handle(request));
2472
+ async onLine(line, transport, requests, signal) {
2473
+ const reply = await answerStdioLine(line, (request) => requests.run(request, { signal }, async (context) => {
2474
+ const principal = transport.principalProvider ? await transport.principalProvider(context) : transport.principal;
2475
+ return this.handler.handle(request, {
2476
+ ...context,
2477
+ principal
2478
+ });
2479
+ }), (message) => requests.cancel(message));
2215
2480
  if (reply !== void 0) process.stdout.write(reply);
2216
2481
  }
2217
2482
  async stop() {
@@ -2674,7 +2939,8 @@ const UNIT_ALIASES = {
2674
2939
  };
2675
2940
  /** Resolve a (possibly aliased / singular) unit token to its ms length. */
2676
2941
  function unitToMs(unit) {
2677
- const ms = UNIT_MS[UNIT_ALIASES[unit] ?? unit];
2942
+ const canonical = UNIT_ALIASES[unit] ?? unit;
2943
+ const ms = UNIT_MS[canonical];
2678
2944
  if (ms === void 0) throw new DateTimeError(`Unknown unit "${unit}". Supported units: ${Object.keys(UNIT_MS).join(", ")}.`, { type: "invalid-unit" });
2679
2945
  return ms;
2680
2946
  }
@@ -3114,7 +3380,8 @@ function fetchUrlTool(options) {
3114
3380
  type: "denied-host",
3115
3381
  cause
3116
3382
  });
3117
- throw new WebToolError(`fetch_url request failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
3383
+ const message = cause instanceof Error ? cause.message : String(cause);
3384
+ throw new WebToolError(`fetch_url request failed: ${message}`, {
3118
3385
  type: "request-failed",
3119
3386
  cause
3120
3387
  });
@@ -3178,7 +3445,8 @@ async function fetchJson(url, init, provider) {
3178
3445
  try {
3179
3446
  response = await fetch(url, init);
3180
3447
  } catch (cause) {
3181
- throw new WebToolError(`web_search request to the "${provider}" provider failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
3448
+ const message = cause instanceof Error ? cause.message : String(cause);
3449
+ throw new WebToolError(`web_search request to the "${provider}" provider failed: ${message}`, {
3182
3450
  type: "request-failed",
3183
3451
  cause
3184
3452
  });
@@ -3283,9 +3551,7 @@ function webSearchTool(options) {
3283
3551
  case "brave":
3284
3552
  results = await searchBrave(input.query, limit, apiKey);
3285
3553
  break;
3286
- case "serpapi":
3287
- results = await searchSerpApi(input.query, limit, apiKey);
3288
- break;
3554
+ case "serpapi": results = await searchSerpApi(input.query, limit, apiKey);
3289
3555
  }
3290
3556
  return { results };
3291
3557
  }
@@ -3321,6 +3587,7 @@ exports.fetchUrlTool = fetchUrlTool;
3321
3587
  exports.httpRequestTool = httpRequestTool;
3322
3588
  exports.jsonSchemaToStandard = jsonSchemaToStandard;
3323
3589
  exports.mcp = mcp;
3590
+ exports.mcpResult = mcpResult;
3324
3591
  exports.serve = serve;
3325
3592
  exports.webSearchTool = webSearchTool;
3326
3593
  //# sourceMappingURL=index.cjs.map