@warlock.js/ai-tools 5.28.0 → 5.30.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 +12 -0
- package/cjs/index.cjs +15 -15
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/http.type.d.mts +1 -1
- package/esm/contracts/http.type.d.mts.map +1 -1
- package/esm/contracts/mcp.type.d.mts +1 -1
- package/esm/contracts/mcp.type.d.mts.map +1 -1
- package/esm/contracts/utility.type.d.mts +1 -1
- package/esm/contracts/utility.type.d.mts.map +1 -1
- package/esm/contracts/web.type.d.mts +1 -1
- package/esm/contracts/web.type.d.mts.map +1 -1
- package/esm/errors.d.mts +1 -1
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +1 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/http/http-request.d.mts +1 -1
- package/esm/http/http-request.d.mts.map +1 -1
- package/esm/http/http-request.mjs +1 -1
- package/esm/http/http-request.mjs.map +1 -1
- package/esm/mcp/client.mjs +1 -1
- package/esm/mcp/client.mjs.map +1 -1
- package/esm/mcp/index.d.mts +1 -1
- package/esm/mcp/index.d.mts.map +1 -1
- package/esm/mcp/index.mjs +1 -1
- package/esm/mcp/index.mjs.map +1 -1
- package/esm/mcp/json-schema-to-standard.d.mts +1 -1
- package/esm/mcp/json-schema-to-standard.d.mts.map +1 -1
- package/esm/mcp/json-schema-to-standard.mjs +1 -1
- package/esm/mcp/json-schema-to-standard.mjs.map +1 -1
- package/esm/mcp/serve.d.mts +1 -1
- package/esm/mcp/serve.d.mts.map +1 -1
- package/esm/mcp/serve.mjs +1 -1
- package/esm/mcp/serve.mjs.map +1 -1
- package/esm/mcp/transport.d.mts +1 -1
- package/esm/mcp/transport.d.mts.map +1 -1
- package/esm/mcp/transport.mjs +1 -1
- package/esm/mcp/transport.mjs.map +1 -1
- package/esm/mcp/transport.type.d.mts +1 -1
- package/esm/mcp/transport.type.d.mts.map +1 -1
- package/esm/register.d.mts +1 -1
- package/esm/register.d.mts.map +1 -1
- package/esm/register.mjs +1 -1
- package/esm/register.mjs.map +1 -1
- package/esm/schema.mjs +1 -1
- package/esm/schema.mjs.map +1 -1
- package/esm/utility/calculator.d.mts +1 -1
- package/esm/utility/calculator.d.mts.map +1 -1
- package/esm/utility/calculator.mjs +1 -1
- package/esm/utility/calculator.mjs.map +1 -1
- package/esm/utility/date-time.d.mts +1 -1
- package/esm/utility/date-time.d.mts.map +1 -1
- package/esm/utility/date-time.mjs +1 -1
- package/esm/utility/date-time.mjs.map +1 -1
- package/esm/utility/schema.mjs +1 -1
- package/esm/utility/schema.mjs.map +1 -1
- package/esm/web/fetch-url.d.mts +1 -1
- package/esm/web/fetch-url.d.mts.map +1 -1
- package/esm/web/fetch-url.mjs +1 -1
- package/esm/web/fetch-url.mjs.map +1 -1
- package/esm/web/schema.mjs +1 -1
- package/esm/web/schema.mjs.map +1 -1
- package/esm/web/web-search.d.mts +1 -1
- package/esm/web/web-search.d.mts.map +1 -1
- package/esm/web/web-search.mjs +1 -1
- package/esm/web/web-search.mjs.map +1 -1
- package/llms-full.txt +14 -14
- package/llms.txt +3 -3
- package/package.json +4 -3
- package/skills/README.md +4 -4
- package/skills/connect-mcp-server/SKILL.md +4 -4
- package/skills/expose-as-mcp-server/SKILL.md +4 -4
- package/skills/use-web-and-http-tools/SKILL.md +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-url.mjs","names":[],"sources":["../../../../../../../ai-tools/src/web/fetch-url.ts"],"sourcesContent":["import {\n guardedFetch,\n OutboundPolicyError,\n tool,\n type ToolContract,\n} from \"@warlock.js/ai\";\nimport type {\n FetchUrlExtract,\n FetchUrlInput,\n FetchUrlOptions,\n FetchUrlResult,\n} from \"../contracts\";\nimport { WebToolError } from \"../errors\";\nimport { objectSchema, stringField } from \"./schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"fetch_url\";\n\n/** Default hard cap on response bytes read before truncation. */\nconst DEFAULT_MAX_BYTES = 1_000_000;\n\n/** Default per-request timeout in milliseconds. */\nconst DEFAULT_TIMEOUT_MS = 15_000;\n\n/** Default rendering mode for the fetched document. */\nconst DEFAULT_EXTRACT: FetchUrlExtract = \"text\";\n\n/** Input schema: `{ url }`. */\nconst inputSchema = objectSchema<FetchUrlInput>({\n url: stringField(),\n});\n\n// ============================================================\n// Lazily-loaded @mozilla/readability + jsdom (OPTIONAL peers)\n// ============================================================\n\n/**\n * Minimal structural shapes of the optional `@mozilla/readability` + `jsdom`\n * peers — only the members this file actually uses. Declared locally so the\n * package type-checks even when the peers are NOT installed (they are lazy\n * optional peers, loaded below via a `string` specifier so `tsc` never tries\n * to statically resolve them).\n */\ninterface ReadabilityArticleLike {\n readonly content?: string | null;\n readonly textContent?: string | null;\n}\ninterface ReadabilityInstanceLike {\n parse(): ReadabilityArticleLike | null;\n}\ninterface ReadabilityModuleLike {\n Readability: new (document: unknown) => ReadabilityInstanceLike;\n}\ninterface JsdomInstanceLike {\n readonly window: { readonly document: unknown };\n}\ninterface JsdomModuleLike {\n JSDOM: new (html: string, options?: { url?: string }) => JsdomInstanceLike;\n}\n\n// String specifiers typed `string` (not a literal) so `tsc` does not attempt\n// to resolve these optional peers at build time.\nconst READABILITY_MODULE_ID: string = \"@mozilla/readability\";\nconst JSDOM_MODULE_ID: string = \"jsdom\";\n\nlet ReadabilitySdk: ReadabilityModuleLike;\nlet JsdomSdk: JsdomModuleLike;\nlet isReadabilityAvailable: boolean | undefined;\nlet loadingPromise: Promise<void> | undefined;\n\nconst READABILITY_INSTALL_INSTRUCTIONS = `\nThe fetch_url text/markdown extractor requires the @mozilla/readability and jsdom packages.\nInstall them with:\n\n npm install @mozilla/readability jsdom\n\nOr with your preferred package manager:\n\n pnpm add @mozilla/readability jsdom\n yarn add @mozilla/readability jsdom\n`.trim();\n\n/**\n * Settle the lazy import of `@mozilla/readability` + `jsdom` once,\n * concurrency-safe. Only needed for the `\"text\"` / `\"markdown\"` extract\n * modes — `\"html\"` returns the raw body and never loads them. A bare\n * `catch` flips the flag to `false`; the curated install string surfaces\n * at use time via {@link WebToolError}, never a raw module-resolution\n * stack trace.\n */\nfunction loadReadability(): Promise<void> {\n if (isReadabilityAvailable !== undefined) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n ReadabilitySdk = (await import(READABILITY_MODULE_ID)) as ReadabilityModuleLike;\n JsdomSdk = (await import(JSDOM_MODULE_ID)) as JsdomModuleLike;\n isReadabilityAvailable = true;\n } catch {\n isReadabilityAvailable = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * Parse the input URL, rejecting anything unparseable or not over\n * `http`/`https` with a typed {@link WebToolError}.\n */\nfunction parseUrl(raw: string): URL {\n let url: URL;\n\n try {\n url = new URL(raw);\n } catch {\n throw new WebToolError(`fetch_url received an unparseable URL: \"${raw}\".`, {\n type: \"invalid-url\",\n });\n }\n\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new WebToolError(\n `fetch_url only supports http(s) URLs; got \"${url.protocol}\".`,\n { type: \"invalid-url\" },\n );\n }\n\n return url;\n}\n\n/**\n * Enforce the host allowlist (an SSRF guardrail) before any network call.\n * When `allowHosts` is set, a request to a host not in it is rejected;\n * matching is exact on the URL hostname (case-insensitive).\n */\nfunction assertHostAllowed(url: URL, allowHosts?: string[]): void {\n if (!allowHosts || allowHosts.length === 0) {\n return;\n }\n\n const host = url.hostname.toLowerCase();\n const allowed = allowHosts.some((entry) => entry.toLowerCase() === host);\n\n if (!allowed) {\n throw new WebToolError(\n `fetch_url blocked host \"${url.hostname}\" — it is not in the allowHosts allowlist.`,\n { type: \"denied-host\", context: { host: url.hostname } },\n );\n }\n}\n\n/**\n * Read the response body up to `maxBytes`, stopping early once the cap is\n * reached. Returns the decoded text and whether it was truncated. When\n * the body has no stream (a stubbed `Response`), falls back to `.text()`\n * and truncates the decoded string at `maxBytes`.\n */\nasync function readBody(\n response: Response,\n maxBytes: number,\n): Promise<{ body: string; truncated: boolean }> {\n const stream = response.body;\n\n if (!stream) {\n const text = await response.text();\n\n if (text.length > maxBytes) {\n return { body: text.slice(0, maxBytes), truncated: true };\n }\n\n return { body: text, truncated: false };\n }\n\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let received = 0;\n let truncated = false;\n\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (value) {\n received += value.byteLength;\n chunks.push(value);\n\n if (received >= maxBytes) {\n truncated = true;\n await reader.cancel();\n break;\n }\n }\n }\n\n const merged = new Uint8Array(received);\n let offset = 0;\n\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n const sliced = truncated ? merged.subarray(0, maxBytes) : merged;\n const body = new TextDecoder().decode(sliced);\n\n return { body, truncated };\n}\n\n/**\n * Render the fetched HTML into the configured `extract` mode. `\"html\"`\n * returns the raw body untouched; `\"text\"` and `\"markdown\"` run\n * `@mozilla/readability` over a `jsdom` document, throwing a curated\n * {@link WebToolError} when those optional peers are absent. `\"markdown\"`\n * applies a light heading/paragraph conversion over the extracted text.\n */\nasync function render(\n html: string,\n url: string,\n extract: FetchUrlExtract,\n): Promise<string> {\n if (extract === \"html\") {\n return html;\n }\n\n await loadReadability();\n\n if (!isReadabilityAvailable) {\n throw new WebToolError(READABILITY_INSTALL_INSTRUCTIONS, { type: \"missing-peer\" });\n }\n\n const dom = new JsdomSdk.JSDOM(html, { url });\n const article = new ReadabilitySdk.Readability(dom.window.document).parse();\n\n if (extract === \"markdown\") {\n return article?.content ? htmlToMarkdown(article.content) : (article?.textContent ?? \"\");\n }\n\n return article?.textContent ?? \"\";\n}\n\n/**\n * Minimal HTML→Markdown reduction for readability's extracted article\n * HTML — headings become `#` prefixes, paragraphs/line-breaks become\n * blank-line separated blocks, and remaining tags are stripped. This is a\n * pragmatic conversion, not a full CommonMark serializer.\n */\nfunction htmlToMarkdown(html: string): string {\n return html\n .replace(/<h([1-6])[^>]*>(.*?)<\\/h\\1>/gis, (_match, level: string, text: string) => {\n const hashes = \"#\".repeat(Number(level));\n\n return `\\n\\n${hashes} ${stripTags(text).trim()}\\n\\n`;\n })\n .replace(/<\\/(p|div|section|article|li)>/gi, \"\\n\\n\")\n .replace(/<br\\s*\\/?>/gi, \"\\n\")\n .replace(/<[^>]+>/g, \"\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n}\n\n/** Strip any remaining HTML tags from a fragment. */\nfunction stripTags(html: string): string {\n return html.replace(/<[^>]+>/g, \"\");\n}\n\n/**\n * Build the agent-facing `fetch_url` tool — fetch a URL over the global\n * `fetch` (Node 18+) and hand the model back rendered `content`.\n *\n * Guardrails, applied in order before/around the network call:\n * - **Private-network deny (default).** Every request — and every\n * redirect hop — goes through the framework's `guardedFetch` outbound\n * policy, which refuses private / loopback / link-local /\n * cloud-metadata 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.fetchUrl()` is\n * not an SSRF primitive.\n * - **`allowHosts`** — when set, a request to any host not in the list is\n * rejected before the fetch, and redirect targets are held to the same\n * allowlist.\n * - **`timeoutMs`** — the request is aborted via `AbortSignal.timeout`\n * (default {@link DEFAULT_TIMEOUT_MS}).\n * - **`maxBytes`** — the response body is read up to this cap and the\n * result flags `truncated` when it was cut off (default\n * {@link DEFAULT_MAX_BYTES}).\n * - **`extract`** — `\"html\"` returns the raw body; `\"text\"` (default) and\n * `\"markdown\"` run `@mozilla/readability` over `jsdom`, lazily imported\n * so they stay OPTIONAL peers — a missing peer throws the curated\n * install string.\n *\n * **Errors flow as data.** Every guardrail rejection and network failure\n * throws a typed {@link WebToolError}; the `tool()` wrapper catches it and\n * surfaces it in the returned `{ error }` field — `invoke()` never throws\n * — so the agent reads the failure and self-corrects.\n *\n * @param options - Tool-name override, byte cap, timeout, extract mode,\n * and host allowlist.\n * @returns A `ToolContract<{ url }, FetchUrlResult>`.\n *\n * @example\n * const fetchUrl = fetchUrlTool({ extract: \"text\", allowHosts: [\"docs.stripe.com\"] });\n * const { data } = await fetchUrl.invoke({ url: \"https://docs.stripe.com/api\" });\n * console.log(data?.content, data?.truncated);\n */\nexport function fetchUrlTool(\n options?: FetchUrlOptions,\n): ToolContract<FetchUrlInput, FetchUrlResult> {\n const maxBytes = options?.maxBytes ?? DEFAULT_MAX_BYTES;\n const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const extract = options?.extract ?? DEFAULT_EXTRACT;\n const allowHosts = options?.allowHosts;\n const allowPrivateNetwork = options?.allowPrivateNetwork ?? false;\n\n // Warm the readability peers non-blockingly when the configured mode\n // needs them, so the curated install string is ready (and logged at\n // first use) without delaying construction.\n if (extract !== \"html\") {\n void loadReadability();\n }\n\n return tool<FetchUrlInput, FetchUrlResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Fetch a web page or document by URL and return its main content as \" +\n \"text (readability-extracted), raw HTML, or Markdown. Use to read a \" +\n \"specific page you already have the URL for.\",\n input: inputSchema,\n async execute(input) {\n const url = parseUrl(input.url);\n assertHostAllowed(url, allowHosts);\n\n let response: Response;\n\n try {\n // The shared hardened outbound path: private-IP deny by default,\n // per-hop redirect re-validation, timeout — never a raw fetch.\n response = await guardedFetch(url.toString(), {\n allowedSchemes: [\"http\", \"https\"],\n hostAllowlist: allowHosts,\n denyPrivateIPsAfterDNS: !allowPrivateNetwork,\n timeoutMs,\n });\n } catch (cause) {\n if (cause instanceof OutboundPolicyError) {\n throw new WebToolError(`fetch_url blocked: ${cause.message}`, {\n type: \"denied-host\",\n cause,\n });\n }\n\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new WebToolError(`fetch_url request failed: ${message}`, {\n type: \"request-failed\",\n cause,\n });\n }\n\n const { body, truncated } = await readBody(response, maxBytes);\n const content = await render(body, response.url || url.toString(), extract);\n\n return {\n url: response.url || url.toString(),\n status: response.status,\n content,\n truncated,\n };\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fetch-url.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/fetch-url.ts"],"sourcesContent":["import {\n guardedFetch,\n OutboundPolicyError,\n tool,\n type ToolContract,\n} from \"@warlock.js/ai\";\nimport type {\n FetchUrlExtract,\n FetchUrlInput,\n FetchUrlOptions,\n FetchUrlResult,\n} from \"../contracts\";\nimport { WebToolError } from \"../errors\";\nimport { objectSchema, stringField } from \"./schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"fetch_url\";\n\n/** Default hard cap on response bytes read before truncation. */\nconst DEFAULT_MAX_BYTES = 1_000_000;\n\n/** Default per-request timeout in milliseconds. */\nconst DEFAULT_TIMEOUT_MS = 15_000;\n\n/** Default rendering mode for the fetched document. */\nconst DEFAULT_EXTRACT: FetchUrlExtract = \"text\";\n\n/** Input schema: `{ url }`. */\nconst inputSchema = objectSchema<FetchUrlInput>({\n url: stringField(),\n});\n\n// ============================================================\n// Lazily-loaded @mozilla/readability + jsdom (OPTIONAL peers)\n// ============================================================\n\n/**\n * Minimal structural shapes of the optional `@mozilla/readability` + `jsdom`\n * peers — only the members this file actually uses. Declared locally so the\n * package type-checks even when the peers are NOT installed (they are lazy\n * optional peers, loaded below via a `string` specifier so `tsc` never tries\n * to statically resolve them).\n */\ninterface ReadabilityArticleLike {\n readonly content?: string | null;\n readonly textContent?: string | null;\n}\ninterface ReadabilityInstanceLike {\n parse(): ReadabilityArticleLike | null;\n}\ninterface ReadabilityModuleLike {\n Readability: new (document: unknown) => ReadabilityInstanceLike;\n}\ninterface JsdomInstanceLike {\n readonly window: { readonly document: unknown };\n}\ninterface JsdomModuleLike {\n JSDOM: new (html: string, options?: { url?: string }) => JsdomInstanceLike;\n}\n\n// String specifiers typed `string` (not a literal) so `tsc` does not attempt\n// to resolve these optional peers at build time.\nconst READABILITY_MODULE_ID: string = \"@mozilla/readability\";\nconst JSDOM_MODULE_ID: string = \"jsdom\";\n\nlet ReadabilitySdk: ReadabilityModuleLike;\nlet JsdomSdk: JsdomModuleLike;\nlet isReadabilityAvailable: boolean | undefined;\nlet loadingPromise: Promise<void> | undefined;\n\nconst READABILITY_INSTALL_INSTRUCTIONS = `\nThe fetch_url text/markdown extractor requires the @mozilla/readability and jsdom packages.\nInstall them with:\n\n npm install @mozilla/readability jsdom\n\nOr with your preferred package manager:\n\n pnpm add @mozilla/readability jsdom\n yarn add @mozilla/readability jsdom\n`.trim();\n\n/**\n * Settle the lazy import of `@mozilla/readability` + `jsdom` once,\n * concurrency-safe. Only needed for the `\"text\"` / `\"markdown\"` extract\n * modes — `\"html\"` returns the raw body and never loads them. A bare\n * `catch` flips the flag to `false`; the curated install string surfaces\n * at use time via {@link WebToolError}, never a raw module-resolution\n * stack trace.\n */\nfunction loadReadability(): Promise<void> {\n if (isReadabilityAvailable !== undefined) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n ReadabilitySdk = (await import(READABILITY_MODULE_ID)) as ReadabilityModuleLike;\n JsdomSdk = (await import(JSDOM_MODULE_ID)) as JsdomModuleLike;\n isReadabilityAvailable = true;\n } catch {\n isReadabilityAvailable = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * Parse the input URL, rejecting anything unparseable or not over\n * `http`/`https` with a typed {@link WebToolError}.\n */\nfunction parseUrl(raw: string): URL {\n let url: URL;\n\n try {\n url = new URL(raw);\n } catch {\n throw new WebToolError(`fetch_url received an unparseable URL: \"${raw}\".`, {\n type: \"invalid-url\",\n });\n }\n\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new WebToolError(\n `fetch_url only supports http(s) URLs; got \"${url.protocol}\".`,\n { type: \"invalid-url\" },\n );\n }\n\n return url;\n}\n\n/**\n * Enforce the host allowlist (an SSRF guardrail) before any network call.\n * When `allowHosts` is set, a request to a host not in it is rejected;\n * matching is exact on the URL hostname (case-insensitive).\n */\nfunction assertHostAllowed(url: URL, allowHosts?: string[]): void {\n if (!allowHosts || allowHosts.length === 0) {\n return;\n }\n\n const host = url.hostname.toLowerCase();\n const allowed = allowHosts.some((entry) => entry.toLowerCase() === host);\n\n if (!allowed) {\n throw new WebToolError(\n `fetch_url blocked host \"${url.hostname}\" — it is not in the allowHosts allowlist.`,\n { type: \"denied-host\", context: { host: url.hostname } },\n );\n }\n}\n\n/**\n * Read the response body up to `maxBytes`, stopping early once the cap is\n * reached. Returns the decoded text and whether it was truncated. When\n * the body has no stream (a stubbed `Response`), falls back to `.text()`\n * and truncates the decoded string at `maxBytes`.\n */\nasync function readBody(\n response: Response,\n maxBytes: number,\n): Promise<{ body: string; truncated: boolean }> {\n const stream = response.body;\n\n if (!stream) {\n const text = await response.text();\n\n if (text.length > maxBytes) {\n return { body: text.slice(0, maxBytes), truncated: true };\n }\n\n return { body: text, truncated: false };\n }\n\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let received = 0;\n let truncated = false;\n\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (value) {\n received += value.byteLength;\n chunks.push(value);\n\n if (received >= maxBytes) {\n truncated = true;\n await reader.cancel();\n break;\n }\n }\n }\n\n const merged = new Uint8Array(received);\n let offset = 0;\n\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n const sliced = truncated ? merged.subarray(0, maxBytes) : merged;\n const body = new TextDecoder().decode(sliced);\n\n return { body, truncated };\n}\n\n/**\n * Render the fetched HTML into the configured `extract` mode. `\"html\"`\n * returns the raw body untouched; `\"text\"` and `\"markdown\"` run\n * `@mozilla/readability` over a `jsdom` document, throwing a curated\n * {@link WebToolError} when those optional peers are absent. `\"markdown\"`\n * applies a light heading/paragraph conversion over the extracted text.\n */\nasync function render(\n html: string,\n url: string,\n extract: FetchUrlExtract,\n): Promise<string> {\n if (extract === \"html\") {\n return html;\n }\n\n await loadReadability();\n\n if (!isReadabilityAvailable) {\n throw new WebToolError(READABILITY_INSTALL_INSTRUCTIONS, { type: \"missing-peer\" });\n }\n\n const dom = new JsdomSdk.JSDOM(html, { url });\n const article = new ReadabilitySdk.Readability(dom.window.document).parse();\n\n if (extract === \"markdown\") {\n return article?.content ? htmlToMarkdown(article.content) : (article?.textContent ?? \"\");\n }\n\n return article?.textContent ?? \"\";\n}\n\n/**\n * Minimal HTML→Markdown reduction for readability's extracted article\n * HTML — headings become `#` prefixes, paragraphs/line-breaks become\n * blank-line separated blocks, and remaining tags are stripped. This is a\n * pragmatic conversion, not a full CommonMark serializer.\n */\nfunction htmlToMarkdown(html: string): string {\n return html\n .replace(/<h([1-6])[^>]*>(.*?)<\\/h\\1>/gis, (_match, level: string, text: string) => {\n const hashes = \"#\".repeat(Number(level));\n\n return `\\n\\n${hashes} ${stripTags(text).trim()}\\n\\n`;\n })\n .replace(/<\\/(p|div|section|article|li)>/gi, \"\\n\\n\")\n .replace(/<br\\s*\\/?>/gi, \"\\n\")\n .replace(/<[^>]+>/g, \"\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n}\n\n/** Strip any remaining HTML tags from a fragment. */\nfunction stripTags(html: string): string {\n return html.replace(/<[^>]+>/g, \"\");\n}\n\n/**\n * Build the agent-facing `fetch_url` tool — fetch a URL over the global\n * `fetch` (Node 18+) and hand the model back rendered `content`.\n *\n * Guardrails, applied in order before/around the network call:\n * - **Private-network deny (default).** Every request — and every\n * redirect hop — goes through the framework's `guardedFetch` outbound\n * policy, which refuses private / loopback / link-local /\n * cloud-metadata 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.fetchUrl()` is\n * not an SSRF primitive.\n * - **`allowHosts`** — when set, a request to any host not in the list is\n * rejected before the fetch, and redirect targets are held to the same\n * allowlist.\n * - **`timeoutMs`** — the request is aborted via `AbortSignal.timeout`\n * (default {@link DEFAULT_TIMEOUT_MS}).\n * - **`maxBytes`** — the response body is read up to this cap and the\n * result flags `truncated` when it was cut off (default\n * {@link DEFAULT_MAX_BYTES}).\n * - **`extract`** — `\"html\"` returns the raw body; `\"text\"` (default) and\n * `\"markdown\"` run `@mozilla/readability` over `jsdom`, lazily imported\n * so they stay OPTIONAL peers — a missing peer throws the curated\n * install string.\n *\n * **Errors flow as data.** Every guardrail rejection and network failure\n * throws a typed {@link WebToolError}; the `tool()` wrapper catches it and\n * surfaces it in the returned `{ error }` field — `invoke()` never throws\n * — so the agent reads the failure and self-corrects.\n *\n * @param options - Tool-name override, byte cap, timeout, extract mode,\n * and host allowlist.\n * @returns A `ToolContract<{ url }, FetchUrlResult>`.\n *\n * @example\n * const fetchUrl = fetchUrlTool({ extract: \"text\", allowHosts: [\"docs.stripe.com\"] });\n * const { data } = await fetchUrl.invoke({ url: \"https://docs.stripe.com/api\" });\n * console.log(data?.content, data?.truncated);\n */\nexport function fetchUrlTool(\n options?: FetchUrlOptions,\n): ToolContract<FetchUrlInput, FetchUrlResult> {\n const maxBytes = options?.maxBytes ?? DEFAULT_MAX_BYTES;\n const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const extract = options?.extract ?? DEFAULT_EXTRACT;\n const allowHosts = options?.allowHosts;\n const allowPrivateNetwork = options?.allowPrivateNetwork ?? false;\n\n // Warm the readability peers non-blockingly when the configured mode\n // needs them, so the curated install string is ready (and logged at\n // first use) without delaying construction.\n if (extract !== \"html\") {\n void loadReadability();\n }\n\n return tool<FetchUrlInput, FetchUrlResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Fetch a web page or document by URL and return its main content as \" +\n \"text (readability-extracted), raw HTML, or Markdown. Use to read a \" +\n \"specific page you already have the URL for.\",\n input: inputSchema,\n async execute(input) {\n const url = parseUrl(input.url);\n assertHostAllowed(url, allowHosts);\n\n let response: Response;\n\n try {\n // The shared hardened outbound path: private-IP deny by default,\n // per-hop redirect re-validation, timeout — never a raw fetch.\n response = await guardedFetch(url.toString(), {\n allowedSchemes: [\"http\", \"https\"],\n hostAllowlist: allowHosts,\n denyPrivateIPsAfterDNS: !allowPrivateNetwork,\n timeoutMs,\n });\n } catch (cause) {\n if (cause instanceof OutboundPolicyError) {\n throw new WebToolError(`fetch_url blocked: ${cause.message}`, {\n type: \"denied-host\",\n cause,\n });\n }\n\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new WebToolError(`fetch_url request failed: ${message}`, {\n type: \"request-failed\",\n cause,\n });\n }\n\n const { body, truncated } = await readBody(response, maxBytes);\n const content = await render(body, response.url || url.toString(), extract);\n\n return {\n url: response.url || url.toString(),\n status: response.status,\n content,\n truncated,\n };\n },\n });\n}\n"],"mappings":"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"}
|
package/esm/web/schema.mjs
CHANGED
package/esm/web/schema.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"schema.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * Tiny, dependency-free [Standard Schema](https://standardschema.dev)\n * builders for the web tools' input validation. The package pins only\n * `@warlock.js/ai` as a runtime peer, so rather than pull in a schema\n * library we hand-roll the few shapes the web tools need — exactly the\n * pattern `@warlock.js/ai`'s own `tool()` tests use and the sibling\n * `@warlock.js/ai-workspace` package follows. Each builder returns a\n * `StandardSchemaV1`, which is what `tool({ input })` validates against\n * before calling `execute`.\n *\n * These intentionally cover only the primitive cases the WEB tools\n * require (`string`, `optional number`, and an `object` of fields). They\n * are not a general-purpose validator.\n */\n\n/** The vendor tag stamped on every issue these builders produce. */\nconst VENDOR = \"ai-tools\";\n\n/**\n * A single field validator inside {@link objectSchema}: given a value,\n * return either the coerced value or a list of issues. Field validators\n * receive the raw property and the property name (for issue messages).\n */\ntype FieldValidator<T> = (\n value: unknown,\n key: string,\n) => { value: T } | { issues: StandardSchemaV1.Issue[] };\n\n/** Required string field — rejects anything that is not a string. */\nexport function stringField(): FieldValidator<string> {\n return (value, key) => {\n if (typeof value === \"string\") {\n return { value };\n }\n\n return { issues: [{ message: `\"${key}\" must be a string`, path: [key] }] };\n };\n}\n\n/**\n * Optional finite-number field — accepts `undefined` or a finite number,\n * rejecting `NaN`/`Infinity` and non-number types.\n */\nexport function optionalNumberField(): FieldValidator<number | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return { value };\n }\n\n return {\n issues: [\n { message: `\"${key}\" must be a finite number when provided`, path: [key] },\n ],\n };\n };\n}\n\n/** The per-key field validator map describing an object schema's shape. */\ntype ObjectShape<T> = {\n [K in keyof T]-?: FieldValidator<T[K]>;\n};\n\n/**\n * Build a {@link StandardSchemaV1} for a flat object whose every property\n * is validated by a {@link FieldValidator}. The input must be a non-null\n * object; each declared field is validated and the (possibly coerced)\n * values are collected into the typed result. All field issues are merged\n * so the caller sees every problem at once.\n *\n * `T` is constrained to `object` rather than `Record<string, unknown>` so\n * the tool IO `interface`s (which carry no implicit string index\n * signature) satisfy it directly — only the declared keys in `shape` are\n * ever read, so a string index signature is never required.\n *\n * @example\n * const schema = objectSchema<{ query: string; maxResults?: number }>({\n * query: stringField(),\n * maxResults: optionalNumberField(),\n * });\n */\nexport function objectSchema<T extends object>(\n shape: ObjectShape<T>,\n): StandardSchemaV1<T> {\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n validate(input) {\n if (typeof input !== \"object\" || input === null || Array.isArray(input)) {\n return { issues: [{ message: \"input must be an object\" }] };\n }\n\n const source = input as Record<string, unknown>;\n const issues: StandardSchemaV1.Issue[] = [];\n const result: Record<string, unknown> = {};\n\n for (const key of Object.keys(shape) as (keyof T)[]) {\n const field = shape[key];\n const outcome = field(source[key as string], key as string);\n\n if (\"issues\" in outcome) {\n issues.push(...outcome.issues);\n\n continue;\n }\n\n // Only carry through keys that resolved to a defined value, so\n // optional-absent fields stay absent rather than becoming\n // explicit `undefined` properties.\n if (outcome.value !== undefined) {\n result[key as string] = outcome.value;\n }\n }\n\n if (issues.length > 0) {\n return { issues };\n }\n\n return { value: result as T };\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAM,SAAS;;AAaf,SAAgB,cAAsC;CACpD,QAAQ,OAAO,QAAQ;EACrB,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EAAE,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAqB,MAAM,CAAC,GAAG;EAAE,CAAC,EAAE;CAC3E;AACF;;;;;AAMA,SAAgB,sBAA0D;CACxE,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GACpD,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CACN;GAAE,SAAS,IAAI,IAAI;GAA0C,MAAM,CAAC,GAAG;EAAE,CAC3E,EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,aACd,OACqB;CACrB,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,SAAS,OAAO;GACd,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,0BAA0B,CAAC,EAAE;GAG5D,MAAM,SAAS;GACf,MAAM,SAAmC,CAAC;GAC1C,MAAM,SAAkC,CAAC;GAEzC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAkB;IACnD,MAAM,QAAQ,MAAM;IACpB,MAAM,UAAU,MAAM,OAAO,MAAgB,GAAa;IAE1D,IAAI,YAAY,SAAS;KACvB,OAAO,KAAK,GAAG,QAAQ,MAAM;KAE7B;IACF;IAKA,IAAI,QAAQ,UAAU,QACpB,OAAO,OAAiB,QAAQ;GAEpC;GAEA,IAAI,OAAO,SAAS,GAClB,OAAO,EAAE,OAAO;GAGlB,OAAO,EAAE,OAAO,OAAY;EAC9B;CACF,EACF;AACF"}
|
package/esm/web/web-search.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WebSearchInput, WebSearchOptions, WebSearchResult } from "../contracts/web.type.mjs";
|
|
2
2
|
import { ToolContract } from "@warlock.js/ai";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../../packages/ai-tools/src/web/web-search.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Build the agent-facing `web_search` tool over a {@link SearchProvider}.
|
|
7
7
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-search.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"web-search.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/web-search.ts"],"mappings":";;;;;;;AAsNA;;;;;;;;;;;;;;;AAE+C;;;;;;;;;;;;iBAF/B,aAAA,CACd,OAAA,EAAS,gBAAA,GACR,YAAA,CAAa,cAAA,EAAgB,eAAA"}
|
package/esm/web/web-search.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { WebToolError } from "../errors.mjs";
|
|
|
2
2
|
import { objectSchema, optionalNumberField, stringField } from "./schema.mjs";
|
|
3
3
|
import { tool } from "@warlock.js/ai";
|
|
4
4
|
|
|
5
|
-
//#region
|
|
5
|
+
//#region ../../packages/ai-tools/src/web/web-search.ts
|
|
6
6
|
/** Default tool name exposed to the LLM. */
|
|
7
7
|
const DEFAULT_NAME = "web_search";
|
|
8
8
|
/** Default per-call result cap when the constructor does not set one. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-search.mjs","names":[],"sources":["../../../../../../../ai-tools/src/web/web-search.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n SearchProvider,\n WebSearchInput,\n WebSearchOptions,\n WebSearchResult,\n WebSearchResultItem,\n} from \"../contracts\";\nimport { WebToolError } from \"../errors\";\nimport { objectSchema, optionalNumberField, stringField } from \"./schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"web_search\";\n\n/** Default per-call result cap when the constructor does not set one. */\nconst DEFAULT_MAX_RESULTS = 5;\n\n/**\n * Per-provider configuration — the environment variable consulted when no\n * `apiKey` is passed, plus the human-readable provider label used in\n * error messages.\n */\nconst PROVIDER_ENV: Record<SearchProvider, string> = {\n tavily: \"TAVILY_API_KEY\",\n brave: \"BRAVE_API_KEY\",\n serpapi: \"SERPAPI_API_KEY\",\n};\n\n/** Input schema: `{ query, maxResults? }`. */\nconst inputSchema = objectSchema<WebSearchInput>({\n query: stringField(),\n maxResults: optionalNumberField(),\n});\n\n/**\n * Resolve the API key from explicit options or the provider's environment\n * variable, throwing a typed {@link WebToolError} when neither is present.\n */\nfunction resolveApiKey(provider: SearchProvider, apiKey?: string): string {\n const key = apiKey ?? process.env[PROVIDER_ENV[provider]];\n\n if (!key) {\n throw new WebToolError(\n `web_search requires an API key for the \"${provider}\" provider. ` +\n `Pass { apiKey } or set the ${PROVIDER_ENV[provider]} environment variable.`,\n { type: \"missing-key\" },\n );\n }\n\n return key;\n}\n\n/**\n * Clamp the model-requested result count into `[1, max]`. An omitted /\n * non-positive request falls back to the configured default.\n */\nfunction clampResults(requested: number | undefined, max: number): number {\n if (requested === undefined || requested < 1) {\n return Math.min(DEFAULT_MAX_RESULTS, max);\n }\n\n return Math.min(Math.max(1, Math.floor(requested)), max);\n}\n\n/**\n * Shape of the relevant slice of a Tavily `/search` response. Tavily\n * returns LLM-ready `content` snippets and a relevance `score` per hit.\n */\ninterface TavilyResponse {\n results?: Array<{ title?: string; url?: string; content?: string; score?: number }>;\n}\n\n/** Shape of the relevant slice of a Brave web-search response. */\ninterface BraveResponse {\n web?: { results?: Array<{ title?: string; url?: string; description?: string }> };\n}\n\n/** Shape of the relevant slice of a SerpAPI `search.json` response. */\ninterface SerpApiResponse {\n organic_results?: Array<{ title?: string; link?: string; snippet?: string }>;\n}\n\n/**\n * Issue the provider HTTP call and return its parsed JSON, mapping a\n * non-OK status or a network failure to a typed {@link WebToolError}.\n */\nasync function fetchJson(\n url: string,\n init: RequestInit,\n provider: SearchProvider,\n): Promise<unknown> {\n let response: Response;\n\n try {\n response = await fetch(url, init);\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new WebToolError(\n `web_search request to the \"${provider}\" provider failed: ${message}`,\n { type: \"request-failed\", cause },\n );\n }\n\n if (!response.ok) {\n throw new WebToolError(\n `web_search \"${provider}\" provider returned HTTP ${response.status}.`,\n { type: \"request-failed\", context: { status: response.status } },\n );\n }\n\n return response.json();\n}\n\n/** Drive Tavily's `/search` HTTP API and normalize its hits. */\nasync function searchTavily(\n query: string,\n maxResults: number,\n apiKey: string,\n): Promise<WebSearchResultItem[]> {\n const json = (await fetchJson(\n \"https://api.tavily.com/search\",\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, query, max_results: maxResults }),\n },\n \"tavily\",\n )) as TavilyResponse;\n\n return (json.results ?? []).slice(0, maxResults).map((hit) => ({\n title: hit.title ?? \"\",\n url: hit.url ?? \"\",\n snippet: hit.content ?? \"\",\n score: hit.score,\n }));\n}\n\n/** Drive Brave's web-search HTTP API and normalize its hits. */\nasync function searchBrave(\n query: string,\n maxResults: number,\n apiKey: string,\n): Promise<WebSearchResultItem[]> {\n const url = new URL(\"https://api.search.brave.com/res/v1/web/search\");\n url.searchParams.set(\"q\", query);\n url.searchParams.set(\"count\", String(maxResults));\n\n const json = (await fetchJson(\n url.toString(),\n {\n method: \"GET\",\n headers: { accept: \"application/json\", \"x-subscription-token\": apiKey },\n },\n \"brave\",\n )) as BraveResponse;\n\n return (json.web?.results ?? []).slice(0, maxResults).map((hit) => ({\n title: hit.title ?? \"\",\n url: hit.url ?? \"\",\n snippet: hit.description ?? \"\",\n }));\n}\n\n/** Drive SerpAPI's `search.json` HTTP API and normalize its hits. */\nasync function searchSerpApi(\n query: string,\n maxResults: number,\n apiKey: string,\n): Promise<WebSearchResultItem[]> {\n const url = new URL(\"https://serpapi.com/search.json\");\n url.searchParams.set(\"q\", query);\n url.searchParams.set(\"num\", String(maxResults));\n url.searchParams.set(\"api_key\", apiKey);\n\n const json = (await fetchJson(url.toString(), { method: \"GET\" }, \"serpapi\")) as SerpApiResponse;\n\n return (json.organic_results ?? []).slice(0, maxResults).map((hit) => ({\n title: hit.title ?? \"\",\n url: hit.link ?? \"\",\n snippet: hit.snippet ?? \"\",\n }));\n}\n\n/**\n * Build the agent-facing `web_search` tool over a {@link SearchProvider}.\n *\n * The provider's HTTP API is called directly through the global `fetch`\n * (Node 18+) — Tavily via `POST /search` (LLM-ready snippets + relevance\n * scores), Brave and SerpAPI via their `GET` endpoints. The API key is\n * taken from `options.apiKey`, falling back to the provider's environment\n * variable (`TAVILY_API_KEY` / `BRAVE_API_KEY` / `SERPAPI_API_KEY`). No\n * provider SDK is required for the HTTP path; `@tavily/core` remains an\n * optional peer for callers who prefer it, but this factory never forces\n * it to be installed.\n *\n * The model passes `{ query, maxResults? }`; `maxResults` is clamped into\n * `[1, options.maxResults]` (default {@link DEFAULT_MAX_RESULTS}).\n *\n * **Errors flow as data.** A missing key, a non-OK provider status, or a\n * network failure throws a typed {@link WebToolError}; the `tool()`\n * wrapper catches it and surfaces it in the returned `{ error }` field —\n * `invoke()` never throws — so the agent can read the failure and\n * self-correct.\n *\n * @param options - Provider selection, API key, result cap, and an\n * optional tool-name override.\n * @returns A `ToolContract<{ query; maxResults? }, WebSearchResult>`.\n *\n * @example\n * const search = webSearchTool({ provider: \"tavily\" });\n * const { data } = await search.invoke({ query: \"warlock.js ai tools\" });\n * for (const hit of data?.results ?? []) console.log(hit.title, hit.url);\n */\nexport function webSearchTool(\n options: WebSearchOptions,\n): ToolContract<WebSearchInput, WebSearchResult> {\n const provider = options.provider;\n const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;\n\n return tool<WebSearchInput, WebSearchResult>({\n name: options.name ?? DEFAULT_NAME,\n description:\n \"Search the web for current information and return a ranked list of \" +\n \"results (title, URL, and a short snippet). Use for facts that may have \" +\n \"changed since training, or anything you are unsure about.\",\n input: inputSchema,\n async execute(input) {\n const apiKey = resolveApiKey(provider, options.apiKey);\n const limit = clampResults(input.maxResults, maxResults);\n\n let results: WebSearchResultItem[];\n\n switch (provider) {\n case \"tavily\":\n results = await searchTavily(input.query, limit, apiKey);\n break;\n case \"brave\":\n results = await searchBrave(input.query, limit, apiKey);\n break;\n case \"serpapi\":\n results = await searchSerpApi(input.query, limit, apiKey);\n break;\n }\n\n return { results };\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"web-search.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/web-search.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n SearchProvider,\n WebSearchInput,\n WebSearchOptions,\n WebSearchResult,\n WebSearchResultItem,\n} from \"../contracts\";\nimport { WebToolError } from \"../errors\";\nimport { objectSchema, optionalNumberField, stringField } from \"./schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"web_search\";\n\n/** Default per-call result cap when the constructor does not set one. */\nconst DEFAULT_MAX_RESULTS = 5;\n\n/**\n * Per-provider configuration — the environment variable consulted when no\n * `apiKey` is passed, plus the human-readable provider label used in\n * error messages.\n */\nconst PROVIDER_ENV: Record<SearchProvider, string> = {\n tavily: \"TAVILY_API_KEY\",\n brave: \"BRAVE_API_KEY\",\n serpapi: \"SERPAPI_API_KEY\",\n};\n\n/** Input schema: `{ query, maxResults? }`. */\nconst inputSchema = objectSchema<WebSearchInput>({\n query: stringField(),\n maxResults: optionalNumberField(),\n});\n\n/**\n * Resolve the API key from explicit options or the provider's environment\n * variable, throwing a typed {@link WebToolError} when neither is present.\n */\nfunction resolveApiKey(provider: SearchProvider, apiKey?: string): string {\n const key = apiKey ?? process.env[PROVIDER_ENV[provider]];\n\n if (!key) {\n throw new WebToolError(\n `web_search requires an API key for the \"${provider}\" provider. ` +\n `Pass { apiKey } or set the ${PROVIDER_ENV[provider]} environment variable.`,\n { type: \"missing-key\" },\n );\n }\n\n return key;\n}\n\n/**\n * Clamp the model-requested result count into `[1, max]`. An omitted /\n * non-positive request falls back to the configured default.\n */\nfunction clampResults(requested: number | undefined, max: number): number {\n if (requested === undefined || requested < 1) {\n return Math.min(DEFAULT_MAX_RESULTS, max);\n }\n\n return Math.min(Math.max(1, Math.floor(requested)), max);\n}\n\n/**\n * Shape of the relevant slice of a Tavily `/search` response. Tavily\n * returns LLM-ready `content` snippets and a relevance `score` per hit.\n */\ninterface TavilyResponse {\n results?: Array<{ title?: string; url?: string; content?: string; score?: number }>;\n}\n\n/** Shape of the relevant slice of a Brave web-search response. */\ninterface BraveResponse {\n web?: { results?: Array<{ title?: string; url?: string; description?: string }> };\n}\n\n/** Shape of the relevant slice of a SerpAPI `search.json` response. */\ninterface SerpApiResponse {\n organic_results?: Array<{ title?: string; link?: string; snippet?: string }>;\n}\n\n/**\n * Issue the provider HTTP call and return its parsed JSON, mapping a\n * non-OK status or a network failure to a typed {@link WebToolError}.\n */\nasync function fetchJson(\n url: string,\n init: RequestInit,\n provider: SearchProvider,\n): Promise<unknown> {\n let response: Response;\n\n try {\n response = await fetch(url, init);\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new WebToolError(\n `web_search request to the \"${provider}\" provider failed: ${message}`,\n { type: \"request-failed\", cause },\n );\n }\n\n if (!response.ok) {\n throw new WebToolError(\n `web_search \"${provider}\" provider returned HTTP ${response.status}.`,\n { type: \"request-failed\", context: { status: response.status } },\n );\n }\n\n return response.json();\n}\n\n/** Drive Tavily's `/search` HTTP API and normalize its hits. */\nasync function searchTavily(\n query: string,\n maxResults: number,\n apiKey: string,\n): Promise<WebSearchResultItem[]> {\n const json = (await fetchJson(\n \"https://api.tavily.com/search\",\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, query, max_results: maxResults }),\n },\n \"tavily\",\n )) as TavilyResponse;\n\n return (json.results ?? []).slice(0, maxResults).map((hit) => ({\n title: hit.title ?? \"\",\n url: hit.url ?? \"\",\n snippet: hit.content ?? \"\",\n score: hit.score,\n }));\n}\n\n/** Drive Brave's web-search HTTP API and normalize its hits. */\nasync function searchBrave(\n query: string,\n maxResults: number,\n apiKey: string,\n): Promise<WebSearchResultItem[]> {\n const url = new URL(\"https://api.search.brave.com/res/v1/web/search\");\n url.searchParams.set(\"q\", query);\n url.searchParams.set(\"count\", String(maxResults));\n\n const json = (await fetchJson(\n url.toString(),\n {\n method: \"GET\",\n headers: { accept: \"application/json\", \"x-subscription-token\": apiKey },\n },\n \"brave\",\n )) as BraveResponse;\n\n return (json.web?.results ?? []).slice(0, maxResults).map((hit) => ({\n title: hit.title ?? \"\",\n url: hit.url ?? \"\",\n snippet: hit.description ?? \"\",\n }));\n}\n\n/** Drive SerpAPI's `search.json` HTTP API and normalize its hits. */\nasync function searchSerpApi(\n query: string,\n maxResults: number,\n apiKey: string,\n): Promise<WebSearchResultItem[]> {\n const url = new URL(\"https://serpapi.com/search.json\");\n url.searchParams.set(\"q\", query);\n url.searchParams.set(\"num\", String(maxResults));\n url.searchParams.set(\"api_key\", apiKey);\n\n const json = (await fetchJson(url.toString(), { method: \"GET\" }, \"serpapi\")) as SerpApiResponse;\n\n return (json.organic_results ?? []).slice(0, maxResults).map((hit) => ({\n title: hit.title ?? \"\",\n url: hit.link ?? \"\",\n snippet: hit.snippet ?? \"\",\n }));\n}\n\n/**\n * Build the agent-facing `web_search` tool over a {@link SearchProvider}.\n *\n * The provider's HTTP API is called directly through the global `fetch`\n * (Node 18+) — Tavily via `POST /search` (LLM-ready snippets + relevance\n * scores), Brave and SerpAPI via their `GET` endpoints. The API key is\n * taken from `options.apiKey`, falling back to the provider's environment\n * variable (`TAVILY_API_KEY` / `BRAVE_API_KEY` / `SERPAPI_API_KEY`). No\n * provider SDK is required for the HTTP path; `@tavily/core` remains an\n * optional peer for callers who prefer it, but this factory never forces\n * it to be installed.\n *\n * The model passes `{ query, maxResults? }`; `maxResults` is clamped into\n * `[1, options.maxResults]` (default {@link DEFAULT_MAX_RESULTS}).\n *\n * **Errors flow as data.** A missing key, a non-OK provider status, or a\n * network failure throws a typed {@link WebToolError}; the `tool()`\n * wrapper catches it and surfaces it in the returned `{ error }` field —\n * `invoke()` never throws — so the agent can read the failure and\n * self-correct.\n *\n * @param options - Provider selection, API key, result cap, and an\n * optional tool-name override.\n * @returns A `ToolContract<{ query; maxResults? }, WebSearchResult>`.\n *\n * @example\n * const search = webSearchTool({ provider: \"tavily\" });\n * const { data } = await search.invoke({ query: \"warlock.js ai tools\" });\n * for (const hit of data?.results ?? []) console.log(hit.title, hit.url);\n */\nexport function webSearchTool(\n options: WebSearchOptions,\n): ToolContract<WebSearchInput, WebSearchResult> {\n const provider = options.provider;\n const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;\n\n return tool<WebSearchInput, WebSearchResult>({\n name: options.name ?? DEFAULT_NAME,\n description:\n \"Search the web for current information and return a ranked list of \" +\n \"results (title, URL, and a short snippet). Use for facts that may have \" +\n \"changed since training, or anything you are unsure about.\",\n input: inputSchema,\n async execute(input) {\n const apiKey = resolveApiKey(provider, options.apiKey);\n const limit = clampResults(input.maxResults, maxResults);\n\n let results: WebSearchResultItem[];\n\n switch (provider) {\n case \"tavily\":\n results = await searchTavily(input.query, limit, apiKey);\n break;\n case \"brave\":\n results = await searchBrave(input.query, limit, apiKey);\n break;\n case \"serpapi\":\n results = await searchSerpApi(input.query, limit, apiKey);\n break;\n }\n\n return { results };\n },\n });\n}\n"],"mappings":"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"}
|
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) —
|
|
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.'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Connect to an external MCP server (Direction A)
|
|
@@ -101,16 +101,16 @@ The MCP client lazily imports `@modelcontextprotocol/sdk` (the protocol SDK) and
|
|
|
101
101
|
|
|
102
102
|
## See also
|
|
103
103
|
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
104
|
+
- The `expose-as-mcp-server` topic — the reverse direction: publish a local primitive AS an MCP server.
|
|
105
|
+
- The `use-web-and-http-tools` topic — the built-in web / HTTP / calculator / date-time belt.
|
|
106
|
+
- The `run-ai-agent` topic of the `warlock-js-ai` skill — running the agent that consumes the adapted tools.
|
|
107
107
|
|
|
108
108
|
|
|
109
109
|
## expose-as-mcp-server `@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`
|
|
110
110
|
|
|
111
111
|
---
|
|
112
112
|
name: expose-as-mcp-server
|
|
113
|
-
description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[]) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)`. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `server.start()`, `server.stop()`, `schemaTarget`, `draft-2020-12`, `extractJsonSchema`, `tools/list`, `tools/call`, `{ type: "stdio" }`, `{ type: "http"; port }`; ''expose my agent as an MCP server'', ''publish my tools to Cursor/Claude Desktop'', ''make my warlock workspace an MCP server'', ''serve my ToolContract[] over MCP'', ''let other MCP clients call my agent''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) —
|
|
113
|
+
description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[]) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)`. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `server.start()`, `server.stop()`, `schemaTarget`, `draft-2020-12`, `extractJsonSchema`, `tools/list`, `tools/call`, `{ type: "stdio" }`, `{ type: "http"; port }`; ''expose my agent as an MCP server'', ''publish my tools to Cursor/Claude Desktop'', ''make my warlock workspace an MCP server'', ''serve my ToolContract[] over MCP'', ''let other MCP clients call my agent''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
|
|
114
114
|
---
|
|
115
115
|
|
|
116
116
|
# Expose tools AS an MCP server (Direction B)
|
|
@@ -190,16 +190,16 @@ const response = await handler.handle(jsonRpcRequest); // request → response,
|
|
|
190
190
|
|
|
191
191
|
## See also
|
|
192
192
|
|
|
193
|
-
-
|
|
194
|
-
-
|
|
195
|
-
-
|
|
193
|
+
- The `connect-mcp-server` topic — the reverse direction: consume an external MCP server's tools.
|
|
194
|
+
- The `use-web-and-http-tools` topic — the built-in tools you might expose.
|
|
195
|
+
- The `use-a-workspace` topic of the `warlock-js-ai-workspace` skill — a workspace whose `tools.all()` is a natural `serve` source.
|
|
196
196
|
|
|
197
197
|
|
|
198
198
|
## use-web-and-http-tools `@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md`
|
|
199
199
|
|
|
200
200
|
---
|
|
201
201
|
name: use-web-and-http-tools
|
|
202
|
-
description: 'Wire the @warlock.js/ai-tools belt — web search, fetch/scrape, HTTP/REST, calculator, date-time — into an agent under their guardrails. Triggers: `ai.tools.webSearch`, `ai.tools.fetchUrl`, `ai.tools.http`, `ai.tools.calculator`, `ai.tools.dateTime`, `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `web_search`, `fetch_url`, `http_request`, `calculator`, `date_time`, `WebToolError`, `HttpPolicyError`, `CalculatorError`, `DateTimeError`, `allowHosts`, `allowMethods`, `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 —
|
|
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.'
|
|
203
203
|
---
|
|
204
204
|
|
|
205
205
|
# Use the web + HTTP tool belt
|
|
@@ -321,10 +321,10 @@ if (error instanceof WebToolError && error.type === "denied-host") {
|
|
|
321
321
|
|
|
322
322
|
## See also
|
|
323
323
|
|
|
324
|
-
-
|
|
325
|
-
-
|
|
326
|
-
-
|
|
327
|
-
-
|
|
328
|
-
-
|
|
324
|
+
- The `connect-mcp-server` topic — adapt an external MCP server's tools as agent tools.
|
|
325
|
+
- The `expose-as-mcp-server` topic — publish these tools AS an MCP server.
|
|
326
|
+
- The `use-a-workspace` topic of the `warlock-js-ai-workspace` skill — the filesystem + shell tool belt (jailed `read_file` / `edit_file` / `run_shell` / …).
|
|
327
|
+
- The `define-ai-tool` topic of the `warlock-js-ai` skill — the `tool()` / `ToolContract` seam every tool here is built on.
|
|
328
|
+
- The `run-ai-agent` topic of the `warlock-js-ai` skill — running the agent that consumes these tools.
|
|
329
329
|
|
|
330
330
|
|
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) —
|
|
10
|
-
- [expose-as-mcp-server](@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md): Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[]) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)`. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `server.start()`, `server.stop()`, `schemaTarget`, `draft-2020-12`, `extractJsonSchema`, `tools/list`, `tools/call`, `{ type: "stdio" }`, `{ type: "http"; port }`; 'expose my agent as an MCP server', 'publish my tools to Cursor/Claude Desktop', 'make my warlock workspace an MCP server', 'serve my ToolContract[] over MCP', 'let other MCP clients call my agent'; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) —
|
|
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 —
|
|
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)`. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `server.start()`, `server.stop()`, `schemaTarget`, `draft-2020-12`, `extractJsonSchema`, `tools/list`, `tools/call`, `{ type: "stdio" }`, `{ type: "http"; port }`; 'expose my agent as an MCP server', 'publish my tools to Cursor/Claude Desktop', 'make my warlock workspace an MCP server', 'serve my ToolContract[] over MCP', 'let other MCP clients call my agent'; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.
|
|
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.
|
package/package.json
CHANGED
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "https://github.com/warlockjs/
|
|
19
|
+
"url": "git+https://github.com/warlockjs/framework.git",
|
|
20
|
+
"directory": "packages/ai-tools"
|
|
20
21
|
},
|
|
21
22
|
"peerDependencies": {
|
|
22
23
|
"@modelcontextprotocol/sdk": "*",
|
|
23
24
|
"@mozilla/readability": "*",
|
|
24
25
|
"@tavily/core": "*",
|
|
25
|
-
"@warlock.js/ai": "5.
|
|
26
|
+
"@warlock.js/ai": "5.30.0",
|
|
26
27
|
"ajv": "*",
|
|
27
28
|
"jsdom": "*"
|
|
28
29
|
},
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@standard-schema/spec": "^1.0.0"
|
|
48
49
|
},
|
|
49
|
-
"version": "5.
|
|
50
|
+
"version": "5.30.0",
|
|
50
51
|
"main": "./cjs/index.cjs",
|
|
51
52
|
"module": "./esm/index.mjs",
|
|
52
53
|
"types": "./esm/index.d.mts",
|
package/skills/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# `@warlock.js/ai-tools` — skills index
|
|
2
2
|
|
|
3
|
-
Per-task skills.
|
|
3
|
+
Per-task skills. Cross-references name the topic ("the `<topic>` topic"), and for another package add its skill ("the `<topic>` topic of the `warlock-js-<pkg>` skill").
|
|
4
4
|
|
|
5
5
|
## Skills
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### `use-web-and-http-tools`
|
|
8
8
|
|
|
9
9
|
Wire the five ready-made tools — `ai.tools.webSearch` (`web_search`), `ai.tools.fetchUrl` (`fetch_url`), `ai.tools.http` (`http_request`), `ai.tools.calculator` (`calculator`), and `ai.tools.dateTime` (`date_time`) — into an agent under their construction-time rails: a search provider + API-key fallback, readability extract modes, the host/method allowlists and byte/timeout caps (the SSRF guardrails), the safe shunting-yard calculator, and the millisecond-unit date-time ops. Covers the option vs per-call-input split, the typed `WebToolError` / `HttpPolicyError` / `CalculatorError` / `DateTimeError`, and the errors-as-data contract. Load when giving an agent web search, fetch/scrape, REST, calculator, or date tools.
|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### `connect-mcp-server`
|
|
12
12
|
|
|
13
13
|
Connect an agent to an external Model Context Protocol server with `ai.mcp(server, options?)` and adapt each of its tools into a native `ToolContract` (Direction A). Covers the stdio (`node:child_process`, no dep) vs Streamable HTTP transport, `namePrefix` / `filter` / `timeoutMs`, the lazy-and-cached `client.tools()` handshake + `tools/list`, the JSON-Schema→Standard-Schema adaptation (Ajv optional peer), the `kind`→`type` translation, and the `McpTransportError` surface. Load when consuming an MCP server's tools inside a warlock agent.
|
|
14
14
|
|
|
15
|
-
###
|
|
15
|
+
### `expose-as-mcp-server`
|
|
16
16
|
|
|
17
17
|
Expose a warlock agent / supervisor / orchestrator (or a raw `ToolContract[]`) AS an MCP server with `ai.mcp.serve(source, options)` (Direction B). Covers the `source` shapes (array or `tools()`), the `name` / `version` / `transport` / `schemaTarget` options, how `initialize` / `tools/list` / `tools/call` map (via `extractJsonSchema` and the never-throwing `invoke()`), the auto-pumped stdio transport vs host-owned HTTP, and the `createServeHandler` pure-protocol seam for HTTP wiring and tests. Load when publishing local tools to Cursor / Claude Desktop / any MCP client.
|
|
@@ -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) —
|
|
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.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Connect to an external MCP server (Direction A)
|
|
@@ -93,6 +93,6 @@ The MCP client lazily imports `@modelcontextprotocol/sdk` (the protocol SDK) and
|
|
|
93
93
|
|
|
94
94
|
## See also
|
|
95
95
|
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
96
|
+
- The `expose-as-mcp-server` topic — the reverse direction: publish a local primitive AS an MCP server.
|
|
97
|
+
- The `use-web-and-http-tools` topic — the built-in web / HTTP / calculator / date-time belt.
|
|
98
|
+
- The `run-ai-agent` topic of the `warlock-js-ai` skill — running the agent that consumes the adapted tools.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: expose-as-mcp-server
|
|
3
|
-
description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[]) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)`. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `server.start()`, `server.stop()`, `schemaTarget`, `draft-2020-12`, `extractJsonSchema`, `tools/list`, `tools/call`, `{ type: "stdio" }`, `{ type: "http"; port }`; ''expose my agent as an MCP server'', ''publish my tools to Cursor/Claude Desktop'', ''make my warlock workspace an MCP server'', ''serve my ToolContract[] over MCP'', ''let other MCP clients call my agent''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) —
|
|
3
|
+
description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[]) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)`. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `server.start()`, `server.stop()`, `schemaTarget`, `draft-2020-12`, `extractJsonSchema`, `tools/list`, `tools/call`, `{ type: "stdio" }`, `{ type: "http"; port }`; ''expose my agent as an MCP server'', ''publish my tools to Cursor/Claude Desktop'', ''make my warlock workspace an MCP server'', ''serve my ToolContract[] over MCP'', ''let other MCP clients call my agent''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Expose tools AS an MCP server (Direction B)
|
|
@@ -80,6 +80,6 @@ const response = await handler.handle(jsonRpcRequest); // request → response,
|
|
|
80
80
|
|
|
81
81
|
## See also
|
|
82
82
|
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
83
|
+
- The `connect-mcp-server` topic — the reverse direction: consume an external MCP server's tools.
|
|
84
|
+
- The `use-web-and-http-tools` topic — the built-in tools you might expose.
|
|
85
|
+
- The `use-a-workspace` topic of the `warlock-js-ai-workspace` skill — a workspace whose `tools.all()` is a natural `serve` source.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: use-web-and-http-tools
|
|
3
|
-
description: 'Wire the @warlock.js/ai-tools belt — web search, fetch/scrape, HTTP/REST, calculator, date-time — into an agent under their guardrails. Triggers: `ai.tools.webSearch`, `ai.tools.fetchUrl`, `ai.tools.http`, `ai.tools.calculator`, `ai.tools.dateTime`, `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `web_search`, `fetch_url`, `http_request`, `calculator`, `date_time`, `WebToolError`, `HttpPolicyError`, `CalculatorError`, `DateTimeError`, `allowHosts`, `allowMethods`, `allowPrivateNetwork`, `baseUrl`, `maxBytes`, `guardedFetch`, `SearchProvider`, `TAVILY_API_KEY`; ''give an agent web search'', ''let an agent fetch/scrape a page'', ''let an agent call a REST API'', ''restrict which hosts/methods an agent can hit'', ''add a calculator/date tool to an agent'', ''SSRF guardrail for an agent tool'', ''let a tool reach an internal/dev service''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: connecting to an external MCP server —
|
|
3
|
+
description: 'Wire the @warlock.js/ai-tools belt — web search, fetch/scrape, HTTP/REST, calculator, date-time — into an agent under their guardrails. Triggers: `ai.tools.webSearch`, `ai.tools.fetchUrl`, `ai.tools.http`, `ai.tools.calculator`, `ai.tools.dateTime`, `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `web_search`, `fetch_url`, `http_request`, `calculator`, `date_time`, `WebToolError`, `HttpPolicyError`, `CalculatorError`, `DateTimeError`, `allowHosts`, `allowMethods`, `allowPrivateNetwork`, `baseUrl`, `maxBytes`, `guardedFetch`, `SearchProvider`, `TAVILY_API_KEY`; ''give an agent web search'', ''let an agent fetch/scrape a page'', ''let an agent call a REST API'', ''restrict which hosts/methods an agent can hit'', ''add a calculator/date tool to an agent'', ''SSRF guardrail for an agent tool'', ''let a tool reach an internal/dev service''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: connecting to an external MCP server — the `connect-mcp-server` topic; exposing tools AS an MCP server — the `expose-as-mcp-server` topic; filesystem/shell tools — the `use-a-workspace` topic of the `warlock-js-ai-workspace` skill; defining your own custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Use the web + HTTP tool belt
|
|
@@ -122,8 +122,8 @@ if (error instanceof WebToolError && error.type === "denied-host") {
|
|
|
122
122
|
|
|
123
123
|
## See also
|
|
124
124
|
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
-
|
|
125
|
+
- The `connect-mcp-server` topic — adapt an external MCP server's tools as agent tools.
|
|
126
|
+
- The `expose-as-mcp-server` topic — publish these tools AS an MCP server.
|
|
127
|
+
- The `use-a-workspace` topic of the `warlock-js-ai-workspace` skill — the filesystem + shell tool belt (jailed `read_file` / `edit_file` / `run_shell` / …).
|
|
128
|
+
- The `define-ai-tool` topic of the `warlock-js-ai` skill — the `tool()` / `ToolContract` seam every tool here is built on.
|
|
129
|
+
- The `run-ai-agent` topic of the `warlock-js-ai` skill — running the agent that consumes these tools.
|