@warlock.js/ai-tools 4.5.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 (83) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +164 -0
  4. package/cjs/index.cjs +2519 -0
  5. package/cjs/index.cjs.map +1 -0
  6. package/esm/contracts/http.type.d.mts +96 -0
  7. package/esm/contracts/http.type.d.mts.map +1 -0
  8. package/esm/contracts/index.d.mts +4 -0
  9. package/esm/contracts/mcp.type.d.mts +216 -0
  10. package/esm/contracts/mcp.type.d.mts.map +1 -0
  11. package/esm/contracts/utility.type.d.mts +96 -0
  12. package/esm/contracts/utility.type.d.mts.map +1 -0
  13. package/esm/contracts/web.type.d.mts +136 -0
  14. package/esm/contracts/web.type.d.mts.map +1 -0
  15. package/esm/errors.d.mts +225 -0
  16. package/esm/errors.d.mts.map +1 -0
  17. package/esm/errors.mjs +136 -0
  18. package/esm/errors.mjs.map +1 -0
  19. package/esm/http/http-request.d.mts +57 -0
  20. package/esm/http/http-request.d.mts.map +1 -0
  21. package/esm/http/http-request.mjs +221 -0
  22. package/esm/http/http-request.mjs.map +1 -0
  23. package/esm/index.d.mts +19 -0
  24. package/esm/index.mjs +15 -0
  25. package/esm/mcp/client.mjs +199 -0
  26. package/esm/mcp/client.mjs.map +1 -0
  27. package/esm/mcp/index.d.mts +43 -0
  28. package/esm/mcp/index.d.mts.map +1 -0
  29. package/esm/mcp/index.mjs +19 -0
  30. package/esm/mcp/index.mjs.map +1 -0
  31. package/esm/mcp/json-schema-to-standard.d.mts +34 -0
  32. package/esm/mcp/json-schema-to-standard.d.mts.map +1 -0
  33. package/esm/mcp/json-schema-to-standard.mjs +147 -0
  34. package/esm/mcp/json-schema-to-standard.mjs.map +1 -0
  35. package/esm/mcp/serve.d.mts +46 -0
  36. package/esm/mcp/serve.d.mts.map +1 -0
  37. package/esm/mcp/serve.mjs +264 -0
  38. package/esm/mcp/serve.mjs.map +1 -0
  39. package/esm/mcp/transport.d.mts +48 -0
  40. package/esm/mcp/transport.d.mts.map +1 -0
  41. package/esm/mcp/transport.mjs +381 -0
  42. package/esm/mcp/transport.mjs.map +1 -0
  43. package/esm/mcp/transport.type.d.mts +51 -0
  44. package/esm/mcp/transport.type.d.mts.map +1 -0
  45. package/esm/node_modules/@standard-schema/spec/dist/index.d.mts +80 -0
  46. package/esm/node_modules/@standard-schema/spec/dist/index.d.mts.map +1 -0
  47. package/esm/register.d.mts +55 -0
  48. package/esm/register.d.mts.map +1 -0
  49. package/esm/register.mjs +21 -0
  50. package/esm/register.mjs.map +1 -0
  51. package/esm/schema.mjs +127 -0
  52. package/esm/schema.mjs.map +1 -0
  53. package/esm/utility/calculator.d.mts +35 -0
  54. package/esm/utility/calculator.d.mts.map +1 -0
  55. package/esm/utility/calculator.mjs +272 -0
  56. package/esm/utility/calculator.mjs.map +1 -0
  57. package/esm/utility/date-time.d.mts +57 -0
  58. package/esm/utility/date-time.d.mts.map +1 -0
  59. package/esm/utility/date-time.mjs +193 -0
  60. package/esm/utility/date-time.mjs.map +1 -0
  61. package/esm/utility/index.d.mts +2 -0
  62. package/esm/utility/index.mjs +4 -0
  63. package/esm/utility/schema.mjs +114 -0
  64. package/esm/utility/schema.mjs.map +1 -0
  65. package/esm/web/fetch-url.d.mts +39 -0
  66. package/esm/web/fetch-url.d.mts.map +1 -0
  67. package/esm/web/fetch-url.mjs +228 -0
  68. package/esm/web/fetch-url.mjs.map +1 -0
  69. package/esm/web/index.d.mts +2 -0
  70. package/esm/web/index.mjs +4 -0
  71. package/esm/web/schema.mjs +86 -0
  72. package/esm/web/schema.mjs.map +1 -0
  73. package/esm/web/web-search.d.mts +38 -0
  74. package/esm/web/web-search.d.mts.map +1 -0
  75. package/esm/web/web-search.mjs +167 -0
  76. package/esm/web/web-search.mjs.map +1 -0
  77. package/llms-full.txt +326 -0
  78. package/llms.txt +11 -0
  79. package/package.json +45 -0
  80. package/skills/README.md +17 -0
  81. package/skills/connect-mcp-server/SKILL.md +98 -0
  82. package/skills/expose-as-mcp-server/SKILL.md +85 -0
  83. package/skills/use-web-and-http-tools/SKILL.md +125 -0
