@warlock.js/ai-tools 5.30.0 → 5.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/README.md +25 -13
  3. package/cjs/index.cjs +843 -101
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/contracts/index.d.mts +2 -1
  6. package/esm/contracts/mcp-serve.type.d.mts +257 -0
  7. package/esm/contracts/mcp-serve.type.d.mts.map +1 -0
  8. package/esm/contracts/mcp.type.d.mts +16 -59
  9. package/esm/contracts/mcp.type.d.mts.map +1 -1
  10. package/esm/errors.d.mts +36 -1
  11. package/esm/errors.d.mts.map +1 -1
  12. package/esm/errors.mjs +20 -1
  13. package/esm/errors.mjs.map +1 -1
  14. package/esm/http/http-request.d.mts.map +1 -1
  15. package/esm/http/http-request.mjs.map +1 -1
  16. package/esm/index.d.mts +6 -4
  17. package/esm/index.mjs +3 -2
  18. package/esm/mcp/client.mjs +35 -5
  19. package/esm/mcp/client.mjs.map +1 -1
  20. package/esm/mcp/index.d.mts +12 -5
  21. package/esm/mcp/index.d.mts.map +1 -1
  22. package/esm/mcp/index.mjs +1 -0
  23. package/esm/mcp/index.mjs.map +1 -1
  24. package/esm/mcp/protocol.d.mts +16 -0
  25. package/esm/mcp/protocol.d.mts.map +1 -0
  26. package/esm/mcp/protocol.mjs +23 -0
  27. package/esm/mcp/protocol.mjs.map +1 -0
  28. package/esm/mcp/serve-http.mjs +443 -0
  29. package/esm/mcp/serve-http.mjs.map +1 -0
  30. package/esm/mcp/serve.d.mts +45 -26
  31. package/esm/mcp/serve.d.mts.map +1 -1
  32. package/esm/mcp/serve.mjs +325 -92
  33. package/esm/mcp/serve.mjs.map +1 -1
  34. package/esm/mcp/transport.d.mts.map +1 -1
  35. package/esm/mcp/transport.mjs.map +1 -1
  36. package/esm/register.d.mts +12 -1
  37. package/esm/register.d.mts.map +1 -1
  38. package/esm/register.mjs +3 -2
  39. package/esm/register.mjs.map +1 -1
  40. package/esm/schema.mjs.map +1 -1
  41. package/esm/utility/calculator.d.mts.map +1 -1
  42. package/esm/utility/calculator.mjs.map +1 -1
  43. package/esm/utility/date-time.d.mts.map +1 -1
  44. package/esm/utility/date-time.mjs.map +1 -1
  45. package/esm/utility/schema.mjs.map +1 -1
  46. package/esm/web/fetch-url.d.mts.map +1 -1
  47. package/esm/web/fetch-url.mjs.map +1 -1
  48. package/esm/web/schema.mjs.map +1 -1
  49. package/esm/web/web-search.mjs.map +1 -1
  50. package/llms-full.txt +194 -57
  51. package/llms.txt +3 -3
  52. package/package.json +2 -6
  53. package/skills/connect-mcp-server/SKILL.md +28 -6
  54. package/skills/expose-as-mcp-server/SKILL.md +117 -36
  55. package/skills/index.md +5 -2
  56. package/skills/use-web-and-http-tools/SKILL.md +49 -15
@@ -1,4 +1,5 @@
1
1
  import { FetchUrlExtract, FetchUrlInput, FetchUrlOptions, FetchUrlResult, SearchProvider, WebSearchInput, WebSearchOptions, WebSearchResult, WebSearchResultItem } from "./web.type.mjs";
2
2
  import { HttpMethod, HttpRequestInput, HttpRequestOptions, HttpRequestResult } from "./http.type.mjs";
3
3
  import { CalculatorInput, CalculatorOptions, CalculatorResult, DateTimeInput, DateTimeOp, DateTimeOptions, DateTimeResult } from "./utility.type.mjs";
