@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
package/llms-full.txt
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
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) — the `expose-as-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
|
|
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: `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.'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Connect to an external MCP server (Direction A)
|
|
@@ -55,20 +55,42 @@ The `server` argument is discriminated by `type`:
|
|
|
55
55
|
|
|
56
56
|
```ts
|
|
57
57
|
ai.mcp(server, {
|
|
58
|
-
namePrefix: "github.",
|
|
59
|
-
filter: (name) => name.startsWith("issues_")
|
|
60
|
-
timeoutMs: 30_000,
|
|
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
|
+
signal: controller.signal, // optional: close the client when this aborts
|
|
61
62
|
});
|
|
62
63
|
```
|
|
63
64
|
|
|
65
|
+
- **`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.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import "@warlock.js/ai-tools";
|
|
69
|
+
import { ai, type ModelContract } from "@warlock.js/ai";
|
|
70
|
+
|
|
71
|
+
export async function triage(model: ModelContract): Promise<void> {
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const github = ai.mcp(
|
|
74
|
+
{ type: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] },
|
|
75
|
+
{ namePrefix: "github.", signal: controller.signal },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const agent = ai.agent({ model, tools: [...(await github.tools())] });
|
|
79
|
+
await agent.execute("Triage the open issues", { signal: controller.signal });
|
|
80
|
+
|
|
81
|
+
controller.abort(); // ends the run AND shuts the MCP server down
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
64
85
|
- **`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
|
|
86
|
+
- **`filter`** receives each tool's _unprefixed_ name; return `false` to drop it.
|
|
66
87
|
|
|
67
88
|
## What `tools()` does
|
|
68
89
|
|
|
69
90
|
`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
91
|
|
|
71
92
|
For each remote tool:
|
|
93
|
+
|
|
72
94
|
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
95
|
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
96
|
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.
|
|
@@ -97,7 +119,7 @@ try {
|
|
|
97
119
|
|
|
98
120
|
## Optional peers
|
|
99
121
|
|
|
100
|
-
The MCP client
|
|
122
|
+
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.
|
|
101
123
|
|
|
102
124
|
## See also
|
|
103
125
|
|
|
@@ -110,83 +132,216 @@ The MCP client lazily imports `@modelcontextprotocol/sdk` (the protocol SDK) and
|
|
|
110
132
|
|
|
111
133
|
---
|
|
112
134
|
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)
|
|
135
|
+
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.'
|
|
114
136
|
---
|
|
115
137
|
|
|
116
138
|
# Expose tools AS an MCP server (Direction B)
|
|
117
139
|
|
|
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
|
|
140
|
+
`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**.
|
|
141
|
+
|
|
142
|
+
## Serve over HTTP with a per-session token
|
|
119
143
|
|
|
120
144
|
```ts
|
|
145
|
+
import { randomBytes } from "node:crypto";
|
|
121
146
|
import "@warlock.js/ai-tools";
|
|
122
147
|
import { ai } from "@warlock.js/ai";
|
|
123
148
|
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
149
|
+
const token = randomBytes(32).toString("hex");
|
|
150
|
+
|
|
151
|
+
const server = ai.mcp.serve([ai.tools.calculator(), ai.tools.dateTime()], {
|
|
152
|
+
name: "session-tools",
|
|
153
|
+
transport: { type: "http", auth: { bearer: token } },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const { url, close } = await server.start();
|
|
157
|
+
// url → "http://127.0.0.1:<free port>/mcp" — hand url + token to the MCP client
|
|
158
|
+
|
|
159
|
+
await close(); // drains in-flight calls, then frees the port
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Defaults: `host: "127.0.0.1"`, `port: 0` (free ephemeral port), `path: "/mcp"`. `start()` resolves `{ url, close }` once bound.
|
|
128
163
|
|
|
129
|
-
|
|
130
|
-
|
|
164
|
+
## Serve over stdio
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import "@warlock.js/ai-tools";
|
|
168
|
+
import { ai } from "@warlock.js/ai";
|
|
169
|
+
|
|
170
|
+
const server = ai.mcp.serve([ai.tools.calculator()], {
|
|
171
|
+
name: "warlock-utils",
|
|
172
|
+
transport: { type: "stdio" },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await server.start(); // reads JSON-RPC lines from stdin, answers on stdout
|
|
176
|
+
// on teardown:
|
|
131
177
|
await server.stop();
|
|
132
178
|
```
|
|
133
179
|
|
|
134
|
-
##
|
|
180
|
+
## HTTP options and security rules
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
transport: {
|
|
184
|
+
type: "http",
|
|
185
|
+
host: "127.0.0.1", // non-loopback (e.g. "0.0.0.0") REQUIRES auth — start() throws otherwise
|
|
186
|
+
port: 0,
|
|
187
|
+
path: "/mcp",
|
|
188
|
+
auth: {
|
|
189
|
+
bearer: ["current-token", "previous-token"], // constant-time compare; several = rotation
|
|
190
|
+
authorize: async (request) => sessions.has(request.headers["x-session-id"] ?? ""), // optional hook
|
|
191
|
+
},
|
|
192
|
+
allowedOrigins: ["https://app.example.com"], // besides loopback origins
|
|
193
|
+
allowedHosts: ["mcp.internal"], // extra Host names while on loopback
|
|
194
|
+
maxBodyBytes: 1_048_576, // larger → 413
|
|
195
|
+
drainTimeoutMs: 5_000, // close() then aborts ctx.signal
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- With `bearer` and `authorize` both set, a request must pass both. A throwing hook denies. Failures → `401`.
|
|
200
|
+
- A foreign or `null` `Origin` → `403` (DNS-rebinding guard from the MCP spec). Requests without `Origin` pass.
|
|
201
|
+
- On loopback, a `Host` other than `localhost` / `127.0.0.1` / `[::1]` (or `allowedHosts`) → `403`.
|
|
202
|
+
- Stateless: no session id; `GET` / `DELETE` → `405`; batches → `400`; unsupported `MCP-Protocol-Version` → `400`.
|
|
203
|
+
- Plain HTTP: put TLS in front before exposing beyond the machine.
|
|
204
|
+
|
|
205
|
+
## Tools read request headers from `ctx.request`
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import "@warlock.js/ai-tools";
|
|
209
|
+
import { ai, type ToolContract } from "@warlock.js/ai";
|
|
210
|
+
|
|
211
|
+
const whoami: ToolContract = ai.tool({
|
|
212
|
+
name: "whoami",
|
|
213
|
+
description: "Report the calling tenant",
|
|
214
|
+
execute: async (_input: unknown, ctx) => ({
|
|
215
|
+
tenant: ctx?.request?.headers["x-tenant-id"] ?? "unknown",
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const { url, close } = await ai.mcp
|
|
220
|
+
.serve([whoami], { name: "tenant-tools", transport: { type: "http" } })
|
|
221
|
+
.start();
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
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.
|
|
225
|
+
|
|
226
|
+
## Return native MCP results
|
|
227
|
+
|
|
228
|
+
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:
|
|
135
229
|
|
|
136
230
|
```ts
|
|
137
|
-
|
|
138
|
-
ai
|
|
231
|
+
import { mcpResult } from "@warlock.js/ai-tools";
|
|
232
|
+
import { ai } from "@warlock.js/ai";
|
|
139
233
|
|
|
140
|
-
|
|
141
|
-
|
|
234
|
+
const count = ai.tool({
|
|
235
|
+
name: "count",
|
|
236
|
+
description: "Return a structured count",
|
|
237
|
+
meta: {
|
|
238
|
+
outputSchema: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: { count: { type: "number" } },
|
|
241
|
+
required: ["count"],
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
execute: async (_input: unknown, ctx) =>
|
|
245
|
+
mcpResult({
|
|
246
|
+
content: [{ type: "text", text: '{"count":2}' }],
|
|
247
|
+
structuredContent: { count: 2 },
|
|
248
|
+
_meta: { tenant: ctx?.request?.headers["x-tenant-id"] ?? "unknown" },
|
|
249
|
+
}),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
ai.mcp.serve([count], { name: "counts" });
|
|
142
253
|
```
|
|
143
254
|
|
|
144
|
-
The
|
|
255
|
+
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.
|
|
145
256
|
|
|
146
|
-
|
|
257
|
+
For tools that already return native objects, opt in at the server or tool level:
|
|
147
258
|
|
|
148
259
|
```ts
|
|
149
|
-
ai.
|
|
150
|
-
name: "
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
260
|
+
const nativeTool = ai.tool({
|
|
261
|
+
name: "native-result",
|
|
262
|
+
description: "Return a native result",
|
|
263
|
+
meta: { mcpResults: "native" }, // this tool opts in even on a default server
|
|
264
|
+
execute: async () => ({ content: [{ type: "text", text: "fixture" }], isError: true }),
|
|
154
265
|
});
|
|
266
|
+
|
|
267
|
+
ai.mcp.serve([nativeTool], { name: "native-tools", results: "native" });
|
|
268
|
+
// On a native-mode server, meta: { mcpResults: "text" } disables detection for a tool.
|
|
269
|
+
// Explicit mcpResult(...) returns always opt in, regardless of the detection setting.
|
|
155
270
|
```
|
|
156
271
|
|
|
157
|
-
|
|
272
|
+
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.
|
|
158
273
|
|
|
159
|
-
|
|
274
|
+
`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.
|
|
160
275
|
|
|
161
|
-
|
|
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.
|
|
276
|
+
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.
|
|
164
277
|
|
|
165
|
-
|
|
278
|
+
## Sources and tool-name collisions
|
|
166
279
|
|
|
167
|
-
|
|
280
|
+
```ts
|
|
281
|
+
ai.mcp.serve([calc, dt], { name: "utils" }); // literal array
|
|
282
|
+
ai.mcp.serve({ tools: () => ws.tools.all() }, { name: "workspace" }); // anything with tools()
|
|
283
|
+
ai.mcp.serve(
|
|
284
|
+
{ groups: { prod: [search], staging: [search] } },
|
|
285
|
+
{
|
|
286
|
+
name: "search",
|
|
287
|
+
prefixToolNames: true, // → prod_search, staging_search; or (group, name) => `${group}.${name}`
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Duplicate exposed names throw `McpServeError` (`type: "collision"`) **when `serve()` is called**. `prefixToolNames` on an ungrouped source throws `type: "config"`.
|
|
293
|
+
|
|
294
|
+
## Resources and prompts (optional)
|
|
168
295
|
|
|
169
296
|
```ts
|
|
170
|
-
|
|
171
|
-
|
|
297
|
+
ai.mcp.serve(tools, {
|
|
298
|
+
name: "handbook",
|
|
299
|
+
resources: [
|
|
300
|
+
{
|
|
301
|
+
uri: "docs://handbook",
|
|
302
|
+
name: "handbook",
|
|
303
|
+
mimeType: "text/markdown",
|
|
304
|
+
read: () => "# Handbook",
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
prompts: [
|
|
308
|
+
{
|
|
309
|
+
name: "review",
|
|
310
|
+
arguments: [{ name: "diff", required: true }],
|
|
311
|
+
get: (args) => `Review this diff:\n${args.diff ?? ""}`, // string → one user message
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
});
|
|
172
315
|
```
|
|
173
316
|
|
|
174
|
-
`
|
|
317
|
+
`read()` returns a string (text) or `Uint8Array` (base64 blob). `get()` returns a string or `McpPromptMessage[]`. Unknown resource → `-32002`; unknown prompt / missing required argument → `-32602`.
|
|
318
|
+
|
|
319
|
+
## How the protocol maps
|
|
320
|
+
|
|
321
|
+
- **`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`).
|
|
322
|
+
- **`tools/list`** → `inputSchema` via `extractJsonSchema(contract.input, { target: schemaTarget })` (default `"draft-2020-12"`).
|
|
323
|
+
- **`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.
|
|
324
|
+
- **`ping`** → `{}`.
|
|
325
|
+
|
|
326
|
+
## Drive the protocol yourself (tests, custom servers)
|
|
175
327
|
|
|
176
328
|
```ts
|
|
177
329
|
import { createServeHandler } from "@warlock.js/ai-tools";
|
|
330
|
+
import { ai } from "@warlock.js/ai";
|
|
178
331
|
|
|
179
|
-
const handler = createServeHandler([
|
|
332
|
+
const handler = createServeHandler([ai.tools.calculator()], { name: "utils" });
|
|
180
333
|
|
|
181
|
-
|
|
182
|
-
|
|
334
|
+
const response = await handler.handle(
|
|
335
|
+
{ jsonrpc: "2.0", id: 1, method: "tools/list" },
|
|
336
|
+
{ request: { method: "POST", path: "/mcp", headers: { "x-tenant-id": "acme" } } },
|
|
337
|
+
);
|
|
183
338
|
```
|
|
184
339
|
|
|
185
|
-
|
|
340
|
+
Mounting it in your own server means you own Origin validation, auth and body limits.
|
|
186
341
|
|
|
187
342
|
## Errors
|
|
188
343
|
|
|
189
|
-
`
|
|
344
|
+
`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`.
|
|
190
345
|
|
|
191
346
|
## See also
|
|
192
347
|
|
|
@@ -199,7 +354,7 @@ const response = await handler.handle(jsonRpcRequest); // request → response,
|
|
|
199
354
|
|
|
200
355
|
---
|
|
201
356
|
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`, `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.'
|
|
357
|
+
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.'
|
|
203
358
|
---
|
|
204
359
|
|
|
205
360
|
# Use the web + HTTP tool belt
|
|
@@ -227,17 +382,17 @@ const agent = ai.agent({
|
|
|
227
382
|
});
|
|
228
383
|
```
|
|
229
384
|
|
|
230
|
-
The construction `options` are the **rails** — they bound what the model may do; the model supplies the per-call inputs
|
|
385
|
+
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
386
|
|
|
232
387
|
## The five tools
|
|
233
388
|
|
|
234
|
-
| Factory
|
|
235
|
-
|
|
236
|
-
| `ai.tools.webSearch(options)`
|
|
237
|
-
| `ai.tools.fetchUrl(options?)`
|
|
238
|
-
| `ai.tools.http(options?)`
|
|
239
|
-
| `ai.tools.calculator(options?)` | `calculator`
|
|
240
|
-
| `ai.tools.dateTime(options?)`
|
|
389
|
+
| Factory | Name | Model input | Returns |
|
|
390
|
+
| ------------------------------- | -------------- | -------------------------------------------------------------- | ------------------------------------------------ |
|
|
391
|
+
| `ai.tools.webSearch(options)` | `web_search` | `{ query, maxResults? }` | `{ results: { title, url, snippet, score? }[] }` |
|
|
392
|
+
| `ai.tools.fetchUrl(options?)` | `fetch_url` | `{ url }` | `{ url, status, content, truncated }` |
|
|
393
|
+
| `ai.tools.http(options?)` | `http_request` | `{ method?, url, headers?, body? }` | `{ status, headers, body, truncated }` |
|
|
394
|
+
| `ai.tools.calculator(options?)` | `calculator` | `{ expression }` | `{ result }` |
|
|
395
|
+
| `ai.tools.dateTime(options?)` | `date_time` | `{ op, iso?, from?, to?, amount?, unit?, timeZone?, format? }` | `{ value }` |
|
|
241
396
|
|
|
242
397
|
## `web_search` — pick a provider, supply a key
|
|
243
398
|
|
|
@@ -252,7 +407,12 @@ ai.tools.webSearch({ provider: "tavily", apiKey: process.env.TAVILY_API_KEY, max
|
|
|
252
407
|
## `fetch_url` — read a page you have the URL for
|
|
253
408
|
|
|
254
409
|
```ts
|
|
255
|
-
ai.tools.fetchUrl({
|
|
410
|
+
ai.tools.fetchUrl({
|
|
411
|
+
extract: "text",
|
|
412
|
+
allowHosts: ["docs.stripe.com"],
|
|
413
|
+
maxBytes: 500_000,
|
|
414
|
+
timeoutMs: 10_000,
|
|
415
|
+
});
|
|
256
416
|
```
|
|
257
417
|
|
|
258
418
|
- `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.
|
|
@@ -267,7 +427,7 @@ ai.tools.fetchUrl({ extract: "text", allowHosts: ["docs.stripe.com"], maxBytes:
|
|
|
267
427
|
ai.tools.http({
|
|
268
428
|
baseUrl: "https://api.stripe.com",
|
|
269
429
|
allowHosts: ["api.stripe.com"],
|
|
270
|
-
allowMethods: ["GET", "POST"],
|
|
430
|
+
allowMethods: ["GET", "POST"], // default ["GET"] — mutating methods are opt-in
|
|
271
431
|
headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },
|
|
272
432
|
timeoutMs: 15_000,
|
|
273
433
|
maxBytes: 1_000_000,
|
|
@@ -276,6 +436,7 @@ ai.tools.http({
|
|
|
276
436
|
```
|
|
277
437
|
|
|
278
438
|
All guardrails are enforced **before the network call** and surface as a typed `HttpPolicyError`:
|
|
439
|
+
|
|
279
440
|
- a method outside `allowMethods` → `"method-not-allowed"`;
|
|
280
441
|
- a host outside `allowHosts` → `"host-not-allowed"` (SSRF guardrail);
|
|
281
442
|
- an unresolvable URL → `"invalid-url"`.
|
|
@@ -296,9 +457,15 @@ Supports `+ - * / % ^`, unary signs, parentheses, and decimal/scientific literal
|
|
|
296
457
|
|
|
297
458
|
```ts
|
|
298
459
|
await ai.tools.dateTime().invoke({ op: "now", format: "iso" });
|
|
299
|
-
await ai.tools
|
|
300
|
-
|
|
301
|
-
|
|
460
|
+
await ai.tools
|
|
461
|
+
.dateTime()
|
|
462
|
+
.invoke({ op: "add", iso: "2026-06-22T00:00:00Z", amount: 3, unit: "days" });
|
|
463
|
+
await ai.tools
|
|
464
|
+
.dateTime()
|
|
465
|
+
.invoke({ op: "diff", iso: "2026-06-22T00:00:00Z", to: "2026-06-25T00:00:00Z", unit: "days" }); // "3"
|
|
466
|
+
await ai.tools
|
|
467
|
+
.dateTime({ defaultTimeZone: "Africa/Cairo" })
|
|
468
|
+
.invoke({ op: "format", iso: "2026-06-22T12:00:00Z", format: "datetime" });
|
|
302
469
|
```
|
|
303
470
|
|
|
304
471
|
- `op`: `"now"` / `"add"` / `"diff"` / `"format"`. `format`: `"iso"` (default), `"date"`, `"time"`, `"datetime"`.
|
|
@@ -306,6 +473,26 @@ await ai.tools.dateTime({ defaultTimeZone: "Africa/Cairo" }).invoke({ op: "forma
|
|
|
306
473
|
- 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"`.
|
|
307
474
|
- 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") })`.
|
|
308
475
|
|
|
476
|
+
## `ai.tools.measure` — what the belt costs per model call
|
|
477
|
+
|
|
478
|
+
`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"`).
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
import "@warlock.js/ai-tools";
|
|
482
|
+
import { ai, type ModelContract } from "@warlock.js/ai";
|
|
483
|
+
|
|
484
|
+
export async function reportBeltCost(model: ModelContract): Promise<void> {
|
|
485
|
+
const belt = [ai.tools.calculator(), ai.tools.dateTime()];
|
|
486
|
+
|
|
487
|
+
const full = await ai.tools.measure(belt, { model });
|
|
488
|
+
const compact = await ai.tools.measure(belt, { model, mode: "compact" });
|
|
489
|
+
|
|
490
|
+
console.log(full.source, full.tokens, "→", compact.tokens, full.perTool);
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
If compact saves enough, run the agent with `toolSchemas: "compact"` (the `define-ai-tool` topic of the `warlock-js-ai` skill).
|
|
495
|
+
|
|
309
496
|
## Errors flow as data, not throws
|
|
310
497
|
|
|
311
498
|
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`:
|
|
@@ -313,7 +500,9 @@ Every failure above is thrown inside the tool handler, caught by the framework's
|
|
|
313
500
|
```ts
|
|
314
501
|
import { WebToolError } from "@warlock.js/ai-tools";
|
|
315
502
|
|
|
316
|
-
const { error } = await ai.tools
|
|
503
|
+
const { error } = await ai.tools
|
|
504
|
+
.fetchUrl({ allowHosts: ["docs.stripe.com"] })
|
|
505
|
+
.invoke({ url: "http://evil.test" });
|
|
317
506
|
if (error instanceof WebToolError && error.type === "denied-host") {
|
|
318
507
|
// rejected before any network call
|
|
319
508
|
}
|
package/llms.txt
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
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) — the `expose-as-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.
|
|
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)
|
|
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`, `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.
|
|
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: `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.
|
|
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[], 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.
|
|
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.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.
|
package/package.json
CHANGED
|
@@ -20,17 +20,13 @@
|
|
|
20
20
|
"directory": "packages/ai-tools"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@modelcontextprotocol/sdk": "*",
|
|
24
23
|
"@mozilla/readability": "*",
|
|
25
24
|
"@tavily/core": "*",
|
|
26
|
-
"@warlock.js/ai": "5.
|
|
25
|
+
"@warlock.js/ai": "5.32.0",
|
|
27
26
|
"ajv": "*",
|
|
28
27
|
"jsdom": "*"
|
|
29
28
|
},
|
|
30
29
|
"peerDependenciesMeta": {
|
|
31
|
-
"@modelcontextprotocol/sdk": {
|
|
32
|
-
"optional": true
|
|
33
|
-
},
|
|
34
30
|
"@mozilla/readability": {
|
|
35
31
|
"optional": true
|
|
36
32
|
},
|
|
@@ -47,7 +43,7 @@
|
|
|
47
43
|
"dependencies": {
|
|
48
44
|
"@standard-schema/spec": "^1.0.0"
|
|
49
45
|
},
|
|
50
|
-
"version": "5.
|
|
46
|
+
"version": "5.32.0",
|
|
51
47
|
"main": "./cjs/index.cjs",
|
|
52
48
|
"module": "./esm/index.mjs",
|
|
53
49
|
"types": "./esm/index.d.mts",
|
|
@@ -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.",
|
|
51
|
-
filter: (name) => name.startsWith("issues_")
|
|
52
|
-
timeoutMs: 30_000,
|
|
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
|
|
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
|
|
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
|
|