@warlock.js/ai-tools 5.32.0 → 5.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cjs/index.cjs CHANGED
@@ -1294,6 +1294,28 @@ function mcp$1(server, options) {
1294
1294
  return new McpClientImpl(server, options);
1295
1295
  }
1296
1296
 
1297
+ //#endregion
1298
+ //#region ../../packages/ai-tools/src/mcp/error.ts
1299
+ /** An application-defined JSON-RPC error thrown by MCP serving policy hooks. */
1300
+ var McpError = class extends Error {
1301
+ constructor(code, message, data) {
1302
+ super(message);
1303
+ this.name = "McpError";
1304
+ this.code = code;
1305
+ this.data = data;
1306
+ }
1307
+ };
1308
+ /**
1309
+ * Recognize our error and the SDK's McpError without a runtime SDK dependency.
1310
+ * SDK source (checked 2026-10-19):
1311
+ * https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x/src/types.ts
1312
+ * Only explicitly named errors with an integer code opt in; ordinary throws
1313
+ * retain the existing mapping. Preserve the error's message verbatim.
1314
+ */
1315
+ function isMcpError(cause) {
1316
+ return cause instanceof Error && cause.name === "McpError" && "code" in cause && typeof cause.code === "number" && Number.isInteger(cause.code);
1317
+ }
1318
+
1297
1319
  //#endregion
1298
1320
  //#region ../../packages/ai-tools/src/mcp/protocol.ts
1299
1321
  /**
@@ -1315,6 +1337,147 @@ const SUPPORTED_PROTOCOL_VERSIONS = [
1315
1337
  /** The newest MCP protocol version the server speaks. */
1316
1338
  const LATEST_PROTOCOL_VERSION = "2025-11-25";
1317
1339
 