4
- import { JsonRpcError, JsonRpcId, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, McpClient, McpClientOptions, McpContentBlock, McpServeOptions, McpServeSource, McpServeTransport, McpServer, McpToolCallResult, McpToolDescriptor, McpTransport } from "./mcp.type.mjs";
4
+ import { JsonRpcError, JsonRpcId, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, McpClient, McpClientOptions, McpContentBlock, McpToolCallResult, McpToolDescriptor, McpTransport } from "./mcp.type.mjs";
5
+ import { McpGroupedToolSource, McpHttpAuth, McpHttpRequestInfo, McpHttpServeHandle, McpHttpServeTransport, McpPromptArgument, McpPromptMessage, McpServeCallContext, McpServeHandle, McpServeOptions, McpServePrompt, McpServeResource, McpServeSource, McpServeTransport, McpServer, McpToolSource } from "./mcp-serve.type.mjs";
@@ -0,0 +1,257 @@
1
+ import { McpContentBlock } from "./mcp.type.mjs";
2
+ import { JsonSchemaTarget, ToolContract, ToolRequestContext } from "@warlock.js/ai";
3
+
4
+ //#region ../../packages/ai-tools/src/contracts/mcp-serve.type.d.ts
5
+ /**
6
+ * Type contracts for `ai.mcp.serve(source, options)` — exposing local
7
+ * tools (plus optional resources and prompts) AS a Model Context Protocol
8
+ * server over stdio or Streamable HTTP. Pure declarations only; the
9
+ * factories live in `../mcp/serve.ts` and `../mcp/serve-http.ts`.
10
+ */
11
+ /**
12
+ * One set of tools: an object that can enumerate its `ToolContract`s (a
13
+ * built agent / supervisor / orchestrator / workspace), or a literal array.
14
+ */
15
+ type McpToolSource = {
16
+ tools(): ToolContract[];
17
+ } | ToolContract[];
18
+ /**
19
+ * Several named tool sets served by one server. Each key names a group;
20
+ * with `prefixToolNames` the group name is prepended to its tools' names
21
+ * so equal names from different groups stay distinct.
22
+ *
23
+ * @example
24
+ * { groups: { github: githubAgent, files: [readFile, writeFile] } }
25
+ */
26
+ type McpGroupedToolSource = {
27
+ /** Tool sets keyed by group name. */groups: Record<string, McpToolSource>;
28
+ };
29
+ /**
30
+ * What `ai.mcp.serve` exposes as tools — one tool set, or several named
31
+ * groups. Every exposed tool name must be unique: a duplicate throws an
32
+ * {@link import("../errors").McpServeError} (`type: "collision"`) when
33
+ * `serve()` is called.
34
+ */
35
+ type McpServeSource = McpToolSource | McpGroupedToolSource;
36
+ /**
37
+ * Normalised metadata of an inbound HTTP request, handed to a custom
38
+ * {@link McpHttpAuth.authorize} hook. Tools receive the same headers on
39
+ * `ctx.request` (see `ToolRequestContext` in `@warlock.js/ai`).
40
+ */
41
+ type McpHttpRequestInfo = ToolRequestContext & {
42
+ /** The HTTP method, upper-case (`"POST"`, `"GET"`, …). */method: string; /** The request path, without the query string. */
43
+ path: string;
44
+ };
45
+ /**
46
+ * Authentication for an HTTP-served MCP endpoint. At least one of
47
+ * `bearer` / `authorize` is required; when both are set, a request must
48
+ * pass both. A request that fails gets HTTP 401 before its body is read.
49
+ */
50
+ type McpHttpAuth = {
51
+ /**
52
+ * Accepted bearer token(s), matched against
53
+ * `Authorization: Bearer <token>`. Compared in constant time over
54
+ * SHA-256 digests, so neither the token's content nor its length leaks
55
+ * through response timing. Several tokens allow rotation without
56
+ * downtime. Empty strings are rejected when the server starts.
57
+ */
58
+ bearer?: string | readonly string[];
59
+ /**
60
+ * Custom check run for every request (after `bearer`, when both are
61
+ * set). Return `true` to allow. A hook that throws or rejects denies the
62
+ * request (fails closed).
63
+ */
64
+ authorize?: (request: McpHttpRequestInfo) => boolean | Promise<boolean>;
65
+ };
66
+ /**
67
+ * Serve over the MCP Streamable HTTP transport.
68
+ *
69
+ * Binds `127.0.0.1` by default. Binding any non-loopback address
70
+ * requires `auth`; `start()` throws otherwise.
71
+ */
72
+ type McpHttpServeTransport = {
73
+ /** Streamable HTTP transport. */type: "http";
74
+ /**
75
+ * Interface to bind. Loopback (`127.0.0.1`, `::1`, `localhost`) needs
76
+ * no auth; anything else (`0.0.0.0`, a LAN address) requires `auth`.
77
+ *
78
+ * @default "127.0.0.1"
79
+ */
80
+ host?: string;
81
+ /**
82
+ * Port to bind. `0` picks a free ephemeral port; read the result from
83
+ * the `url` that `start()` resolves.
84
+ *
85
+ * @default 0
86
+ */
87
+ port?: number;
88
+ /**
89
+ * The single MCP endpoint path. Other paths answer 404.
90
+ *
91
+ * @default "/mcp"
92
+ */
93
+ path?: string; /** Bearer token(s) and/or a custom authorize hook. Required off loopback. */
94
+ auth?: McpHttpAuth;
95
+ /**
96
+ * Extra browser origins allowed to call the endpoint, as full origins
97
+ * (`"https://app.example.com"`). Loopback origins (`http://localhost:*`,
98
+ * `http://127.0.0.1:*`, `http://[::1]:*`) are always allowed; any other
99
+ * `Origin` header is refused with 403, which blocks DNS-rebinding
100
+ * attacks from web pages. Requests without an `Origin` header (non-browser
101
+ * clients) are unaffected.
102
+ */
103
+ allowedOrigins?: readonly string[];
104
+ /**
105
+ * Extra `Host` header names accepted while bound to loopback (where only
106
+ * `localhost`, `127.0.0.1` and `[::1]` are accepted by default — the
107
+ * second DNS-rebinding guard). When bound off loopback, setting this
108
+ * restricts `Host` to exactly these names.
109
+ */
110
+ allowedHosts?: readonly string[];
111
+ /**
112
+ * Maximum accepted request body, in bytes. Larger bodies get 413.
113
+ *
114
+ * @default 1_048_576 (1 MiB)
115
+ */
116
+ maxBodyBytes?: number;
117
+ /**
118
+ * How long `close()` waits for in-flight requests to finish before it
119
+ * aborts their `ctx.signal` and destroys the remaining connections.
120
+ *
121
+ * @default 5_000
122
+ */
123
+ drainTimeoutMs?: number;
124
+ };
125
+ /**
126
+ * Where an `ai.mcp.serve` server listens. Discriminated by `type`.
127
+ *
128
+ * - `"stdio"` — serve JSON-RPC over this process's stdin/stdout.
129
+ * - `"http"` — serve MCP Streamable HTTP (see {@link McpHttpServeTransport}).
130
+ */
131
+ type McpServeTransport = {
132
+ type: "stdio";
133
+ } | McpHttpServeTransport;
134
+ /**
135
+ * Per-call context handed to resource readers and prompt builders (tools
136
+ * receive the same data on their `ToolContext`).
137
+ */
138
+ type McpServeCallContext = {
139
+ /** The inbound HTTP request; absent over stdio. */request?: McpHttpRequestInfo; /** Aborted when an HTTP server's `close()` gives up draining. */
140
+ signal?: AbortSignal;
141
+ };
142
+ /**
143
+ * A static MCP resource served by `resources/list` + `resources/read`.
144
+ */
145
+ type McpServeResource = {
146
+ /** Unique resource URI, e.g. `"docs://handbook"`. */uri: string; /** Resource name. */
147
+ name: string; /** Optional display title. */
148
+ title?: string; /** Optional description. */
149
+ description?: string; /** Optional MIME type, echoed on the read result. */
150
+ mimeType?: string;
151
+ /**
152
+ * Produce the resource's contents. A string is sent as `text`; bytes
153
+ * are sent base64-encoded as `blob`.
154
+ */
155
+ read(context: McpServeCallContext): string | Uint8Array | Promise<string | Uint8Array>;
156
+ };
157
+ /** One declared argument of an {@link McpServePrompt}. */
158
+ type McpPromptArgument = {
159
+ /** Argument name. */name: string; /** Optional description shown to the user. */
160
+ description?: string; /** When `true`, `prompts/get` without it answers an invalid-params error. */
161
+ required?: boolean;
162
+ };
163
+ /** One message of a prompt returned by `prompts/get`. */
164
+ type McpPromptMessage = {
165
+ /** Who speaks the message. */role: "user" | "assistant"; /** The content block (`{ type: "text", text }` for plain text). */
166
+ content: McpContentBlock;
167
+ };
168
+ /**
169
+ * A prompt template served by `prompts/list` + `prompts/get`.
170
+ */
171
+ type McpServePrompt = {
172
+ /** Unique prompt name. */name: string; /** Optional display title. */
173
+ title?: string; /** Optional description. */
174
+ description?: string; /** Declared arguments. */
175
+ arguments?: McpPromptArgument[];
176
+ /**
177
+ * Build the prompt. A string becomes one `user` text message.
178
+ *
179
+ * @param args - The client's arguments (string values per the MCP spec).
180
+ */
181
+ get(args: Record<string, string>, context: McpServeCallContext): string | McpPromptMessage[] | Promise<string | McpPromptMessage[]>;
182
+ };
183
+ /**
184
+ * Options for `ai.mcp.serve(source, options)` (Direction B: local
185
+ * primitive → MCP server other clients can consume).
186
+ */
187
+ interface McpServeOptions {
188
+ /** Server name advertised in the `initialize` response. */
189
+ name: string;
190
+ /**
191
+ * Server version advertised in `initialize`.
192
+ *
193
+ * @default the package version
194
+ */
195
+ version?: string;
196
+ /**
197
+ * Where to listen.
198
+ *
199
+ * @default { type: "stdio" }
200
+ */
201
+ transport?: McpServeTransport;
202
+ /**
203
+ * JSON-Schema dialect emitted for each tool's `inputSchema`. Defaults
204
+ * to a neutral MCP draft rather than `extractJsonSchema`'s own
205
+ * `"openai-strict"` default.
206
+ *
207
+ * @default "draft-2020-12"
208
+ */
209
+ schemaTarget?: JsonSchemaTarget;
210
+ /**
211
+ * Prefix each tool of a grouped source (`{ groups: { … } }`) with its
212
+ * group name. `true` exposes `<group>_<tool>`; a function builds the
213
+ * exposed name itself. Only valid with a grouped source.
214
+ *
215
+ * @default false — names are exposed as-is and duplicates throw
216
+ */
217
+ prefixToolNames?: boolean | ((group: string, toolName: string) => string);
218
+ /** Static resources to serve. Omit to leave the `resources` capability off. */
219
+ resources?: McpServeResource[];
220
+ /** Prompt templates to serve. Omit to leave the `prompts` capability off. */
221
+ prompts?: McpServePrompt[];
222
+ }
223
+ /**
224
+ * What a started server resolves with. Over HTTP, `url` is the full
225
+ * endpoint (`http://127.0.0.1:53211/mcp`); over stdio it is absent.
226
+ */
227
+ interface McpServeHandle {
228
+ /** The endpoint URL (HTTP only). */
229
+ url?: string;
230
+ /**
231
+ * Stop serving. Over HTTP: stop accepting connections, let in-flight
232
+ * requests finish (up to `drainTimeoutMs`), then free the port.
233
+ * Idempotent.
234
+ */
235
+ close(): Promise<void>;
236
+ }
237
+ /** The handle of an HTTP-served MCP endpoint — `url` is always set. */
238
+ interface McpHttpServeHandle extends McpServeHandle {
239
+ /** The endpoint URL, e.g. `http://127.0.0.1:53211/mcp`. */
240
+ url: string;
241
+ }
242
+ /**
243
+ * A `ai.mcp.serve` server. Nothing listens until `start()`.
244
+ */
245
+ interface McpServer<THandle extends McpServeHandle = McpServeHandle> {
246
+ /**
247
+ * Begin serving. Over HTTP, resolves once the port is bound with
248
+ * `{ url, close }`. Calling it again while running resolves the same
249
+ * handle.
250
+ */
251
+ start(): Promise<THandle>;
252
+ /** Stop serving and release the transport (same as `handle.close()`). */
253
+ stop(): Promise<void>;
254
+ }
255
+ //#endregion
256
+ export { McpGroupedToolSource, McpHttpAuth, McpHttpRequestInfo, McpHttpServeHandle, McpHttpServeTransport, McpPromptArgument, McpPromptMessage, McpServeCallContext, McpServeHandle, McpServeOptions, McpServePrompt, McpServeResource, McpServeSource, McpServeTransport, McpServer, McpToolSource };
257
+ //# sourceMappingURL=mcp-serve.type.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-serve.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/contracts/mcp-serve.type.ts"],"mappings":";;;;;;AAcA;;;;;;;;KAAY,aAAA;EAAkB,KAAA,IAAS,YAAA;AAAA,IAAmB,YAAY;;;;;;;AAYhC;AAStC;KAXY,oBAAA;uCAEV,MAAA,EAAQ,MAAM,SAAS,aAAA;AAAA;AAgBzB;;;;;;AAAA,KAPY,cAAA,GAAiB,aAAA,GAAgB,oBAAoB;;AAW3D;AAQN;;;KAZY,kBAAA,GAAqB,kBAAkB;EAoBjD,0DAlBA,MAAA,UAwBsB;EAtBtB,IAAA;AAAA;;AAsB8D;AAShE;;;KAvBY,WAAA;EAyBV;;;;;;;EAjBA,MAAA;EA6DA;;;AAOc;AAShB;EAvEE,SAAA,IAAa,OAAA,EAAS,kBAAA,eAAiC,OAAO;AAAA;;AAuES;AAMzE;;;;KApEY,qBAAA;EAsEA,iCApEV,IAAA;EAsES;;AAAW;AAMtB;;;EArEE,IAAA;EAoF6C;;;;;;EA7E7C,IAAA;EAoEA;;;;;EA9DA,IAAA,WAuE6C;EArE7C,IAAA,GAAO,WAAW;EAqEyD;;AAAU;AAIvF;;;;;EAhEE,cAAA;EAsEA;;AAAQ;AAIV;;;EAnEE,YAAA;EAqEA;;;;AAEwB;EAjExB,YAAA;EAuEwB;;;;;;EAhExB,cAAA;AAAA;;;;;;;KASU,iBAAA;EAAsB,IAAA;AAAA,IAAkB,qBAAqB;;;;;KAM7D,mBAAA;EAkEwC,mDAhElD,OAAA,GAAU,kBAAA,EAgEwD;EA9DlE,MAAA,GAAS,WAAW;AAAA;;;;KAMV,gBAAA;EAiGA,qDA/FV,GAAA,UA+FwB;EA7FxB,IAAA,UAmEA;EAjEA,KAAA,WAuEY;EArEZ,WAAA,WA6Ee;EA3Ef,QAAA;EAmF8B;;;;EA9E9B,IAAA,CAAK,OAAA,EAAS,mBAAA,YAA+B,UAAA,GAAa,OAAA,UAAiB,UAAA;AAAA;;KAIjE,iBAAA;EAqFK,qBAnFf,IAAA;EAEA,WAAA,WAmFA;EAjFA,QAAA;AAAA;;KAIU,gBAAA;EAuFK,8BArFf,IAAA;EAEA,OAAA,EAAS,eAAe;AAAA;AA2F1B;;;AAAA,KArFY,cAAA;EAqFgD,0BAnF1D,IAAA,UAyFS;EAvFT,KAAA,WAyFe;EAvFf,WAAA,WA+EyB;EA7EzB,SAAA,GAAY,iBAAA;EA6E8C;;;;;EAvE1D,GAAA,CACE,IAAA,EAAM,MAAA,kBACN,OAAA,EAAS,mBAAA,YACC,gBAAA,KAAqB,OAAA,UAAiB,gBAAA;AAAA;AA4EnC;;;;AAAA,UArEA,eAAA;;EAEf,IAAA;;;;;;EAMA,OAAA;;;;;;EAMA,SAAA,GAAY,iBAAA;;;;;;;;EAQZ,YAAA,GAAe,gBAAA;;;;;;;;EAQf,eAAA,eAA8B,KAAA,UAAe,QAAA;;EAE7C,SAAA,GAAY,gBAAA;;EAEZ,OAAA,GAAU,cAAA;AAAA;;;;;UAOK,cAAA;;EAEf,GAAA;;;;;;EAMA,KAAA,IAAS,OAAO;AAAA;;UAID,kBAAA,SAA2B,cAAc;;EAExD,GAAG;AAAA;;;;UAMY,SAAA,iBAA0B,cAAA,GAAiB,cAAA;;;;;;EAM1D,KAAA,IAAS,OAAA,CAAQ,OAAA;;EAEjB,IAAA,IAAQ,OAAA;AAAA"}
@@ -1,4 +1,4 @@
1
- import { JsonSchemaTarget, ToolContract } from "@warlock.js/ai";
1
+ import { ToolContract } from "@warlock.js/ai";
2
2
 
3
3
  //#region ../../packages/ai-tools/src/contracts/mcp.type.d.ts
4
4
  /**
@@ -53,6 +53,20 @@ interface McpClientOptions {
53
53
  * @default 30_000
54
54
  */
55
55
  timeoutMs?: number;
56
+ /**
57
+ * Bind the client's lifetime to a run. When it aborts, the client
58
+ * closes its transport (kills the stdio child / ends the HTTP session),
59
+ * in-flight requests reject, and later `tools()` / tool calls fail with
60
+ * an `McpTransportError` of type `"closed"`. Every `tools/call` also
61
+ * honors it, alongside the per-call `ctx.signal` the agent passes.
62
+ *
63
+ * @example
64
+ * const controller = new AbortController();
65
+ * const github = ai.mcp(server, { signal: controller.signal });
66
+ * await agent.execute(task, { signal: controller.signal, tools: await github.tools() });
67
+ * controller.abort(); // also shuts the MCP server down
68
+ */
69
+ signal?: AbortSignal;
56
70
  }
57
71
  /**
58
72
  * The client handle returned by `ai.mcp(server, options)`.
@@ -66,63 +80,6 @@ interface McpClient {
66
80
  /** Close the underlying transport (kills the child / ends the HTTP session). */
67
81
  close(): Promise<void>;
68
82
  }
