@warlock.js/ai-tools 5.33.0 → 5.34.1

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,29 @@ 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.34.1 - 2026-10-19
8
+
9
+ ### Added
10
+
11
+ - HTTP `transport.onAuthRefusal` customizes denied authentication responses for the MCP endpoint and side routes using typed safe context, 4xx/5xx statuses, allowlisted headers and JSON bodies. Host/Origin guards retain precedence, invalid or throwing hooks retain the default refusal, and handlers never run after a denial.
12
+
13
+ ### Fixed
14
+
15
+ - Tools adapted from a remote MCP server now show the model their real input schema. `jsonSchemaToStandard` implements Standard JSON Schema (`~standard.jsonSchema`), so `extractJsonSchema` and every provider adapter read the server's `inputSchema` instead of falling back to a parameterless `{ type: "object" }`, which made models guess argument names and retry. Adapters receive copies of the schema; a tool without one advertises `{ type: "object", properties: {} }`.
16
+
17
+ ## 5.34.0 - 2026-10-19
18
+
19
+ ### Added
20
+
21
+ - `ai.mcp.serve` supports async `listFilter(context, tools)` for per-request discovery independent of callability, and `validateCallParams(context, params)` for application envelope policy before lookup, schema validation and call policy, over HTTP and stdio. Default handling of unknown fields and unsupported task metadata is unchanged.
22
+ - HTTP `transport.routes` adds exact method/path application routes beneath the MCP URL with shared authentication, principal/context, Host/Origin guards, body limits, disconnect signals and shutdown draining. Conflicting or invalid route configuration fails before binding.
23
+ - Export `McpError(code, message, data?)` and the new discovery, envelope and HTTP route contracts.
24
+
25
+ ### Fixed
26
+
27
+ - `beforeCall` and envelope policy preserve a thrown `McpError`'s integer code, message and optional data, including official SDK errors, without a runtime SDK dependency. Other throws retain the existing invalid-params mapping.
28
+ - Extend the core `ai.tools` namespace on registration, preserving `catalogue` and `measure` while adding the ready-made factories. `AiToolsNamespace` remains re-exported for consumers.
29
+
7
30
  ## 5.33.0 - 2026-10-19
8
31
 
9
32
  ### Fixed
@@ -65,71 +88,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
65
88
  - **BREAKING:** `ai.mcp.serve()` throws `McpServeError` (`type: "collision"`) when two exposed tools share a name, and for duplicate resource URIs or prompt names. Previously both tools were listed and every call reached the last one. Rename one tool, or serve a grouped source with `prefixToolNames: true`.
66
89
  - `serve(..., { transport: { type: "http" } }).start()` no longer rejects: it binds a real endpoint. Code that implemented `McpServer` itself must return a handle from `start()`.
67
90
 
68
- ## 5.30.0 - 2026-10-16
69
-
70
- ### Changed
71
-
72
- - Lockstep release; package APIs are unchanged.
73
-
74
91
  ## 5.29.0 - 2026-10-01
75
92
 
76
93
  ### Changed
77
94
 
78
- - Lockstep release; package APIs are unchanged. Skills refer to other topics by name instead of file paths, so cross-references keep working after `agent-kit` copies them into an app.
95
+ - Skills refer to other topics by name instead of file paths, so cross-references keep working after `agent-kit` copies them into an app.
79
96
 
80
97
  ## 5.28.0 - 2026-10-01
81
98
 
82
99
  ### Changed
83
100
 
