@warlock.js/ai-tools 5.34.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,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.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
+
7
17
  ## 5.34.0 - 2026-10-19
8
18
 
9
19
  ### Added
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 }] };
@@ -1621,6 +1610,7 @@ function resolveSettings(transport) {
1621
1610
  tokenDigests: tokens.map(digest),
1622
1611
  authorize: auth?.authorize,
1623
1612
  authenticate: auth?.authenticate,
1613
+ onAuthRefusal: transport.onAuthRefusal,
1624
1614
  authRequired,
1625
1615
  allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
1626
1616
  allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name))),
@@ -1661,6 +1651,52 @@ function sendRefusal(response, status, code, message, headers = {}) {
1661
1651
  }
1662
1652
  }, headers);
1663
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
+ }
1664
1700
  /** Write an empty-bodied status (202 / 405). */
1665
1701
  function sendEmpty(response, status, headers = {}) {
1666
1702
  if (response.headersSent || response.destroyed) return;
@@ -1776,8 +1812,12 @@ async function handleRequest(request, response, settings, dispatch, signal, requ
1776
1812
  if (request.socket.remoteAddress !== void 0) info.remoteAddress = request.socket.remoteAddress;
1777
1813
  const authentication = await authorized(info, settings);
1778
1814
  if (!authentication.allowed) {
1779
- const challenge = settings.tokenDigests.length > 0 || settings.authenticate ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {};
1780
- 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
+ });
1781
1821
  return;
1782
1822
  }
1783
1823
  if (routes) {
@@ -1925,21 +1965,38 @@ async function authorized(info, settings) {
1925
1965
  const presented = digest(match?.[1]?.trim() ?? "");
1926
1966
  let matched = false;
1927
1967
  for (const expected of settings.tokenDigests) matched = (0, node_crypto.timingSafeEqual)(presented, expected) || matched;
1928
- if (!match || !matched) return { allowed: false };
1968
+ if (!match || !matched) return {
1969
+ allowed: false,
1970
+ check: "bearer"
1971
+ };
1929
1972
  }
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 };
1973
+ if (settings.authorize) try {
1974
+ if (await settings.authorize(info) !== true) return {
1975
+ allowed: false,
1976
+ check: "authorize"
1977
+ };
1940
1978
  } catch {
1941
- return { allowed: false };
1979
+ return {
1980
+ allowed: false,
1981
+ check: "authorize"
1982
+ };
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
+ };
1942
1998
  }
1999
+ return { allowed: true };
1943
2000
  }
1944
2001
  /**
1945
2002
  * Stateless HTTP has no connection/session id. Repeat the caller's credential