69
- /**
70
- * A source of tools to expose via `ai.mcp.serve` — either an object that
71
- * can enumerate its `ToolContract`s (a built agent / supervisor /
72
- * orchestrator), or a literal array of contracts.
73
- */
74
- type McpServeSource = {
75
- tools(): ToolContract[];
76
- } | ToolContract[];
77
- /**
78
- * Where an `ai.mcp.serve` server listens. Discriminated by `type`.
79
- *
80
- * - `"stdio"` — serve JSON-RPC over this process's stdin/stdout.
81
- * - `"http"` — serve Streamable HTTP on `port`.
82
- */
83
- type McpServeTransport = {
84
- type: "stdio";
85
- } | {
86
- type: "http";
87
- port: number;
88
- };
89
- /**
90
- * Options for `ai.mcp.serve(source, options)` (Direction B: local
91
- * primitive → MCP server other clients can consume).
92
- */
93
- interface McpServeOptions {
94
- /** Server name advertised in the `initialize` response. */
95
- name: string;
96
- /**
97
- * Server version advertised in `initialize`.
98
- *
99
- * @default the package version
100
- */
101
- version?: string;
102
- /**
103
- * Where to listen.
104
- *
105
- * @default { type: "stdio" }
106
- */
107
- transport?: McpServeTransport;
108
- /**
109
- * JSON-Schema dialect emitted for each tool's `inputSchema`. Defaults
110
- * to a neutral MCP draft rather than `extractJsonSchema`'s own
111
- * `"openai-strict"` default.
112
- *
113
- * @default "draft-2020-12"
114
- */
115
- schemaTarget?: JsonSchemaTarget;
116
- }
117
- /**
118
- * A running `ai.mcp.serve` server.
119
- */
120
- interface McpServer {
121
- /** Begin accepting connections / reading the transport. */
122
- start(): Promise<void>;
123
- /** Stop serving and release the transport. */
124
- stop(): Promise<void>;
125
- }
126
83
  /** The JSON-RPC version literal every message carries. */
127
84
  type JsonRpcVersion = "2.0";
128
85
  /** A JSON-RPC id — a string or number (we never send notifications with `null`). */
@@ -213,5 +170,5 @@ interface McpToolCallResult {
213
170
  isError?: boolean;
214
171
  }
215
172
  //#endregion
216
- export { JsonRpcError, JsonRpcId, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, McpClient, McpClientOptions, McpContentBlock, McpServeOptions, McpServeSource, McpServeTransport, McpServer, McpToolCallResult, McpToolDescriptor, McpTransport };
173
+ export { JsonRpcError, JsonRpcId, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, McpClient, McpClientOptions, McpContentBlock, McpToolCallResult, McpToolDescriptor, McpTransport };
217
174
  //# sourceMappingURL=mcp.type.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/contracts/mcp.type.ts"],"mappings":";;;;;AAqBA;;;;;;;;;;;;;;AAqBsB;AAOtB;KA5BY,YAAA;mDAGN,IAAA,WA+BJ;EA7BI,OAAA,UA+BM;EA7BN,IAAA;EAmCK;AAAA;AAMX;;;EAnCM,GAAA,GAAM,MAAA;AAAA;EA0CD,iCAtCL,IAAA,UAsCY;EApCZ,GAAA,UAkCK;EAhCL,OAAA,GAAU,MAAM;AAAA;;;AAkCJ;AAQlB;UAnCiB,gBAAA;;;;;;EAMf,UAAA;EA+Bc;EA7Bd,MAAA,IAAU,QAAA;EAqCiB;;;;;EA/B3B,SAAA;AAAA;AAiCsB;AAMxB;;AANwB,UA3BP,SAAA;EAuDgB;;;;EAlD/B,KAAA,IAAS,OAAA,CAAQ,YAAA;EAkDjB;EAhDA,KAAA,IAAS,OAAA;AAAA;AAgDsB;AAMjC;;;;AANiC,KAxCrB,cAAA;EACN,KAAA,IAAS,YAAA;AAAA,IACX,YAAY;;;AAgDC;AAYjB;;;KApDY,iBAAA;EACN,IAAA;AAAA;EACA,IAAA;EAAc,IAAA;AAAA;AAqDC;AAKrB;;;AALqB,UA/CJ,eAAA;EAsDX;EApDJ,IAAA;EAwDgB;;;;;EAlDhB,OAAA;EA8CI;;;;;EAxCJ,SAAA,GAAY,iBAAA;EAkDG;;;;;;;EA1Cf,YAAA,GAAe,gBAAgB;AAAA;;;AA+Cf;UAzCD,SAAA;EA+CY;EA7C3B,KAAA,IAAS,OAAA;EA6CkB;EA3C3B,IAAA,IAAQ,OAAO;AAAA;;KAYL,cAAA;AAqCN;AAAA,KAlCM,SAAA;;;;UAKK,cAAA;EACf,OAAA,EAAS,cAAA;EACT,EAAA,EAAI,SAAA;EAuCgB;EArCpB,MAAA;EA+B+B;EA7B/B,MAAA,GAAS,OAAA;AAAA;;;;UAMM,mBAAA;EACf,OAAA,EAAS,cAAA;EA4BD;EA1BR,MAAA;EA0BoB;EAxBpB,MAAA,GAAS,OAAO;AAAA;;;;UAMD,YAAA;EA2BE;EAzBjB,IAAA;EAuBE;EArBF,OAAA;EAuBE;EArBF,IAAA;AAAA;AA4BF;;;AAAA,UAtBiB,eAAA;EACf,OAAA,EAAS,cAAA;EACT,EAAA,EAAI,SAAA;EA0BJ;EAxBA,MAAA,GAAS,OAAA;EAwBW;EAtBpB,KAAA,GAAQ,YAAA;AAAA;;;;KAME,cAAA,GACR,cAAA,GACA,mBAAA,GACA,eAAA;;;;AA4BY;AAQhB;UA7BiB,iBAAA;;EAEf,IAAA;EA6BA;EA3BA,WAAA;EA6BA;EA3BA,WAAA,GAAc,MAAM;AAAA;;;;;;;UASL,eAAA;;EAEf,IAAA;;EAEA,IAAA;;GAEC,KAAA;AAAA;;;;;;UAQc,iBAAA;;EAEf,OAAA,EAAS,eAAe;;EAExB,OAAA;AAAA"}
1
+ {"version":3,"file":"mcp.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/contracts/mcp.type.ts"],"mappings":";;;;;AAqBA;;;;;;;;;;;;;;AAqBsB;AAOtB;KA5BY,YAAA;mDAGN,IAAA,WA+BJ;EA7BI,OAAA,UA+BM;EA7BN,IAAA;EAiDJ;;;AAAoB;AAMtB;EAjDM,GAAA,GAAM,MAAA;AAAA;EAsDO,iCAlDb,IAAA,UAoDK;EAlDL,GAAA,UAkDY;EAhDZ,OAAA,GAAU,MAAM;AAAA;;;;;UAOL,gBAAA;EAqDL;;;;AAAc;EA/CxB,UAAA;EAkDmB;EAhDnB,MAAA,IAAU,QAAA;EAgDS;AAAA;AAKrB;;;EA/CE,SAAA;EAiDI;;;;;;;;;;;;;EAnCJ,MAAA,GAAS,WAAW;AAAA;;;;UAML,SAAA;EAwCf;;;;EAnCA,KAAA,IAAS,OAAA,CAAQ,YAAA;EAuCD;EArChB,KAAA,IAAS,OAAA;AAAA;;KAYC,cAAA;;KAGA,SAAA;;;;UAKK,cAAA;EACf,OAAA,EAAS,cAAA;EACT,EAAA,EAAI,SAAA;EAiC0B;EA/B9B,MAAA;EAiCI;EA/BJ,MAAA,GAAS,OAAA;AAAA;;;;UAMM,mBAAA;EACf,OAAA,EAAS,cAAA;EAwBT;EAtBA,MAAA;EAwBA;EAtBA,MAAA,GAAS,OAAO;AAAA;;;AAwBI;UAlBL,YAAA;EAwBS;EAtBxB,IAAA;EAsB2B;EApB3B,OAAA;EAoBkE;EAlBlE,IAAA;AAAA;;;;UAMe,eAAA;EACf,OAAA,EAAS,cAAA;EACT,EAAA,EAAI,SAAA;EAiB4B;EAfhC,MAAA,GAAS,OAAA;EAqBW;EAnBpB,KAAA,GAAQ,YAAA;AAAA;;;;KAME,cAAA,GAAiB,cAAA,GAAiB,mBAAA,GAAsB,eAAA;AAsBpE;;;;;AAAA,UAfiB,iBAAA;EAqBd;EAnBD,IAAA;EAmBc;EAjBd,WAAA;EAyBgC;EAvBhC,WAAA,GAAc,MAAM;AAAA;;;;;AA2Bb;;UAlBQ,eAAA;;EAEf,IAAA;;EAEA,IAAA;;GAEC,KAAA;AAAA;;;;;;UAQc,iBAAA;;EAEf,OAAA,EAAS,eAAe;;EAExB,OAAA;AAAA"}
package/esm/errors.d.mts CHANGED
@@ -126,6 +126,41 @@ declare class McpTransportError extends AIError {
126
126
  readonly method?: string;
127
127
  constructor(message: string, options: McpTransportErrorOptions);
128
128
  }