1340
+ //#endregion
1341
+ //#region ../../packages/ai-tools/src/mcp/result.ts
1342
+ const NATIVE_RESULT = Symbol("mcpResult");
1343
+ /**
1344
+ * Opt in to native MCP content, error status, structured output and metadata.
1345
+ * Validation happens after invoke(), so malformed payloads become JSON-RPC
1346
+ * errors rather than being caught as tool execution errors.
1347
+ */
1348
+ function mcpResult(result) {
1349
+ return { [NATIVE_RESULT]: result };
1350
+ }
1351
+ /** Read a branded value without accepting lookalike application objects. */
1352
+ function isMcpResult(value) {
1353
+ return isRecord(value) && NATIVE_RESULT in value;
1354
+ }
1355
+ /** Get the original payload; no cloning, normalization or field stripping. */
1356
+ function unwrapMcpResult(value) {
1357
+ return value[NATIVE_RESULT];
1358
+ }
1359
+ /** Objects with result fields are candidates only when native mode is enabled. */
1360
+ function looksLikeMcpResult(value) {
1361
+ return isRecord(value) && [
1362
+ "content",
1363
+ "isError",
1364
+ "structuredContent",
1365
+ "_meta"
1366
+ ].some((key) => key in value);
1367
+ }
1368
+ /** Non-array object used by the MCP JSON object fields. */
1369
+ function isRecord(value) {
1370
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1371
+ }
1372
+ function optionalString(value) {
1373
+ return value === void 0 || typeof value === "string";
1374
+ }
1375
+ function validAnnotations(value) {
1376
+ if (value === void 0) return true;
1377
+ if (!isRecord(value)) return false;
1378
+ 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);
1379
+ }
1380
+ function validMetadata(value) {
1381
+ return (value["_meta"] === void 0 || isRecord(value["_meta"])) && validAnnotations(value.annotations);
1382
+ }
1383
+ function validIcon(value) {
1384
+ 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");
1385
+ }
1386
+ function validContent(value) {
1387
+ if (!isRecord(value) || !validMetadata(value)) return false;
1388
+ switch (value.type) {
1389
+ case "text": return typeof value.text === "string";
1390
+ case "image":
1391
+ case "audio": return typeof value.data === "string" && typeof value.mimeType === "string";
1392
+ 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));
1393
+ case "resource": {
1394
+ const resource = value.resource;
1395
+ return isRecord(resource) && typeof resource.uri === "string" && optionalString(resource.mimeType) && validMetadata(resource) && (typeof resource.text === "string" || typeof resource.blob === "string");
1396
+ }
1397
+ default: return false;
1398
+ }
1399
+ }
1400
+ /** Reject values JSON would drop, coerce or fail to encode (including cycles). */
1401
+ function isJsonValue(value, ancestors = /* @__PURE__ */ new Set()) {
1402
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1403
+ if (typeof value === "number") return Number.isFinite(value);
1404
+ if (typeof value !== "object" || ancestors.has(value)) return false;
1405
+ const prototype = Object.getPrototypeOf(value);
1406
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
1407
+ ancestors.add(value);
1408
+ const valid = Array.isArray(value) ? Array.from(value).every((entry) => isJsonValue(entry, ancestors)) : Object.values(value).every((entry) => entry === void 0 || isJsonValue(entry, ancestors));
1409
+ ancestors.delete(value);
1410
+ return valid;
1411
+ }
1412
+ /**
1413
+ * MCP 2025-11-25 CallToolResult and ContentBlock fields:
1414
+ * https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-result
1415
+ * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts
1416
+ * structuredContent is preserved even when tools/list declares outputSchema.
1417
+ * Tool authors remain responsible for conformity to that application schema.
1418
+ */
1419
+ function assertMcpResult(value) {
1420
+ 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.");
1421
+ }
1422
+
1423
+ //#endregion
1424
+ //#region ../../packages/ai-tools/src/mcp/serve-requests.ts
1425
+ /**
1426
+ * In-flight requests for one transport. HTTP partitions these by credentials;
1427
+ * stdio has a single connection. Cancellation never crosses those boundaries.
1428
+ * https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation
1429
+ * (checked 2026-10-19): ignore unknown/completed ids and never answer cancelled work.
1430
+ */
1431
+ var McpServeRequests = class {
1432
+ constructor() {
1433
+ this.scopes = /* @__PURE__ */ new Map();
1434
+ }
1435
+ cancel(message, scope = "") {
1436
+ if (!isRecord(message) || message.jsonrpc !== "2.0" || message.method !== "notifications/cancelled" || message.id !== void 0 || !isRecord(message.params)) return;
1437
+ const { requestId, reason } = message.params;
1438
+ if (typeof requestId !== "string" && typeof requestId !== "number" || reason !== void 0 && typeof reason !== "string") return;
1439
+ const pending = this.scopes.get(scope)?.get(requestId);
1440
+ if (pending?.size === 1) {
1441
+ for (const entry of pending) if (entry.cancellable) entry.controller.abort(new DOMException(typeof reason === "string" ? reason : "Request cancelled.", "AbortError"));
1442
+ }
1443
+ }
1444
+ async run(request, context, dispatch, scope = "") {
1445
+ let pending = this.scopes.get(scope);
1446
+ if (!pending) {
1447
+ pending = /* @__PURE__ */ new Map();
1448
+ this.scopes.set(scope, pending);
1449
+ }
1450
+ const controller = new AbortController();
1451
+ const entry = {
1452
+ controller,
1453
+ cancellable: request.method !== "initialize"
1454
+ };
1455
+ const calls = pending.get(request.id) ?? /* @__PURE__ */ new Set();
1456
+ calls.add(entry);
1457
+ pending.set(request.id, calls);
1458
+ const onAbort = () => controller.abort(context.signal?.reason);
1459
+ context.signal?.addEventListener("abort", onAbort, { once: true });
1460
+ if (context.signal?.aborted) onAbort();
1461
+ try {
1462
+ if (controller.signal.aborted) return;
1463
+ const response = await dispatch({
1464
+ ...context,
1465
+ requestId: request.id,
1466
+ signal: controller.signal
1467
+ });
1468
+ return controller.signal.aborted ? void 0 : response;
1469
+ } catch (cause) {
1470
+ if (controller.signal.aborted) return;
1471
+ throw cause;
1472
+ } finally {
1473
+ context.signal?.removeEventListener("abort", onAbort);
1474
+ calls.delete(entry);
1475
+ if (calls.size === 0) pending.delete(request.id);
1476
+ if (pending.size === 0) this.scopes.delete(scope);
1477
+ }
1478
+ }
1479
+ };
1480
+
1318
1481
  //#endregion
1319
1482
  //#region ../../packages/ai-tools/src/mcp/serve-http.ts
1320
1483
  /**
@@ -1324,7 +1487,7 @@ const LATEST_PROTOCOL_VERSION = "2025-11-25";
1324
1487
  * specification (https://modelcontextprotocol.io/specification/2025-11-25/basic/transports,
1325
1488
  * checked 2026-10-17) on `node:http`, with no SDK dependency:
1326
1489
  *
1327
- * - one endpoint path; each POST carries exactly one JSON-RPC message
1490
+ * - one MCP endpoint path (plus optional authenticated app routes); each MCP POST carries exactly one JSON-RPC message
1328
1491
  * (batches were removed from the protocol in 2025-06-18 and get 400);
1329
1492
  * - a request is answered with `Content-Type: application/json` (the spec
1330
1493
  * lets a server pick JSON or SSE per request); a notification or a
@@ -1382,6 +1545,35 @@ function hostnameOf(hostHeader) {
1382
1545
  function digest(value) {
1383
1546
  return (0, node_crypto.createHash)("sha256").update(value, "utf8").digest();
1384
1547
  }
1548
+ /** Resolve exact descendant routes; no route may replace the MCP endpoint. */
1549
+ function resolveRoutes(path, routes) {
1550
+ const resolved = /* @__PURE__ */ new Map();
1551
+ const methods = /* @__PURE__ */ new Set([
1552
+ "GET",
1553
+ "POST",
1554
+ "PUT",
1555
+ "PATCH",
1556
+ "DELETE",
1557
+ "HEAD",
1558
+ "OPTIONS"
1559
+ ]);
1560
+ if (routes.length > 0) {
1561
+ let canonical = false;
1562
+ try {
1563
+ canonical = !path.startsWith("//") && new URL(path, "http://localhost").pathname === path;
1564
+ } catch {}
1565
+ if (!canonical) throw new McpServeError("MCP http routes require a canonical endpoint pathname.", { type: "config" });
1566
+ }
1567
+ for (const route of routes) {
1568
+ if (!methods.has(route.method) || !/^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(route.path) || route.path.split("/").some((segment) => segment === "." || segment === "..") || typeof route.handler !== "function") throw new McpServeError("Invalid MCP http route method, path or handler.", { type: "config" });
1569
+ const fullPath = `${path.replace(/\/+$/, "")}${route.path}`;
1570
+ const byMethod = resolved.get(fullPath) ?? /* @__PURE__ */ new Map();
1571
+ if (fullPath === path || byMethod.has(route.method)) throw new McpServeError(`MCP http route "${route.method} ${fullPath}" conflicts.`, { type: "collision" });
1572
+ byMethod.set(route.method, { ...route });
1573
+ resolved.set(fullPath, byMethod);
1574
+ }
1575
+ return resolved;
1576
+ }
1385
1577
  /** Normalise a configured origin to `scheme://host[:port]`, or throw a config error. */
