@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 +15 -103
- package/cjs/index.cjs +123 -14
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -2
- package/esm/contracts/mcp-serve.type.d.mts +48 -3
- package/esm/contracts/mcp-serve.type.d.mts.map +1 -1
- package/esm/index.d.mts +3 -2
- 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 +2 -1
- 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/serve-http.mjs +73 -6
- 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 +123 -2
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/expose-as-mcp-server/SKILL.md +102 -2
- package/skills/use-web-and-http-tools/SKILL.md +21 -0
|
@@ -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.
|