84
- - Lockstep release; package APIs are unchanged. `llms.txt` now lists every skill topic with its description (frontmatter in CRLF files was skipped).
85
-
86
- ## 5.27.0 - 2026-10-01
87
-
88
- ### Changed
89
-
90
- - Lockstep release; package APIs are unchanged.
91
-
92
- ## 5.26.0 - 2026-09-30
93
-
94
- ### Changed
95
-
96
- - Lockstep release; package APIs are unchanged.
97
-
98
- ## 5.25.0 - 2026-09-28
99
-
100
- ### Changed
101
-
102
- - Lockstep release; package APIs are unchanged.
103
-
104
- ## 5.24.0 - 2026-09-27
105
-
106
- ### Changed
107
-
108
- - Lockstep release; package APIs are unchanged.
109
-
110
- ## 5.23.2 - 2026-09-26
111
-
112
- ### Changed
113
-
114
- - Lockstep patch release; package APIs are unchanged.
115
-
116
- ## 5.23.1 - 2026-09-26
117
-
118
- ### Changed
119
-
120
- - Lockstep patch release; package APIs are unchanged.
121
-
122
- ## 5.23.0 - 2026-09-25
123
-
124
- ### Changed
125
-
126
- - Lockstep patch release; package APIs are unchanged.
127
-
128
- ## 5.22.1 - 2026-09-25
129
-
130
- ### Changed
131
-
132
- - Lockstep patch release; package APIs are unchanged.
101
+ - `llms.txt` now lists every skill topic with its description (frontmatter in CRLF files was skipped).
133
102
 
134
103
  ## 5.22.0 - 2026-09-25
135
104
 
@@ -137,12 +106,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
137
106
 
138
107
  - Stdio MCP children receive only a minimal execution environment plus explicitly supplied variables.
139
108
 
140
- ## 5.21.0 - 2026-09-25
141
-
142
- ### Changed
143
-
144
- - Lockstep release maintenance and dependency refresh.
145
-
146
109
  ## 5.20.0 - 2026-09-24
147
110
 
148
111
  ### Changed
@@ -155,53 +118,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
155
118
 
156
119
  - Refined package skill-discovery descriptions and regenerated the llms projections.
157
120
 
158
- ## 5.11.0 - 2026-09-14
159
-
160
- _Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.11.0._
161
-
162
- ## 5.10.0 - 2026-09-14
163
-
164
- _Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.10.0._
165
-
166
- ## 5.9.0 - 2026-09-13
167
-
168
- _Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.9.0._
169
-
170
- ## 5.2.3 - 2026-09-02
171
-
172
- ### Fixed
173
-
174
- - Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
175
-
176
121
  ## 5.2.2
177
122
 
178
123
  ### Maintenance
179
124
 
180
125
  - Restored the Warlock family to one exact, installable lockstep version.
181
126
 
182
- ## 5.1.0
183
-
184
- No changes to `@warlock.js/ai-tools`. Released in lockstep with the `@warlock.js/web`
185
- React-execution fix and the `@warlock.js/core` CLI additions — see those packages'
186
- changelogs.
187
-
188
- ## 5.0.2 - 2026-08-25
189
-
190
- No changes to `@warlock.js/ai-tools`. Released in lockstep with the `@warlock.js/web` SSR
191
- fix (`ssr.noExternal`) — see that package's changelog.
192
-
193
- ## 5.0.1 - 2026-08-25
194
-
195
- No changes to `@warlock.js/ai-tools`. Released in lockstep with the `create-warlock` vite
196
- resolution pin and the `@warlock.js/web` peer narrowing — see those packages'
197
- changelogs.
198
-
199
- ## 5.0.0 - 2026-08-25
200
-
201
- ### Changed
202
-
203
- - This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
204
-
205
127
  ## 4.16.0 - 2026-08-18
206
128
 
207
129
  ### Security
package/cjs/index.cjs CHANGED
@@ -587,42 +587,31 @@ function makeCompiler(schema) {
587
587
  };
588
588
  }