package/llms-full.txt ADDED
@@ -0,0 +1,326 @@
1
+ # Warlock AI Tools — full skills
2
+
3
+ > Package: `@warlock.js/ai-tools`
4
+
5
+ > Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/ai-tools/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
6
+
7
+ ## connect-mcp-server `@warlock.js/ai-tools/connect-mcp-server/SKILL.md`
8
+
9
+ ---
10
+ name: connect-mcp-server
11
+ 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) — `@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`; the built-in web/HTTP/calc/date tools — `@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md`.'
12
+ ---
13
+
14
+ # Connect to an external MCP server (Direction A)
15
+
16
+ `ai.mcp(server, options?)` opens a connection to any Model Context Protocol server, lists its tools, and adapts each one into a native `ToolContract` — so a remote MCP tool is indistinguishable from a local one and drops straight into `ai.agent({ tools: [...] })`.
17
+
18
+ ```ts
19
+ import "@warlock.js/ai-tools";
20
+ import { ai } from "@warlock.js/ai";
21
+
22
+ const github = ai.mcp(
23
+ { type: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] },
24
+ { namePrefix: "github." },
25
+ );
26
+
27
+ const dev = ai.agent({
28
+ model,
29
+ systemPrompt: "Use the GitHub tools to triage issues.",
30
+ tools: [...(await github.tools())], // handshake + tools/list, run once and cached
31
+ maxTrips: 20,
32
+ });
33
+
34
+ await dev.execute("Find the oldest open 'bug' issue and propose a fix.");
35
+
36
+ await github.close(); // on teardown — kills the child / ends the HTTP session
37
+ ```
38
+
39
+ ## The transport — `stdio` or `http`
40
+
41
+ The `server` argument is discriminated by `type`:
42
+
43
+ ```ts
44
+ // Spawn a child process and speak JSON-RPC over its stdin/stdout.
45
+ // Uses node:child_process + node:readline — NO dependency.
46
+ { type: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"], env: { GITHUB_TOKEN: "…" } }
47
+
48
+ // Streamable HTTP against an endpoint.
49
+ { type: "http", url: "https://mcp.example.com/rpc", headers: { authorization: "Bearer …" } }
50
+ ```
51
+
52
+ > **stdio env is not inherited.** When you pass `env`, only those keys reach the child — pass what the server needs (e.g. its API token) explicitly.
53
+
54
+ ## Client options
55
+
56
+ ```ts
57
+ ai.mcp(server, {
58
+ namePrefix: "github.", // prepended to every remote tool name
59
+ filter: (name) => name.startsWith("issues_"),// only adapt tools whose name passes
60
+ timeoutMs: 30_000, // per tools/call deadline (default 30s)
61
+ });
62
+ ```
63
+
64
+ - **`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.
65
+ - **`filter`** receives each tool's *unprefixed* name; return `false` to drop it.
66
+
67
+ ## What `tools()` does
68
+
69
+ `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.
70
+
71
+ For each remote tool:
72
+ 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);
73
+ 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);
74
+ 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.
75
+
76
+ ## `type`, never `kind`
77
+
78
+ MCP's wire vocabulary uses `kind` in some content/capability descriptors. The client translates any inbound `kind` to `type` when normalizing content blocks, so a value a tool returns never carries MCP's `kind` — every discriminator the agent sees is `type`.
79
+
80
+ ## Errors
81
+
82
+ Connection / handshake failures surface **at agent-construction time** because the example `await`s `github.tools()` — you see them then, not mid-run. A `tools/call` failure raised mid-run is wrapped by `tool()` and reaches the model as data. Branch on `McpTransportError.type`:
83
+
84
+ ```ts
85
+ import { McpTransportError } from "@warlock.js/ai-tools";
86
+
87
+ try {
88
+ await github.tools();
89
+ } catch (error) {
90
+ if (error instanceof McpTransportError && error.type === "connect") {
91
+ // the child failed to spawn, the endpoint was unreachable, or the handshake failed
92
+ }
93
+ }
94
+ ```
95
+
96
+ `McpTransportError.type`: `"connect"` | `"protocol"` | `"timeout"` | `"closed"` (plus an optional `.method` naming the in-flight JSON-RPC method).
97
+
98
+ ## Optional peers
99
+
100
+ 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.
101
+
102
+ ## See also
103
+
104
+ - [`@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`](@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md) — the reverse direction: publish a local primitive AS an MCP server.
105
+ - [`@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md`](@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md) — the built-in web / HTTP / calculator / date-time belt.
106
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — running the agent that consumes the adapted tools.
107
+
108
+
109
+ ## expose-as-mcp-server `@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`
110
+
111
+ ---
112
+ name: expose-as-mcp-server
113
+ 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) — `@warlock.js/ai-tools/connect-mcp-server/SKILL.md`; the built-in web/HTTP/calc/date tools — `@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md`.'
114
+ ---
115
+
116
+ # Expose tools AS an MCP server (Direction B)
117
+
118
+ `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.
119
+
120
+ ```ts
121
+ import "@warlock.js/ai-tools";
122
+ import { ai } from "@warlock.js/ai";
123
+
124
+ const server = ai.mcp.serve(
125
+ [ai.tools.calculator(), ai.tools.dateTime()],
126
+ { name: "warlock-utils", transport: { type: "stdio" } },
127
+ );
128
+
129
+ await server.start();
130
+ // ...later, on teardown:
131
+ await server.stop();
132
+ ```
133
+
134
+ ## The `source` — array or anything with `tools()`
135
+
136
+ ```ts
137
+ // A literal array of contracts:
138
+ ai.mcp.serve([calc, dt], { name: "utils" });
139
+
140
+ // Anything that can enumerate its ToolContracts — e.g. a workspace:
141
+ ai.mcp.serve({ tools: () => ws.tools.all() }, { name: "acme-workspace" });
142
+ ```
143
+
144
+ The tools are snapshotted at construction (`source.tools()` is called once), so a stable surface is advertised for the life of the server.
145
+
146
+ ## Serve options
147
+
148
+ ```ts
149
+ ai.mcp.serve(source, {
150
+ name: "warlock-utils", // advertised in the initialize response (required)
151
+ version: "1.0.0", // default: the package version
152
+ transport: { type: "stdio" }, // default: stdio
153
+ schemaTarget: "draft-2020-12", // JSON-Schema dialect for each tool's inputSchema
154
+ });
155
+ ```
156
+
157
+ - **`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.
158
+
159
+ ## How the protocol maps
160
+
161
+ - **`initialize`** → advertises `{ name, version }` and `capabilities: { tools: {} }`.
162
+ - **`tools/list`** → one descriptor per contract; each tool's `inputSchema` is produced by `extractJsonSchema(contract.input, { target: schemaTarget })`.
163
+ - **`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.
164
+
165
+ Everything the server emits uses `type`, never `kind`.
166
+
167
+ ## Transport — stdio is auto-pumped; HTTP is host-owned
168
+
169
+ ```ts
170
+ { type: "stdio" } // default — reads JSON-RPC lines from process.stdin, writes to stdout
171
+ { type: "http", port: 8080 } // accepted in options, but start() does NOT bind a socket
172
+ ```
173
+
174
+ `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`:
175
+
176
+ ```ts
177
+ import { createServeHandler } from "@warlock.js/ai-tools";
178
+
179
+ const handler = createServeHandler([calc, dt], { name: "warlock-utils" });
180
+
181
+ // In your own HTTP handler:
182
+ const response = await handler.handle(jsonRpcRequest); // request → response, no I/O
183
+ ```
184
+
185
+ `createServeHandler` is also the cleanest seam for unit tests: feed it a JSON-RPC request, assert the response — no real transport needed.
186
+
187
+ ## Errors
188
+
189
+ `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.
190
+
191
+ ## See also
192
+
193
+ - [`@warlock.js/ai-tools/connect-mcp-server/SKILL.md`](@warlock.js/ai-tools/connect-mcp-server/SKILL.md) — the reverse direction: consume an external MCP server's tools.
194
+ - [`@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md`](@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md) — the built-in tools you might expose.
195
+ - [`@warlock.js/ai-workspace/use-a-workspace/SKILL.md`](@warlock.js/ai-workspace/use-a-workspace/SKILL.md) — a workspace whose `tools.all()` is a natural `serve` source.
196
+
197
+
198
+ ## use-web-and-http-tools `@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md`
199
+
200
+ ---
201
+ name: use-web-and-http-tools
202
+ 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`, `baseUrl`, `maxBytes`, `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''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: connecting to an external MCP server — `@warlock.js/ai-tools/connect-mcp-server/SKILL.md`; exposing tools AS an MCP server — `@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`; filesystem/shell tools — `@warlock.js/ai-workspace/use-a-workspace/SKILL.md`; defining your own custom tool — `@warlock.js/ai/define-ai-tool/SKILL.md`.'
203
+ ---
204
+
205
+ # Use the web + HTTP tool belt
206
+
207
+ `@warlock.js/ai-tools` ships five ready-made tools. Import the package once for its side effect and they register on the shared `ai` object under `ai.tools.*` — each is a factory returning a `ToolContract` you spread into `ai.agent({ tools: [...] })`.
208
+
209
+ ```ts
210
+ import "@warlock.js/ai-tools";
211
+ import { ai } from "@warlock.js/ai";
212
+ import { OpenAISDK } from "@warlock.js/ai-openai";
213
+
214
+ const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
215
+
216
+ const agent = ai.agent({
217
+ model: openai.model({ name: "gpt-4o" }),
218
+ systemPrompt: "Research, then call the API. Read tool errors and adjust.",
219
+ tools: [
220
+ ai.tools.webSearch({ provider: "tavily" }),
221
+ ai.tools.fetchUrl({ extract: "text", allowHosts: ["docs.stripe.com"] }),
222
+ ai.tools.http({ baseUrl: "https://api.stripe.com", allowHosts: ["api.stripe.com"] }),
223
+ ai.tools.calculator(),
224
+ ai.tools.dateTime({ defaultTimeZone: "UTC" }),
225
+ ],
226
+ maxTrips: 15,
227
+ });
228
+ ```
229
+
230
+ 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.
231
+
232
+ ## The five tools
233
+
234
+ | Factory | Name | Model input | Returns |
235
+ |---|---|---|---|
236
+ | `ai.tools.webSearch(options)` | `web_search` | `{ query, maxResults? }` | `{ results: { title, url, snippet, score? }[] }` |
237
+ | `ai.tools.fetchUrl(options?)` | `fetch_url` | `{ url }` | `{ url, status, content, truncated }` |
238
+ | `ai.tools.http(options?)` | `http_request` | `{ method?, url, headers?, body? }` | `{ status, headers, body, truncated }` |
239
+ | `ai.tools.calculator(options?)` | `calculator` | `{ expression }` | `{ result }` |
240
+ | `ai.tools.dateTime(options?)` | `date_time` | `{ op, iso?, from?, to?, amount?, unit?, timeZone?, format? }` | `{ value }` |
241
+
242
+ ## `web_search` — pick a provider, supply a key
243
+
244
+ ```ts
245
+ ai.tools.webSearch({ provider: "tavily", apiKey: process.env.TAVILY_API_KEY, maxResults: 8 });
246
+ ```
247
+
248
+ - `provider`: `"tavily"` (LLM-ready snippets + a relevance `score`), `"brave"`, or `"serpapi"`. Each is called over the global `fetch` — **no provider SDK is required**.
249
+ - `apiKey` falls back to a per-provider env var: `TAVILY_API_KEY` / `BRAVE_API_KEY` / `SERPAPI_API_KEY`. A missing key is a `WebToolError` of type `"missing-key"`.
250
+ - The model's `maxResults` is clamped into `[1, options.maxResults]` (default `5`).
251
+
252
+ ## `fetch_url` — read a page you have the URL for
253
+
254
+ ```ts
255
+ ai.tools.fetchUrl({ extract: "text", allowHosts: ["docs.stripe.com"], maxBytes: 500_000, timeoutMs: 10_000 });
256
+ ```
257
+
258
+ - `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.
259
+ - `allowHosts` (when set) rejects any other host **before the fetch** (an SSRF guardrail) → `"denied-host"`.
260
+ - `maxBytes` caps the body and flags `truncated`; `timeoutMs` aborts via `AbortSignal.timeout`.
261
+
262
+ ## `http_request` — a guarded REST client
263
+
264
+ ```ts
265
+ ai.tools.http({
266
+ baseUrl: "https://api.stripe.com",
267
+ allowHosts: ["api.stripe.com"],
268
+ allowMethods: ["GET", "POST"], // default ["GET"] — mutating methods are opt-in
269
+ headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },
270
+ timeoutMs: 15_000,
271
+ maxBytes: 1_000_000,
272
+ });
273
+ ```
274
+
275
+ All guardrails are enforced **before the network call** and surface as a typed `HttpPolicyError`:
276
+ - a method outside `allowMethods` → `"method-not-allowed"`;
277
+ - a host outside `allowHosts` → `"host-not-allowed"` (SSRF guardrail);
278
+ - an unresolvable URL → `"invalid-url"`.
279
+
280
+ When `baseUrl` is set the model passes a **path** joined against it; otherwise it must pass an absolute `http(s)` URL. Static `headers` merge **under** the per-call headers (per-call wins). An object `body` is JSON-serialized (with a default `content-type: application/json`); a string `body` is sent verbatim; `body` is dropped for `GET`. The response body is JSON-parsed when the `content-type` is JSON, else returned as text.
281
+
282
+ ## `calculator` — SAFE arithmetic (never `eval`)
283
+
284
+ ```ts
285
+ const { data } = await ai.tools.calculator().invoke({ expression: "(3 + 4) * 2 ^ 3" });
286
+ data?.result; // 56
287
+ ```
288
+
289
+ Supports `+ - * / % ^`, unary signs, parentheses, and decimal/scientific literals, with the usual precedence (`^` highest and right-associative). The expression is tokenized and evaluated with a shunting-yard pass — there is no path to an identifier, function call, or property access, so nothing code-like can be smuggled in. Failures are a `CalculatorError`: `"syntax"`, `"divide-by-zero"`, or `"overflow"`.
290
+
291
+ ## `date_time` — clock + calendar
292
+
293
+ ```ts
294
+ await ai.tools.dateTime().invoke({ op: "now", format: "iso" });
295
+ await ai.tools.dateTime().invoke({ op: "add", iso: "2026-06-22T00:00:00Z", amount: 3, unit: "days" });
296
+ await ai.tools.dateTime().invoke({ op: "diff", iso: "2026-06-22T00:00:00Z", to: "2026-06-25T00:00:00Z", unit: "days" }); // "3"
297
+ await ai.tools.dateTime({ defaultTimeZone: "Africa/Cairo" }).invoke({ op: "format", iso: "2026-06-22T12:00:00Z", format: "datetime" });
298
+ ```
299
+
300
+ - `op`: `"now"` / `"add"` / `"diff"` / `"format"`. `format`: `"iso"` (default), `"date"`, `"time"`, `"datetime"`.
301
+ - For `diff`, pass the start instant as `iso` **or** `from` (an alias models reach for naturally with `from`/`to`); `iso` wins when both are set. The end instant is always `to`.
302
+ - 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"`.
303
+ - 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") })`.
304
+
305
+ ## Errors flow as data, not throws
306
+
307
+ 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`:
308
+
309
+ ```ts
310
+ import { WebToolError } from "@warlock.js/ai-tools";
311
+
312
+ const { error } = await ai.tools.fetchUrl({ allowHosts: ["docs.stripe.com"] }).invoke({ url: "http://evil.test" });
313
+ if (error instanceof WebToolError && error.type === "denied-host") {
314
+ // rejected before any network call
315
+ }
316
+ ```
317
+
318
+ ## See also
319
+
320
+ - [`@warlock.js/ai-tools/connect-mcp-server/SKILL.md`](@warlock.js/ai-tools/connect-mcp-server/SKILL.md) — adapt an external MCP server's tools as agent tools.
321
+ - [`@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`](@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md) — publish these tools AS an MCP server.
322
+ - [`@warlock.js/ai-workspace/use-a-workspace/SKILL.md`](@warlock.js/ai-workspace/use-a-workspace/SKILL.md) — the filesystem + shell tool belt (jailed `read_file` / `edit_file` / `run_shell` / …).
323
+ - [`@warlock.js/ai/define-ai-tool/SKILL.md`](@warlock.js/ai/define-ai-tool/SKILL.md) — the `tool()` / `ToolContract` seam every tool here is built on.
324
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — running the agent that consumes these tools.
325
+
326
+
package/llms.txt ADDED
@@ -0,0 +1,11 @@
1
+ # Warlock AI Tools
2
+
3
+ > Package: `@warlock.js/ai-tools`
4
+
5
+ > Ready-made agent tools (web search, fetch, HTTP, calculator, date-time) + an MCP client/server for @warlock.js/ai
6
+
7
+ ## Skills
8
+
9
+ - [connect-mcp-server](@warlock.js/ai-tools/connect-mcp-server/SKILL.md): 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) — `@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`; the built-in web/HTTP/calc/date tools — `@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md`.
10
+ - [expose-as-mcp-server](@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md): 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) — `@warlock.js/ai-tools/connect-mcp-server/SKILL.md`; the built-in web/HTTP/calc/date tools — `@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md`.
11
+ - [use-web-and-http-tools](@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md): 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`, `baseUrl`, `maxBytes`, `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'; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: connecting to an external MCP server — `@warlock.js/ai-tools/connect-mcp-server/SKILL.md`; exposing tools AS an MCP server — `@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`; filesystem/shell tools — `@warlock.js/ai-workspace/use-a-workspace/SKILL.md`; defining your own custom tool — `@warlock.js/ai/define-ai-tool/SKILL.md`.
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@warlock.js/ai-tools",
3
+ "description": "Ready-made agent tools (web search, fetch, HTTP, calculator, date-time) + an MCP client/server for @warlock.js/ai",
4
+ "keywords": [
5
+ "warlock",
6
+ "ai",
7
+ "agent",
8
+ "tools",
9
+ "web-search",
10
+ "fetch",
11
+ "http",
12
+ "mcp",
13
+ "model-context-protocol"
14
+ ],
15
+ "author": "Hasan Zohdy",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/warlockjs/ai-tools"
20
+ },
21
+ "peerDependencies": {
22
+ "@modelcontextprotocol/sdk": "*",
23
+ "@mozilla/readability": "*",
24
+ "@tavily/core": "*",
25
+ "@warlock.js/ai": "4.5.0",
26
+ "ajv": "*",
27
+ "jsdom": "*"
28
+ },
29
+ "version": "4.5.0",
30
+ "main": "./cjs/index.cjs",
31
+ "module": "./esm/index.mjs",
32
+ "types": "./esm/index.d.mts",
33
+ "exports": {
34
+ ".": {
35
+ "import": {
36
+ "types": "./esm/index.d.mts",
37
+ "default": "./esm/index.mjs"
38
+ },
39
+ "require": {
40
+ "types": "./esm/index.d.mts",
41
+ "default": "./cjs/index.cjs"
42
+ }
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,17 @@
1
+ # `@warlock.js/ai-tools` — skills index
2
+
3
+ Per-task skills. All cross-references use the form `@warlock.js/<pkg>/<skill>/SKILL.md`.
4
+
5
+ ## Skills
6
+
7
+ ### [`use-web-and-http-tools/`](./use-web-and-http-tools/SKILL.md)
8
+
9
+ Wire the five ready-made tools — `ai.tools.webSearch` (`web_search`), `ai.tools.fetchUrl` (`fetch_url`), `ai.tools.http` (`http_request`), `ai.tools.calculator` (`calculator`), and `ai.tools.dateTime` (`date_time`) — into an agent under their construction-time rails: a search provider + API-key fallback, readability extract modes, the host/method allowlists and byte/timeout caps (the SSRF guardrails), the safe shunting-yard calculator, and the millisecond-unit date-time ops. Covers the option vs per-call-input split, the typed `WebToolError` / `HttpPolicyError` / `CalculatorError` / `DateTimeError`, and the errors-as-data contract. Load when giving an agent web search, fetch/scrape, REST, calculator, or date tools.
10
+
11
+ ### [`connect-mcp-server/`](./connect-mcp-server/SKILL.md)
12
+
13
+ Connect an agent to an external Model Context Protocol server with `ai.mcp(server, options?)` and adapt each of its tools into a native `ToolContract` (Direction A). Covers the stdio (`node:child_process`, no dep) vs Streamable HTTP transport, `namePrefix` / `filter` / `timeoutMs`, the lazy-and-cached `client.tools()` handshake + `tools/list`, the JSON-Schema→Standard-Schema adaptation (Ajv optional peer), the `kind`→`type` translation, and the `McpTransportError` surface. Load when consuming an MCP server's tools inside a warlock agent.
14
+
15
+ ### [`expose-as-mcp-server/`](./expose-as-mcp-server/SKILL.md)
16
+
17
+ Expose a warlock agent / supervisor / orchestrator (or a raw `ToolContract[]`) AS an MCP server with `ai.mcp.serve(source, options)` (Direction B). Covers the `source` shapes (array or `tools()`), the `name` / `version` / `transport` / `schemaTarget` options, how `initialize` / `tools/list` / `tools/call` map (via `extractJsonSchema` and the never-throwing `invoke()`), the auto-pumped stdio transport vs host-owned HTTP, and the `createServeHandler` pure-protocol seam for HTTP wiring and tests. Load when publishing local tools to Cursor / Claude Desktop / any MCP client.
@@ -0,0 +1,98 @@
1
+ ---
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) — `@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`; the built-in web/HTTP/calc/date tools — `@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md`.'
4
+ ---
5
+
6
+ # Connect to an external MCP server (Direction A)
7
+
8
+ `ai.mcp(server, options?)` opens a connection to any Model Context Protocol server, lists its tools, and adapts each one into a native `ToolContract` — so a remote MCP tool is indistinguishable from a local one and drops straight into `ai.agent({ tools: [...] })`.
9
+
10
+ ```ts
11
+ import "@warlock.js/ai-tools";
12
+ import { ai } from "@warlock.js/ai";
13
+
14
+ const github = ai.mcp(
15
+ { type: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] },
16
+ { namePrefix: "github." },
17
+ );
18
+
19
+ const dev = ai.agent({
20
+ model,
21
+ systemPrompt: "Use the GitHub tools to triage issues.",
22
+ tools: [...(await github.tools())], // handshake + tools/list, run once and cached
23
+ maxTrips: 20,
24
+ });
25
+
26
+ await dev.execute("Find the oldest open 'bug' issue and propose a fix.");
27
+
28
+ await github.close(); // on teardown — kills the child / ends the HTTP session
29
+ ```
30
+
31
+ ## The transport — `stdio` or `http`
32
+
33
+ The `server` argument is discriminated by `type`:
34
+
35
+ ```ts
36
+ // Spawn a child process and speak JSON-RPC over its stdin/stdout.
37
+ // Uses node:child_process + node:readline — NO dependency.
38
+ { type: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"], env: { GITHUB_TOKEN: "…" } }
39
+
40
+ // Streamable HTTP against an endpoint.
41
+ { type: "http", url: "https://mcp.example.com/rpc", headers: { authorization: "Bearer …" } }
42
+ ```
43
+
44
+ > **stdio env is not inherited.** When you pass `env`, only those keys reach the child — pass what the server needs (e.g. its API token) explicitly.
45
+
46
+ ## Client options
47
+
48
+ ```ts
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)
53
+ });
54
+ ```
55
+
56
+ - **`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.
58
+
59
+ ## What `tools()` does
60
+
61
+ `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
+
63
+ For each remote tool:
64
+ 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
+ 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
+ 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.
67
+
68
+ ## `type`, never `kind`
69
+
70
+ MCP's wire vocabulary uses `kind` in some content/capability descriptors. The client translates any inbound `kind` to `type` when normalizing content blocks, so a value a tool returns never carries MCP's `kind` — every discriminator the agent sees is `type`.
71
+
72
+ ## Errors
73
+
74
+ Connection / handshake failures surface **at agent-construction time** because the example `await`s `github.tools()` — you see them then, not mid-run. A `tools/call` failure raised mid-run is wrapped by `tool()` and reaches the model as data. Branch on `McpTransportError.type`:
75
+
76
+ ```ts
77
+ import { McpTransportError } from "@warlock.js/ai-tools";
78
+
79
+ try {
80
+ await github.tools();
81
+ } catch (error) {
82
+ if (error instanceof McpTransportError && error.type === "connect") {
83
+ // the child failed to spawn, the endpoint was unreachable, or the handshake failed
84
+ }
85
+ }
86
+ ```
87
+
88
+ `McpTransportError.type`: `"connect"` | `"protocol"` | `"timeout"` | `"closed"` (plus an optional `.method` naming the in-flight JSON-RPC method).
89
+
90
+ ## Optional peers
91
+
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.
93
+
94
+ ## See also
95
+
96
+ - [`@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`](@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md) — the reverse direction: publish a local primitive AS an MCP server.
97
+ - [`@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md`](@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md) — the built-in web / HTTP / calculator / date-time belt.
98
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — running the agent that consumes the adapted tools.
@@ -0,0 +1,85 @@
1
+ ---
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) — `@warlock.js/ai-tools/connect-mcp-server/SKILL.md`; the built-in web/HTTP/calc/date tools — `@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md`.'
4
+ ---
5
+
6
+ # Expose tools AS an MCP server (Direction B)
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.
9
+
10
+ ```ts
11
+ import "@warlock.js/ai-tools";
12
+ import { ai } from "@warlock.js/ai";
13
+
14
+ const server = ai.mcp.serve(
15
+ [ai.tools.calculator(), ai.tools.dateTime()],
16
+ { name: "warlock-utils", transport: { type: "stdio" } },
17
+ );
18
+
19
+ await server.start();
20
+ // ...later, on teardown:
21
+ await server.stop();
22
+ ```
23
+
24
+ ## The `source` — array or anything with `tools()`
25
+
26
+ ```ts
27
+ // A literal array of contracts:
28
+ ai.mcp.serve([calc, dt], { name: "utils" });
29
+
30
+ // Anything that can enumerate its ToolContracts — e.g. a workspace:
31
+ ai.mcp.serve({ tools: () => ws.tools.all() }, { name: "acme-workspace" });
32
+ ```
33
+
34
+ The tools are snapshotted at construction (`source.tools()` is called once), so a stable surface is advertised for the life of the server.
35
+
36
+ ## Serve options
37
+
38
+ ```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
44
+ });
45
+ ```
46
+
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.
48
+
49
+ ## How the protocol maps
50
+
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.
54
+
55
+ Everything the server emits uses `type`, never `kind`.
56
+
57
+ ## Transport — stdio is auto-pumped; HTTP is host-owned
58
+
59
+ ```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
62
+ ```
63
+
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`:
65
+
66
+ ```ts
67
+ import { createServeHandler } from "@warlock.js/ai-tools";
68
+
69
+ const handler = createServeHandler([calc, dt], { name: "warlock-utils" });
70
+
71
+ // In your own HTTP handler:
72
+ const response = await handler.handle(jsonRpcRequest); // request → response, no I/O
73
+ ```
74
+
75
+ `createServeHandler` is also the cleanest seam for unit tests: feed it a JSON-RPC request, assert the response — no real transport needed.
76
+
77
+ ## Errors
78
+
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.
80
+
81
+ ## See also
82
+
83
+ - [`@warlock.js/ai-tools/connect-mcp-server/SKILL.md`](@warlock.js/ai-tools/connect-mcp-server/SKILL.md) — the reverse direction: consume an external MCP server's tools.
84
+ - [`@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md`](@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md) — the built-in tools you might expose.
85
+ - [`@warlock.js/ai-workspace/use-a-workspace/SKILL.md`](@warlock.js/ai-workspace/use-a-workspace/SKILL.md) — a workspace whose `tools.all()` is a natural `serve` source.