@warlock.js/ai-tools 5.30.0 → 5.31.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/README.md +25 -13
  3. package/cjs/index.cjs +843 -101
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/contracts/index.d.mts +2 -1
  6. package/esm/contracts/mcp-serve.type.d.mts +257 -0
  7. package/esm/contracts/mcp-serve.type.d.mts.map +1 -0
  8. package/esm/contracts/mcp.type.d.mts +16 -59
  9. package/esm/contracts/mcp.type.d.mts.map +1 -1
  10. package/esm/errors.d.mts +36 -1
  11. package/esm/errors.d.mts.map +1 -1
  12. package/esm/errors.mjs +20 -1
  13. package/esm/errors.mjs.map +1 -1
  14. package/esm/http/http-request.d.mts.map +1 -1
  15. package/esm/http/http-request.mjs.map +1 -1
  16. package/esm/index.d.mts +6 -4
  17. package/esm/index.mjs +3 -2
  18. package/esm/mcp/client.mjs +35 -5
  19. package/esm/mcp/client.mjs.map +1 -1
  20. package/esm/mcp/index.d.mts +12 -5
  21. package/esm/mcp/index.d.mts.map +1 -1
  22. package/esm/mcp/index.mjs +1 -0
  23. package/esm/mcp/index.mjs.map +1 -1
  24. package/esm/mcp/protocol.d.mts +16 -0
  25. package/esm/mcp/protocol.d.mts.map +1 -0
  26. package/esm/mcp/protocol.mjs +23 -0
  27. package/esm/mcp/protocol.mjs.map +1 -0
  28. package/esm/mcp/serve-http.mjs +443 -0
  29. package/esm/mcp/serve-http.mjs.map +1 -0
  30. package/esm/mcp/serve.d.mts +45 -26
  31. package/esm/mcp/serve.d.mts.map +1 -1
  32. package/esm/mcp/serve.mjs +325 -92
  33. package/esm/mcp/serve.mjs.map +1 -1
  34. package/esm/mcp/transport.d.mts.map +1 -1
  35. package/esm/mcp/transport.mjs.map +1 -1
  36. package/esm/register.d.mts +12 -1
  37. package/esm/register.d.mts.map +1 -1
  38. package/esm/register.mjs +3 -2
  39. package/esm/register.mjs.map +1 -1
  40. package/esm/schema.mjs.map +1 -1
  41. package/esm/utility/calculator.d.mts.map +1 -1
  42. package/esm/utility/calculator.mjs.map +1 -1
  43. package/esm/utility/date-time.d.mts.map +1 -1
  44. package/esm/utility/date-time.mjs.map +1 -1
  45. package/esm/utility/schema.mjs.map +1 -1
  46. package/esm/web/fetch-url.d.mts.map +1 -1
  47. package/esm/web/fetch-url.mjs.map +1 -1
  48. package/esm/web/schema.mjs.map +1 -1
  49. package/esm/web/web-search.mjs.map +1 -1
  50. package/llms-full.txt +194 -57
  51. package/llms.txt +3 -3
  52. package/package.json +2 -6
  53. package/skills/connect-mcp-server/SKILL.md +28 -6
  54. package/skills/expose-as-mcp-server/SKILL.md +117 -36
  55. package/skills/index.md +5 -2
  56. package/skills/use-web-and-http-tools/SKILL.md +49 -15
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: connect-mcp-server
3
- description: 'Connect a @warlock.js/ai agent to an external Model Context Protocol (MCP) server and adapt its tools as native agent tools (Direction A) with `ai.mcp(server)`. Triggers: `ai.mcp(`, `ai.mcp.serve` (the other direction), `mcp(`, `McpClient`, `McpClientOptions`, `McpTransport`, `client.tools()`, `client.close()`, `namePrefix`, `filter`, `McpTransportError`, `{ type: "stdio" }`, `{ type: "http" }`, `tools/list`, `tools/call`, `jsonSchemaToStandard`, `@modelcontextprotocol/sdk`, `ajv`; ''consume an MCP server'', ''use an external MCP server''s tools in my agent'', ''connect to the GitHub/Slack/Filesystem MCP server'', ''adapt MCP tools as agent tools'', ''bridge my agent to the MCP ecosystem'', ''run an MCP server over stdio/http''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: exposing YOUR agent/tools AS an MCP server (the reverse direction) — the `expose-as-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
3
+ description: 'Connect a @warlock.js/ai agent to an external Model Context Protocol (MCP) server and adapt its tools as native agent tools (Direction A) with `ai.mcp(server)`. Triggers: `signal`, `ai.mcp(`, `ai.mcp.serve` (the other direction), `mcp(`, `McpClient`, `McpClientOptions`, `McpTransport`, `client.tools()`, `client.close()`, `namePrefix`, `filter`, `McpTransportError`, `{ type: "stdio" }`, `{ type: "http" }`, `tools/list`, `tools/call`, `jsonSchemaToStandard`, `@modelcontextprotocol/sdk`, `ajv`; ''consume an MCP server'', ''use an external MCP server''s tools in my agent'', ''connect to the GitHub/Slack/Filesystem MCP server'', ''adapt MCP tools as agent tools'', ''bridge my agent to the MCP ecosystem'', ''run an MCP server over stdio/http''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: exposing YOUR agent/tools AS an MCP server (the reverse direction) — the `expose-as-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
4
4
  ---