589
589
  /**
590
- * Wrap a raw JSON Schema as a {@link StandardSchemaV1} whose
591
- * `~standard.validate` runs Ajv. The shape mirrors `passthroughSchema()`
592
- * (`{ "~standard": { version: 1, vendor, validate } }`) so it drops into
593
- * `tool({ input })` exactly like a native seal schema.
594
- *
595
- * Validation behavior:
596
- * - **Valid input** → `{ value }` (the input is passed through unchanged;
597
- * Ajv validates, it does not transform).
598
- * - **Invalid input** → `{ issues }` carrying Ajv's `instancePath` +
599
- * message per failure, so `tool()` produces a `SchemaValidationError`.
600
- * - **Missing `ajv` peer** → a single issue carrying the curated install
601
- * string, surfaced the same way (a developer-facing message in logs).
602
- * - **No / empty schema** → an accept-all passthrough (an MCP tool may
603
- * advertise no `inputSchema`).
604
- *
605
- * @param schema - The JSON Schema (an MCP tool's `inputSchema`), or
606
- * `undefined` for a no-argument tool.
607
- * @returns A `StandardSchemaV1<TInput>` ready for `tool({ input })`.
608
- *
609
- * @example
610
- * const input = jsonSchemaToStandard<{ q: string }>({
611
- * type: "object",
612
- * properties: { q: { type: "string" } },
613
- * required: ["q"],
614
- * });
590
+ * Standard JSON Schema converter returning a copy of the wrapped schema, so
591
+ * provider adapters (which may compact or rewrite it) never mutate the source.
592
+ * The remote server owns its schema, so every target receives it unchanged.
615
593
  */
594
+ function exposeSchema(schema) {
595
+ return {
596
+ input: () => structuredClone(schema),
597
+ output: () => structuredClone(schema)
598
+ };
599
+ }
616
600
  function jsonSchemaToStandard(schema) {
617
601
  if (!schema || Object.keys(schema).length === 0) return { "~standard": {
618
602
  version: 1,
619
603
  vendor: VENDOR$2,
620
- validate: (value) => ({ value })
604
+ validate: (value) => ({ value }),
605
+ jsonSchema: exposeSchema({
606
+ type: "object",
607
+ properties: {}
608
+ })
621
609
  } };
622
610
  const compile = makeCompiler(schema);
623
611
  return { "~standard": {
624
612
  version: 1,
625
613
  vendor: VENDOR$2,
614
+ jsonSchema: exposeSchema(schema),
626
615
  async validate(value) {
627
616
  await loadAjv();
628
617
  if (!isAjvAvailable) return { issues: [{ message: AJV_INSTALL_INSTRUCTIONS }] };
@@ -1294,6 +1283,28 @@ function mcp$1(server, options) {
1294
1283
  return new McpClientImpl(server, options);
1295
1284
  }
1296
1285
 
1286
+ //#endregion
1287
+ //#region ../../packages/ai-tools/src/mcp/error.ts
1288
+ /** An application-defined JSON-RPC error thrown by MCP serving policy hooks. */
1289
+ var McpError = class extends Error {
1290
+ constructor(code, message, data) {
1291
+ super(message);
1292
+ this.name = "McpError";
1293
+ this.code = code;
1294
+ this.data = data;
1295
+ }
1296
+ };
1297
+ /**
1298
+ * Recognize our error and the SDK's McpError without a runtime SDK dependency.
1299
+ * SDK source (checked 2026-10-19):
1300
+ * https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x/src/types.ts
1301
+ * Only explicitly named errors with an integer code opt in; ordinary throws
1302
+ * retain the existing mapping. Preserve the error's message verbatim.
1303
+ */
1304
+ function isMcpError(cause) {
1305
+ return cause instanceof Error && cause.name === "McpError" && "code" in cause && typeof cause.code === "number" && Number.isInteger(cause.code);
1306
+ }
1307
+
1297
1308
  //#endregion
1298
1309
  //#region ../../packages/ai-tools/src/mcp/protocol.ts
1299
1310
  /**
@@ -1465,7 +1476,7 @@ var McpServeRequests = class {
1465
1476
  * specification (https://modelcontextprotocol.io/specification/2025-11-25/basic/transports,
1466
1477
  * checked 2026-10-17) on `node:http`, with no SDK dependency:
1467
1478
  *
1468
- * - one endpoint path; each POST carries exactly one JSON-RPC message
1479
+ * - one MCP endpoint path (plus optional authenticated app routes); each MCP POST carries exactly one JSON-RPC message
1469
1480
  * (batches were removed from the protocol in 2025-06-18 and get 400);
1470
1481
  * - a request is answered with `Content-Type: application/json` (the spec
1471
1482
  * lets a server pick JSON or SSE per request); a notification or a
@@ -1523,6 +1534,35 @@ function hostnameOf(hostHeader) {
1523
1534
  function digest(value) {
1524
1535
  return (0, node_crypto.createHash)("sha256").update(value, "utf8").digest();
1525
1536
  }
1537
+ /** Resolve exact descendant routes; no route may replace the MCP endpoint. */
1538
+ function resolveRoutes(path, routes) {
1539
+ const resolved = /* @__PURE__ */ new Map();
1540
+ const methods = /* @__PURE__ */ new Set([
1541
+ "GET",
1542
+ "POST",
1543
+ "PUT",
1544
+ "PATCH",
1545
+ "DELETE",
1546
+ "HEAD",
1547
+ "OPTIONS"
1548
+ ]);
1549
+ if (routes.length > 0) {
1550
+ let canonical = false;
1551
+ try {
1552
+ canonical = !path.startsWith("//") && new URL(path, "http://localhost").pathname === path;
1553
+ } catch {}
1554
+ if (!canonical) throw new McpServeError("MCP http routes require a canonical endpoint pathname.", { type: "config" });
1555
+ }
1556
+ for (const route of routes) {
1557
+ 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" });
1558
+ const fullPath = `${path.replace(/\/+$/, "")}${route.path}`;
1559
+ const byMethod = resolved.get(fullPath) ?? /* @__PURE__ */ new Map();
1560
+ if (fullPath === path || byMethod.has(route.method)) throw new McpServeError(`MCP http route "${route.method} ${fullPath}" conflicts.`, { type: "collision" });
1561
+ byMethod.set(route.method, { ...route });
1562
+ resolved.set(fullPath, byMethod);
1563
+ }
1564
+ return resolved;
1565
+ }
1526
1566
  /** Normalise a configured origin to `scheme://host[:port]`, or throw a config error. */
1527
1567
  function normalizeOrigin(origin) {
1528
1568
  try {
@@ -1570,9 +1610,11 @@ function resolveSettings(transport) {
1570
1610
  tokenDigests: tokens.map(digest),
1571
1611
  authorize: auth?.authorize,
1572
1612
  authenticate: auth?.authenticate,
1613
+ onAuthRefusal: transport.onAuthRefusal,
1573
1614
  authRequired,
1574
1615
  allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
1575
- allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name)))
1616
+ allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name))),
1617
+ routes: resolveRoutes(path, transport.routes ?? [])
1576
1618
  };