1386
1578
  function normalizeOrigin(origin) {
1387
1579
  try {
@@ -1413,9 +1605,9 @@ function resolveSettings(transport) {
1413
1605
  const tokens = auth?.bearer === void 0 ? [] : typeof auth.bearer === "string" ? [auth.bearer] : [...auth.bearer];
1414
1606
  if (tokens.some((token) => token.length === 0)) throw new McpServeError("MCP http auth.bearer tokens must be non-empty strings.", { type: "config" });
1415
1607
  if (auth && auth.bearer !== void 0 && tokens.length === 0) throw new McpServeError("MCP http auth.bearer needs at least one token.", { type: "config" });
1416
- if (auth && tokens.length === 0 && !auth.authorize) throw new McpServeError("MCP http auth needs `bearer` and/or `authorize`.", { type: "config" });
1417
- const authRequired = tokens.length > 0 || auth?.authorize !== void 0;
1418
- if (!loopback && !authRequired) throw new McpServeError(`Refusing to serve MCP over http on non-loopback host "${host}" without auth. Set transport.auth ({ bearer } or { authorize }), or bind 127.0.0.1.`, {
1608
+ if (auth && tokens.length === 0 && !auth.authorize && !auth.authenticate) throw new McpServeError("MCP http auth needs `bearer`, `authorize` and/or `authenticate`.", { type: "config" });
1609
+ const authRequired = tokens.length > 0 || auth?.authorize !== void 0 || auth?.authenticate !== void 0;
1610
+ 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.`, {
1419
1611
  type: "config",
1420
1612
  context: { host }
1421
1613
  });
@@ -1428,9 +1620,11 @@ function resolveSettings(transport) {
1428
1620
  drainTimeoutMs,
1429
1621
  tokenDigests: tokens.map(digest),
1430
1622
  authorize: auth?.authorize,
1623
+ authenticate: auth?.authenticate,
1431
1624
  authRequired,
1432
1625
  allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
1433
- allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name)))
1626
+ allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name))),
1627
+ routes: resolveRoutes(path, transport.routes ?? [])
1434
1628
  };
1435
1629
  }
1436
1630
  /** Flatten Node's header bag into lower-cased `name → value` strings. */
@@ -1486,6 +1680,7 @@ function sendEmpty(response, status, headers = {}) {
1486
1680
  async function startHttpServer(transport, dispatch) {
1487
1681
  const settings = resolveSettings(transport);
1488
1682
  const shutdown = new AbortController();
1683
+ const requests = new McpServeRequests();
1489
1684
  const inFlight = /* @__PURE__ */ new Set();
1490
1685
  let closing;
1491
1686
  const server = (0, node_http.createServer)((request, response) => {
@@ -1493,7 +1688,7 @@ async function startHttpServer(transport, dispatch) {
1493
1688
  sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, "Server is shutting down.", { connection: "close" });
1494
1689
  return;
1495
1690
  }
1496
- const work = handleRequest(request, response, settings, dispatch, shutdown.signal).catch(() => {
1691
+ const work = handleRequest(request, response, settings, dispatch, shutdown.signal, requests).catch(() => {
1497
1692
  sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, "Internal server error.");
1498
1693
  });
1499
1694
  inFlight.add(work);
@@ -1552,13 +1747,15 @@ async function startHttpServer(transport, dispatch) {
1552
1747
  };
1553
1748
  }
1554
1749
  /**
1555
- * Validate and answer one HTTP request: path → Host / Origin → auth →
1556
- * method → headers → body → JSON-RPC dispatch.
1750
+ * Validate and answer one HTTP request: Host / Origin → path → auth →
1751
+ * method → route handler or MCP headers/body/JSON-RPC dispatch.
1557
1752
  */
1558
- async function handleRequest(request, response, settings, dispatch, signal) {
1753
+ async function handleRequest(request, response, settings, dispatch, signal, requests) {
1559
1754
  const headers = flattenHeaders(request.headers);
1560
1755
  const method = (request.method ?? "GET").toUpperCase();
1561
- const path = new URL(request.url ?? "/", "http://localhost").pathname;
1756
+ const url = new URL(request.url ?? "/", "http://localhost");
1757
+ const path = url.pathname;
1758
+ const routes = settings.routes.get(path);
1562
1759
  if (!hostAllowed(headers.host, settings)) {
1563
1760
  sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Host header not allowed.");
1564
1761
  return;
@@ -1567,7 +1764,7 @@ async function handleRequest(request, response, settings, dispatch, signal) {
1567
1764
  sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Origin not allowed.");
1568
1765
  return;
1569
1766
  }
1570
- if (path !== settings.path) {
1767
+ if (path !== settings.path && !routes) {
1571
1768
  sendRefusal(response, 404, JSON_RPC_SERVER_ERROR, "Not found.");
1572
1769
  return;
1573
1770
  }
@@ -1577,11 +1774,21 @@ async function handleRequest(request, response, settings, dispatch, signal) {
1577
1774
  headers
1578
1775
  };
1579
1776
  if (request.socket.remoteAddress !== void 0) info.remoteAddress = request.socket.remoteAddress;
1580
- if (!await authorized(info, settings)) {
1581
- const challenge = settings.tokenDigests.length > 0 ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {};
1777
+ const authentication = await authorized(info, settings);
1778
+ if (!authentication.allowed) {
1779
+ const challenge = settings.tokenDigests.length > 0 || settings.authenticate ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {};
1582
1780
  sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", challenge);
1583
1781
  return;
1584
1782
  }
1783
+ if (routes) {
1784
+ const route = routes.get(method);
1785
+ if (!route) {
1786
+ sendEmpty(response, 405, { allow: [...routes.keys()].sort().join(", ") });
1787
+ return;
1788
+ }
1789
+ await handleRoute(request, response, route, info, url, authentication.principal, signal, settings);
1790
+ return;
1791
+ }
1585
1792
  if (method !== "POST") {
1586
1793
  sendEmpty(response, 405, { allow: "POST" });
1587
1794
  return;
@@ -1623,6 +1830,7 @@ async function handleRequest(request, response, settings, dispatch, signal) {
1623
1830
  const id = record.id;
1624
1831
  if (typeof record.method === "string") {
1625
1832
  if (typeof id !== "string" && typeof id !== "number") {
1833
+ requests.cancel(record, cancellationScope(info));
1626
1834
  sendEmpty(response, 202);
1627
1835
  return;
1628
1836
  }
@@ -1632,10 +1840,20 @@ async function handleRequest(request, response, settings, dispatch, signal) {
1632
1840
  method: record.method
1633
1841
  };
1634
1842
  if (record.params !== void 0) rpcRequest.params = record.params;
1635
- sendJson(response, 200, await dispatch(rpcRequest, {
1636
- request: info,
1637
- signal
1638
- }));
1843
+ const disconnected = new AbortController();
1844
+ const onClose = () => disconnected.abort();
1845
+ response.once("close", onClose);
1846
+ try {
1847
+ const rpcResponse = await requests.run(rpcRequest, {
1848
+ request: info,
1849
+ principal: authentication.principal,
1850
+ signal: AbortSignal.any([signal, disconnected.signal])
1851
+ }, (context) => dispatch(rpcRequest, context), cancellationScope(info));
1852
+ if (rpcResponse === void 0) sendEmpty(response, 202);
1853
+ else sendJson(response, 200, rpcResponse);
1854
+ } finally {
1855
+ response.off("close", onClose);
1856
+ }
1639
1857
  return;
1640
1858
  }
1641
1859
  if ((typeof id === "string" || typeof id === "number") && ("result" in record || "error" in record)) {
@@ -1644,6 +1862,32 @@ async function handleRequest(request, response, settings, dispatch, signal) {
1644
1862
  }
1645
1863
  sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST$1, "Invalid request: not a JSON-RPC message.");
1646
1864
  }
1865
+ /** Run a side route after the shared guards, tracking disconnect and shutdown. */
1866
+ async function handleRoute(request, response, route, info, url, principal, shutdown, settings) {
1867
+ const disconnected = new AbortController();
1868
+ const onClose = () => disconnected.abort();
1869
+ response.once("close", onClose);
1870
+ try {
1871
+ const body = await readBody$1(request, settings.maxBodyBytes);
1872
+ if (body.type === "too-large") {
1873
+ sendRefusal(response, 413, JSON_RPC_SERVER_ERROR, `Request body exceeds ${settings.maxBodyBytes} bytes.`, { connection: "close" });
1874
+ return;
1875
+ }
1876
+ const signal = AbortSignal.any([shutdown, disconnected.signal]);
1877
+ signal.throwIfAborted();
1878
+ const result = await route.handler({
1879
+ request: info,
1880
+ url,
1881
+ principal,
1882
+ body: body.text,
1883
+ signal
1884
+ });
1885
+ if (result.body === void 0 || info.method === "HEAD") sendEmpty(response, result.status ?? 200, result.headers);
1886
+ else sendJson(response, result.status ?? 200, result.body, result.headers);
1887
+ } finally {
1888
+ response.off("close", onClose);
1889
+ }
1890
+ }
1647
1891
  /**
1648
1892
  * Host-header check. While bound to loopback only loopback names (plus
1649
1893
  * `allowedHosts`) are accepted, so a DNS-rebound name pointing at
@@ -1674,27 +1918,50 @@ function originAllowed(origin, settings) {
1674
1918
  if (settings.allowedOrigins.has(parsed.origin)) return true;
1675
1919
  return (parsed.protocol === "http:" || parsed.protocol === "https:") && isLoopbackHost(parsed.hostname);
1676
1920
  }
1677
- /**
1678
- * Run the configured auth. Bearer tokens are compared in constant time
1679
- * over SHA-256 digests (equal-length buffers for `timingSafeEqual`), and
1680
- * every configured token is compared so the match position does not leak.
1681
- * An `authorize` hook that throws denies the request.
1682
- */
1683
1921
  async function authorized(info, settings) {
1684
- if (!settings.authRequired) return true;
1922
+ if (!settings.authRequired) return { allowed: true };
1685
1923
  if (settings.tokenDigests.length > 0) {
1686
1924
  const match = /^Bearer[ \t]+(.+)$/i.exec(info.headers.authorization ?? "");
1687
1925
  const presented = digest(match?.[1]?.trim() ?? "");
1688
1926
  let matched = false;
1689
1927
  for (const expected of settings.tokenDigests) matched = (0, node_crypto.timingSafeEqual)(presented, expected) || matched;
1690
- if (!match || !matched) return false;
1928
+ if (!match || !matched) return { allowed: false };
1691
1929
  }
1692
- if (settings.authorize) try {
1693
- return await settings.authorize(info) === true;
1930
+ try {
1931
+ if (settings.authorize && await settings.authorize(info) !== true) return { allowed: false };
1932
+ if (settings.authenticate) {
1933
+ const principal = await settings.authenticate(info);
1934
+ return principal === void 0 ? { allowed: false } : {
1935
+ allowed: true,
1936
+ principal
1937
+ };
1938
+ }
1939
+ return { allowed: true };
1694
1940
  } catch {
1695
- return false;
1696
- }
1697
- return true;
1941
+ return { allowed: false };
1942
+ }
1943
+ }
1944
+ /**
1945
+ * Stateless HTTP has no connection/session id. Repeat the caller's credential
1946
+ * and application headers on notifications; per-message transport headers do
1947
+ * not identify a caller. Hash the material rather than retaining secrets as keys.
1948
+ */
1949
+ function cancellationScope(info) {
1950
+ const transportHeaders = /* @__PURE__ */ new Set([
1951
+ "host",
1952
+ "origin",
1953
+ "accept",
1954
+ "accept-encoding",
1955
+ "accept-language",
1956
+ "connection",
1957
+ "content-type",
1958
+ "content-length",
1959
+ "transfer-encoding",
1960
+ "mcp-protocol-version",
1961
+ "user-agent"
1962
+ ]);
1963
+ const credentials = Object.entries(info.headers).filter(([name]) => !transportHeaders.has(name)).sort(([left], [right]) => left.localeCompare(right));
1964
+ return digest(JSON.stringify([info.remoteAddress, credentials])).toString("hex");
1698
1965
  }
1699
1966
  /** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */
1700
1967
  function isJsonContentType(contentType) {
@@ -1751,89 +2018,6 @@ function readBody$1(request, limit) {
1751
2018
  });
1752
2019
  }
1753
2020
 
1754
- //#endregion
1755
- //#region ../../packages/ai-tools/src/mcp/result.ts
1756
- const NATIVE_RESULT = Symbol("mcpResult");
1757
- /**
1758
- * Opt in to native MCP content, error status, structured output and metadata.
1759
- * Validation happens after invoke(), so malformed payloads become JSON-RPC
1760
- * errors rather than being caught as tool execution errors.
1761
- */
1762
- function mcpResult(result) {
1763
- return { [NATIVE_RESULT]: result };
1764
- }
1765
- /** Read a branded value without accepting lookalike application objects. */
1766
- function isMcpResult(value) {
1767
- return isRecord(value) && NATIVE_RESULT in value;
1768
- }
1769
- /** Get the original payload; no cloning, normalization or field stripping. */
1770
- function unwrapMcpResult(value) {
1771
- return value[NATIVE_RESULT];
1772
- }
1773
- /** Objects with result fields are candidates only when native mode is enabled. */
1774
- function looksLikeMcpResult(value) {
1775
- return isRecord(value) && [
1776
- "content",
1777
- "isError",
1778
- "structuredContent",
1779
- "_meta"
1780
- ].some((key) => key in value);
1781
- }
1782
- /** Non-array object used by the MCP JSON object fields. */
1783
- function isRecord(value) {
1784
- return typeof value === "object" && value !== null && !Array.isArray(value);
1785
- }
1786
- function optionalString(value) {
1787
- return value === void 0 || typeof value === "string";
1788
- }
1789
- function validAnnotations(value) {
1790
- if (value === void 0) return true;
1791
- if (!isRecord(value)) return false;
1792
- return (value.audience === void 0 || Array.isArray(value.audience) && value.audience.every((role) => role === "user" || role === "assistant")) && (value.priority === void 0 || typeof value.priority === "number" && value.priority >= 0 && value.priority <= 1) && optionalString(value.lastModified);
1793
- }
1794
- function validMetadata(value) {
1795
- return (value["_meta"] === void 0 || isRecord(value["_meta"])) && validAnnotations(value.annotations);
1796
- }
1797
- function validIcon(value) {
1798
- return isRecord(value) && typeof value.src === "string" && optionalString(value.mimeType) && (value.sizes === void 0 || Array.isArray(value.sizes) && value.sizes.every((size) => typeof size === "string")) && (value.theme === void 0 || value.theme === "light" || value.theme === "dark");
1799
- }
1800
- function validContent(value) {
1801
- if (!isRecord(value) || !validMetadata(value)) return false;
1802
- switch (value.type) {
1803
- case "text": return typeof value.text === "string";
1804
- case "image":
1805
- case "audio": return typeof value.data === "string" && typeof value.mimeType === "string";
1806
- case "resource_link": return typeof value.uri === "string" && typeof value.name === "string" && optionalString(value.title) && optionalString(value.description) && optionalString(value.mimeType) && (value.size === void 0 || typeof value.size === "number") && (value.icons === void 0 || Array.isArray(value.icons) && value.icons.every(validIcon));
1807
- case "resource": {
1808
- const resource = value.resource;
1809
- return isRecord(resource) && typeof resource.uri === "string" && optionalString(resource.mimeType) && validMetadata(resource) && (typeof resource.text === "string" || typeof resource.blob === "string");
1810
- }
1811
- default: return false;
1812
- }
1813
- }
1814
- /** Reject values JSON would drop, coerce or fail to encode (including cycles). */
1815
- function isJsonValue(value, ancestors = /* @__PURE__ */ new Set()) {
1816
- if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1817
- if (typeof value === "number") return Number.isFinite(value);
1818
- if (typeof value !== "object" || ancestors.has(value)) return false;
1819
- const prototype = Object.getPrototypeOf(value);
1820
- if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
1821
- ancestors.add(value);
1822
- const valid = Array.isArray(value) ? Array.from(value).every((entry) => isJsonValue(entry, ancestors)) : Object.values(value).every((entry) => entry === void 0 || isJsonValue(entry, ancestors));
1823
- ancestors.delete(value);
1824
- return valid;
1825
- }
1826
- /**
1827
- * MCP 2025-11-25 CallToolResult and ContentBlock fields:
1828
- * https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-result
1829
- * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts
1830
- * structuredContent is preserved even when tools/list declares outputSchema.
1831
- * Tool authors remain responsible for conformity to that application schema.
1832
- */
1833
- function assertMcpResult(value) {
1834
- if (!isRecord(value) || !Array.isArray(value.content) || !Array.from(value.content).every(validContent) || value.isError !== void 0 && typeof value.isError !== "boolean" || value.structuredContent !== void 0 && !isRecord(value.structuredContent) || value["_meta"] !== void 0 && !isRecord(value["_meta"]) || !isJsonValue(value)) throw new TypeError("Invalid native MCP CallToolResult.");
1835
- }
1836
-
1837
2021
  //#endregion
1838
2022
  //#region ../../packages/ai-tools/src/mcp/serve.ts
1839
2023
  /** JSON-RPC version literal every outbound message carries. */
@@ -1952,7 +2136,10 @@ var McpServeHandler = class {
1952
2136
  */
1953
2137
  async handle(request, context = {}) {
1954
2138
  try {
1955
- return this.ok(request.id, await this.dispatch(request, context));
2139
+ return this.ok(request.id, await this.dispatch(request, {
2140
+ ...context,
2141
+ requestId: request.id
2142
+ }));
1956
2143
  } catch (cause) {
1957
2144
  if (cause instanceof RpcFailure) return this.error(request.id, cause.code, cause.message, cause.data);
1958
2145
  const message = cause instanceof Error ? cause.message : String(cause);
@@ -1964,8 +2151,14 @@ var McpServeHandler = class {
1964
2151
  switch (request.method) {
1965
2152
  case "initialize": return this.initializeResult(request.params);
1966
2153
  case "ping": return {};
1967
- case "tools/list": return { tools: this.listTools() };
1968
- case "tools/call": return this.callTool(request.params, context);
2154
+ case "tools/list": return { tools: await this.listTools({
2155
+ ...context,
2156
+ requestId: request.id
2157
+ }) };
2158
+ case "tools/call": return this.callTool(request.params, {
2159
+ ...context,
2160
+ requestId: request.id
2161
+ });
1969
2162
  }
1970
2163
  if (this.resources) switch (request.method) {
1971
2164
  case "resources/list": return { resources: this.listResources(this.resources) };
@@ -2004,9 +2197,9 @@ var McpServeHandler = class {
2004
2197
  * the configured dialect (default `draft-2020-12` — overriding
2005
2198
  * `extractJsonSchema`'s own `openai-strict` default to a neutral MCP draft).
2006
2199
  */
2007
- listTools() {
2200
+ async listTools(context) {
2008
2201
  const target = this.options.schemaTarget ?? DEFAULT_SCHEMA_TARGET;
2009
- return this.tools.map(({ name, contract }) => {
2202
+ const tools = this.tools.map(({ name, contract }) => {
2010
2203
  const inputSchema = (0, _warlock_js_ai.extractJsonSchema)(contract.input, { target }) ?? {
2011
2204
  type: "object",
2012
2205
  properties: {}
@@ -2018,6 +2211,8 @@ var McpServeHandler = class {
2018
2211
  ...isRecord(contract.meta?.outputSchema) ? { outputSchema: contract.meta.outputSchema } : {}
2019
2212
  };
2020
2213
  });
2214
+ context.signal?.throwIfAborted();
2215
+ return this.options.listFilter ? this.options.listFilter(context, tools) : tools;
2021
2216
  }
2022
2217
  /**
2023
2218
  * Route a `tools/call` to the named contract's `invoke()` and map the
@@ -2031,12 +2226,25 @@ var McpServeHandler = class {
2031
2226
  */
2032
2227
  async callTool(params, context) {
2033
2228
  const { name, args } = readCallParams(params);
2229
+ if (this.options.validateCallParams) {
2230
+ context.signal?.throwIfAborted();
2231
+ try {
2232
+ await this.options.validateCallParams(context, readRecord(params));
2233
+ } catch (cause) {
2234
+ if (isMcpError(cause)) throw new RpcFailure(cause.code, cause.message, cause.data);
2235
+ throw cause;
2236
+ }
2237
+ }
2034
2238
  const contract = this.byName.get(name);
2035
2239
  if (!contract) throw new McpTransportError(`Unknown tool "${name}".`, {
2036
2240
  type: "protocol",
2037
2241
  method: "tools/call"
2038
2242
  });
2039
- const toolContext = { artifacts: {} };
2243
+ const toolContext = {
2244
+ artifacts: {},
2245
+ requestId: context.requestId,
2246
+ principal: context.principal
2247
+ };
2040
2248
  if (context.signal) toolContext.signal = context.signal;
2041
2249
  if (context.request) {
2042
2250
  const { headers, remoteAddress } = context.request;
@@ -2045,6 +2253,49 @@ var McpServeHandler = class {
2045
2253
  remoteAddress
2046
2254
  };
2047
2255
  }
2256
+ if (this.options.beforeCall) {
2257
+ let input = args;
2258
+ if (contract.input) {
2259
+ let validation;
2260
+ try {
2261
+ validation = await contract.input["~standard"].validate(args);
2262
+ } catch (cause) {
2263
+ return {
2264
+ content: [{
2265
+ type: "text",
2266
+ text: `Schema validation threw for tool "${name}": ${cause instanceof Error ? cause.message : String(cause)}`
2267
+ }],
2268
+ isError: true
2269
+ };
2270
+ }
2271
+ if (validation.issues) return {
2272
+ content: [{
2273
+ type: "text",
2274
+ text: `Validation failed: ${validation.issues.map((issue) => issue.message).join("; ")}`
2275
+ }],
2276
+ isError: true
2277
+ };
2278
+ input = validation.value;
2279
+ }
2280
+ context.signal?.throwIfAborted();
2281
+ let refusal;
2282
+ try {
2283
+ refusal = await this.options.beforeCall(context, contract, input);
2284
+ } catch (cause) {
2285
+ if (isMcpError(cause)) throw new RpcFailure(cause.code, cause.message, cause.data);
2286
+ throw cause;
2287
+ }
2288
+ if (refusal !== void 0) {
2289
+ try {
2290
+ assertMcpResult(refusal);
2291
+ if (refusal.isError !== true) throw new Error("A refusal must be an error result.");
2292
+ } catch {
2293
+ throw new RpcFailure(JSON_RPC_INTERNAL_ERROR, "Invalid MCP beforeCall refusal.");
2294
+ }
2295
+ return refusal;
2296
+ }
2297
+ }
2298
+ context.signal?.throwIfAborted();
2048
2299
  const result = await contract.invoke(args, toolContext);
2049
2300
  if (result.error) return {
2050
2301
  content: [{
@@ -2175,7 +2426,7 @@ const JSON_RPC_INTERNAL_ERROR = -32603;
2175
2426
  * answered with a JSON-RPC "Invalid Request" error whose `id` is `null`.
2176
2427
  * Never rejects: a dispatch failure is answered as an internal error.
2177
2428
  */
2178
- async function answerStdioLine(line, dispatch) {
2429
+ async function answerStdioLine(line, dispatch, notify) {
2179
2430
  const trimmed = line.trim();
2180
2431
  if (!trimmed) return;
2181
2432
  let message;
@@ -2194,10 +2445,21 @@ async function answerStdioLine(line, dispatch) {
2194
2445
  })}\n`;
2195
2446
  const request = message;
2196
2447
  const id = request.id;
2197
- if (id === void 0 || id === null) return;
2448
+ if (id === void 0 || id === null) {
2449
+ notify?.(message);
2450
+ return;
2451
+ }
2452
+ if (typeof id !== "string" && typeof id !== "number") return `${JSON.stringify({
2453
+ jsonrpc: JSONRPC_VERSION,
2454
+ id: null,
2455
+ error: {
2456
+ code: JSON_RPC_INVALID_REQUEST,
2457
+ message: "Invalid request: id must be a string or number."
2458
+ }
2459
+ })}\n`;
2198
2460
  try {
2199
2461
  const response = await dispatch(request);
2200
- return `${JSON.stringify(response)}\n`;
2462
+ return response === void 0 ? void 0 : `${JSON.stringify(response)}\n`;
2201
2463
  } catch {
2202
2464
  return `${JSON.stringify({
2203
2465
  jsonrpc: JSONRPC_VERSION,
@@ -2281,7 +2543,7 @@ var McpServerImpl = class {
2281
2543
  start() {
2282
2544
  if (!this.running) {
2283
2545
  const transport = this.options.transport ?? { type: "stdio" };
2284
- const starting = transport.type === "http" ? this.startHttp(transport) : Promise.resolve(this.startStdio());
2546
+ const starting = transport.type === "http" ? this.startHttp(transport) : Promise.resolve(this.startStdio(transport));
2285
2547
  this.running = starting.catch((error) => {
2286
2548
  this.running = void 0;
2287
2549
  throw error;
@@ -2301,19 +2563,29 @@ var McpServerImpl = class {
2301
2563
  };
2302
2564
  }
2303
2565
  /** Wire the stdin line reader and answer each request on stdout. */
2304
- startStdio() {
2566
+ startStdio(transport) {
2567
+ const shutdown = new AbortController();
2568
+ const requests = new McpServeRequests();
2305
2569
  const reader = (0, node_readline.createInterface)({ input: process.stdin });
2570
+ reader.once("close", () => shutdown.abort());
2306
2571
  reader.on("line", (line) => {
2307
- this.onLine(line).catch(() => {});
2572
+ this.onLine(line, transport, requests, shutdown.signal).catch(() => {});
2308
2573
  });
2309
2574
  return { close: async () => {
2575
+ shutdown.abort();
2310
2576
  reader.close();
2311
2577
  this.running = void 0;
2312
2578
  } };
2313
2579
  }
2314
2580
  /** Answer one stdin line on stdout (see {@link answerStdioLine}). */
2315
- async onLine(line) {
2316
- const reply = await answerStdioLine(line, (request) => this.handler.handle(request));
2581
+ async onLine(line, transport, requests, signal) {
2582
+ const reply = await answerStdioLine(line, (request) => requests.run(request, { signal }, async (context) => {
2583
+ const principal = transport.principalProvider ? await transport.principalProvider(context) : transport.principal;
2584
+ return this.handler.handle(request, {
2585
+ ...context,
2586
+ principal
2587
+ });
2588
+ }), (message) => requests.cancel(message));
2317
2589
  if (reply !== void 0) process.stdout.write(reply);
2318
2590
  }
2319
2591
  async stop() {
@@ -3397,20 +3669,20 @@ function webSearchTool(options) {
3397
3669
 
3398
3670
  //#endregion
3399
3671
  //#region ../../packages/ai-tools/src/register.ts
3400
- _warlock_js_ai.ai.tools = {
3672
+ Object.assign(_warlock_js_ai.ai.tools, {
3401
3673
  webSearch: webSearchTool,
3402
3674
  fetchUrl: fetchUrlTool,
3403
3675
  http: httpRequestTool,
3404
3676
  calculator: calculatorTool,
3405
- dateTime: (options) => dateTimeTool(options),
3406
- measure: _warlock_js_ai.measureTools
3407
- };
3677
+ dateTime: (options) => dateTimeTool(options)
3678
+ });
3408
3679
  _warlock_js_ai.ai.mcp = mcp;
3409
3680
 
3410
3681
  //#endregion
3411
3682
  exports.CalculatorError = CalculatorError;
3412
3683
  exports.DateTimeError = DateTimeError;
3413
3684
  exports.HttpPolicyError = HttpPolicyError;
3685
+ exports.McpError = McpError;
3414
3686
  exports.McpServeError = McpServeError;
3415
3687
  exports.McpTransportError = McpTransportError;
3416
3688
  exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;