5
5
 
6
6
  # Connect to an external MCP server (Direction A)
@@ -47,20 +47,42 @@ The `server` argument is discriminated by `type`:
47
47
 
48
48
  ```ts
49
49
  ai.mcp(server, {
50
- namePrefix: "github.", // prepended to every remote tool name
51
- filter: (name) => name.startsWith("issues_"),// only adapt tools whose name passes
52
- timeoutMs: 30_000, // per tools/call deadline (default 30s)
50
+ namePrefix: "github.", // prepended to every remote tool name
51
+ filter: (name) => name.startsWith("issues_"), // only adapt tools whose name passes
52
+ timeoutMs: 30_000, // per tools/call deadline (default 30s)
53
+ signal: controller.signal, // optional: close the client when this aborts
53
54
  });
54
55
  ```
55
56
 
57
+ - **`signal`** binds the client's lifetime to a run: on abort the client closes its transport (kills the stdio child / ends the HTTP session), in-flight requests reject, and later `tools()` / tool calls fail with `McpTransportError` type `"closed"` without sending anything. Each `tools/call` honors it together with the per-call `ctx.signal` — whichever aborts first. `close()` stays idempotent.
58
+
59
+ ```ts
60
+ import "@warlock.js/ai-tools";
61
+ import { ai, type ModelContract } from "@warlock.js/ai";
62
+
63
+ export async function triage(model: ModelContract): Promise<void> {
64
+ const controller = new AbortController();
65
+ const github = ai.mcp(
66
+ { type: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] },
67
+ { namePrefix: "github.", signal: controller.signal },
68
+ );
69
+
70
+ const agent = ai.agent({ model, tools: [...(await github.tools())] });
71
+ await agent.execute("Triage the open issues", { signal: controller.signal });
72
+
73
+ controller.abort(); // ends the run AND shuts the MCP server down
74
+ }
75
+ ```
76
+
56
77
  - **`namePrefix`** is the collision mitigation — there is **no runtime tool-name collision guard** (the agent resolves a tool by first name match), so the author owns uniqueness. Prefix remote tools to keep them distinct from local ones.
57
- - **`filter`** receives each tool's *unprefixed* name; return `false` to drop it.
78
+ - **`filter`** receives each tool's _unprefixed_ name; return `false` to drop it.
58
79
 
59
80
  ## What `tools()` does
60
81
 
61
82
  `client.tools()` is lazy and cached: the **first** call runs the `initialize` handshake, sends `notifications/initialized`, then `tools/list`, and maps each descriptor into a `ToolContract`. Repeat calls return the cached array without re-handshaking.
62
83
 
63
84
  For each remote tool:
85
+
64
86
  1. its JSON-Schema `inputSchema` is wrapped as a Standard Schema via `jsonSchemaToStandard` (Ajv-backed — `ajv` is an optional peer needed only for the client);
65
87
  2. `execute` issues `tools/call` honoring `ctx.signal` (cooperative abort) and the configured `timeoutMs`, then unwraps the result content (a single JSON text block is parsed back into an object);
66
88
  3. an MCP `isError` result is thrown so the surrounding `tool()` wraps it as a `ToolExecutionError` — the agent reads it as `{ error }` data and self-corrects.
