@warlock.js/ai-tools 5.30.0 → 5.32.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 +53 -1
- package/README.md +25 -13
- package/cjs/index.cjs +968 -122
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/http.type.d.mts +4 -5
- package/esm/contracts/http.type.d.mts.map +1 -1
- package/esm/contracts/index.d.mts +3 -1
- package/esm/contracts/mcp-serve.type.d.mts +285 -0
- package/esm/contracts/mcp-serve.type.d.mts.map +1 -0
- package/esm/contracts/mcp.type.d.mts +44 -77
- package/esm/contracts/mcp.type.d.mts.map +1 -1
- package/esm/contracts/utility.type.d.mts +7 -8
- package/esm/contracts/utility.type.d.mts.map +1 -1
- package/esm/contracts/web.type.d.mts +9 -10
- package/esm/contracts/web.type.d.mts.map +1 -1
- package/esm/errors.d.mts +62 -22
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +20 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/http/http-request.d.mts +2 -3
- package/esm/http/http-request.d.mts.map +1 -1
- package/esm/http/http-request.mjs +7 -5
- package/esm/http/http-request.mjs.map +1 -1
- package/esm/index.d.mts +10 -4
- package/esm/index.mjs +4 -2
- package/esm/mcp/client.mjs +37 -6
- package/esm/mcp/client.mjs.map +1 -1
- package/esm/mcp/index.d.mts +17 -9
- package/esm/mcp/index.d.mts.map +1 -1
- package/esm/mcp/index.mjs +3 -1
- package/esm/mcp/index.mjs.map +1 -1
- package/esm/mcp/json-schema-to-standard.d.mts +1 -3
- package/esm/mcp/json-schema-to-standard.d.mts.map +1 -1
- package/esm/mcp/protocol.d.mts +15 -0
- package/esm/mcp/protocol.d.mts.map +1 -0
- package/esm/mcp/protocol.mjs +23 -0
- package/esm/mcp/protocol.mjs.map +1 -0
- package/esm/mcp/result.d.mts +17 -0
- package/esm/mcp/result.d.mts.map +1 -0
- package/esm/mcp/result.mjs +85 -0
- package/esm/mcp/result.mjs.map +1 -0
- package/esm/mcp/serve-http.mjs +444 -0
- package/esm/mcp/serve-http.mjs.map +1 -0
- package/esm/mcp/serve.d.mts +46 -27
- package/esm/mcp/serve.d.mts.map +1 -1
- package/esm/mcp/serve.mjs +339 -96
- package/esm/mcp/serve.mjs.map +1 -1
- package/esm/mcp/transport.d.mts +4 -5
- package/esm/mcp/transport.d.mts.map +1 -1
- package/esm/mcp/transport.mjs +12 -6
- package/esm/mcp/transport.mjs.map +1 -1
- package/esm/mcp/transport.type.d.mts +2 -2
- package/esm/mcp/transport.type.d.mts.map +1 -1
- package/esm/register.d.mts +15 -5
- package/esm/register.d.mts.map +1 -1
- package/esm/register.mjs +3 -2
- package/esm/register.mjs.map +1 -1
- package/esm/schema.mjs.map +1 -1
- package/esm/utility/calculator.d.mts +1 -3
- package/esm/utility/calculator.d.mts.map +1 -1
- package/esm/utility/calculator.mjs.map +1 -1
- package/esm/utility/date-time.d.mts +3 -5
- package/esm/utility/date-time.d.mts.map +1 -1
- package/esm/utility/date-time.mjs +2 -1
- package/esm/utility/date-time.mjs.map +1 -1
- package/esm/utility/index.d.mts +2 -1
- package/esm/utility/index.mjs +1 -1
- package/esm/utility/schema.mjs.map +1 -1
- package/esm/web/fetch-url.d.mts +2 -3
- package/esm/web/fetch-url.d.mts.map +1 -1
- package/esm/web/fetch-url.mjs +2 -1
- package/esm/web/fetch-url.mjs.map +1 -1
- package/esm/web/index.d.mts +2 -1
- package/esm/web/index.mjs +1 -1
- package/esm/web/schema.mjs.map +1 -1
- package/esm/web/web-search.d.mts +2 -3
- package/esm/web/web-search.d.mts.map +1 -1
- package/esm/web/web-search.mjs +3 -4
- package/esm/web/web-search.mjs.map +1 -1
- package/llms-full.txt +245 -56
- package/llms.txt +3 -3
- package/package.json +2 -6
- package/skills/connect-mcp-server/SKILL.md +28 -6
- package/skills/expose-as-mcp-server/SKILL.md +168 -35
- package/skills/index.md +5 -2
- package/skills/use-web-and-http-tools/SKILL.md +49 -15
|
@@ -1,82 +1,215 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: expose-as-mcp-server
|
|
3
|
-
description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[]) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)
|
|
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.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Expose tools AS an MCP server (Direction B)
|
|
7
7
|
|
|
8
|
-
`ai.mcp.serve(source, options)` turns a local set of `ToolContract`s into a Model Context Protocol server — so Cursor, Claude Desktop, or any MCP client can call your warlock tools / agent
|
|
8
|
+
`ai.mcp.serve(source, options)` turns a local set of `ToolContract`s into a Model Context Protocol server — so Cursor, Claude Desktop, a coding CLI, or any MCP client can call your warlock tools / agent. Transports: **stdio** (default) and MCP **Streamable HTTP**.
|
|
9
|
+
|
|
10
|
+
## Serve over HTTP with a per-session token
|
|
9
11
|
|
|
10
12
|
```ts
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
11
14
|
import "@warlock.js/ai-tools";
|
|
12
15
|
import { ai } from "@warlock.js/ai";
|
|
13
16
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
const token = randomBytes(32).toString("hex");
|
|
18
|
+
|
|
19
|
+
const server = ai.mcp.serve([ai.tools.calculator(), ai.tools.dateTime()], {
|
|
20
|
+
name: "session-tools",
|
|
21
|
+
transport: { type: "http", auth: { bearer: token } },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const { url, close } = await server.start();
|
|
25
|
+
// url → "http://127.0.0.1:<free port>/mcp" — hand url + token to the MCP client
|
|
26
|
+
|
|
27
|
+
await close(); // drains in-flight calls, then frees the port
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Defaults: `host: "127.0.0.1"`, `port: 0` (free ephemeral port), `path: "/mcp"`. `start()` resolves `{ url, close }` once bound.
|
|
31
|
+
|
|
32
|
+
## Serve over stdio
|
|
18
33
|
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
```ts
|
|
35
|
+
import "@warlock.js/ai-tools";
|
|
36
|
+
import { ai } from "@warlock.js/ai";
|
|
37
|
+
|
|
38
|
+
const server = ai.mcp.serve([ai.tools.calculator()], {
|
|
39
|
+
name: "warlock-utils",
|
|
40
|
+
transport: { type: "stdio" },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await server.start(); // reads JSON-RPC lines from stdin, answers on stdout
|
|
44
|
+
// on teardown:
|
|
21
45
|
await server.stop();
|
|
22
46
|
```
|
|
23
47
|
|
|
24
|
-
##
|
|
48
|
+
## HTTP options and security rules
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
transport: {
|
|
52
|
+
type: "http",
|
|
53
|
+
host: "127.0.0.1", // non-loopback (e.g. "0.0.0.0") REQUIRES auth — start() throws otherwise
|
|
54
|
+
port: 0,
|
|
55
|
+
path: "/mcp",
|
|
56
|
+
auth: {
|
|
57
|
+
bearer: ["current-token", "previous-token"], // constant-time compare; several = rotation
|
|
58
|
+
authorize: async (request) => sessions.has(request.headers["x-session-id"] ?? ""), // optional hook
|
|
59
|
+
},
|
|
60
|
+
allowedOrigins: ["https://app.example.com"], // besides loopback origins
|
|
61
|
+
allowedHosts: ["mcp.internal"], // extra Host names while on loopback
|
|
62
|
+
maxBodyBytes: 1_048_576, // larger → 413
|
|
63
|
+
drainTimeoutMs: 5_000, // close() then aborts ctx.signal
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- With `bearer` and `authorize` both set, a request must pass both. A throwing hook denies. Failures → `401`.
|
|
68
|
+
- A foreign or `null` `Origin` → `403` (DNS-rebinding guard from the MCP spec). Requests without `Origin` pass.
|
|
69
|
+
- On loopback, a `Host` other than `localhost` / `127.0.0.1` / `[::1]` (or `allowedHosts`) → `403`.
|
|
70
|
+
- Stateless: no session id; `GET` / `DELETE` → `405`; batches → `400`; unsupported `MCP-Protocol-Version` → `400`.
|
|
71
|
+
- Plain HTTP: put TLS in front before exposing beyond the machine.
|
|
72
|
+
|
|
73
|
+
## Tools read request headers from `ctx.request`
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import "@warlock.js/ai-tools";
|
|
77
|
+
import { ai, type ToolContract } from "@warlock.js/ai";
|
|
78
|
+
|
|
79
|
+
const whoami: ToolContract = ai.tool({
|
|
80
|
+
name: "whoami",
|
|
81
|
+
description: "Report the calling tenant",
|
|
82
|
+
execute: async (_input: unknown, ctx) => ({
|
|
83
|
+
tenant: ctx?.request?.headers["x-tenant-id"] ?? "unknown",
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const { url, close } = await ai.mcp
|
|
88
|
+
.serve([whoami], { name: "tenant-tools", transport: { type: "http" } })
|
|
89
|
+
.start();
|
|
90
|
+
```
|
|
91
|
+
|
|
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
|
+
|
|
94
|
+
## Return native MCP results
|
|
95
|
+
|
|
96
|
+
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:
|
|
25
97
|
|
|
26
98
|
```ts
|
|
27
|
-
|
|
28
|
-
ai
|
|
99
|
+
import { mcpResult } from "@warlock.js/ai-tools";
|
|
100
|
+
import { ai } from "@warlock.js/ai";
|
|
101
|
+
|
|
102
|
+
const count = ai.tool({
|
|
103
|
+
name: "count",
|
|
104
|
+
description: "Return a structured count",
|
|
105
|
+
meta: {
|
|
106
|
+
outputSchema: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: { count: { type: "number" } },
|
|
109
|
+
required: ["count"],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
execute: async (_input: unknown, ctx) =>
|
|
113
|
+
mcpResult({
|
|
114
|
+
content: [{ type: "text", text: '{"count":2}' }],
|
|
115
|
+
structuredContent: { count: 2 },
|
|
116
|
+
_meta: { tenant: ctx?.request?.headers["x-tenant-id"] ?? "unknown" },
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
29
119
|
|
|
30
|
-
|
|
31
|
-
ai.mcp.serve({ tools: () => ws.tools.all() }, { name: "acme-workspace" });
|
|
120
|
+
ai.mcp.serve([count], { name: "counts" });
|
|
32
121
|
```
|
|
33
122
|
|
|
34
|
-
The
|
|
123
|
+
The helper returns a branded `McpNativeResult` wrapper. The serving boundary validates and unwraps it without altering its payload. `isError: true` is sent as-is; thrown tool errors retain the existing mapping.
|
|
35
124
|
|
|
36
|
-
|
|
125
|
+
For tools that already return native objects, opt in at the server or tool level:
|
|
37
126
|
|
|
38
127
|
```ts
|
|
39
|
-
ai.
|
|
40
|
-
name: "
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
128
|
+
const nativeTool = ai.tool({
|
|
129
|
+
name: "native-result",
|
|
130
|
+
description: "Return a native result",
|
|
131
|
+
meta: { mcpResults: "native" }, // this tool opts in even on a default server
|
|
132
|
+
execute: async () => ({ content: [{ type: "text", text: "fixture" }], isError: true }),
|
|
44
133
|
});
|
|
134
|
+
|
|
135
|
+
ai.mcp.serve([nativeTool], { name: "native-tools", results: "native" });
|
|
136
|
+
// On a native-mode server, meta: { mcpResults: "text" } disables detection for a tool.
|
|
137
|
+
// Explicit mcpResult(...) returns always opt in, regardless of the detection setting.
|
|
45
138
|
```
|
|
46
139
|
|
|
47
|
-
|
|
140
|
+
Native mode detects objects containing any of `content`, `isError`, `structuredContent` or `_meta`; other values keep text serialization. `content` must be an array of valid MCP text/image/audio/resource-link/embedded-resource blocks. Optional `isError` must be boolean; `structuredContent` and `_meta` must be JSON objects. Invalid or non-JSON native payloads answer a JSON-RPC internal error (`-32603`), including malformed helper payloads.
|
|
48
141
|
|
|
49
|
-
|
|
142
|
+
`meta.outputSchema` is emitted as `outputSchema` in `tools/list`; `structuredContent` passes through unchanged. Tool authors own schema conformity. Follow the [MCP 2025-11-25 tools specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#structured-content): also include serialized structured output in a text block for compatibility.
|
|
50
143
|
|
|
51
|
-
|
|
52
|
-
- **`tools/list`** → one descriptor per contract; each tool's `inputSchema` is produced by `extractJsonSchema(contract.input, { target: schemaTarget })`.
|
|
53
|
-
- **`tools/call`** → routes to the named contract's `invoke()` and maps the **never-throwing** result: `data` becomes a `{ type: "text" }` content block (a string is sent verbatim, anything else is JSON-stringified), and `error` becomes an `isError: true` result. So a failing tool surfaces as a normal MCP tool error — the server never crashes on it. An unknown tool name answers with a JSON-RPC error.
|
|
144
|
+
Request identity is unchanged: native tools receive each HTTP call's headers and remote address on `ctx.request`, plus `ctx.signal`. Over stdio, `ctx.request` is absent. Returning a native result does not automatically expose request metadata; the tool controls its response.
|
|
54
145
|
|
|
55
|
-
|
|
146
|
+
## Sources and tool-name collisions
|
|
56
147
|
|
|
57
|
-
|
|
148
|
+
```ts
|
|
149
|
+
ai.mcp.serve([calc, dt], { name: "utils" }); // literal array
|
|
150
|
+
ai.mcp.serve({ tools: () => ws.tools.all() }, { name: "workspace" }); // anything with tools()
|
|
151
|
+
ai.mcp.serve(
|
|
152
|
+
{ groups: { prod: [search], staging: [search] } },
|
|
153
|
+
{
|
|
154
|
+
name: "search",
|
|
155
|
+
prefixToolNames: true, // → prod_search, staging_search; or (group, name) => `${group}.${name}`
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Duplicate exposed names throw `McpServeError` (`type: "collision"`) **when `serve()` is called**. `prefixToolNames` on an ungrouped source throws `type: "config"`.
|
|
161
|
+
|
|
162
|
+
## Resources and prompts (optional)
|
|
58
163
|
|
|
59
164
|
```ts
|
|
60
|
-
|
|
61
|
-
|
|
165
|
+
ai.mcp.serve(tools, {
|
|
166
|
+
name: "handbook",
|
|
167
|
+
resources: [
|
|
168
|
+
{
|
|
169
|
+
uri: "docs://handbook",
|
|
170
|
+
name: "handbook",
|
|
171
|
+
mimeType: "text/markdown",
|
|
172
|
+
read: () => "# Handbook",
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
prompts: [
|
|
176
|
+
{
|
|
177
|
+
name: "review",
|
|
178
|
+
arguments: [{ name: "diff", required: true }],
|
|
179
|
+
get: (args) => `Review this diff:\n${args.diff ?? ""}`, // string → one user message
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
});
|
|
62
183
|
```
|
|
63
184
|
|
|
64
|
-
`
|
|
185
|
+
`read()` returns a string (text) or `Uint8Array` (base64 blob). `get()` returns a string or `McpPromptMessage[]`. Unknown resource → `-32002`; unknown prompt / missing required argument → `-32602`.
|
|
186
|
+
|
|
187
|
+
## How the protocol maps
|
|
188
|
+
|
|
189
|
+
- **`initialize`** → `{ name, version }`, capabilities `tools` (+ `resources` / `prompts` when configured); protocol version negotiated from `SUPPORTED_PROTOCOL_VERSIONS` (`2025-11-25`, `2025-06-18`, `2025-03-26`).
|
|
190
|
+
- **`tools/list`** → `inputSchema` via `extractJsonSchema(contract.input, { target: schemaTarget })` (default `"draft-2020-12"`).
|
|
191
|
+
- **`tools/call`** → `invoke()`; default `data` → text block (JSON-stringified unless a string), opted-in native results → validated passthrough, `error` → `isError: true`. Unknown tool → JSON-RPC error.
|
|
192
|
+
- **`ping`** → `{}`.
|
|
193
|
+
|
|
194
|
+
## Drive the protocol yourself (tests, custom servers)
|
|
65
195
|
|
|
66
196
|
```ts
|
|
67
197
|
import { createServeHandler } from "@warlock.js/ai-tools";
|
|
198
|
+
import { ai } from "@warlock.js/ai";
|
|
68
199
|
|
|
69
|
-
const handler = createServeHandler([
|
|
200
|
+
const handler = createServeHandler([ai.tools.calculator()], { name: "utils" });
|
|
70
201
|
|
|
71
|
-
|
|
72
|
-
|
|
202
|
+
const response = await handler.handle(
|
|
203
|
+
{ jsonrpc: "2.0", id: 1, method: "tools/list" },
|
|
204
|
+
{ request: { method: "POST", path: "/mcp", headers: { "x-tenant-id": "acme" } } },
|
|
205
|
+
);
|
|
73
206
|
```
|
|
74
207
|
|
|
75
|
-
|
|
208
|
+
Mounting it in your own server means you own Origin validation, auth and body limits.
|
|
76
209
|
|
|
77
210
|
## Errors
|
|
78
211
|
|
|
79
|
-
`
|
|
212
|
+
`McpServeError.type`: `"collision"` (from `serve()`), `"config"` (unsafe/invalid options, from `start()`), `"listen"` (bind failed). Bad requests never throw — they get an HTTP status or JSON-RPC error; failing tools return `isError: true`.
|
|
80
213
|
|
|
81
214
|
## See also
|
|
82
215
|
|
package/skills/index.md
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Ready-made tools for @warlock.js/ai agents: web search, URL fetch, HTTP requests, calculator, date-time, and MCP client and server support. Exports `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `mcp`, `serve`, `createServeHandler`, `McpTransportError`. Use for: let an agent search the web, fetch a page, call an HTTP API, do math, get the current date, connect to an MCP server, expose my tools as an MCP server. Not this package: defining your own tool or the agent itself → @warlock.js/ai; file and shell tools → @warlock.js/ai-workspace."
|
|
2
|
+
description: "Ready-made tools for @warlock.js/ai agents: web search, URL fetch, HTTP requests, calculator, date-time, and MCP client and server support. Exports `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `mcp`, `serve`, `createServeHandler`, `McpTransportError`, `McpServeError`. Use for: let an agent search the web, fetch a page, call an HTTP API, do math, get the current date, connect to an MCP server, expose my tools as an MCP server over stdio or authenticated HTTP. Not this package: defining your own tool or the agent itself → @warlock.js/ai; file and shell tools → @warlock.js/ai-workspace."
|
|
3
3
|
---
|
|
4
|
+
|
|
4
5
|
# @warlock.js/ai-tools
|
|
5
6
|
|
|
6
7
|
A belt of prebuilt tools for `@warlock.js/ai` agents plus the Model Context Protocol bridge in both directions. Importing the package attaches `ai.tools.*` and `ai.mcp` onto the shared `ai` object; the factories are also exported directly.
|
|
7
8
|
|
|
8
9
|
## The 80% path
|
|
10
|
+
|
|
9
11
|
1. Give an agent web or API access with the factories (`use-web-and-http-tools.md`); pass the results in the agent's `tools` array.
|
|
10
12
|
2. Consume tools from an external MCP server (`connect-mcp-server.md`); they arrive as normal agent tools.
|
|
11
|
-
3. Publish your own tools to MCP clients such as editors (`expose-as-mcp-server.md`).
|
|
13
|
+
3. Publish your own tools to MCP clients such as editors or agent CLIs, over stdio or a loopback HTTP endpoint with a bearer token (`expose-as-mcp-server.md`).
|
|
12
14
|
|
|
13
15
|
## Conventions and pitfalls
|
|
16
|
+
|
|
14
17
|
- Direction A (`connect-mcp-server.md`) consumes; Direction B (`expose-as-mcp-server.md`) serves. Do not mix them up.
|
|
15
18
|
- `fetchUrlTool` and `httpRequestTool` are policy-gated: set host allowlists and byte caps rather than opening them fully.
|
|
16
19
|
- `webSearchTool` needs a search provider choice and its API key (tavily, brave, or serpapi).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: use-web-and-http-tools
|
|
3
|
-
description: 'Wire the @warlock.js/ai-tools belt — web search, fetch/scrape, HTTP/REST, calculator, date-time — into an agent under their guardrails. Triggers: `ai.tools.webSearch`, `ai.tools.fetchUrl`, `ai.tools.http`, `ai.tools.calculator`, `ai.tools.dateTime`, `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `web_search`, `fetch_url`, `http_request`, `calculator`, `date_time`, `WebToolError`, `HttpPolicyError`, `CalculatorError`, `DateTimeError`, `allowHosts`, `allowMethods`, `allowPrivateNetwork`, `baseUrl`, `maxBytes`, `guardedFetch`, `SearchProvider`, `TAVILY_API_KEY`; ''give an agent web search'', ''let an agent fetch/scrape a page'', ''let an agent call a REST API'', ''restrict which hosts/methods an agent can hit'', ''add a calculator/date tool to an agent'', ''SSRF guardrail for an agent tool'', ''let a tool reach an internal/dev service''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: connecting to an external MCP server — the `connect-mcp-server` topic; exposing tools AS an MCP server — the `expose-as-mcp-server` topic; filesystem/shell tools — the `use-a-workspace` topic of the `warlock-js-ai-workspace` skill; defining your own custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.'
|
|
3
|
+
description: 'Wire the @warlock.js/ai-tools belt — web search, fetch/scrape, HTTP/REST, calculator, date-time — into an agent under their guardrails. Triggers: `ai.tools.measure`, `measureTools`, `ai.tools.webSearch`, `ai.tools.fetchUrl`, `ai.tools.http`, `ai.tools.calculator`, `ai.tools.dateTime`, `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `web_search`, `fetch_url`, `http_request`, `calculator`, `date_time`, `WebToolError`, `HttpPolicyError`, `CalculatorError`, `DateTimeError`, `allowHosts`, `allowMethods`, `allowPrivateNetwork`, `baseUrl`, `maxBytes`, `guardedFetch`, `SearchProvider`, `TAVILY_API_KEY`; ''give an agent web search'', ''let an agent fetch/scrape a page'', ''let an agent call a REST API'', ''restrict which hosts/methods an agent can hit'', ''add a calculator/date tool to an agent'', ''SSRF guardrail for an agent tool'', ''let a tool reach an internal/dev service''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: connecting to an external MCP server — the `connect-mcp-server` topic; exposing tools AS an MCP server — the `expose-as-mcp-server` topic; filesystem/shell tools — the `use-a-workspace` topic of the `warlock-js-ai-workspace` skill; defining your own custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Use the web + HTTP tool belt
|
|
@@ -28,17 +28,17 @@ const agent = ai.agent({
|
|
|
28
28
|
});
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The construction `options` are the **rails** — they bound what the model may do; the model supplies the per-call inputs
|
|
31
|
+
The construction `options` are the **rails** — they bound what the model may do; the model supplies the per-call inputs _within_ those rails. Least-privilege is just array composition: hand an agent only the tools (and only the methods/hosts) it needs.
|
|
32
32
|
|
|
33
33
|
## The five tools
|
|
34
34
|
|
|
35
|
-
| Factory
|
|
36
|
-
|
|
37
|
-
| `ai.tools.webSearch(options)`
|
|
38
|
-
| `ai.tools.fetchUrl(options?)`
|
|
39
|
-
| `ai.tools.http(options?)`
|
|
40
|
-
| `ai.tools.calculator(options?)` | `calculator`
|
|
41
|
-
| `ai.tools.dateTime(options?)`
|
|
35
|
+
| Factory | Name | Model input | Returns |
|
|
36
|
+
| ------------------------------- | -------------- | -------------------------------------------------------------- | ------------------------------------------------ |
|
|
37
|
+
| `ai.tools.webSearch(options)` | `web_search` | `{ query, maxResults? }` | `{ results: { title, url, snippet, score? }[] }` |
|
|
38
|
+
| `ai.tools.fetchUrl(options?)` | `fetch_url` | `{ url }` | `{ url, status, content, truncated }` |
|
|
39
|
+
| `ai.tools.http(options?)` | `http_request` | `{ method?, url, headers?, body? }` | `{ status, headers, body, truncated }` |
|
|
40
|
+
| `ai.tools.calculator(options?)` | `calculator` | `{ expression }` | `{ result }` |
|
|
41
|
+
| `ai.tools.dateTime(options?)` | `date_time` | `{ op, iso?, from?, to?, amount?, unit?, timeZone?, format? }` | `{ value }` |
|
|
42
42
|
|
|
43
43
|
## `web_search` — pick a provider, supply a key
|
|
44
44
|
|
|
@@ -53,7 +53,12 @@ ai.tools.webSearch({ provider: "tavily", apiKey: process.env.TAVILY_API_KEY, max
|
|
|
53
53
|
## `fetch_url` — read a page you have the URL for
|
|
54
54
|
|
|
55
55
|
```ts
|
|
56
|
-
ai.tools.fetchUrl({
|
|
56
|
+
ai.tools.fetchUrl({
|
|
57
|
+
extract: "text",
|
|
58
|
+
allowHosts: ["docs.stripe.com"],
|
|
59
|
+
maxBytes: 500_000,
|
|
60
|
+
timeoutMs: 10_000,
|
|
61
|
+
});
|
|
57
62
|
```
|
|
58
63
|
|
|
59
64
|
- `extract`: `"text"` (default — readability-extracted main text), `"html"` (raw body), or `"markdown"`. **`"text"`/`"markdown"` lazily import the `@mozilla/readability` + `jsdom` optional peers** — a missing peer is a `WebToolError` of type `"missing-peer"` carrying the `npm install @mozilla/readability jsdom` string. `"html"` needs nothing.
|
|
@@ -68,7 +73,7 @@ ai.tools.fetchUrl({ extract: "text", allowHosts: ["docs.stripe.com"], maxBytes:
|
|
|
68
73
|
ai.tools.http({
|
|
69
74
|
baseUrl: "https://api.stripe.com",
|
|
70
75
|
allowHosts: ["api.stripe.com"],
|
|
71
|
-
allowMethods: ["GET", "POST"],
|
|
76
|
+
allowMethods: ["GET", "POST"], // default ["GET"] — mutating methods are opt-in
|
|
72
77
|
headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },
|
|
73
78
|
timeoutMs: 15_000,
|
|
74
79
|
maxBytes: 1_000_000,
|
|
@@ -77,6 +82,7 @@ ai.tools.http({
|
|
|
77
82
|
```
|
|
78
83
|
|
|
79
84
|
All guardrails are enforced **before the network call** and surface as a typed `HttpPolicyError`:
|
|
85
|
+
|
|
80
86
|
- a method outside `allowMethods` → `"method-not-allowed"`;
|
|
81
87
|
- a host outside `allowHosts` → `"host-not-allowed"` (SSRF guardrail);
|
|
82
88
|
- an unresolvable URL → `"invalid-url"`.
|
|
@@ -97,9 +103,15 @@ Supports `+ - * / % ^`, unary signs, parentheses, and decimal/scientific literal
|
|
|
97
103
|
|
|
98
104
|
```ts
|
|
99
105
|
await ai.tools.dateTime().invoke({ op: "now", format: "iso" });
|
|
100
|
-
await ai.tools
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
await ai.tools
|
|
107
|
+
.dateTime()
|
|
108
|
+
.invoke({ op: "add", iso: "2026-06-22T00:00:00Z", amount: 3, unit: "days" });
|
|
109
|
+
await ai.tools
|
|
110
|
+
.dateTime()
|
|
111
|
+
.invoke({ op: "diff", iso: "2026-06-22T00:00:00Z", to: "2026-06-25T00:00:00Z", unit: "days" }); // "3"
|
|
112
|
+
await ai.tools
|
|
113
|
+
.dateTime({ defaultTimeZone: "Africa/Cairo" })
|
|
114
|
+
.invoke({ op: "format", iso: "2026-06-22T12:00:00Z", format: "datetime" });
|
|
103
115
|
```
|
|
104
116
|
|
|
105
117
|
- `op`: `"now"` / `"add"` / `"diff"` / `"format"`. `format`: `"iso"` (default), `"date"`, `"time"`, `"datetime"`.
|
|
@@ -107,6 +119,26 @@ await ai.tools.dateTime({ defaultTimeZone: "Africa/Cairo" }).invoke({ op: "forma
|
|
|
107
119
|
- Units are millisecond-based: `milliseconds`, `seconds`, `minutes`, `hours`, `days`, `weeks` (plus singular/short aliases like `day`/`d`). Calendar-relative `month`/`year` are intentionally **unsupported** — they have no fixed length. An unknown unit → `DateTimeError` `"invalid-unit"`; an unknown IANA zone → `"invalid-time-zone"`.
|
|
108
120
|
- For deterministic tests, inject a clock by calling the factory directly: `import { dateTimeTool } from "@warlock.js/ai-tools"; dateTimeTool({}, { clock: () => Date.parse("2026-06-22T00:00:00Z") })`.
|
|
109
121
|
|
|
122
|
+
## `ai.tools.measure` — what the belt costs per model call
|
|
123
|
+
|
|
124
|
+
`ai.tools.measure(tools, options?)` is `measureTools` from `@warlock.js/ai`: the input tokens a tool list adds to every call, per tool and in total. Native and exact when the model has `countTokens` (`source: "native"`, `tools.length + 2` count calls, `overhead` = the provider's once-per-request tool preamble); estimated from the schema JSON otherwise (`source: "estimate"`).
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import "@warlock.js/ai-tools";
|
|
128
|
+
import { ai, type ModelContract } from "@warlock.js/ai";
|
|
129
|
+
|
|
130
|
+
export async function reportBeltCost(model: ModelContract): Promise<void> {
|
|
131
|
+
const belt = [ai.tools.calculator(), ai.tools.dateTime()];
|
|
132
|
+
|
|
133
|
+
const full = await ai.tools.measure(belt, { model });
|
|
134
|
+
const compact = await ai.tools.measure(belt, { model, mode: "compact" });
|
|
135
|
+
|
|
136
|
+
console.log(full.source, full.tokens, "→", compact.tokens, full.perTool);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If compact saves enough, run the agent with `toolSchemas: "compact"` (the `define-ai-tool` topic of the `warlock-js-ai` skill).
|
|
141
|
+
|
|
110
142
|
## Errors flow as data, not throws
|
|
111
143
|
|
|
112
144
|
Every failure above is thrown inside the tool handler, caught by the framework's `tool()` wrapper, and surfaced in the returned `{ error }` field — `invoke()` never throws, so the agent reads the failure as a `role: "tool"` message and self-corrects on the next trip. Each error class extends the `@warlock.js/ai` `AIError` base; branch on `.type`:
|
|
@@ -114,7 +146,9 @@ Every failure above is thrown inside the tool handler, caught by the framework's
|
|
|
114
146
|
```ts
|
|
115
147
|
import { WebToolError } from "@warlock.js/ai-tools";
|
|
116
148
|
|
|
117
|
-
const { error } = await ai.tools
|
|
149
|
+
const { error } = await ai.tools
|
|
150
|
+
.fetchUrl({ allowHosts: ["docs.stripe.com"] })
|
|
151
|
+
.invoke({ url: "http://evil.test" });
|
|
118
152
|
if (error instanceof WebToolError && error.type === "denied-host") {
|
|
119
153
|
// rejected before any network call
|
|
120
154
|
}
|