1577
1619
  }
1578
1620
  /** Flatten Node's header bag into lower-cased `name → value` strings. */
@@ -1609,6 +1651,52 @@ function sendRefusal(response, status, code, message, headers = {}) {
1609
1651
  }
1610
1652
  }, headers);
1611
1653
  }
1654
+ const AUTH_REFUSAL_HEADERS = /* @__PURE__ */ new Set([
1655
+ "www-authenticate",
1656
+ "cache-control",
1657
+ "retry-after",
1658
+ "content-language",
1659
+ "x-request-id"
1660
+ ]);
1661
+ /** Snapshot plain JSON, rejecting cycles and values JSON would silently coerce/drop. */
1662
+ function copyRefusalBody(value, ancestors = /* @__PURE__ */ new Set()) {
1663
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
1664
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1665
+ if (typeof value !== "object" || value === null || ancestors.has(value)) throw new TypeError("Invalid auth refusal body.");
1666
+ const prototype = Object.getPrototypeOf(value);
1667
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) throw new TypeError("Invalid auth refusal body.");
1668
+ ancestors.add(value);
1669
+ const result = Array.isArray(value) ? Array.from(value, (entry) => copyRefusalBody(entry, ancestors)) : Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, copyRefusalBody(entry, ancestors)]));
1670
+ ancestors.delete(value);
1671
+ return result;
1672
+ }
1673
+ /** A custom response can only end a denied exchange; it cannot authorize dispatch. */
1674
+ async function sendAuthRefusal(response, settings, context) {
1675
+ const challenge = settings.tokenDigests.length > 0 || settings.authenticate ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {};
1676
+ if (settings.onAuthRefusal) try {
1677
+ const result = await settings.onAuthRefusal(Object.freeze(context));
1678
+ if (!isRecord(result)) throw new TypeError("Invalid auth refusal response.");
1679
+ const customStatus = result.status;
1680
+ const status = customStatus === void 0 ? 401 : customStatus;
1681
+ if (typeof status !== "number" || !Number.isInteger(status) || status < 400 || status > 599) throw new TypeError("Invalid auth refusal status.");
1682
+ const headers = { ...challenge };
1683
+ const customHeaders = result.headers;
1684
+ if (customHeaders !== void 0) {
1685
+ if (!isRecord(customHeaders)) throw new TypeError("Invalid auth refusal headers.");
1686
+ const prototype = Object.getPrototypeOf(customHeaders);
1687
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError("Invalid auth refusal headers.");
1688
+ for (const [name, value] of Object.entries(customHeaders)) {
1689
+ const normalized = name.toLowerCase();
1690
+ if (!AUTH_REFUSAL_HEADERS.has(normalized) || typeof value !== "string") throw new TypeError("Invalid auth refusal header.");
1691
+ (0, node_http.validateHeaderValue)(normalized, value);
1692
+ headers[normalized] = value;
1693
+ }
1694
+ }
1695
+ sendJson(response, status, copyRefusalBody(result.body), headers);
1696
+ return;
1697
+ } catch {}
1698
+ sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", challenge);
1699
+ }
1612
1700
  /** Write an empty-bodied status (202 / 405). */
