@warlock.js/ai-tools 5.32.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ 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
+
7
17
  ## 5.32.0 - 2026-10-18
8
18
 
9
19
  ### Changed
package/cjs/index.cjs CHANGED
@@ -1315,6 +1315,147 @@ const SUPPORTED_PROTOCOL_VERSIONS = [
1315
1315
  /** The newest MCP protocol version the server speaks. */
1316
1316
  const LATEST_PROTOCOL_VERSION = "2025-11-25";
1317
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
+
1318
1459
  //#endregion
1319
1460
  //#region ../../packages/ai-tools/src/mcp/serve-http.ts
1320
1461
  /**
@@ -1413,9 +1554,9 @@ function resolveSettings(transport) {
1413
1554
  const tokens = auth?.bearer === void 0 ? [] : typeof auth.bearer === "string" ? [auth.bearer] : [...auth.bearer];
1414
1555
  if (tokens.some((token) => token.length === 0)) throw new McpServeError("MCP http auth.bearer tokens must be non-empty strings.", { type: "config" });
1415
1556
  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.`, {
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.`, {
1419
1560
  type: "config",
1420
1561
  context: { host }
1421
1562
  });
@@ -1428,6 +1569,7 @@ function resolveSettings(transport) {
1428
1569
  drainTimeoutMs,
1429
1570
  tokenDigests: tokens.map(digest),
1430
1571
  authorize: auth?.authorize,
1572
+ authenticate: auth?.authenticate,
1431
1573
  authRequired,
1432
1574
  allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
1433
1575
  allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name)))
@@ -1486,6 +1628,7 @@ function sendEmpty(response, status, headers = {}) {
1486
1628
  async function startHttpServer(transport, dispatch) {
1487
1629
  const settings = resolveSettings(transport);
1488
1630
  const shutdown = new AbortController();
1631
+ const requests = new McpServeRequests();
1489
1632
  const inFlight = /* @__PURE__ */ new Set();
1490
1633
  let closing;
1491
1634
  const server = (0, node_http.createServer)((request, response) => {
@@ -1493,7 +1636,7 @@ async function startHttpServer(transport, dispatch) {
1493
1636
  sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, "Server is shutting down.", { connection: "close" });
1494
1637
  return;
1495
1638
  }
1496
- const work = handleRequest(request, response, settings, dispatch, shutdown.signal).catch(() => {
1639
+ const work = handleRequest(request, response, settings, dispatch, shutdown.signal, requests).catch(() => {
1497
1640
  sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, "Internal server error.");
1498
1641
  });
1499
1642
  inFlight.add(work);
@@ -1555,7 +1698,7 @@ async function startHttpServer(transport, dispatch) {
1555
1698
  * Validate and answer one HTTP request: path → Host / Origin → auth →
1556
1699
  * method → headers → body → JSON-RPC dispatch.
1557
1700
  */
1558
- async function handleRequest(request, response, settings, dispatch, signal) {
1701
+ async function handleRequest(request, response, settings, dispatch, signal, requests) {
1559
1702
  const headers = flattenHeaders(request.headers);
1560
1703
  const method = (request.method ?? "GET").toUpperCase();
1561
1704
  const path = new URL(request.url ?? "/", "http://localhost").pathname;
@@ -1577,8 +1720,9 @@ async function handleRequest(request, response, settings, dispatch, signal) {
1577
1720
  headers
1578
1721
  };
1579
1722
  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\"" } : {};
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\"" } : {};
1582
1726
  sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", challenge);
1583
1727
  return;
1584
1728
  }
@@ -1623,6 +1767,7 @@ async function handleRequest(request, response, settings, dispatch, signal) {
1623
1767
  const id = record.id;
1624
1768
  if (typeof record.method === "string") {
1625
1769
  if (typeof id !== "string" && typeof id !== "number") {
1770
+ requests.cancel(record, cancellationScope(info));
1626
1771
  sendEmpty(response, 202);
1627
1772
  return;
1628
1773
  }
@@ -1632,10 +1777,20 @@ async function handleRequest(request, response, settings, dispatch, signal) {
1632
1777
  method: record.method
1633
1778
  };
1634
1779
  if (record.params !== void 0) rpcRequest.params = record.params;
1635
- sendJson(response, 200, await dispatch(rpcRequest, {
1636
- request: info,
1637
- signal
1638
- }));
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
+ }
1639
1794
  return;
1640
1795
  }
1641
1796
  if ((typeof id === "string" || typeof id === "number") && ("result" in record || "error" in record)) {
@@ -1674,27 +1829,50 @@ function originAllowed(origin, settings) {
1674
1829
  if (settings.allowedOrigins.has(parsed.origin)) return true;
1675
1830
  return (parsed.protocol === "http:" || parsed.protocol === "https:") && isLoopbackHost(parsed.hostname);
1676
1831
  }
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
1832
  async function authorized(info, settings) {
1684
- if (!settings.authRequired) return true;
1833
+ if (!settings.authRequired) return { allowed: true };
1685
1834
  if (settings.tokenDigests.length > 0) {
1686
1835
  const match = /^Bearer[ \t]+(.+)$/i.exec(info.headers.authorization ?? "");
1687
1836
  const presented = digest(match?.[1]?.trim() ?? "");
1688
1837
  let matched = false;
1689
1838
  for (const expected of settings.tokenDigests) matched = (0, node_crypto.timingSafeEqual)(presented, expected) || matched;
1690
- if (!match || !matched) return false;
1839
+ if (!match || !matched) return { allowed: false };
1691
1840
  }
1692
- if (settings.authorize) try {
1693
- 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 };
1694
1851
  } catch {
1695
- return false;
1696
- }
1697
- 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");
1698
1876
  }
1699
1877
  /** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */
1700
1878
  function isJsonContentType(contentType) {
@@ -1751,89 +1929,6 @@ function readBody$1(request, limit) {
1751
1929
  });
1752
1930
  }
1753
1931
 
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
1932
  //#endregion
1838
1933
  //#region ../../packages/ai-tools/src/mcp/serve.ts
1839
1934
  /** JSON-RPC version literal every outbound message carries. */
@@ -1952,7 +2047,10 @@ var McpServeHandler = class {
1952
2047
  */
1953
2048
  async handle(request, context = {}) {
1954
2049
  try {
1955
- 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
+ }));
1956
2054
  } catch (cause) {
1957
2055
  if (cause instanceof RpcFailure) return this.error(request.id, cause.code, cause.message, cause.data);
1958
2056
  const message = cause instanceof Error ? cause.message : String(cause);
@@ -1965,7 +2063,10 @@ var McpServeHandler = class {
1965
2063
  case "initialize": return this.initializeResult(request.params);
1966
2064
  case "ping": return {};
1967
2065
  case "tools/list": return { tools: this.listTools() };
1968
- case "tools/call": return this.callTool(request.params, context);
2066
+ case "tools/call": return this.callTool(request.params, {
2067
+ ...context,
2068
+ requestId: request.id
2069
+ });
1969
2070
  }
1970
2071
  if (this.resources) switch (request.method) {
1971
2072
  case "resources/list": return { resources: this.listResources(this.resources) };
@@ -2036,7 +2137,11 @@ var McpServeHandler = class {
2036
2137
  type: "protocol",
2037
2138
  method: "tools/call"
2038
2139
  });
2039
- const toolContext = { artifacts: {} };
2140
+ const toolContext = {
2141
+ artifacts: {},
2142
+ requestId: context.requestId,
2143
+ principal: context.principal
2144
+ };
2040
2145
  if (context.signal) toolContext.signal = context.signal;
2041
2146
  if (context.request) {
2042
2147
  const { headers, remoteAddress } = context.request;
@@ -2045,6 +2150,43 @@ var McpServeHandler = class {
2045
2150
  remoteAddress
2046
2151
  };
2047
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();
2048
2190
  const result = await contract.invoke(args, toolContext);
2049
2191
  if (result.error) return {
2050
2192
  content: [{
@@ -2175,7 +2317,7 @@ const JSON_RPC_INTERNAL_ERROR = -32603;
2175
2317
  * answered with a JSON-RPC "Invalid Request" error whose `id` is `null`.
2176
2318
  * Never rejects: a dispatch failure is answered as an internal error.
2177
2319
  */
2178
- async function answerStdioLine(line, dispatch) {
2320
+ async function answerStdioLine(line, dispatch, notify) {
2179
2321
  const trimmed = line.trim();
2180
2322
  if (!trimmed) return;
2181
2323
  let message;
@@ -2194,10 +2336,21 @@ async function answerStdioLine(line, dispatch) {
2194
2336
  })}\n`;
2195
2337
  const request = message;
2196
2338
  const id = request.id;
2197
- 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`;
2198
2351
  try {
2199
2352
  const response = await dispatch(request);
2200
- return `${JSON.stringify(response)}\n`;
2353
+ return response === void 0 ? void 0 : `${JSON.stringify(response)}\n`;
2201
2354
  } catch {
2202
2355
  return `${JSON.stringify({
2203
2356
  jsonrpc: JSONRPC_VERSION,
@@ -2281,7 +2434,7 @@ var McpServerImpl = class {
2281
2434
  start() {
2282
2435
  if (!this.running) {
2283
2436
  const transport = this.options.transport ?? { type: "stdio" };
2284
- 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));
2285
2438
  this.running = starting.catch((error) => {
2286
2439
  this.running = void 0;
2287
2440
  throw error;
@@ -2301,19 +2454,29 @@ var McpServerImpl = class {
2301
2454
  };
2302
2455
  }
2303
2456
  /** Wire the stdin line reader and answer each request on stdout. */
2304
- startStdio() {
2457
+ startStdio(transport) {
2458
+ const shutdown = new AbortController();
2459
+ const requests = new McpServeRequests();
2305
2460
  const reader = (0, node_readline.createInterface)({ input: process.stdin });
2461
+ reader.once("close", () => shutdown.abort());
2306
2462
  reader.on("line", (line) => {
2307
- this.onLine(line).catch(() => {});
2463
+ this.onLine(line, transport, requests, shutdown.signal).catch(() => {});
2308
2464
  });
2309
2465
  return { close: async () => {
2466
+ shutdown.abort();
2310
2467
  reader.close();
2311
2468
  this.running = void 0;
2312
2469
  } };
2313
2470
  }
2314
2471
  /** Answer one stdin line on stdout (see {@link answerStdioLine}). */
2315
- async onLine(line) {
2316
- 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));
2317
2480
  if (reply !== void 0) process.stdout.write(reply);
2318
2481
  }
2319
2482
  async stop() {