129
+ /**
130
+ * Why `ai.mcp.serve` refused to build or start a server.
131
+ *
132
+ * - `"config"` — invalid serve options: a non-loopback HTTP bind without
133
+ * `auth`, an empty bearer token, a bad `path` / `port`, or
134
+ * `prefixToolNames` without a grouped source.
135
+ * - `"collision"` — two exposed tools (or resources / prompts) share a
136
+ * name (URI for resources).
137
+ * - `"listen"` — the HTTP server could not bind (port in use, permission
138
+ * denied, unknown host).
139
+ */
140
+ type McpServeFailure = "config" | "collision" | "listen";
141
+ /**
142
+ * Options for {@link McpServeError} — the structured `type` discriminator.
143
+ */
144
+ type McpServeErrorOptions = AIErrorOptions & {
145
+ /** Which class of serve failure occurred. */type: McpServeFailure;
146
+ };
147
+ /**
148
+ * `ai.mcp.serve` could not build or start the server. Thrown by `serve()`
149
+ * itself for an invalid source or a name collision, and by `start()` for
150
+ * transport configuration and bind failures — never for a single bad request, which gets an HTTP / JSON-RPC
151
+ * error response instead. Extends {@link AIError} (code `TOOL_EXEC_FAILED`);
152
+ * branch on `error.type`.
153
+ *
154
+ * @example
155
+ * if (error instanceof McpServeError && error.type === "collision") {
156
+ * // two tools share a name — rename one or use a grouped source + prefixToolNames
157
+ * }
158
+ */
159
+ declare class McpServeError extends AIError {
160
+ /** Which class of serve failure occurred. */
161
+ readonly type: McpServeFailure;
162
+ constructor(message: string, options: McpServeErrorOptions);
163
+ }
129
164
  /**
130
165
  * Why a web tool (`ai.tools.webSearch` / `ai.tools.fetchUrl`) failed
131
166
  * before or during a network call.
@@ -227,5 +262,5 @@ declare class HttpPolicyError extends AIError {
227
262
  constructor(message: string, options: HttpPolicyErrorOptions);
228
263
  }
229
264
  //#endregion
230
- export { CalculatorError, CalculatorErrorOptions, CalculatorFailure, DateTimeError, DateTimeErrorOptions, DateTimeFailure, HttpPolicyError, HttpPolicyErrorOptions, HttpPolicyFailure, McpTransportError, McpTransportErrorOptions, McpTransportFailure, WebToolError, WebToolErrorOptions, WebToolFailure };
265
+ export { CalculatorError, CalculatorErrorOptions, CalculatorFailure, DateTimeError, DateTimeErrorOptions, DateTimeFailure, HttpPolicyError, HttpPolicyErrorOptions, HttpPolicyFailure, McpServeError, McpServeErrorOptions, McpServeFailure, McpTransportError, McpTransportErrorOptions, McpTransportFailure, WebToolError, WebToolErrorOptions, WebToolFailure };
231
266
  //# sourceMappingURL=errors.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../packages/ai-tools/src/errors.ts"],"mappings":";;;;;AAUA;;;;AAA6B;AAM7B;KANY,iBAAA;;;;;KAMA,sBAAA,GAAyB,cAAA;EAEZ,kDAAvB,IAAA,EAAM,iBAAiB;AAAA;;;;;;;;;;;;;;;;AAyB4C;AAiBrE;;cArBa,eAAA,SAAwB,OAAA;EAqBV;EAAA,SAnBT,IAAA,EAAM,iBAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,sBAAA;AAAA;;;;;;;AA6BxB;AAmBvB;;KA/BY,eAAA;;;;;KAUA,oBAAA,GAAuB,cAAA;EAqBA,iDAnBjC,IAAA,EAAM,eAAe;AAAA;;;;;;AAuB4C;AAoBnE;;;;AAA+B;AAO/B;;;;;cA/Ba,aAAA,SAAsB,OAAA;EAiC3B;EAAA,SA/BU,IAAA,EAAM,eAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,oBAAA;AAAA;AAqD/C;;;;;;;;;;;;AAAA,KAjCY,mBAAA;;;;;AAuC2D;KAhC3D,wBAAA,GAA2B,cAAA;EA6Db,iDA3DxB,IAAA,EAAM,mBAAmB,EA2DD;EAzDxB,MAAA;AAAA;;;;;;;;AAsEoB;AAsBtB;;;;;;;;;;;cAtEa,iBAAA,SAA0B,OAAA;EA0ElB;EAAA,SAxEH,IAAA,EAAM,mBAAA;EAwEc;EAAA,SAtEpB,MAAA;cAEG,OAAA,UAAiB,OAAA,EAAS,wBAAA;AAAA;;;;AA0FlB;AAM7B;;;;;;;;AAEyB;AAwBzB;;;;;;;KA7FY,cAAA;;;;;KAWA,mBAAA,GAAsB,cAAA;EAsFa,gDApF7C,IAAA,EAAM,cAAc;AAAA;AAoF+C;;;;;;;;;;;;;;;;;;;AAAA,cA9DxD,YAAA,SAAqB,OAAA;;WAEhB,IAAA,EAAM,cAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,mBAAA;AAAA;;;;;;;;;;;;;;;KAsBnC,iBAAA;;;;;KAMA,sBAAA,GAAyB,cAAA;kDAEnC,IAAA,EAAM,iBAAiB;AAAA;;;;;;;;;;;;;;;;;;;;;;cAwBZ,eAAA,SAAwB,OAAA;;WAEnB,IAAA,EAAM,iBAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,sBAAA;AAAA"}
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../packages/ai-tools/src/errors.ts"],"mappings":";;;;;AAUA;;;;AAA6B;AAM7B;KANY,iBAAA;;;;;KAMA,sBAAA,GAAyB,cAAA;EAEZ,kDAAvB,IAAA,EAAM,iBAAiB;AAAA;;;;;;;;;;;;;;;;AAyB4C;AAiBrE;;cArBa,eAAA,SAAwB,OAAA;EAqBV;EAAA,SAnBT,IAAA,EAAM,iBAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,sBAAA;AAAA;;;;;;;AA6BxB;AAmBvB;;KA/BY,eAAA;;;;;KAUA,oBAAA,GAAuB,cAAA;EAqBA,iDAnBjC,IAAA,EAAM,eAAe;AAAA;;;;;;AAuB4C;AAoBnE;;;;AAA+B;AAO/B;;;;;cA/Ba,aAAA,SAAsB,OAAA;EAiC3B;EAAA,SA/BU,IAAA,EAAM,eAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,oBAAA;AAAA;AAqD/C;;;;;;;;;;;;AAAA,KAjCY,mBAAA;;;;;AAuC2D;KAhC3D,wBAAA,GAA2B,cAAA;EAoDZ,iDAlDzB,IAAA,EAAM,mBAAmB,EAkDA;EAhDzB,MAAA;AAAA;;;;;;;;AAuDqB;AAevB;;;;;;;;;;;cAhDa,iBAAA,SAA0B,OAAA;EAoDlB;EAAA,SAlDH,IAAA,EAAM,mBAAA;EAkDc;EAAA,SAhDpB,MAAA;cAEG,OAAA,UAAiB,OAAA,EAAS,wBAAA;AAAA;;;;AA0ErB;AAW1B;;;;;;;KAjEY,eAAA;AAmEU;AAsBtB;;AAtBsB,KA9DV,oBAAA,GAAuB,cAAA;EAsFX,6CApFtB,IAAA,EAAM,eAAe;AAAA;;;;;;;;;;;AAsF2C;AAsBlE;cA7Fa,aAAA,SAAsB,OAAA;;WAEjB,IAAA,EAAM,eAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,oBAAA;AAAA;;;;;;;;AAiGtB;AAwBzB;;;;;;;;;;;;KA7FY,cAAA;;;;AAiGyD;KAtFzD,mBAAA,GAAsB,cAAA;kDAEhC,IAAA,EAAM,cAAc;AAAA;;;;;;;;;;;;;;;;;;;;cAsBT,YAAA,SAAqB,OAAA;;WAEhB,IAAA,EAAM,cAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,mBAAA;AAAA;;;;;;;;;;;;;;;KAsBnC,iBAAA;;;;;KAMA,sBAAA,GAAyB,cAAA;kDAEnC,IAAA,EAAM,iBAAiB;AAAA;;;;;;;;;;;;;;;;;;;;;;cAwBZ,eAAA,SAAwB,OAAA;;WAEnB,IAAA,EAAM,iBAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,sBAAA;AAAA"}
package/esm/errors.mjs CHANGED
@@ -77,6 +77,25 @@ var McpTransportError = class extends AIError {
77
77
  }
78
78
  };
79
79
  /**
80
+ * `ai.mcp.serve` could not build or start the server. Thrown by `serve()`
81
+ * itself for an invalid source or a name collision, and by `start()` for
82
+ * transport configuration and bind failures — never for a single bad request, which gets an HTTP / JSON-RPC
83
+ * error response instead. Extends {@link AIError} (code `TOOL_EXEC_FAILED`);
84
+ * branch on `error.type`.
85
+ *
86
+ * @example
87
+ * if (error instanceof McpServeError && error.type === "collision") {
88
+ * // two tools share a name — rename one or use a grouped source + prefixToolNames
89
+ * }
90
+ */
91
+ var McpServeError = class extends AIError {
92
+ constructor(message, options) {
93
+ super("TOOL_EXEC_FAILED", message, options);
94
+ this.name = "McpServeError";
95
+ this.type = options.type;
96
+ }
97
+ };
98
+ /**
80
99
  * A web tool failed — a missing optional peer, an absent API key, a host
81
100
  * rejected by the `allowHosts` guardrail, an unparseable URL, or a failed
82
101
  * network call.
@@ -132,5 +151,5 @@ var HttpPolicyError = class extends AIError {
132
151
  };
133
152
 
134
153
  //#endregion
135
- export { CalculatorError, DateTimeError, HttpPolicyError, McpTransportError, WebToolError };
154
+ export { CalculatorError, DateTimeError, HttpPolicyError, McpServeError, McpTransportError, WebToolError };
136
155
  //# sourceMappingURL=errors.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../packages/ai-tools/src/errors.ts"],"sourcesContent":["import { AIError, type AIErrorOptions } from \"@warlock.js/ai\";\n\n/**\n * Why the calculator rejected an expression.\n *\n * - `\"syntax\"` — the expression could not be tokenized or parsed\n * (an unknown character, a misplaced operator, unbalanced parens).\n * - `\"divide-by-zero\"` — evaluation divided (or took a modulo) by zero.\n * - `\"overflow\"` — the computed result was not a finite number.\n */\nexport type CalculatorFailure = \"syntax\" | \"divide-by-zero\" | \"overflow\";\n\n/**\n * Options for {@link CalculatorError} — the structured `type`\n * discriminator so a caller can branch without parsing the message.\n */\nexport type CalculatorErrorOptions = AIErrorOptions & {\n /** Which class of calculator failure occurred. */\n type: CalculatorFailure;\n};\n\n/**\n * The `calculator` tool could not evaluate an expression — it was not\n * valid arithmetic, divided by zero, or overflowed to a non-finite\n * value.\n *\n * **Surface.** Thrown inside the tool handler, where `tool()` wraps it\n * into a `ToolExecutionError` whose message is preserved verbatim and\n * reaches the model as `{ error }` data, so the agent self-corrects\n * rather than crashing. Extends the framework {@link AIError} (category\n * `\"tool\"` via code `TOOL_EXEC_FAILED`) so it flows through the same\n * typed error contract as every other AI error; branch on `error.type`\n * for the specific failure.\n *\n * @example\n * if (error instanceof CalculatorError && error.type === \"divide-by-zero\") {\n * // the expression divided by zero — ask the model to revise it\n * }\n */\nexport class CalculatorError extends AIError {\n /** Which class of calculator failure occurred. */\n public readonly type: CalculatorFailure;\n\n public constructor(message: string, options: CalculatorErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"CalculatorError\";\n this.type = options.type;\n }\n}\n\n/**\n * Why the date-time tool rejected a call.\n *\n * - `\"invalid-input\"` — a required field for the chosen `op` was\n * missing or malformed (an unparsable ISO instant, a bad `amount`).\n * - `\"invalid-unit\"` — `unit` was not one of the supported units.\n * - `\"invalid-time-zone\"` — the IANA time zone was not recognized.\n * - `\"unsupported-op\"` — the `op` was not one this tool implements.\n */\nexport type DateTimeFailure =\n | \"invalid-input\"\n | \"invalid-unit\"\n | \"invalid-time-zone\"\n | \"unsupported-op\";\n\n/**\n * Options for {@link DateTimeError} — the structured `type`\n * discriminator so a caller can branch without parsing the message.\n */\nexport type DateTimeErrorOptions = AIErrorOptions & {\n /** Which class of date-time failure occurred. */\n type: DateTimeFailure;\n};\n\n/**\n * The `date_time` tool could not complete a call — a required field was\n * missing or malformed, the unit/time zone was unrecognized, or the\n * operation is unsupported.\n *\n * **Surface.** Thrown inside the tool handler, where `tool()` wraps it\n * into a `ToolExecutionError` whose message reaches the model as\n * `{ error }` data so the agent self-corrects. Extends the framework\n * {@link AIError} (category `\"tool\"` via code `TOOL_EXEC_FAILED`); branch\n * on `error.type` for the specific failure.\n *\n * @example\n * if (error instanceof DateTimeError && error.type === \"invalid-unit\") {\n * // the model passed an unknown unit — re-prompt with the allowed set\n * }\n */\nexport class DateTimeError extends AIError {\n /** Which class of date-time failure occurred. */\n public readonly type: DateTimeFailure;\n\n public constructor(message: string, options: DateTimeErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"DateTimeError\";\n this.type = options.type;\n }\n}\n\n/**\n * Why an MCP transport operation failed.\n *\n * - `\"connect\"` — the transport could not be opened (child process\n * failed to spawn, HTTP endpoint unreachable) or the `initialize`\n * handshake failed.\n * - `\"protocol\"` — a malformed / unexpected JSON-RPC message, a\n * response that matched no in-flight request, or a missing field.\n * - `\"timeout\"` — a request exceeded its per-call deadline.\n * - `\"closed\"` — the transport was used after it was closed, or the\n * peer closed it mid-call.\n */\nexport type McpTransportFailure = \"connect\" | \"protocol\" | \"timeout\" | \"closed\";\n\n/**\n * Options for {@link McpTransportError} — the structured `type`\n * discriminator plus an optional JSON-RPC method name for branchable\n * diagnostics without parsing the message.\n */\nexport type McpTransportErrorOptions = AIErrorOptions & {\n /** Which class of transport failure occurred. */\n type: McpTransportFailure;\n /** The JSON-RPC method in flight when the failure occurred, if any. */\n method?: string;\n};\n\n/**\n * The MCP client's transport layer failed — it could not connect, the\n * peer spoke malformed JSON-RPC, a call timed out, or the transport was\n * already closed.\n *\n * **Surface.** Connection / handshake failures surface at\n * agent-construction time (the caller `await`s `client.tools()`). A\n * `tools/call` failure raised mid-run is wrapped by `tool()` into a\n * `ToolExecutionError` and reaches the model as `{ error }` data, so the\n * agent self-corrects rather than crashing. Extends the framework\n * {@link AIError} (category `\"tool\"`, code `TOOL_EXEC_FAILED`) so it\n * flows through the same typed error contract as every other AI error;\n * branch on `error.type` for the specific failure.\n *\n * @example\n * if (error instanceof McpTransportError && error.type === \"timeout\") {\n * // the remote call exceeded its deadline — retry or escalate\n * }\n */\nexport class McpTransportError extends AIError {\n /** Which class of transport failure occurred. */\n public readonly type: McpTransportFailure;\n /** The JSON-RPC method in flight when the failure occurred, if any. */\n public readonly method?: string;\n\n public constructor(message: string, options: McpTransportErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"McpTransportError\";\n this.type = options.type;\n this.method = options.method;\n }\n}\n\n/**\n * Why a web tool (`ai.tools.webSearch` / `ai.tools.fetchUrl`) failed\n * before or during a network call.\n *\n * - `\"missing-peer\"` — an optional peer dependency the chosen mode needs\n * (`@mozilla/readability` + `jsdom` for text/markdown extraction, a\n * search provider SDK) is not installed. The message carries a curated\n * `npm install` string for the developer.\n * - `\"missing-key\"` — no API key was supplied via options or the\n * provider's environment variable.\n * - `\"denied-host\"` — the requested URL (or a redirect target) was\n * refused by the outbound policy: a host outside the configured\n * `allowHosts` allowlist, or a private / loopback / link-local /\n * cloud-metadata address blocked by the default private-network deny\n * (an SSRF guardrail).\n * - `\"invalid-url\"` — the supplied URL could not be parsed, or used a\n * non-`http(s)` scheme.\n * - `\"request-failed\"` — the network call itself failed (DNS, connection\n * reset, timeout) or the provider returned a non-OK status.\n */\nexport type WebToolFailure =\n | \"missing-peer\"\n | \"missing-key\"\n | \"denied-host\"\n | \"invalid-url\"\n | \"request-failed\";\n\n/**\n * Options for {@link WebToolError} — the structured `type` discriminator\n * so a caller can branch without parsing the message.\n */\nexport type WebToolErrorOptions = AIErrorOptions & {\n /** Which class of web-tool failure occurred. */\n type: WebToolFailure;\n};\n\n/**\n * A web tool failed — a missing optional peer, an absent API key, a host\n * rejected by the `allowHosts` guardrail, an unparseable URL, or a failed\n * network call.\n *\n * **Surface.** Thrown from inside a tool's `execute`, so the framework's\n * `tool()` wrapper catches it and surfaces it in the returned `{ error }`\n * field (`invoke()` never throws) — the agent reads the failure as data\n * and self-corrects rather than crashing. Extends the framework\n * {@link AIError} (category `\"tool\"` via code `TOOL_EXEC_FAILED`) so it\n * flows through the same typed error contract as every other AI error;\n * branch on `error.type` for the specific failure.\n *\n * @example\n * const { error } = await fetchTool.invoke({ url: \"http://evil.test\" });\n * if (error instanceof WebToolError && error.type === \"denied-host\") {\n * // the host was not in allowHosts — surfaced before any network call\n * }\n */\nexport class WebToolError extends AIError {\n /** Which class of web-tool failure occurred. */\n public readonly type: WebToolFailure;\n\n public constructor(message: string, options: WebToolErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WebToolError\";\n this.type = options.type;\n }\n}\n\n/**\n * Why an `http_request` call was rejected by its own guardrails, before\n * the network request was ever issued.\n *\n * - `\"method-not-allowed\"` — the model requested an HTTP method that is\n * not on the tool's `allowMethods` allowlist (defaults to `[\"GET\"]`).\n * - `\"host-not-allowed\"` — the resolved request host (or a redirect\n * target) was refused by the outbound policy: not on the tool's\n * `allowHosts` allowlist, or a private / loopback / link-local /\n * cloud-metadata address blocked by the default private-network deny\n * (an SSRF guardrail).\n * - `\"invalid-url\"` — the supplied URL (or its join with `baseUrl`)\n * could not be parsed into an absolute `http(s)` URL.\n */\nexport type HttpPolicyFailure = \"method-not-allowed\" | \"host-not-allowed\" | \"invalid-url\";\n\n/**\n * Options for {@link HttpPolicyError} — the structured `type`\n * discriminator so a caller can branch without parsing the message.\n */\nexport type HttpPolicyErrorOptions = AIErrorOptions & {\n /** Which class of policy rejection occurred. */\n type: HttpPolicyFailure;\n};\n\n/**\n * The `http_request` tool refused a call its construction-time policy\n * does not permit — a disallowed method, a host outside the allowlist,\n * or an unparseable URL. The rejection happens *before* any network\n * request, so a guarded tool can never be coaxed into reaching an\n * off-allowlist host (an SSRF guardrail).\n *\n * **Surface.** Thrown from inside the tool's `execute`, so the framework's\n * `tool()` wrapper catches it and surfaces it in the returned `{ error }`\n * field (`invoke()` never throws) — the agent reads the typed failure as\n * data and self-corrects rather than crashing. Extends the framework\n * {@link AIError} (category `\"tool\"` via code `TOOL_EXEC_FAILED`) so it\n * flows through the same typed error contract as every other AI error;\n * branch on `error.type` for the specific failure.\n *\n * @example\n * const { error } = await httpTool.invoke({ url: \"https://evil.test\" });\n * if (error instanceof HttpPolicyError && error.type === \"host-not-allowed\") {\n * // the model tried to reach a host outside the configured allowlist\n * }\n */\nexport class HttpPolicyError extends AIError {\n /** Which class of policy rejection occurred. */\n public readonly type: HttpPolicyFailure;\n\n public constructor(message: string, options: HttpPolicyErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"HttpPolicyError\";\n this.type = options.type;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,kBAAb,cAAqC,QAAQ;CAI3C,AAAO,YAAY,SAAiB,SAAiC;EACnE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF;;;;;;;;;;;;;;;;;AA0CA,IAAa,gBAAb,cAAmC,QAAQ;CAIzC,AAAO,YAAY,SAAiB,SAA+B;EACjE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF;;;;;;;;;;;;;;;;;;;;AA+CA,IAAa,oBAAb,cAAuC,QAAQ;CAM7C,AAAO,YAAY,SAAiB,SAAmC;EACrE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;EACpB,KAAK,SAAS,QAAQ;CACxB;AACF;;;;;;;;;;;;;;;;;;;;AAyDA,IAAa,eAAb,cAAkC,QAAQ;CAIxC,AAAO,YAAY,SAAiB,SAA8B;EAChE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF;;;;;;;;;;;;;;;;;;;;;;AAgDA,IAAa,kBAAb,cAAqC,QAAQ;CAI3C,AAAO,YAAY,SAAiB,SAAiC;EACnE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF"}