@@ -89,7 +111,7 @@ try {
89
111
 
90
112
  ## Optional peers
91
113
 
92
- The MCP client lazily imports `@modelcontextprotocol/sdk` (the protocol SDK) and `ajv` (runtime JSON-Schema validation) — both optional peers. A missing one surfaces a curated `npm install` string at first use, never a raw module-resolution crash at import time. The stdio transport itself needs no dependency.
114
+ The MCP client does not use `@modelcontextprotocol/sdk`: it speaks MCP's JSON-RPC itself, over a spawned child process (`node:child_process`) for `{ type: "stdio" }` and the global `fetch` for `{ type: "http" }`, so neither transport needs a dependency. Its only optional peer is `ajv`, which validates tool-call input against each MCP tool's `inputSchema`. It is imported lazily the first time a schema is checked; when it is missing, the call fails validation with a curated `npm install ajv` message instead of crashing at import time. A tool with no `inputSchema` needs no validator.
93
115
 
94
116
  ## See also
95
117
 
@@ -1,82 +1,163 @@
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)`. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `server.start()`, `server.stop()`, `schemaTarget`, `draft-2020-12`, `extractJsonSchema`, `tools/list`, `tools/call`, `{ type: "stdio" }`, `{ type: "http"; port }`; ''expose my agent as an MCP server'', ''publish my tools to Cursor/Claude Desktop'', ''make my warlock workspace an MCP server'', ''serve my ToolContract[] over MCP'', ''let other MCP clients call my agent''; 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`, `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 over the wire.
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 server = ai.mcp.serve(
15
- [ai.tools.calculator(), ai.tools.dateTime()],
16
- { name: "warlock-utils", transport: { type: "stdio" } },
17
- );
17
+ const token = randomBytes(32).toString("hex");
18
18
 
19
- await server.start();
20
- // ...later, on teardown:
21
- await server.stop();
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
22
28
  ```
23
29
 
24
- ## The `source` — array or anything with `tools()`
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
25
33
 
26
34
  ```ts
27
- // A literal array of contracts:
28
- ai.mcp.serve([calc, dt], { name: "utils" });
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
+ });
29
42
 
30
- // Anything that can enumerate its ToolContracts — e.g. a workspace:
31
- ai.mcp.serve({ tools: () => ws.tools.all() }, { name: "acme-workspace" });
43
+ await server.start(); // reads JSON-RPC lines from stdin, answers on stdout
44
+ // on teardown:
45
+ await server.stop();
32
46
  ```
33
47
 
34
- The tools are snapshotted at construction (`source.tools()` is called once), so a stable surface is advertised for the life of the server.
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
+ ```
35
66
 
36
- ## Serve options
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`
37
74
 
38
75
  ```ts
39
- ai.mcp.serve(source, {
40
- name: "warlock-utils", // advertised in the initialize response (required)
41
- version: "1.0.0", // default: the package version
42
- transport: { type: "stdio" }, // default: stdio
43
- schemaTarget: "draft-2020-12", // JSON-Schema dialect for each tool's inputSchema
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
+ }),
44
85
  });
86
+
87
+ const { url, close } = await ai.mcp
88
+ .serve([whoami], { name: "tenant-tools", transport: { type: "http" } })
89
+ .start();
45
90
  ```
46
91
 
47
- - **`schemaTarget`** defaults to `"draft-2020-12"` — a neutral MCP draft, deliberately overriding `extractJsonSchema`'s own `"openai-strict"` default. Set it to match a consuming client's expectation.
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.
48
93
 
49
- ## How the protocol maps
94
+ ## Sources and tool-name collisions
50
95
 
51
- - **`initialize`** → advertises `{ name, version }` and `capabilities: { tools: {} }`.
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.
96
+ ```ts
97
+ ai.mcp.serve([calc, dt], { name: "utils" }); // literal array
98
+ ai.mcp.serve({ tools: () => ws.tools.all() }, { name: "workspace" }); // anything with tools()
99
+ ai.mcp.serve(
100
+ { groups: { prod: [search], staging: [search] } },
101
+ {
102
+ name: "search",
103
+ prefixToolNames: true, // → prod_search, staging_search; or (group, name) => `${group}.${name}`
104
+ },
105
+ );
106
+ ```
54
107
 
55
- Everything the server emits uses `type`, never `kind`.
108
+ Duplicate exposed names throw `McpServeError` (`type: "collision"`) **when `serve()` is called**. `prefixToolNames` on an ungrouped source throws `type: "config"`.
56
109
 
