@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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: expose-as-mcp-server
3
- description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[], or named tool groups) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)` — over stdio or authenticated Streamable HTTP. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `McpHttpServeTransport`, `McpHttpAuth`, `McpServeError`, `server.start()`, `server.stop()`, `{ url, close }`, `{ type: "http", host, port, path, auth }`, `bearer`, `authorize`, `authenticate`, `beforeCall`, `requestId`, `principal`, `notifications/cancelled`, `allowedOrigins`, `maxBodyBytes`, `prefixToolNames`, `groups`, `resources`, `prompts`, `ctx.request.headers`, `schemaTarget`, `tools/list`, `tools/call`, `{ type: "stdio" }`; ''expose my agent as an MCP server'', ''serve my tools over HTTP'', ''give each agent session its own MCP endpoint'', ''protect my MCP server with a token'', ''publish my tools to Cursor/Claude Desktop'', ''serve MCP resources or prompts'', ''tool name collision''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
3
+ description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[], or named tool groups) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)` — over stdio or authenticated Streamable HTTP. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `McpHttpServeTransport`, `McpHttpAuth`, `McpServeError`, `server.start()`, `server.stop()`, `{ url, close }`, `{ type: "http", host, port, path, auth }`, `bearer`, `authorize`, `authenticate`, `beforeCall`, `listFilter`, `validateCallParams`, `McpError`, `routes`, `McpHttpRouteContext`, `requestId`, `principal`, `notifications/cancelled`, `allowedOrigins`, `maxBodyBytes`, `prefixToolNames`, `groups`, `resources`, `prompts`, `ctx.request.headers`, `schemaTarget`, `tools/list`, `tools/call`, `{ type: "stdio" }`; ''expose my agent as an MCP server'', ''serve my tools over HTTP'', ''give each agent session its own MCP endpoint'', ''protect my MCP server with a token'', ''publish my tools to Cursor/Claude Desktop'', ''serve MCP resources or prompts'', ''tool name collision''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
4
4
  ---
5
5
 
6
6
  # Expose tools AS an MCP server (Direction B)
