@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 +25 -103
- package/cjs/index.cjs +220 -54
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -2
- package/esm/contracts/mcp-serve.type.d.mts +74 -4
- package/esm/contracts/mcp-serve.type.d.mts.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/index.mjs +2 -1
- package/esm/mcp/error.d.mts +9 -0
- package/esm/mcp/error.d.mts.map +1 -0
- package/esm/mcp/error.mjs +24 -0
- package/esm/mcp/error.mjs.map +1 -0
- package/esm/mcp/index.d.mts +3 -2
- package/esm/mcp/index.d.mts.map +1 -1
- package/esm/mcp/index.mjs +2 -1
- package/esm/mcp/index.mjs.map +1 -1
- package/esm/mcp/json-schema-to-standard.d.mts +6 -3
- package/esm/mcp/json-schema-to-standard.d.mts.map +1 -1
- package/esm/mcp/json-schema-to-standard.mjs +15 -26
- package/esm/mcp/json-schema-to-standard.mjs.map +1 -1
- package/esm/mcp/serve-http.mjs +157 -21
- package/esm/mcp/serve-http.mjs.map +1 -1
- package/esm/mcp/serve.d.mts.map +1 -1
- package/esm/mcp/serve.mjs +25 -4
- package/esm/mcp/serve.mjs.map +1 -1
- package/esm/register.d.mts +13 -52
- package/esm/register.d.mts.map +1 -1
- package/esm/register.mjs +4 -5
- package/esm/register.mjs.map +1 -1
- package/llms-full.txt +173 -4
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/connect-mcp-server/SKILL.md +1 -1
- package/skills/expose-as-mcp-server/SKILL.md +151 -3
- package/skills/use-web-and-http-tools/SKILL.md +21 -0
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
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:
|
|
1699
|
-
* method →
|
|
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
|
|
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
|
-
|
|
1726
|
-
|
|
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 {
|
|
1968
|
+
if (!match || !matched) return {
|
|
1969
|
+
allowed: false,
|
|
1970
|
+
check: "bearer"
|
|
1971
|
+
};
|
|
1840
1972
|
}
|
|
1841
|
-
try {
|
|
1842
|
-
if (
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|