@warlock.js/ai-tools 5.33.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/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ 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.0 - 2026-10-19
8
+
9
+ ### Added
10
+
11
+ - `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.
12
+ - 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.
13
+ - Export `McpError(code, message, data?)` and the new discovery, envelope and HTTP route contracts.
14
+
15
+ ### Fixed
16
+
17
+ - `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.
18
+ - Extend the core `ai.tools` namespace on registration, preserving `catalogue` and `measure` while adding the ready-made factories. `AiToolsNamespace` remains re-exported for consumers.
19
+
7
20
  ## 5.33.0 - 2026-10-19
8
21
 
9
22
  ### Fixed
@@ -65,71 +78,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
65
78
  - **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
79
  - `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
80
 
68
- ## 5.30.0 - 2026-10-16
69
-
70
- ### Changed
71
-
72
- - Lockstep release; package APIs are unchanged.
73
-
74
81
  ## 5.29.0 - 2026-10-01
75
82
 
76
83
  ### Changed
77
84
 
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.
85
+ - 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
86
 
80
87
  ## 5.28.0 - 2026-10-01
81
88
 
82
89
  ### Changed
83
90
 
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.
91
+ - `llms.txt` now lists every skill topic with its description (frontmatter in CRLF files was skipped).
133
92
 
134
93
  ## 5.22.0 - 2026-09-25
135
94
 
@@ -137,12 +96,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
137
96
 
138
97
  - Stdio MCP children receive only a minimal execution environment plus explicitly supplied variables.
139
98
 
140
- ## 5.21.0 - 2026-09-25
141
-
142
- ### Changed
143
-
144
- - Lockstep release maintenance and dependency refresh.
145
-
146
99
  ## 5.20.0 - 2026-09-24
147
100
 
148
101
  ### Changed
@@ -155,53 +108,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
155
108
 
156
109
  - Refined package skill-discovery descriptions and regenerated the llms projections.
157
110
 
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
111
  ## 5.2.2
177
112
 
178
113
  ### Maintenance
179
114
 
180
115
  - Restored the Warlock family to one exact, installable lockstep version.
181
116
 
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
117
  ## 4.16.0 - 2026-08-18
206
118
 
207
119
  ### Security
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
  /**
@@ -1465,7 +1487,7 @@ var McpServeRequests = class {
1465
1487
  * specification (https://modelcontextprotocol.io/specification/2025-11-25/basic/transports,
1466
1488
  * checked 2026-10-17) on `node:http`, with no SDK dependency:
1467
1489
  *
1468
- * - 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
1469
1491
  * (batches were removed from the protocol in 2025-06-18 and get 400);
1470
1492
  * - a request is answered with `Content-Type: application/json` (the spec
1471
1493
  * lets a server pick JSON or SSE per request); a notification or a
@@ -1523,6 +1545,35 @@ function hostnameOf(hostHeader) {
1523
1545
  function digest(value) {
1524
1546
  return (0, node_crypto.createHash)("sha256").update(value, "utf8").digest();
1525
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
+ }
1526
1577
  /** Normalise a configured origin to `scheme://host[:port]`, or throw a config error. */
1527
1578
  function normalizeOrigin(origin) {
1528
1579
  try {
@@ -1572,7 +1623,8 @@ function resolveSettings(transport) {
1572
1623
  authenticate: auth?.authenticate,
1573
1624
  authRequired,
1574
1625
  allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
1575
- 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 ?? [])
1576
1628
  };
1577
1629
  }
1578
1630
  /** Flatten Node's header bag into lower-cased `name → value` strings. */
@@ -1695,13 +1747,15 @@ async function startHttpServer(transport, dispatch) {
1695
1747
  };
1696
1748
  }
1697
1749
  /**
1698
- * Validate and answer one HTTP request: path → Host / Origin → auth →
1699
- * 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.
1700
1752
  */
