@warlock.js/ai-tools 5.32.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 +25 -103
- package/cjs/index.cjs +403 -131
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -2
- package/esm/contracts/mcp-serve.type.d.mts +85 -8
- 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 +134 -29
- package/esm/mcp/serve-http.mjs.map +1 -1
- package/esm/mcp/serve-requests.mjs +62 -0
- package/esm/mcp/serve-requests.mjs.map +1 -0
- package/esm/mcp/serve.d.mts.map +1 -1
- package/esm/mcp/serve.mjs +105 -15
- 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 +190 -2
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/connect-mcp-server/SKILL.md +15 -0
- package/skills/expose-as-mcp-server/SKILL.md +154 -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`, `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)
|
|
@@ -64,7 +64,7 @@ transport: {
|
|
|
64
64
|
}
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
-
|
|
67
|
+
- Checks run in order: `bearer`, `authorize`, `authenticate`; every configured check must pass. A failed check skips later hooks. `authenticate` alone satisfies the non-loopback auth requirement. Host/Origin checks run first. A throwing authentication hook denies. Failures → `401`; bearer or authenticate failures include a Bearer `WWW-Authenticate` challenge.
|
|
68
68
|
- A foreign or `null` `Origin` → `403` (DNS-rebinding guard from the MCP spec). Requests without `Origin` pass.
|
|
69
69
|
- On loopback, a `Host` other than `localhost` / `127.0.0.1` / `[::1]` (or `allowedHosts`) → `403`.
|
|
70
70
|
- Stateless: no session id; `GET` / `DELETE` → `405`; batches → `400`; unsupported `MCP-Protocol-Version` → `400`.
|
|
@@ -91,6 +91,158 @@ const { url, close } = await ai.mcp
|
|
|
91
91
|
|
|
92
92
|
Header names are lower-cased; every client header is present (including `authorization`). Over stdio `ctx.request` is absent. Nothing on `ctx.request` reaches the model.
|
|
93
93
|
|
|
94
|
+
## Authenticated context, policy and cancellation
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { ai } from "@warlock.js/ai";
|
|
98
|
+
import "@warlock.js/ai-tools";
|
|
99
|
+
import type { McpHttpRequestInfo, McpCallRefusal } from "@warlock.js/ai-tools";
|
|
100
|
+
|
|
101
|
+
declare function findMember(
|
|
102
|
+
request: McpHttpRequestInfo,
|
|
103
|
+
): Promise<{ id: string; canCall: boolean } | undefined>;
|
|
104
|
+
const denied: McpCallRefusal = {
|
|
105
|
+
isError: true,
|
|
106
|
+
content: [{ type: "text", text: "Member cannot call tools" }],
|
|
107
|
+
_meta: { policy: "membership" },
|
|
108
|
+
};
|
|
109
|
+
const identity = ai.tool({
|
|
110
|
+
name: "identity",
|
|
111
|
+
description: "Report the authenticated caller and request id",
|
|
112
|
+
execute: async (_input: unknown, ctx) => {
|
|
113
|
+
ctx?.signal?.throwIfAborted();
|
|
114
|
+
return { requestId: ctx?.requestId, principal: ctx?.principal };
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const server = ai.mcp.serve([identity], {
|
|
119
|
+
name: "member-tools",
|
|
120
|
+
transport: { type: "http", auth: { authenticate: findMember } },
|
|
121
|
+
beforeCall: async (context, tool, input) => {
|
|
122
|
+
const member = context.principal; // unknown — narrow before use
|
|
123
|
+
if (
|
|
124
|
+
typeof member !== "object" ||
|
|
125
|
+
member === null ||
|
|
126
|
+
!("canCall" in member) ||
|
|
127
|
+
member.canCall !== true
|
|
128
|
+
) {
|
|
129
|
+
return denied;
|
|
130
|
+
}
|
|
131
|
+
// context.requestId: JSON-RPC id; tool: original ToolContract;
|
|
132
|
+
// input: validated data, treated as read-only.
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
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 `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
|
+
- 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
|
+
- Public types: `McpBeforeCall`, `McpCallRefusal`, `McpServeToolContext`, `McpStdioServeTransport`; `ToolContext` in `@warlock.js/ai` adds optional `requestId` and `principal`.
|
|
141
|
+
|
|
142
|
+
[Cancellation (MCP 2025-11-25)](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation): send `{ jsonrpc: "2.0", method: "notifications/cancelled", params: { requestId: 42, reason: "Caller cancelled" } }`. The matching signal aborts; no JSON-RPC response is sent even if the tool later returns or throws. Unknown/completed/malformed ids are ignored; `initialize` cannot be cancelled. String and numeric ids are distinct; calls sharing an in-flight id still execute, but ambiguous cancellation in one scope is ignored.
|
|
143
|
+
|
|
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
|
+
|
|
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
|
+
|
|
94
246
|
## Return native MCP results
|
|
95
247
|
|
|
96
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.
|