@@ -135,7 +135,7 @@ const server = ai.mcp.serve([identity], {
135
135
  ```
136
136
 
137
137
  - `authenticate(request)` may be synchronous or async. `undefined` or a thrown error → `401` with `WWW-Authenticate: Bearer realm="mcp"`; every other value becomes `context.principal` and `ctx.principal`. Secrets in thrown auth errors are not exposed.
138
- - `beforeCall(context, tool, input)` runs after schema validation. Return nothing to allow; return `McpCallRefusal` (`isError: true`) to send native content/metadata unchanged, independent of result mode. Throw → JSON-RPC `-32602`. Invalid refusals → `-32603`. Invalid input skips policy. The boundary validates before policy and `invoke` keeps its own validation, so schemas must be deterministic and policy input read-only.
138
+ - `beforeCall(context, tool, input)` runs after schema validation. Return nothing to allow; return `McpCallRefusal` (`isError: true`) to send native content/metadata unchanged, independent of result mode. Throw `McpError` → its own code/message/data; other throws → JSON-RPC `-32602`. Invalid refusals → `-32603`. Invalid input skips policy. The boundary validates before policy and `invoke` keeps its own validation, so schemas must be deterministic and policy input read-only.
139
139
  - Stdio identity: `transport: { type: "stdio", principal: { id: "local" } }` or `transport: { type: "stdio", principalProvider: async (context) => ({ id: "local", requestId: context.requestId }) }`. The provider runs per request, takes precedence over static principal, and receives the request id and abort signal. A provider throw → JSON-RPC internal error. No HTTP request metadata exists over stdio.
140
140
  - Public types: `McpBeforeCall`, `McpCallRefusal`, `McpServeToolContext`, `McpStdioServeTransport`; `ToolContext` in `@warlock.js/ai` adds optional `requestId` and `principal`.
141
141
 
@@ -143,6 +143,106 @@ const server = ai.mcp.serve([identity], {
143
143
 
144
144
  HTTP cancellation POSTs must repeat caller credentials and application headers (`Authorization`, `Cookie`, tenant/custom auth headers), from the same remote address. Transport headers such as content length, accept, protocol version and connection may differ. Different credentials can reuse ids safely. Clients sharing credential/application headers must use unique in-flight ids for cancellation to identify a call. Notifications get `202`; a cancelled HTTP call ends with `202`, no JSON-RPC body, when its handler settles. Stdio scopes cancellation to the connection. HTTP disconnect, stdio EOF/close/stop, or HTTP shutdown after the drain timeout also abort tools. Pass `ctx.signal` into cancellable work.
145
145
 
146
+ ## Control discovery, call envelopes and HTTP side routes
147
+
148
+ Use this complete example for hidden-but-callable tools, custom protocol errors,
149
+ unknown envelope policy, and side routes under the same authentication:
150
+
151
+ ```ts
152
+ import { randomBytes } from "node:crypto";
153
+ import { ai } from "@warlock.js/ai";
154
+ import { McpError } from "@warlock.js/ai-tools";
155
+
156
+ const tools = [ai.tools.calculator(), ai.tools.dateTime()];
157
+ const token = randomBytes(32).toString("hex");
158
+ const canCall = (principal: unknown, name: string) =>
159
+ principal === "member" && tools.some((tool) => tool.name === name);
160
+
161
+ const server = ai.mcp.serve(tools, {
162
+ name: "member-tools",
163
+ listFilter: async (context, descriptors) =>
164
+ context.principal === "member"
165
+ ? descriptors.filter((entry) => entry.name === "calculator")
166
+ : [],
167
+ validateCallParams: (_context, params) => {
168
+ const unsupported = Object.keys(params).filter(
169
+ (key) => !["name", "arguments", "_meta"].includes(key),
170
+ );
171
+ if (unsupported.length > 0) {
172
+ throw new McpError(-32600, "Unsupported call envelope", { fields: unsupported });
173
+ }
174
+ },
175
+ beforeCall: (context, tool) => {
176
+ if (!canCall(context.principal, tool.name)) {
177
+ throw new McpError(-32600, "Denied");
178
+ }
179
+ },
180
+ transport: {
181
+ type: "http",
182
+ path: "/mcp",
183
+ auth: { bearer: token, authenticate: () => "member" },
184
+ routes: [
185
+ {
186
+ method: "GET",
187
+ path: "/tool-policy",
188
+ handler: (context) => ({
189
+ body: { allowed: canCall(context.principal, context.url.searchParams.get("name") ?? "") },
190
+ }),
191
+ },
192
+ ],
193
+ },
194
+ });
195
+ const { url, close } = await server.start();
196
+ // `${url}/tool-policy?name=date_time` requires the same bearer token.
197
+ // date_time stays callable via tools/call, under beforeCall, despite being hidden.
198
+ await close();
199
+ ```
200
+
201
+ - `listFilter(context, tools)` runs per `tools/list`, on fresh wire descriptors
202
+ with schemas and exposed (possibly prefixed) names. Return a descriptor array,
203
+ possibly empty or reordered. It only affects discovery; call lookup and policy
204
+ are independent. A throwing filter fails that list request, leaving tools callable.
205
+ - `validateCallParams(context, params)` runs after the basic object/name check,
206
+ before lookup/schema validation/`beforeCall`/invoke. It sees the full read-only
207
+ params including unknown fields, `_meta` and `task`. Return nothing or throw.
208
+ Both hooks support promises and the same request id/principal/request/signal
209
+ as `beforeCall`, on HTTP and stdio. For stdio, use
210
+ `transport: { type: "stdio", principal: "member" }` with these same hooks.
211
+ - `McpError(code, message, data?)` extends `Error`. `beforeCall` and
212
+ `validateCallParams` preserve its integer code, message and optional data in
213
+ JSON-RPC errors. SDK `McpError` is recognized by Error/name/integer code without
214
+ a runtime SDK dependency; its `.message` is preserved including the SDK prefix.
215
+ Other thrown values stay `-32602`; tool execution errors stay `isError: true`.
216
+ - Defaults continue ignoring extra envelope fields. The server speaks
217
+ `2025-11-25`, `2025-06-18`, `2025-03-26`, and advertises no task capability.
218
+ [MCP 2025-11-25 task handling](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#task-support-and-handling)
219
+ requires ignoring task metadata without that capability. The example's
220
+ rejection of `task` is an explicit app policy; omit it to retain default behavior.
221
+ Allow `_meta`, which is protocol metadata.
222
+ - `transport.routes` is HTTP-only. Host/Origin and all configured bearer,
223
+ authorize and authenticate checks run before body reads/handlers. Auth is
224
+ optional on loopback and required elsewhere, just as for MCP. `context` has
225
+ `request`, `principal`, parsed `url`, raw UTF-8 `body`, `signal`; no request id.
226
+ Route bodies share `maxBodyBytes`. MCP-only JSON/header checks do not apply.
227
+ A handler returns `{ status?: number, headers?: Record<string, string>, body?: unknown }`;
228
+ status defaults to 200, body is JSON, omitted body is empty, HEAD suppresses it.
229
+ Exceptions return a generic 500. Disconnect and shutdown abort the signal;
230
+ routes share `close()` draining. Reuse your policy function explicitly;
231
+ route requests do not invoke `beforeCall`.
232
+ - Route suffix `/tool-policy` appends to MCP path `/mcp` or `/mcp/` as
233
+ `/mcp/tool-policy` (root MCP path `/` gives `/tool-policy`). Exact methods:
234
+ GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS. No wildcard, automatic HEAD or CORS.
235
+ Query strings do not affect matching; read them with `url.searchParams`.
236
+ Known path + wrong method → 405 with Allow; unknown path → 404.
237
+ - Route suffixes require nonempty segments containing letters/digits/`_-.~`.
238
+ Empty suffix, `/`, dot segments, percent escapes, repeated or trailing slashes,
239
+ queries, fragments and wildcards fail start with `McpServeError.type: "config"`.
240
+ With routes, the MCP base must be a canonical pathname. All methods on the
241
+ MCP endpoint are reserved, so routes cannot override it. Duplicate method/path
242
+ pairs fail start with `"collision"`; different methods on a suffix are allowed.
243
+ - Public types: `McpListFilter`, `McpValidateCallParams`, `McpHttpRoute`,
244
+ `McpHttpRouteContext`, `McpHttpRouteResponse`.
245
+
146
246
  ## Return native MCP results
147
247
 
148
248
  Default results become one text block, even when an object resembles an MCP result. Return the exported `mcpResult(...)` helper to preserve native content blocks, `isError`, `structuredContent` and `_meta` over stdio or HTTP:
@@ -161,3 +161,24 @@ if (error instanceof WebToolError && error.type === "denied-host") {
161
161
  - The `use-a-workspace` topic of the `warlock-js-ai-workspace` skill — the filesystem + shell tool belt (jailed `read_file` / `edit_file` / `run_shell` / …).
162
162
  - The `define-ai-tool` topic of the `warlock-js-ai` skill — the `tool()` / `ToolContract` seam every tool here is built on.
163
163
  - The `run-ai-agent` topic of the `warlock-js-ai` skill — running the agent that consumes these tools.
164
+
165
+ ## Add the belt to a discovery catalogue
166
+
167
+ Registration extends the core namespace, preserving `ai.tools.catalogue` and `ai.tools.measure`:
168
+
169
+ ```ts
170
+ import "@warlock.js/ai-tools";
171
+ import { ai, type ModelContract } from "@warlock.js/ai";
172
+
173
+ declare const model: ModelContract;
174
+ const surface = ai.tools.catalogue({
175
+ core: [ai.tools.calculator()],
176
+ searchable: [ai.tools.dateTime()],
177
+ limit: 8,
178
+ });
179
+ const assistant = ai.agent({ model, tools: surface.tools });
180
+ const measured = await ai.measure({ model, tools: surface.tools });
181
+ console.log(measured.parts.tools.tokens);
182
+ ```
183
+
184
+ The core plus `tool_search` / `tool_call` stays advertised. Hidden calls still traverse target policy, approval, validation, accounting and tracing. The `define-ai-tool` topic covers lazy descriptors, ranking and measurement comparisons. Native provider tools stay alongside the catalogue; eager ones included in its lists pass through unchanged and are excluded from discovery.