1701
1753
  async function handleRequest(request, response, settings, dispatch, signal, requests) {
1702
1754
  const headers = flattenHeaders(request.headers);
1703
1755
  const method = (request.method ?? "GET").toUpperCase();
1704
- 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);
1705
1759
  if (!hostAllowed(headers.host, settings)) {
1706
1760
  sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Host header not allowed.");
1707
1761
  return;
@@ -1710,7 +1764,7 @@ async function handleRequest(request, response, settings, dispatch, signal, requ
1710
1764
  sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Origin not allowed.");
1711
1765
  return;
1712
1766
  }
1713
- if (path !== settings.path) {
1767
+ if (path !== settings.path && !routes) {
1714
1768
  sendRefusal(response, 404, JSON_RPC_SERVER_ERROR, "Not found.");
1715
1769
  return;
1716
1770
  }
@@ -1726,6 +1780,15 @@ async function handleRequest(request, response, settings, dispatch, signal, requ
1726
1780
  sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", challenge);
1727
1781
  return;
1728
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
+ }
1729
1792
  if (method !== "POST") {
1730
1793
  sendEmpty(response, 405, { allow: "POST" });
1731
1794
  return;
@@ -1799,6 +1862,32 @@ async function handleRequest(request, response, settings, dispatch, signal, requ
1799
1862
  }
1800
1863
  sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST$1, "Invalid request: not a JSON-RPC message.");
1801
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
+ }
1802
1891
  /**
1803
1892
  * Host-header check. While bound to loopback only loopback names (plus
1804
1893
  * `allowedHosts`) are accepted, so a DNS-rebound name pointing at
@@ -2062,7 +2151,10 @@ var McpServeHandler = class {
2062
2151
  switch (request.method) {
2063
2152
  case "initialize": return this.initializeResult(request.params);
2064
2153
  case "ping": return {};
2065
- case "tools/list": return { tools: this.listTools() };
2154
+ case "tools/list": return { tools: await this.listTools({
2155
+ ...context,
2156
+ requestId: request.id
2157
+ }) };
2066
2158
  case "tools/call": return this.callTool(request.params, {
2067
2159
  ...context,
2068
2160
  requestId: request.id
@@ -2105,9 +2197,9 @@ var McpServeHandler = class {
2105
2197
  * the configured dialect (default `draft-2020-12` — overriding
2106
2198
  * `extractJsonSchema`'s own `openai-strict` default to a neutral MCP draft).
2107
2199
  */
2108
- listTools() {
2200
+ async listTools(context) {
2109
2201
  const target = this.options.schemaTarget ?? DEFAULT_SCHEMA_TARGET;
2110
- return this.tools.map(({ name, contract }) => {
2202
+ const tools = this.tools.map(({ name, contract }) => {
2111
2203
  const inputSchema = (0, _warlock_js_ai.extractJsonSchema)(contract.input, { target }) ?? {
2112
2204
  type: "object",
2113
2205
  properties: {}
@@ -2119,6 +2211,8 @@ var McpServeHandler = class {
2119
2211
  ...isRecord(contract.meta?.outputSchema) ? { outputSchema: contract.meta.outputSchema } : {}
2120
2212
  };
2121
2213
  });
2214
+ context.signal?.throwIfAborted();
2215
+ return this.options.listFilter ? this.options.listFilter(context, tools) : tools;
2122
2216
  }
2123
2217
  /**
2124
2218
  * Route a `tools/call` to the named contract's `invoke()` and map the
@@ -2132,6 +2226,15 @@ var McpServeHandler = class {
2132
2226
  */
2133
2227
  async callTool(params, context) {
2134
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
+ }
2135
2238
  const contract = this.byName.get(name);
2136
2239
  if (!contract) throw new McpTransportError(`Unknown tool "${name}".`, {
2137
2240
  type: "protocol",
@@ -2175,7 +2278,13 @@ var McpServeHandler = class {
2175
2278
  input = validation.value;
2176
2279
  }
2177
2280
  context.signal?.throwIfAborted();
2178
- const refusal = await this.options.beforeCall(context, contract, input);
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
+ }
2179
2288
  if (refusal !== void 0) {
2180
2289
  try {
2181
2290
  assertMcpResult(refusal);
@@ -3560,20 +3669,20 @@ function webSearchTool(options) {
3560
3669
 
3561
3670
  //#endregion
3562
3671
  //#region ../../packages/ai-tools/src/register.ts
3563
- _warlock_js_ai.ai.tools = {
3672
+ Object.assign(_warlock_js_ai.ai.tools, {
3564
3673
  webSearch: webSearchTool,
3565
3674
  fetchUrl: fetchUrlTool,
3566
3675
  http: httpRequestTool,
3567
3676
  calculator: calculatorTool,
3568
- dateTime: (options) => dateTimeTool(options),
3569
- measure: _warlock_js_ai.measureTools
3570
- };
3677
+ dateTime: (options) => dateTimeTool(options)
3678
+ });
3571
3679
  _warlock_js_ai.ai.mcp = mcp;
3572
3680
 
3573
3681
  //#endregion
3574
3682
  exports.CalculatorError = CalculatorError;
3575
3683
  exports.DateTimeError = DateTimeError;
3576
3684
  exports.HttpPolicyError = HttpPolicyError;
3685
+ exports.McpError = McpError;
3577
3686
  exports.McpServeError = McpServeError;
3578
3687
  exports.McpTransportError = McpTransportError;
3579
3688
  exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;