57
- ## Transport — stdio is auto-pumped; HTTP is host-owned
110
+ ## Resources and prompts (optional)
58
111
 
59
112
  ```ts
60
- { type: "stdio" } // default — reads JSON-RPC lines from process.stdin, writes to stdout
61
- { type: "http", port: 8080 } // accepted in options, but start() does NOT bind a socket
113
+ ai.mcp.serve(tools, {
114
+ name: "handbook",
115
+ resources: [
116
+ {
117
+ uri: "docs://handbook",
118
+ name: "handbook",
119
+ mimeType: "text/markdown",
120
+ read: () => "# Handbook",
121
+ },
122
+ ],
123
+ prompts: [
124
+ {
125
+ name: "review",
126
+ arguments: [{ name: "diff", required: true }],
127
+ get: (args) => `Review this diff:\n${args.diff ?? ""}`, // string → one user message
128
+ },
129
+ ],
130
+ });
62
131
  ```
63
132
 
64
- `start()` over `stdio` wires a `node:readline` line reader on `process.stdin` and writes one-line JSON-RPC responses to `process.stdout` (notifications without an `id` are ignored). **Serving over HTTP is left to a server you own** — `start()` rejects an `http` transport. For that path, drive the pure protocol core yourself with `createServeHandler`:
133
+ `read()` returns a string (text) or `Uint8Array` (base64 blob). `get()` returns a string or `McpPromptMessage[]`. Unknown resource → `-32002`; unknown prompt / missing required argument → `-32602`.
134
+
135
+ ## How the protocol maps
136
+
137
+ - **`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`).
138
+ - **`tools/list`** → `inputSchema` via `extractJsonSchema(contract.input, { target: schemaTarget })` (default `"draft-2020-12"`).
139
+ - **`tools/call`** → `invoke()`; `data` → text block (JSON-stringified unless a string), `error` → `isError: true`. Unknown tool → JSON-RPC error.
140
+ - **`ping`** → `{}`.
141
+
142
+ ## Drive the protocol yourself (tests, custom servers)
65
143
 
66
144
  ```ts
67
145
  import { createServeHandler } from "@warlock.js/ai-tools";
146
+ import { ai } from "@warlock.js/ai";
68
147
 
69
- const handler = createServeHandler([calc, dt], { name: "warlock-utils" });
148
+ const handler = createServeHandler([ai.tools.calculator()], { name: "utils" });
70
149
 
71
- // In your own HTTP handler:
72
- const response = await handler.handle(jsonRpcRequest); // request → response, no I/O
150
+ const response = await handler.handle(
151
+ { jsonrpc: "2.0", id: 1, method: "tools/list" },
152
+ { request: { method: "POST", path: "/mcp", headers: { "x-tenant-id": "acme" } } },
153
+ );
73
154
  ```
74
155
 
75
- `createServeHandler` is also the cleanest seam for unit tests: feed it a JSON-RPC request, assert the response — no real transport needed.
156
+ Mounting it in your own server means you own Origin validation, auth and body limits.
76
157
 
77
158
  ## Errors
78
159
 
79
- `McpTransportError` covers serve-side failures (`type: "connect"` when `start()` is asked to bind an unsupported transport; `type: "protocol"` for a malformed `tools/call` or an unknown tool name). Tool-level failures are *not* errors here — they come back as `isError: true` results, exactly as MCP expects.
160
+ `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
161
 
81
162
  ## See also
82
163
 
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 *within* those rails. Least-privilege is just array composition: hand an agent only the tools (and only the methods/hosts) it needs.
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 | 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 }` |
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({ extract: "text", allowHosts: ["docs.stripe.com"], maxBytes: 500_000, timeoutMs: 10_000 });
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"], // default ["GET"] — mutating methods are opt-in
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.dateTime().invoke({ op: "add", iso: "2026-06-22T00:00:00Z", amount: 3, unit: "days" });
101
- await ai.tools.dateTime().invoke({ op: "diff", iso: "2026-06-22T00:00:00Z", to: "2026-06-25T00:00:00Z", unit: "days" }); // "3"
102
- await ai.tools.dateTime({ defaultTimeZone: "Africa/Cairo" }).invoke({ op: "format", iso: "2026-06-22T12:00:00Z", format: "datetime" });
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.fetchUrl({ allowHosts: ["docs.stripe.com"] }).invoke({ url: "http://evil.test" });
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
  }