1
+ {"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../packages/ai-tools/src/errors.ts"],"sourcesContent":["import { AIError, type AIErrorOptions } from \"@warlock.js/ai\";\n\n/**\n * Why the calculator rejected an expression.\n *\n * - `\"syntax\"` — the expression could not be tokenized or parsed\n * (an unknown character, a misplaced operator, unbalanced parens).\n * - `\"divide-by-zero\"` — evaluation divided (or took a modulo) by zero.\n * - `\"overflow\"` — the computed result was not a finite number.\n */\nexport type CalculatorFailure = \"syntax\" | \"divide-by-zero\" | \"overflow\";\n\n/**\n * Options for {@link CalculatorError} — the structured `type`\n * discriminator so a caller can branch without parsing the message.\n */\nexport type CalculatorErrorOptions = AIErrorOptions & {\n /** Which class of calculator failure occurred. */\n type: CalculatorFailure;\n};\n\n/**\n * The `calculator` tool could not evaluate an expression — it was not\n * valid arithmetic, divided by zero, or overflowed to a non-finite\n * value.\n *\n * **Surface.** Thrown inside the tool handler, where `tool()` wraps it\n * into a `ToolExecutionError` whose message is preserved verbatim and\n * reaches the model as `{ error }` data, so the agent self-corrects\n * rather than crashing. Extends the framework {@link AIError} (category\n * `\"tool\"` via code `TOOL_EXEC_FAILED`) so it flows through the same\n * typed error contract as every other AI error; branch on `error.type`\n * for the specific failure.\n *\n * @example\n * if (error instanceof CalculatorError && error.type === \"divide-by-zero\") {\n * // the expression divided by zero — ask the model to revise it\n * }\n */\nexport class CalculatorError extends AIError {\n /** Which class of calculator failure occurred. */\n public readonly type: CalculatorFailure;\n\n public constructor(message: string, options: CalculatorErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"CalculatorError\";\n this.type = options.type;\n }\n}\n\n/**\n * Why the date-time tool rejected a call.\n *\n * - `\"invalid-input\"` — a required field for the chosen `op` was\n * missing or malformed (an unparsable ISO instant, a bad `amount`).\n * - `\"invalid-unit\"` — `unit` was not one of the supported units.\n * - `\"invalid-time-zone\"` — the IANA time zone was not recognized.\n * - `\"unsupported-op\"` — the `op` was not one this tool implements.\n */\nexport type DateTimeFailure =\n | \"invalid-input\"\n | \"invalid-unit\"\n | \"invalid-time-zone\"\n | \"unsupported-op\";\n\n/**\n * Options for {@link DateTimeError} — the structured `type`\n * discriminator so a caller can branch without parsing the message.\n */\nexport type DateTimeErrorOptions = AIErrorOptions & {\n /** Which class of date-time failure occurred. */\n type: DateTimeFailure;\n};\n\n/**\n * The `date_time` tool could not complete a call — a required field was\n * missing or malformed, the unit/time zone was unrecognized, or the\n * operation is unsupported.\n *\n * **Surface.** Thrown inside the tool handler, where `tool()` wraps it\n * into a `ToolExecutionError` whose message reaches the model as\n * `{ error }` data so the agent self-corrects. Extends the framework\n * {@link AIError} (category `\"tool\"` via code `TOOL_EXEC_FAILED`); branch\n * on `error.type` for the specific failure.\n *\n * @example\n * if (error instanceof DateTimeError && error.type === \"invalid-unit\") {\n * // the model passed an unknown unit — re-prompt with the allowed set\n * }\n */\nexport class DateTimeError extends AIError {\n /** Which class of date-time failure occurred. */\n public readonly type: DateTimeFailure;\n\n public constructor(message: string, options: DateTimeErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"DateTimeError\";\n this.type = options.type;\n }\n}\n\n/**\n * Why an MCP transport operation failed.\n *\n * - `\"connect\"` — the transport could not be opened (child process\n * failed to spawn, HTTP endpoint unreachable) or the `initialize`\n * handshake failed.\n * - `\"protocol\"` — a malformed / unexpected JSON-RPC message, a\n * response that matched no in-flight request, or a missing field.\n * - `\"timeout\"` — a request exceeded its per-call deadline.\n * - `\"closed\"` — the transport was used after it was closed, or the\n * peer closed it mid-call.\n */\nexport type McpTransportFailure = \"connect\" | \"protocol\" | \"timeout\" | \"closed\";\n\n/**\n * Options for {@link McpTransportError} — the structured `type`\n * discriminator plus an optional JSON-RPC method name for branchable\n * diagnostics without parsing the message.\n */\nexport type McpTransportErrorOptions = AIErrorOptions & {\n /** Which class of transport failure occurred. */\n type: McpTransportFailure;\n /** The JSON-RPC method in flight when the failure occurred, if any. */\n method?: string;\n};\n\n/**\n * The MCP client's transport layer failed — it could not connect, the\n * peer spoke malformed JSON-RPC, a call timed out, or the transport was\n * already closed.\n *\n * **Surface.** Connection / handshake failures surface at\n * agent-construction time (the caller `await`s `client.tools()`). A\n * `tools/call` failure raised mid-run is wrapped by `tool()` into a\n * `ToolExecutionError` and reaches the model as `{ error }` data, so the\n * agent self-corrects rather than crashing. Extends the framework\n * {@link AIError} (category `\"tool\"`, code `TOOL_EXEC_FAILED`) so it\n * flows through the same typed error contract as every other AI error;\n * branch on `error.type` for the specific failure.\n *\n * @example\n * if (error instanceof McpTransportError && error.type === \"timeout\") {\n * // the remote call exceeded its deadline — retry or escalate\n * }\n */\nexport class McpTransportError extends AIError {\n /** Which class of transport failure occurred. */\n public readonly type: McpTransportFailure;\n /** The JSON-RPC method in flight when the failure occurred, if any. */\n public readonly method?: string;\n\n public constructor(message: string, options: McpTransportErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"McpTransportError\";\n this.type = options.type;\n this.method = options.method;\n }\n}\n\n/**\n * Why `ai.mcp.serve` refused to build or start a server.\n *\n * - `\"config\"` — invalid serve options: a non-loopback HTTP bind without\n * `auth`, an empty bearer token, a bad `path` / `port`, or\n * `prefixToolNames` without a grouped source.\n * - `\"collision\"` — two exposed tools (or resources / prompts) share a\n * name (URI for resources).\n * - `\"listen\"` — the HTTP server could not bind (port in use, permission\n * denied, unknown host).\n */\nexport type McpServeFailure = \"config\" | \"collision\" | \"listen\";\n\n/**\n * Options for {@link McpServeError} — the structured `type` discriminator.\n */\nexport type McpServeErrorOptions = AIErrorOptions & {\n /** Which class of serve failure occurred. */\n type: McpServeFailure;\n};\n\n/**\n * `ai.mcp.serve` could not build or start the server. Thrown by `serve()`\n * itself for an invalid source or a name collision, and by `start()` for\n * transport configuration and bind failures — never for a single bad request, which gets an HTTP / JSON-RPC\n * error response instead. Extends {@link AIError} (code `TOOL_EXEC_FAILED`);\n * branch on `error.type`.\n *\n * @example\n * if (error instanceof McpServeError && error.type === \"collision\") {\n * // two tools share a name — rename one or use a grouped source + prefixToolNames\n * }\n */\nexport class McpServeError extends AIError {\n /** Which class of serve failure occurred. */\n public readonly type: McpServeFailure;\n\n public constructor(message: string, options: McpServeErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"McpServeError\";\n this.type = options.type;\n }\n}\n\n/**\n * Why a web tool (`ai.tools.webSearch` / `ai.tools.fetchUrl`) failed\n * before or during a network call.\n *\n * - `\"missing-peer\"` — an optional peer dependency the chosen mode needs\n * (`@mozilla/readability` + `jsdom` for text/markdown extraction, a\n * search provider SDK) is not installed. The message carries a curated\n * `npm install` string for the developer.\n * - `\"missing-key\"` — no API key was supplied via options or the\n * provider's environment variable.\n * - `\"denied-host\"` — the requested URL (or a redirect target) was\n * refused by the outbound policy: a host outside the configured\n * `allowHosts` allowlist, or a private / loopback / link-local /\n * cloud-metadata address blocked by the default private-network deny\n * (an SSRF guardrail).\n * - `\"invalid-url\"` — the supplied URL could not be parsed, or used a\n * non-`http(s)` scheme.\n * - `\"request-failed\"` — the network call itself failed (DNS, connection\n * reset, timeout) or the provider returned a non-OK status.\n */\nexport type WebToolFailure =\n | \"missing-peer\"\n | \"missing-key\"\n | \"denied-host\"\n | \"invalid-url\"\n | \"request-failed\";\n\n/**\n * Options for {@link WebToolError} — the structured `type` discriminator\n * so a caller can branch without parsing the message.\n */\nexport type WebToolErrorOptions = AIErrorOptions & {\n /** Which class of web-tool failure occurred. */\n type: WebToolFailure;\n};\n\n/**\n * A web tool failed — a missing optional peer, an absent API key, a host\n * rejected by the `allowHosts` guardrail, an unparseable URL, or a failed\n * network call.\n *\n * **Surface.** Thrown from inside a tool's `execute`, so the framework's\n * `tool()` wrapper catches it and surfaces it in the returned `{ error }`\n * field (`invoke()` never throws) — the agent reads the failure as data\n * and self-corrects rather than crashing. Extends the framework\n * {@link AIError} (category `\"tool\"` via code `TOOL_EXEC_FAILED`) so it\n * flows through the same typed error contract as every other AI error;\n * branch on `error.type` for the specific failure.\n *\n * @example\n * const { error } = await fetchTool.invoke({ url: \"http://evil.test\" });\n * if (error instanceof WebToolError && error.type === \"denied-host\") {\n * // the host was not in allowHosts — surfaced before any network call\n * }\n */\nexport class WebToolError extends AIError {\n /** Which class of web-tool failure occurred. */\n public readonly type: WebToolFailure;\n\n public constructor(message: string, options: WebToolErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WebToolError\";\n this.type = options.type;\n }\n}\n\n/**\n * Why an `http_request` call was rejected by its own guardrails, before\n * the network request was ever issued.\n *\n * - `\"method-not-allowed\"` — the model requested an HTTP method that is\n * not on the tool's `allowMethods` allowlist (defaults to `[\"GET\"]`).\n * - `\"host-not-allowed\"` — the resolved request host (or a redirect\n * target) was refused by the outbound policy: not on the tool's\n * `allowHosts` allowlist, or a private / loopback / link-local /\n * cloud-metadata address blocked by the default private-network deny\n * (an SSRF guardrail).\n * - `\"invalid-url\"` — the supplied URL (or its join with `baseUrl`)\n * could not be parsed into an absolute `http(s)` URL.\n */\nexport type HttpPolicyFailure = \"method-not-allowed\" | \"host-not-allowed\" | \"invalid-url\";\n\n/**\n * Options for {@link HttpPolicyError} — the structured `type`\n * discriminator so a caller can branch without parsing the message.\n */\nexport type HttpPolicyErrorOptions = AIErrorOptions & {\n /** Which class of policy rejection occurred. */\n type: HttpPolicyFailure;\n};\n\n/**\n * The `http_request` tool refused a call its construction-time policy\n * does not permit — a disallowed method, a host outside the allowlist,\n * or an unparseable URL. The rejection happens *before* any network\n * request, so a guarded tool can never be coaxed into reaching an\n * off-allowlist host (an SSRF guardrail).\n *\n * **Surface.** Thrown from inside the tool's `execute`, so the framework's\n * `tool()` wrapper catches it and surfaces it in the returned `{ error }`\n * field (`invoke()` never throws) — the agent reads the typed failure as\n * data and self-corrects rather than crashing. Extends the framework\n * {@link AIError} (category `\"tool\"` via code `TOOL_EXEC_FAILED`) so it\n * flows through the same typed error contract as every other AI error;\n * branch on `error.type` for the specific failure.\n *\n * @example\n * const { error } = await httpTool.invoke({ url: \"https://evil.test\" });\n * if (error instanceof HttpPolicyError && error.type === \"host-not-allowed\") {\n * // the model tried to reach a host outside the configured allowlist\n * }\n */\nexport class HttpPolicyError extends AIError {\n /** Which class of policy rejection occurred. */\n public readonly type: HttpPolicyFailure;\n\n public constructor(message: string, options: HttpPolicyErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"HttpPolicyError\";\n this.type = options.type;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,kBAAb,cAAqC,QAAQ;CAI3C,AAAO,YAAY,SAAiB,SAAiC;EACnE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF;;;;;;;;;;;;;;;;;AA0CA,IAAa,gBAAb,cAAmC,QAAQ;CAIzC,AAAO,YAAY,SAAiB,SAA+B;EACjE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF;;;;;;;;;;;;;;;;;;;;AA+CA,IAAa,oBAAb,cAAuC,QAAQ;CAM7C,AAAO,YAAY,SAAiB,SAAmC;EACrE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;EACpB,KAAK,SAAS,QAAQ;CACxB;AACF;;;;;;;;;;;;;AAmCA,IAAa,gBAAb,cAAmC,QAAQ;CAIzC,AAAO,YAAY,SAAiB,SAA+B;EACjE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF;;;;;;;;;;;;;;;;;;;;AAyDA,IAAa,eAAb,cAAkC,QAAQ;CAIxC,AAAO,YAAY,SAAiB,SAA8B;EAChE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF;;;;;;;;;;;;;;;;;;;;;;AAgDA,IAAa,kBAAb,cAAqC,QAAQ;CAI3C,AAAO,YAAY,SAAiB,SAAiC;EACnE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"http-request.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/http/http-request.ts"],"mappings":";;;;;;;AAqNA;;;;;;;;;;;;;;;AAEmD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAFnC,eAAA,CACd,OAAA,GAAS,kBAAA,GACR,YAAA,CAAa,gBAAA,EAAkB,iBAAA"}
1
+ {"version":3,"file":"http-request.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/http/http-request.ts"],"mappings":";;;;;;;AAgNA;;;;;;;;;;;;;;;AAEmD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAFnC,eAAA,CACd,OAAA,GAAS,kBAAA,GACR,YAAA,CAAa,gBAAA,EAAkB,iBAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"http-request.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/http/http-request.ts"],"sourcesContent":["import {\n guardedFetch,\n OutboundPolicyError,\n tool,\n type ToolContract,\n} from \"@warlock.js/ai\";\nimport type {\n HttpMethod,\n HttpRequestInput,\n HttpRequestOptions,\n HttpRequestResult,\n} from \"../contracts\";\nimport { HttpPolicyError } from \"../errors\";\nimport {\n objectSchema,\n optionalStringEnumField,\n optionalStringRecordField,\n passthroughField,\n stringField,\n} from \"../schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"http_request\";\n\n/** Default per-request wall-clock timeout, in milliseconds. */\nconst DEFAULT_TIMEOUT_MS = 15_000;\n\n/** Default hard cap on response-body bytes before truncation. */\nconst DEFAULT_MAX_BYTES = 1_000_000;\n\n/** The full set of HTTP methods, in the order they appear in {@link HttpMethod}. */\nconst ALL_METHODS: readonly HttpMethod[] = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n\n/** Methods that conventionally carry no request body — `body` is dropped for these. */\nconst BODYLESS_METHODS: ReadonlySet<HttpMethod> = new Set<HttpMethod>([\"GET\"]);\n\n/**\n * Standard Schema for {@link HttpRequestInput}. `url` is required;\n * `method` is constrained to the canonical HTTP verb set (further\n * narrowed to the tool's `allowMethods` at runtime); `headers` is an\n * optional string-to-string record; `body` is an opaque passthrough the\n * handler serializes based on its runtime type.\n */\nconst httpRequestInputSchema = objectSchema<HttpRequestInput>({\n method: optionalStringEnumField<HttpMethod>(ALL_METHODS),\n url: stringField(),\n headers: optionalStringRecordField(),\n body: passthroughField(),\n});\n\n/**\n * Resolve the request target. With a `baseUrl` configured the model\n * supplies a path joined against it; otherwise the model's `url` must be\n * an absolute `http(s)` URL. Throws a typed {@link HttpPolicyError} of\n * type `\"invalid-url\"` when the result cannot be parsed or is not an\n * `http`/`https` URL — surfaced as `{ error }` data, never a crash.\n */\nfunction resolveUrl(rawUrl: string, baseUrl: string | undefined): URL {\n let resolved: URL;\n\n try {\n // `new URL(input, base)` joins relative paths against `base` and\n // ignores `base` when `input` is already absolute, which is exactly\n // the \"path vs full URL\" behavior the design specifies.\n resolved = baseUrl !== undefined ? new URL(rawUrl, baseUrl) : new URL(rawUrl);\n } catch {\n throw new HttpPolicyError(\n `http_request could not resolve a valid URL from \"${rawUrl}\"` +\n (baseUrl !== undefined ? ` against base \"${baseUrl}\".` : \".\"),\n { type: \"invalid-url\" },\n );\n }\n\n if (resolved.protocol !== \"http:\" && resolved.protocol !== \"https:\") {\n throw new HttpPolicyError(\n `http_request only permits http(s) URLs; got \"${resolved.protocol}\".`,\n { type: \"invalid-url\" },\n );\n }\n\n return resolved;\n}\n\n/**\n * Read a `Response` body, capping at `maxBytes`. Returns the decoded text\n * and whether it was cut off. Streams chunk-by-chunk so an oversized body\n * is abandoned at the cap rather than fully buffered; falls back to\n * `response.text()` (then a post-hoc byte slice) when the body is not a\n * readable stream (e.g. a stubbed `Response` in tests).\n */\nasync function readCappedBody(\n response: Response,\n maxBytes: number,\n): Promise<{ text: string; truncated: boolean }> {\n const body = response.body;\n\n if (!body) {\n return { text: \"\", truncated: false };\n }\n\n const decoder = new TextDecoder();\n const reader = body.getReader();\n let received = 0;\n let truncated = false;\n let text = \"\";\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (!value) {\n continue;\n }\n\n const remaining = maxBytes - received;\n\n if (value.byteLength > remaining) {\n text += decoder.decode(value.subarray(0, remaining), { stream: true });\n received = maxBytes;\n truncated = true;\n break;\n }\n\n text += decoder.decode(value, { stream: true });\n received += value.byteLength;\n }\n } finally {\n // Release the lock and abandon any unread remainder.\n await reader.cancel().catch(() => undefined);\n reader.releaseLock();\n }\n\n text += decoder.decode();\n\n return { text, truncated };\n}\n\n/**\n * Decide whether a response's `content-type` indicates JSON. Matches\n * `application/json` and the `+json` structured-suffix convention\n * (e.g. `application/vnd.api+json`), case-insensitively.\n */\nfunction isJsonContentType(contentType: string | undefined): boolean {\n if (!contentType) {\n return false;\n }\n\n const value = contentType.toLowerCase();\n\n return value.includes(\"application/json\") || value.includes(\"+json\");\n}\n\n/**\n * Build the agent-facing `http_request` tool — a guarded HTTP/REST client\n * over the global `fetch`. The `options` bound what the model may do; the\n * model supplies the per-call URL / method / headers / body within those\n * rails.\n *\n * **Guardrails (all enforced before the network call).**\n * - **Method allowlist** — `allowMethods` (default `[\"GET\"]`). A method\n * outside the list is rejected with a typed\n * {@link HttpPolicyError} (`type: \"method-not-allowed\"`).\n * - **Private-network deny (default).** The request — and every redirect\n * hop — goes through the framework's `guardedFetch` outbound policy,\n * which refuses private / loopback / link-local / cloud-metadata\n * addresses (and hostnames resolving to them) unless\n * `allowPrivateNetwork: true` is set. This applies even when\n * `allowHosts` is not configured, so a bare `ai.tools.http()` is not\n * an SSRF primitive (`type: \"host-not-allowed\"`).\n * - **Host allowlist** — when `allowHosts` is set, any other host is\n * rejected (`type: \"host-not-allowed\"`), an SSRF guardrail; redirect\n * targets are held to the same allowlist.\n * - **`baseUrl` join** — when configured, the model passes a path that\n * is resolved against `baseUrl`; otherwise it must pass an absolute\n * `http(s)` URL. An unresolvable URL is rejected\n * (`type: \"invalid-url\"`).\n *\n * **Request shaping.** Static `options.headers` are merged under the\n * per-call `headers` (the per-call value wins). An object `body` is\n * JSON-serialized with a `content-type: application/json` default; a\n * string `body` is sent verbatim; `body` is dropped for bodyless methods\n * (`GET`). The call is bounded by `timeoutMs` (default `15_000`) via an\n * `AbortController`, also wired to `ctx.signal` for cooperative\n * cancellation.\n *\n * **Response shaping.** Headers are returned with lower-cased keys. The\n * body is read up to `maxBytes` (default `1_000_000`) and JSON-parsed\n * when the response `content-type` is JSON, otherwise returned as text;\n * `truncated` is `true` when the body was cut off at the cap (a truncated\n * JSON body is returned as the raw partial string, since it can no longer\n * be parsed).\n *\n * **Errors flow as data.** Every guardrail rejection and network failure\n * is thrown inside `execute`; the framework's `tool()` wrapper catches it\n * and surfaces it in the returned `{ error }` field, so the agent reads\n * the failure and self-corrects rather than crashing.\n *\n * @param options - Construction-time policy bounding the tool.\n * @returns A {@link ToolContract} the agent can call as `http_request`.\n *\n * @example\n * const stripe = httpRequestTool({\n * baseUrl: \"https://api.stripe.com\",\n * allowHosts: [\"api.stripe.com\"],\n * allowMethods: [\"GET\", \"POST\"],\n * headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },\n * });\n * const { data } = await stripe.invoke({ method: \"GET\", url: \"/v1/charges\" });\n */\nexport function httpRequestTool(\n options: HttpRequestOptions = {},\n): ToolContract<HttpRequestInput, HttpRequestResult> {\n const allowMethods = options.allowMethods ?? [\"GET\"];\n const allowedMethodSet = new Set<HttpMethod>(allowMethods);\n const allowHostSet = options.allowHosts ? new Set(options.allowHosts) : undefined;\n const allowPrivateNetwork = options.allowPrivateNetwork ?? false;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n const staticHeaders = options.headers;\n\n return tool<HttpRequestInput, HttpRequestResult>({\n name: options.name ?? DEFAULT_NAME,\n description:\n \"Issue an HTTP request and return the status, response headers, and \" +\n \"parsed body. Allowed methods and hosts are restricted by the tool's \" +\n \"configuration; a request outside those rails is rejected before any \" +\n \"network call. Pass an object body to send JSON, or a string to send \" +\n \"it verbatim. The response body is JSON-parsed when the content-type \" +\n \"is JSON, otherwise returned as text, and is capped — `truncated` is \" +\n \"true when the body was cut off.\",\n action: (input) => `Requesting ${input.method ?? \"GET\"} ${input.url}`,\n input: httpRequestInputSchema,\n async execute(input, ctx) {\n const method: HttpMethod = input.method ?? \"GET\";\n\n // 1. Method allowlist — rejected before anything else.\n if (!allowedMethodSet.has(method)) {\n throw new HttpPolicyError(\n `http_request method \"${method}\" is not allowed. ` +\n `Permitted methods: ${[...allowedMethodSet].join(\", \")}.`,\n { type: \"method-not-allowed\" },\n );\n }\n\n // 2. URL resolution (baseUrl join when configured).\n const url = resolveUrl(input.url, options.baseUrl);\n\n // 3. Host allowlist — SSRF guardrail, before the fetch.\n if (allowHostSet && !allowHostSet.has(url.hostname)) {\n throw new HttpPolicyError(\n `http_request host \"${url.hostname}\" is not in the allowlist. ` +\n `Permitted hosts: ${[...allowHostSet].join(\", \")}.`,\n { type: \"host-not-allowed\" },\n );\n }\n\n // 4. Merge headers — static option headers under the per-call ones,\n // so a per-call header overrides a static default of the same name.\n const headers: Record<string, string> = { ...staticHeaders, ...input.headers };\n\n // 5. Shape the body. Dropped for bodyless methods; objects become\n // JSON (with a default content-type); strings are sent verbatim.\n let body: string | undefined;\n\n if (!BODYLESS_METHODS.has(method) && input.body !== undefined) {\n if (typeof input.body === \"string\") {\n body = input.body;\n } else {\n body = JSON.stringify(input.body);\n\n const hasContentType = Object.keys(headers).some(\n (key) => key.toLowerCase() === \"content-type\",\n );\n\n if (!hasContentType) {\n headers[\"content-type\"] = \"application/json\";\n }\n }\n }\n\n // 6. Issue the request through the shared hardened outbound path:\n // private-IP deny by default, per-hop redirect re-validation,\n // timeout, and the caller's signal — never a raw fetch.\n let response: Response;\n\n try {\n response = await guardedFetch(\n url.toString(),\n {\n allowedSchemes: [\"http\", \"https\"],\n hostAllowlist: options.allowHosts,\n denyPrivateIPsAfterDNS: !allowPrivateNetwork,\n timeoutMs,\n signal: ctx?.signal,\n },\n { method, headers, body },\n );\n } catch (cause) {\n if (cause instanceof OutboundPolicyError) {\n throw new HttpPolicyError(`http_request blocked: ${cause.message}`, {\n type: \"host-not-allowed\",\n cause,\n });\n }\n\n throw cause;\n }\n\n // 7. Collect response headers with lower-cased keys.\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key.toLowerCase()] = value;\n });\n\n // 8. Read the body up to the cap, then parse-or-pass.\n const { text, truncated } = await readCappedBody(response, maxBytes);\n\n let parsedBody: unknown = text;\n\n // A truncated body can no longer be valid JSON, so only attempt a\n // parse on a complete JSON response; otherwise hand back the raw text.\n if (!truncated && isJsonContentType(responseHeaders[\"content-type\"]) && text.length > 0) {\n try {\n parsedBody = JSON.parse(text);\n } catch {\n // Content-type claimed JSON but the body was not — fall back to\n // the raw text rather than failing the whole call.\n parsedBody = text;\n }\n }\n\n return {\n status: response.status,\n headers: responseHeaders,\n body: parsedBody,\n truncated,\n };\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"http-request.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/http/http-request.ts"],"sourcesContent":["import { guardedFetch, OutboundPolicyError, tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n HttpMethod,\n HttpRequestInput,\n HttpRequestOptions,\n HttpRequestResult,\n} from \"../contracts\";\nimport { HttpPolicyError } from \"../errors\";\nimport {\n objectSchema,\n optionalStringEnumField,\n optionalStringRecordField,\n passthroughField,\n stringField,\n} from \"../schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"http_request\";\n\n/** Default per-request wall-clock timeout, in milliseconds. */\nconst DEFAULT_TIMEOUT_MS = 15_000;\n\n/** Default hard cap on response-body bytes before truncation. */\nconst DEFAULT_MAX_BYTES = 1_000_000;\n\n/** The full set of HTTP methods, in the order they appear in {@link HttpMethod}. */\nconst ALL_METHODS: readonly HttpMethod[] = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n\n/** Methods that conventionally carry no request body — `body` is dropped for these. */\nconst BODYLESS_METHODS: ReadonlySet<HttpMethod> = new Set<HttpMethod>([\"GET\"]);\n\n/**\n * Standard Schema for {@link HttpRequestInput}. `url` is required;\n * `method` is constrained to the canonical HTTP verb set (further\n * narrowed to the tool's `allowMethods` at runtime); `headers` is an\n * optional string-to-string record; `body` is an opaque passthrough the\n * handler serializes based on its runtime type.\n */\nconst httpRequestInputSchema = objectSchema<HttpRequestInput>({\n method: optionalStringEnumField<HttpMethod>(ALL_METHODS),\n url: stringField(),\n headers: optionalStringRecordField(),\n body: passthroughField(),\n});\n\n/**\n * Resolve the request target. With a `baseUrl` configured the model\n * supplies a path joined against it; otherwise the model's `url` must be\n * an absolute `http(s)` URL. Throws a typed {@link HttpPolicyError} of\n * type `\"invalid-url\"` when the result cannot be parsed or is not an\n * `http`/`https` URL — surfaced as `{ error }` data, never a crash.\n */\nfunction resolveUrl(rawUrl: string, baseUrl: string | undefined): URL {\n let resolved: URL;\n\n try {\n // `new URL(input, base)` joins relative paths against `base` and\n // ignores `base` when `input` is already absolute, which is exactly\n // the \"path vs full URL\" behavior the design specifies.\n resolved = baseUrl !== undefined ? new URL(rawUrl, baseUrl) : new URL(rawUrl);\n } catch {\n throw new HttpPolicyError(\n `http_request could not resolve a valid URL from \"${rawUrl}\"` +\n (baseUrl !== undefined ? ` against base \"${baseUrl}\".` : \".\"),\n { type: \"invalid-url\" },\n );\n }\n\n if (resolved.protocol !== \"http:\" && resolved.protocol !== \"https:\") {\n throw new HttpPolicyError(\n `http_request only permits http(s) URLs; got \"${resolved.protocol}\".`,\n { type: \"invalid-url\" },\n );\n }\n\n return resolved;\n}\n\n/**\n * Read a `Response` body, capping at `maxBytes`. Returns the decoded text\n * and whether it was cut off. Streams chunk-by-chunk so an oversized body\n * is abandoned at the cap rather than fully buffered; falls back to\n * `response.text()` (then a post-hoc byte slice) when the body is not a\n * readable stream (e.g. a stubbed `Response` in tests).\n */\nasync function readCappedBody(\n response: Response,\n maxBytes: number,\n): Promise<{ text: string; truncated: boolean }> {\n const body = response.body;\n\n if (!body) {\n return { text: \"\", truncated: false };\n }\n\n const decoder = new TextDecoder();\n const reader = body.getReader();\n let received = 0;\n let truncated = false;\n let text = \"\";\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (!value) {\n continue;\n }\n\n const remaining = maxBytes - received;\n\n if (value.byteLength > remaining) {\n text += decoder.decode(value.subarray(0, remaining), { stream: true });\n received = maxBytes;\n truncated = true;\n break;\n }\n\n text += decoder.decode(value, { stream: true });\n received += value.byteLength;\n }\n } finally {\n // Release the lock and abandon any unread remainder.\n await reader.cancel().catch(() => undefined);\n reader.releaseLock();\n }\n\n text += decoder.decode();\n\n return { text, truncated };\n}\n\n/**\n * Decide whether a response's `content-type` indicates JSON. Matches\n * `application/json` and the `+json` structured-suffix convention\n * (e.g. `application/vnd.api+json`), case-insensitively.\n */\nfunction isJsonContentType(contentType: string | undefined): boolean {\n if (!contentType) {\n return false;\n }\n\n const value = contentType.toLowerCase();\n\n return value.includes(\"application/json\") || value.includes(\"+json\");\n}\n\n/**\n * Build the agent-facing `http_request` tool — a guarded HTTP/REST client\n * over the global `fetch`. The `options` bound what the model may do; the\n * model supplies the per-call URL / method / headers / body within those\n * rails.\n *\n * **Guardrails (all enforced before the network call).**\n * - **Method allowlist** — `allowMethods` (default `[\"GET\"]`). A method\n * outside the list is rejected with a typed\n * {@link HttpPolicyError} (`type: \"method-not-allowed\"`).\n * - **Private-network deny (default).** The request — and every redirect\n * hop — goes through the framework's `guardedFetch` outbound policy,\n * which refuses private / loopback / link-local / cloud-metadata\n * addresses (and hostnames resolving to them) unless\n * `allowPrivateNetwork: true` is set. This applies even when\n * `allowHosts` is not configured, so a bare `ai.tools.http()` is not\n * an SSRF primitive (`type: \"host-not-allowed\"`).\n * - **Host allowlist** — when `allowHosts` is set, any other host is\n * rejected (`type: \"host-not-allowed\"`), an SSRF guardrail; redirect\n * targets are held to the same allowlist.\n * - **`baseUrl` join** — when configured, the model passes a path that\n * is resolved against `baseUrl`; otherwise it must pass an absolute\n * `http(s)` URL. An unresolvable URL is rejected\n * (`type: \"invalid-url\"`).\n *\n * **Request shaping.** Static `options.headers` are merged under the\n * per-call `headers` (the per-call value wins). An object `body` is\n * JSON-serialized with a `content-type: application/json` default; a\n * string `body` is sent verbatim; `body` is dropped for bodyless methods\n * (`GET`). The call is bounded by `timeoutMs` (default `15_000`) via an\n * `AbortController`, also wired to `ctx.signal` for cooperative\n * cancellation.\n *\n * **Response shaping.** Headers are returned with lower-cased keys. The\n * body is read up to `maxBytes` (default `1_000_000`) and JSON-parsed\n * when the response `content-type` is JSON, otherwise returned as text;\n * `truncated` is `true` when the body was cut off at the cap (a truncated\n * JSON body is returned as the raw partial string, since it can no longer\n * be parsed).\n *\n * **Errors flow as data.** Every guardrail rejection and network failure\n * is thrown inside `execute`; the framework's `tool()` wrapper catches it\n * and surfaces it in the returned `{ error }` field, so the agent reads\n * the failure and self-corrects rather than crashing.\n *\n * @param options - Construction-time policy bounding the tool.\n * @returns A {@link ToolContract} the agent can call as `http_request`.\n *\n * @example\n * const stripe = httpRequestTool({\n * baseUrl: \"https://api.stripe.com\",\n * allowHosts: [\"api.stripe.com\"],\n * allowMethods: [\"GET\", \"POST\"],\n * headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },\n * });\n * const { data } = await stripe.invoke({ method: \"GET\", url: \"/v1/charges\" });\n */\nexport function httpRequestTool(\n options: HttpRequestOptions = {},\n): ToolContract<HttpRequestInput, HttpRequestResult> {\n const allowMethods = options.allowMethods ?? [\"GET\"];\n const allowedMethodSet = new Set<HttpMethod>(allowMethods);\n const allowHostSet = options.allowHosts ? new Set(options.allowHosts) : undefined;\n const allowPrivateNetwork = options.allowPrivateNetwork ?? false;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n const staticHeaders = options.headers;\n\n return tool<HttpRequestInput, HttpRequestResult>({\n name: options.name ?? DEFAULT_NAME,\n description:\n \"Issue an HTTP request and return the status, response headers, and \" +\n \"parsed body. Allowed methods and hosts are restricted by the tool's \" +\n \"configuration; a request outside those rails is rejected before any \" +\n \"network call. Pass an object body to send JSON, or a string to send \" +\n \"it verbatim. The response body is JSON-parsed when the content-type \" +\n \"is JSON, otherwise returned as text, and is capped — `truncated` is \" +\n \"true when the body was cut off.\",\n action: (input) => `Requesting ${input.method ?? \"GET\"} ${input.url}`,\n input: httpRequestInputSchema,\n async execute(input, ctx) {\n const method: HttpMethod = input.method ?? \"GET\";\n\n // 1. Method allowlist — rejected before anything else.\n if (!allowedMethodSet.has(method)) {\n throw new HttpPolicyError(\n `http_request method \"${method}\" is not allowed. ` +\n `Permitted methods: ${[...allowedMethodSet].join(\", \")}.`,\n { type: \"method-not-allowed\" },\n );\n }\n\n // 2. URL resolution (baseUrl join when configured).\n const url = resolveUrl(input.url, options.baseUrl);\n\n // 3. Host allowlist — SSRF guardrail, before the fetch.\n if (allowHostSet && !allowHostSet.has(url.hostname)) {\n throw new HttpPolicyError(\n `http_request host \"${url.hostname}\" is not in the allowlist. ` +\n `Permitted hosts: ${[...allowHostSet].join(\", \")}.`,\n { type: \"host-not-allowed\" },\n );\n }\n\n // 4. Merge headers — static option headers under the per-call ones,\n // so a per-call header overrides a static default of the same name.\n const headers: Record<string, string> = { ...staticHeaders, ...input.headers };\n\n // 5. Shape the body. Dropped for bodyless methods; objects become\n // JSON (with a default content-type); strings are sent verbatim.\n let body: string | undefined;\n\n if (!BODYLESS_METHODS.has(method) && input.body !== undefined) {\n if (typeof input.body === \"string\") {\n body = input.body;\n } else {\n body = JSON.stringify(input.body);\n\n const hasContentType = Object.keys(headers).some(\n (key) => key.toLowerCase() === \"content-type\",\n );\n\n if (!hasContentType) {\n headers[\"content-type\"] = \"application/json\";\n }\n }\n }\n\n // 6. Issue the request through the shared hardened outbound path:\n // private-IP deny by default, per-hop redirect re-validation,\n // timeout, and the caller's signal — never a raw fetch.\n let response: Response;\n\n try {\n response = await guardedFetch(\n url.toString(),\n {\n allowedSchemes: [\"http\", \"https\"],\n hostAllowlist: options.allowHosts,\n denyPrivateIPsAfterDNS: !allowPrivateNetwork,\n timeoutMs,\n signal: ctx?.signal,\n },\n { method, headers, body },\n );\n } catch (cause) {\n if (cause instanceof OutboundPolicyError) {\n throw new HttpPolicyError(`http_request blocked: ${cause.message}`, {\n type: \"host-not-allowed\",\n cause,\n });\n }\n\n throw cause;\n }\n\n // 7. Collect response headers with lower-cased keys.\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key.toLowerCase()] = value;\n });\n\n // 8. Read the body up to the cap, then parse-or-pass.\n const { text, truncated } = await readCappedBody(response, maxBytes);\n\n let parsedBody: unknown = text;\n\n // A truncated body can no longer be valid JSON, so only attempt a\n // parse on a complete JSON response; otherwise hand back the raw text.\n if (!truncated && isJsonContentType(responseHeaders[\"content-type\"]) && text.length > 0) {\n try {\n parsedBody = JSON.parse(text);\n } catch {\n // Content-type claimed JSON but the body was not — fall back to\n // the raw text rather than failing the whole call.\n parsedBody = text;\n }\n }\n\n return {\n status: response.status,\n headers: responseHeaders,\n body: parsedBody,\n truncated,\n };\n },\n });\n}\n"],"mappings":"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"}