1613
1701
  function sendEmpty(response, status, headers = {}) {
1614
1702
  if (response.headersSent || response.destroyed) return;
@@ -1695,13 +1783,15 @@ async function startHttpServer(transport, dispatch) {
1695
1783
  };
1696
1784
  }
1697
1785
  /**
1698
- * Validate and answer one HTTP request: path → Host / Origin → auth →
1699
- * method → headers → body → JSON-RPC dispatch.
1786
+ * Validate and answer one HTTP request: Host / Origin → path → auth →
1787
+ * method → route handler or MCP headers/body/JSON-RPC dispatch.
1700
1788
  */
1701
1789
  async function handleRequest(request, response, settings, dispatch, signal, requests) {
1702
1790
  const headers = flattenHeaders(request.headers);
1703
1791
  const method = (request.method ?? "GET").toUpperCase();
1704
- const path = new URL(request.url ?? "/", "http://localhost").pathname;
1792
+ const url = new URL(request.url ?? "/", "http://localhost");
1793
+ const path = url.pathname;
1794
+ const routes = settings.routes.get(path);
1705
1795
  if (!hostAllowed(headers.host, settings)) {
1706
1796
  sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Host header not allowed.");
1707
1797
  return;
@@ -1710,7 +1800,7 @@ async function handleRequest(request, response, settings, dispatch, signal, requ
1710
1800
  sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Origin not allowed.");
1711
1801
  return;
1712
1802
  }
1713
- if (path !== settings.path) {
1803
+ if (path !== settings.path && !routes) {
1714
1804
  sendRefusal(response, 404, JSON_RPC_SERVER_ERROR, "Not found.");
1715
1805
  return;
1716
1806
  }
@@ -1722,8 +1812,21 @@ async function handleRequest(request, response, settings, dispatch, signal, requ
1722
1812
  if (request.socket.remoteAddress !== void 0) info.remoteAddress = request.socket.remoteAddress;
1723
1813
  const authentication = await authorized(info, settings);
1724
1814
  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);
1815
+ await sendAuthRefusal(response, settings, {
1816
+ check: authentication.check,
1817
+ method,
1818
+ path,
1819
+ target: routes ? "route" : "mcp"
1820
+ });
1821
+ return;
1822
+ }
1823
+ if (routes) {
1824
+ const route = routes.get(method);
1825
+ if (!route) {
1826
+ sendEmpty(response, 405, { allow: [...routes.keys()].sort().join(", ") });
1827
+ return;
1828
+ }
1829
+ await handleRoute(request, response, route, info, url, authentication.principal, signal, settings);
1727
1830
  return;
1728
1831
  }
1729
1832
  if (method !== "POST") {
@@ -1799,6 +1902,32 @@ async function handleRequest(request, response, settings, dispatch, signal, requ
1799
1902
  }
1800
1903
  sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST$1, "Invalid request: not a JSON-RPC message.");
1801
1904
  }
1905
+ /** Run a side route after the shared guards, tracking disconnect and shutdown. */
1906
+ async function handleRoute(request, response, route, info, url, principal, shutdown, settings) {
1907
+ const disconnected = new AbortController();
1908
+ const onClose = () => disconnected.abort();
1909
+ response.once("close", onClose);
1910
+ try {
1911
+ const body = await readBody$1(request, settings.maxBodyBytes);
1912
+ if (body.type === "too-large") {
1913
+ sendRefusal(response, 413, JSON_RPC_SERVER_ERROR, `Request body exceeds ${settings.maxBodyBytes} bytes.`, { connection: "close" });
1914
+ return;
1915
+ }
1916
+ const signal = AbortSignal.any([shutdown, disconnected.signal]);
1917
+ signal.throwIfAborted();
1918
+ const result = await route.handler({
1919
+ request: info,
1920
+ url,
1921
+ principal,
1922
+ body: body.text,
1923
+ signal
1924
+ });
1925
+ if (result.body === void 0 || info.method === "HEAD") sendEmpty(response, result.status ?? 200, result.headers);
1926
+ else sendJson(response, result.status ?? 200, result.body, result.headers);
1927
+ } finally {
1928
+ response.off("close", onClose);
1929
+ }
1930
+ }
1802
1931
  /**
1803
1932
  * Host-header check. While bound to loopback only loopback names (plus
1804
1933
  * `allowedHosts`) are accepted, so a DNS-rebound name pointing at
@@ -1836,21 +1965,38 @@ async function authorized(info, settings) {
1836
1965
  const presented = digest(match?.[1]?.trim() ?? "");
1837
1966
  let matched = false;
1838
1967
  for (const expected of settings.tokenDigests) matched = (0, node_crypto.timingSafeEqual)(presented, expected) || matched;
1839
- if (!match || !matched) return { allowed: false };
1968
+ if (!match || !matched) return {
1969
+ allowed: false,
1970
+ check: "bearer"
1971
+ };
1840
1972
  }
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 };
1973
+ if (settings.authorize) try {
1974
+ if (await settings.authorize(info) !== true) return {
1975
+ allowed: false,
1976
+ check: "authorize"
1977
+ };
1851
1978
  } catch {
1852
- return { allowed: false };
1979
+ return {
1980
+ allowed: false,
1981
+ check: "authorize"
1982
+ };
1853
1983
  }
1984
+ if (settings.authenticate) try {
1985
+ const principal = await settings.authenticate(info);
1986
+ return principal === void 0 ? {
1987
+ allowed: false,
1988
+ check: "authenticate"
1989
+ } : {
1990
+ allowed: true,
1991
+ principal
1992
+ };
1993
+ } catch {
1994
+ return {
1995
+ allowed: false,
1996
+ check: "authenticate"
1997
+ };
1998
+ }
1999
+ return { allowed: true };
1854
2000
  }
1855
2001
  /**
1856
2002
  * Stateless HTTP has no connection/session id. Repeat the caller's credential
@@ -2062,7 +2208,10 @@ var McpServeHandler = class {
2062
2208
  switch (request.method) {
2063
2209
  case "initialize": return this.initializeResult(request.params);
2064
2210
  case "ping": return {};
2065
- case "tools/list": return { tools: this.listTools() };
2211
+ case "tools/list": return { tools: await this.listTools({
2212
+ ...context,
2213
+ requestId: request.id
2214
+ }) };
2066
2215
  case "tools/call": return this.callTool(request.params, {
2067
2216
  ...context,
2068
2217
  requestId: request.id
@@ -2105,9 +2254,9 @@ var McpServeHandler = class {
2105
2254
  * the configured dialect (default `draft-2020-12` — overriding
2106
2255
  * `extractJsonSchema`'s own `openai-strict` default to a neutral MCP draft).
2107
2256
  */
2108
- listTools() {
2257
+ async listTools(context) {
2109
2258
  const target = this.options.schemaTarget ?? DEFAULT_SCHEMA_TARGET;
2110
- return this.tools.map(({ name, contract }) => {
2259
+ const tools = this.tools.map(({ name, contract }) => {
2111
2260
  const inputSchema = (0, _warlock_js_ai.extractJsonSchema)(contract.input, { target }) ?? {
2112
2261
  type: "object",
2113
2262
  properties: {}
@@ -2119,6 +2268,8 @@ var McpServeHandler = class {
2119
2268
  ...isRecord(contract.meta?.outputSchema) ? { outputSchema: contract.meta.outputSchema } : {}
2120
2269
  };
2121
2270
  });
2271
+ context.signal?.throwIfAborted();
2272
+ return this.options.listFilter ? this.options.listFilter(context, tools) : tools;
2122
2273
  }
2123
2274
  /**
2124
2275
  * Route a `tools/call` to the named contract's `invoke()` and map the
@@ -2132,6 +2283,15 @@ var McpServeHandler = class {
2132
2283
  */
2133
2284
  async callTool(params, context) {
2134
2285
  const { name, args } = readCallParams(params);
2286
+ if (this.options.validateCallParams) {
2287
+ context.signal?.throwIfAborted();
2288
+ try {
2289
+ await this.options.validateCallParams(context, readRecord(params));
2290
+ } catch (cause) {
2291
+ if (isMcpError(cause)) throw new RpcFailure(cause.code, cause.message, cause.data);
2292
+ throw cause;
2293
+ }
2294
+ }
2135
2295
  const contract = this.byName.get(name);
2136
2296
  if (!contract) throw new McpTransportError(`Unknown tool "${name}".`, {
2137
2297
  type: "protocol",
@@ -2175,7 +2335,13 @@ var McpServeHandler = class {
2175
2335
  input = validation.value;
2176
2336
  }
2177
2337
  context.signal?.throwIfAborted();
2178
- const refusal = await this.options.beforeCall(context, contract, input);
2338
+ let refusal;
2339
+ try {
2340
+ refusal = await this.options.beforeCall(context, contract, input);
2341
+ } catch (cause) {
2342
+ if (isMcpError(cause)) throw new RpcFailure(cause.code, cause.message, cause.data);
2343
+ throw cause;
2344
+ }
2179
2345
  if (refusal !== void 0) {
2180
2346
  try {
2181
2347
  assertMcpResult(refusal);
@@ -3560,20 +3726,20 @@ function webSearchTool(options) {
3560
3726
 
3561
3727
  //#endregion
3562
3728
  //#region ../../packages/ai-tools/src/register.ts
3563
- _warlock_js_ai.ai.tools = {
3729
+ Object.assign(_warlock_js_ai.ai.tools, {
3564
3730
  webSearch: webSearchTool,
3565
3731
  fetchUrl: fetchUrlTool,
3566
3732
  http: httpRequestTool,
3567
3733
  calculator: calculatorTool,
3568
- dateTime: (options) => dateTimeTool(options),
3569
- measure: _warlock_js_ai.measureTools
3570
- };
3734
+ dateTime: (options) => dateTimeTool(options)
3735
+ });
3571
3736
  _warlock_js_ai.ai.mcp = mcp;
3572
3737
 
3573
3738
  //#endregion
3574
3739
  exports.CalculatorError = CalculatorError;
3575
3740
  exports.DateTimeError = DateTimeError;
3576
3741
  exports.HttpPolicyError = HttpPolicyError;
3742
+ exports.McpError = McpError;
3577
3743
  exports.McpServeError = McpServeError;
3578
3744
  exports.McpTransportError = McpTransportError;
3579
3745
  exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;