@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.
- package/CHANGELOG.md +37 -1
- package/README.md +25 -13
- package/cjs/index.cjs +843 -101
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -1
- package/esm/contracts/mcp-serve.type.d.mts +257 -0
- package/esm/contracts/mcp-serve.type.d.mts.map +1 -0
- package/esm/contracts/mcp.type.d.mts +16 -59
- package/esm/contracts/mcp.type.d.mts.map +1 -1
- package/esm/errors.d.mts +36 -1
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +20 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/http/http-request.d.mts.map +1 -1
- package/esm/http/http-request.mjs.map +1 -1
- package/esm/index.d.mts +6 -4
- package/esm/index.mjs +3 -2
- package/esm/mcp/client.mjs +35 -5
- package/esm/mcp/client.mjs.map +1 -1
- package/esm/mcp/index.d.mts +12 -5
- package/esm/mcp/index.d.mts.map +1 -1
- package/esm/mcp/index.mjs +1 -0
- package/esm/mcp/index.mjs.map +1 -1
- package/esm/mcp/protocol.d.mts +16 -0
- package/esm/mcp/protocol.d.mts.map +1 -0
- package/esm/mcp/protocol.mjs +23 -0
- package/esm/mcp/protocol.mjs.map +1 -0
- package/esm/mcp/serve-http.mjs +443 -0
- package/esm/mcp/serve-http.mjs.map +1 -0
- package/esm/mcp/serve.d.mts +45 -26
- package/esm/mcp/serve.d.mts.map +1 -1
- package/esm/mcp/serve.mjs +325 -92
- package/esm/mcp/serve.mjs.map +1 -1
- package/esm/mcp/transport.d.mts.map +1 -1
- package/esm/mcp/transport.mjs.map +1 -1
- package/esm/register.d.mts +12 -1
- package/esm/register.d.mts.map +1 -1
- package/esm/register.mjs +3 -2
- package/esm/register.mjs.map +1 -1
- package/esm/schema.mjs.map +1 -1
- package/esm/utility/calculator.d.mts.map +1 -1
- package/esm/utility/calculator.mjs.map +1 -1
- package/esm/utility/date-time.d.mts.map +1 -1
- package/esm/utility/date-time.mjs.map +1 -1
- package/esm/utility/schema.mjs.map +1 -1
- package/esm/web/fetch-url.d.mts.map +1 -1
- package/esm/web/fetch-url.mjs.map +1 -1
- package/esm/web/schema.mjs.map +1 -1
- package/esm/web/web-search.mjs.map +1 -1
- package/llms-full.txt +194 -57
- package/llms.txt +3 -3
- package/package.json +2 -6
- package/skills/connect-mcp-server/SKILL.md +28 -6
- package/skills/expose-as-mcp-server/SKILL.md +117 -36
- package/skills/index.md +5 -2
- package/skills/use-web-and-http-tools/SKILL.md +49 -15
package/esm/web/schema.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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: [
|
|
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: [{ message: `\"${key}\" must be a finite number when provided`, path: [key] }],\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>(shape: ObjectShape<T>): 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,CAAC;GAAE,SAAS,IAAI,IAAI;GAA0C,MAAM,CAAC,GAAG;EAAE,CAAC,EACrF;CACF;AACF;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,aAA+B,OAA4C;CACzF,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"}
|
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
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(`web_search request to the \"${provider}\" provider failed: ${message}`, {\n type: \"request-failed\",\n cause,\n });\n }\n\n if (!response.ok) {\n throw new WebToolError(`web_search \"${provider}\" provider returned HTTP ${response.status}.`, {\n type: \"request-failed\",\n 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) — the `expose-as-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
|
|
11
|
+
description: 'Connect a @warlock.js/ai agent to an external Model Context Protocol (MCP) server and adapt its tools as native agent tools (Direction A) with `ai.mcp(server)`. Triggers: `signal`, `ai.mcp(`, `ai.mcp.serve` (the other direction), `mcp(`, `McpClient`, `McpClientOptions`, `McpTransport`, `client.tools()`, `client.close()`, `namePrefix`, `filter`, `McpTransportError`, `{ type: "stdio" }`, `{ type: "http" }`, `tools/list`, `tools/call`, `jsonSchemaToStandard`, `@modelcontextprotocol/sdk`, `ajv`; ''consume an MCP server'', ''use an external MCP server''s tools in my agent'', ''connect to the GitHub/Slack/Filesystem MCP server'', ''adapt MCP tools as agent tools'', ''bridge my agent to the MCP ecosystem'', ''run an MCP server over stdio/http''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: exposing YOUR agent/tools AS an MCP server (the reverse direction) — the `expose-as-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Connect to an external MCP server (Direction A)
|
|
@@ -55,20 +55,42 @@ The `server` argument is discriminated by `type`:
|
|
|
55
55
|
|
|
56
56
|
```ts
|
|
57
57
|
ai.mcp(server, {
|
|
58
|
-
namePrefix: "github.",
|
|
59
|
-
filter: (name) => name.startsWith("issues_")
|
|
60
|
-
timeoutMs: 30_000,
|
|
58
|
+
namePrefix: "github.", // prepended to every remote tool name
|
|
59
|
+
filter: (name) => name.startsWith("issues_"), // only adapt tools whose name passes
|
|
60
|
+
timeoutMs: 30_000, // per tools/call deadline (default 30s)
|
|
61
|
+
signal: controller.signal, // optional: close the client when this aborts
|
|
61
62
|
});
|
|
62
63
|
```
|
|
63
64
|
|
|
65
|
+
- **`signal`** binds the client's lifetime to a run: on abort the client closes its transport (kills the stdio child / ends the HTTP session), in-flight requests reject, and later `tools()` / tool calls fail with `McpTransportError` type `"closed"` without sending anything. Each `tools/call` honors it together with the per-call `ctx.signal` — whichever aborts first. `close()` stays idempotent.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import "@warlock.js/ai-tools";
|
|
69
|
+
import { ai, type ModelContract } from "@warlock.js/ai";
|
|
70
|
+
|
|
71
|
+
export async function triage(model: ModelContract): Promise<void> {
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const github = ai.mcp(
|
|
74
|
+
{ type: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] },
|
|
75
|
+
{ namePrefix: "github.", signal: controller.signal },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const agent = ai.agent({ model, tools: [...(await github.tools())] });
|
|
79
|
+
await agent.execute("Triage the open issues", { signal: controller.signal });
|
|
80
|
+
|
|
81
|
+
controller.abort(); // ends the run AND shuts the MCP server down
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
64
85
|
- **`namePrefix`** is the collision mitigation — there is **no runtime tool-name collision guard** (the agent resolves a tool by first name match), so the author owns uniqueness. Prefix remote tools to keep them distinct from local ones.
|
|
65
|
-
- **`filter`** receives each tool's
|
|
86
|
+
- **`filter`** receives each tool's _unprefixed_ name; return `false` to drop it.
|
|
66
87
|
|
|
67
88
|
## What `tools()` does
|
|
68
89
|
|
|
69
90
|
`client.tools()` is lazy and cached: the **first** call runs the `initialize` handshake, sends `notifications/initialized`, then `tools/list`, and maps each descriptor into a `ToolContract`. Repeat calls return the cached array without re-handshaking.
|
|
70
91
|
|
|
71
92
|
For each remote tool:
|
|
93
|
+
|
|
72
94
|
1. its JSON-Schema `inputSchema` is wrapped as a Standard Schema via `jsonSchemaToStandard` (Ajv-backed — `ajv` is an optional peer needed only for the client);
|
|
73
95
|
2. `execute` issues `tools/call` honoring `ctx.signal` (cooperative abort) and the configured `timeoutMs`, then unwraps the result content (a single JSON text block is parsed back into an object);
|
|
74
96
|
3. an MCP `isError` result is thrown so the surrounding `tool()` wraps it as a `ToolExecutionError` — the agent reads it as `{ error }` data and self-corrects.
|
|
@@ -97,7 +119,7 @@ try {
|
|
|
97
119
|
|
|
98
120
|
## Optional peers
|
|
99
121
|
|
|
100
|
-
The MCP client
|
|
122
|
+
The MCP client does not use `@modelcontextprotocol/sdk`: it speaks MCP's JSON-RPC itself, over a spawned child process (`node:child_process`) for `{ type: "stdio" }` and the global `fetch` for `{ type: "http" }`, so neither transport needs a dependency. Its only optional peer is `ajv`, which validates tool-call input against each MCP tool's `inputSchema`. It is imported lazily the first time a schema is checked; when it is missing, the call fails validation with a curated `npm install ajv` message instead of crashing at import time. A tool with no `inputSchema` needs no validator.
|
|
101
123
|
|
|
102
124
|
## See also
|
|
103
125
|
|
|
@@ -110,83 +132,164 @@ The MCP client lazily imports `@modelcontextprotocol/sdk` (the protocol SDK) and
|
|
|
110
132
|
|
|
111
133
|
---
|
|
112
134
|
name: expose-as-mcp-server
|
|
113
|
-
description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[]) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)
|
|
135
|
+
description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[], or named tool groups) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)` — over stdio or authenticated Streamable HTTP. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `McpHttpServeTransport`, `McpHttpAuth`, `McpServeError`, `server.start()`, `server.stop()`, `{ url, close }`, `{ type: "http", host, port, path, auth }`, `bearer`, `authorize`, `allowedOrigins`, `maxBodyBytes`, `prefixToolNames`, `groups`, `resources`, `prompts`, `ctx.request.headers`, `schemaTarget`, `tools/list`, `tools/call`, `{ type: "stdio" }`; ''expose my agent as an MCP server'', ''serve my tools over HTTP'', ''give each agent session its own MCP endpoint'', ''protect my MCP server with a token'', ''publish my tools to Cursor/Claude Desktop'', ''serve MCP resources or prompts'', ''tool name collision''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
|
|
114
136
|
---
|
|
115
137
|
|
|
116
138
|
# Expose tools AS an MCP server (Direction B)
|
|
117
139
|
|
|
118
|
-
`ai.mcp.serve(source, options)` turns a local set of `ToolContract`s into a Model Context Protocol server — so Cursor, Claude Desktop, or any MCP client can call your warlock tools / agent
|
|
140
|
+
`ai.mcp.serve(source, options)` turns a local set of `ToolContract`s into a Model Context Protocol server — so Cursor, Claude Desktop, a coding CLI, or any MCP client can call your warlock tools / agent. Transports: **stdio** (default) and MCP **Streamable HTTP**.
|
|
141
|
+
|
|
142
|
+
## Serve over HTTP with a per-session token
|
|
119
143
|
|
|
120
144
|
```ts
|
|
145
|
+
import { randomBytes } from "node:crypto";
|
|
121
146
|
import "@warlock.js/ai-tools";
|
|
122
147
|
import { ai } from "@warlock.js/ai";
|
|
123
148
|
|
|
124
|
-
const
|
|
125
|
-
[ai.tools.calculator(), ai.tools.dateTime()],
|
|
126
|
-
{ name: "warlock-utils", transport: { type: "stdio" } },
|
|
127
|
-
);
|
|
149
|
+
const token = randomBytes(32).toString("hex");
|
|
128
150
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
151
|
+
const server = ai.mcp.serve([ai.tools.calculator(), ai.tools.dateTime()], {
|
|
152
|
+
name: "session-tools",
|
|
153
|
+
transport: { type: "http", auth: { bearer: token } },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const { url, close } = await server.start();
|
|
157
|
+
// url → "http://127.0.0.1:<free port>/mcp" — hand url + token to the MCP client
|
|
158
|
+
|
|
159
|
+
await close(); // drains in-flight calls, then frees the port
|
|
132
160
|
```
|
|
133
161
|
|
|
134
|
-
|
|
162
|
+
Defaults: `host: "127.0.0.1"`, `port: 0` (free ephemeral port), `path: "/mcp"`. `start()` resolves `{ url, close }` once bound.
|
|
163
|
+
|
|
164
|
+
## Serve over stdio
|
|
135
165
|
|
|
136
166
|
```ts
|
|
137
|
-
|
|
138
|
-
ai
|
|
167
|
+
import "@warlock.js/ai-tools";
|
|
168
|
+
import { ai } from "@warlock.js/ai";
|
|
169
|
+
|
|
170
|
+
const server = ai.mcp.serve([ai.tools.calculator()], {
|
|
171
|
+
name: "warlock-utils",
|
|
172
|
+
transport: { type: "stdio" },
|
|
173
|
+
});
|
|
139
174
|
|
|
140
|
-
|
|
141
|
-
|
|
175
|
+
await server.start(); // reads JSON-RPC lines from stdin, answers on stdout
|
|
176
|
+
// on teardown:
|
|
177
|
+
await server.stop();
|
|
142
178
|
```
|
|
143
179
|
|
|
144
|
-
|
|
180
|
+
## HTTP options and security rules
|
|
145
181
|
|
|
146
|
-
|
|
182
|
+
```ts
|
|
183
|
+
transport: {
|
|
184
|
+
type: "http",
|
|
185
|
+
host: "127.0.0.1", // non-loopback (e.g. "0.0.0.0") REQUIRES auth — start() throws otherwise
|
|
186
|
+
port: 0,
|
|
187
|
+
path: "/mcp",
|
|
188
|
+
auth: {
|
|
189
|
+
bearer: ["current-token", "previous-token"], // constant-time compare; several = rotation
|
|
190
|
+
authorize: async (request) => sessions.has(request.headers["x-session-id"] ?? ""), // optional hook
|
|
191
|
+
},
|
|
192
|
+
allowedOrigins: ["https://app.example.com"], // besides loopback origins
|
|
193
|
+
allowedHosts: ["mcp.internal"], // extra Host names while on loopback
|
|
194
|
+
maxBodyBytes: 1_048_576, // larger → 413
|
|
195
|
+
drainTimeoutMs: 5_000, // close() then aborts ctx.signal
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- With `bearer` and `authorize` both set, a request must pass both. A throwing hook denies. Failures → `401`.
|
|
200
|
+
- A foreign or `null` `Origin` → `403` (DNS-rebinding guard from the MCP spec). Requests without `Origin` pass.
|
|
201
|
+
- On loopback, a `Host` other than `localhost` / `127.0.0.1` / `[::1]` (or `allowedHosts`) → `403`.
|
|
202
|
+
- Stateless: no session id; `GET` / `DELETE` → `405`; batches → `400`; unsupported `MCP-Protocol-Version` → `400`.
|
|
203
|
+
- Plain HTTP: put TLS in front before exposing beyond the machine.
|
|
204
|
+
|
|
205
|
+
## Tools read request headers from `ctx.request`
|
|
147
206
|
|
|
148
207
|
```ts
|
|
149
|
-
ai
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
208
|
+
import "@warlock.js/ai-tools";
|
|
209
|
+
import { ai, type ToolContract } from "@warlock.js/ai";
|
|
210
|
+
|
|
211
|
+
const whoami: ToolContract = ai.tool({
|
|
212
|
+
name: "whoami",
|
|
213
|
+
description: "Report the calling tenant",
|
|
214
|
+
execute: async (_input: unknown, ctx) => ({
|
|
215
|
+
tenant: ctx?.request?.headers["x-tenant-id"] ?? "unknown",
|
|
216
|
+
}),
|
|
154
217
|
});
|
|
218
|
+
|
|
219
|
+
const { url, close } = await ai.mcp
|
|
220
|
+
.serve([whoami], { name: "tenant-tools", transport: { type: "http" } })
|
|
221
|
+
.start();
|
|
155
222
|
```
|
|
156
223
|
|
|
157
|
-
|
|
224
|
+
Header names are lower-cased; every client header is present (including `authorization`). Over stdio `ctx.request` is absent. Nothing on `ctx.request` reaches the model.
|
|
158
225
|
|
|
159
|
-
##
|
|
226
|
+
## Sources and tool-name collisions
|
|
160
227
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
228
|
+
```ts
|
|
229
|
+
ai.mcp.serve([calc, dt], { name: "utils" }); // literal array
|
|
230
|
+
ai.mcp.serve({ tools: () => ws.tools.all() }, { name: "workspace" }); // anything with tools()
|
|
231
|
+
ai.mcp.serve(
|
|
232
|
+
{ groups: { prod: [search], staging: [search] } },
|
|
233
|
+
{
|
|
234
|
+
name: "search",
|
|
235
|
+
prefixToolNames: true, // → prod_search, staging_search; or (group, name) => `${group}.${name}`
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
```
|
|
164
239
|
|
|
165
|
-
|
|
240
|
+
Duplicate exposed names throw `McpServeError` (`type: "collision"`) **when `serve()` is called**. `prefixToolNames` on an ungrouped source throws `type: "config"`.
|
|
166
241
|
|
|
167
|
-
##
|
|
242
|
+
## Resources and prompts (optional)
|
|
168
243
|
|
|
169
244
|
```ts
|
|
170
|
-
|
|
171
|
-
|
|
245
|
+
ai.mcp.serve(tools, {
|
|
246
|
+
name: "handbook",
|
|
247
|
+
resources: [
|
|
248
|
+
{
|
|
249
|
+
uri: "docs://handbook",
|
|
250
|
+
name: "handbook",
|
|
251
|
+
mimeType: "text/markdown",
|
|
252
|
+
read: () => "# Handbook",
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
prompts: [
|
|
256
|
+
{
|
|
257
|
+
name: "review",
|
|
258
|
+
arguments: [{ name: "diff", required: true }],
|
|
259
|
+
get: (args) => `Review this diff:\n${args.diff ?? ""}`, // string → one user message
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
});
|
|
172
263
|
```
|
|
173
264
|
|
|
174
|
-
`
|
|
265
|
+
`read()` returns a string (text) or `Uint8Array` (base64 blob). `get()` returns a string or `McpPromptMessage[]`. Unknown resource → `-32002`; unknown prompt / missing required argument → `-32602`.
|
|
266
|
+
|
|
267
|
+
## How the protocol maps
|
|
268
|
+
|
|
269
|
+
- **`initialize`** → `{ name, version }`, capabilities `tools` (+ `resources` / `prompts` when configured); protocol version negotiated from `SUPPORTED_PROTOCOL_VERSIONS` (`2025-11-25`, `2025-06-18`, `2025-03-26`).
|
|
270
|
+
- **`tools/list`** → `inputSchema` via `extractJsonSchema(contract.input, { target: schemaTarget })` (default `"draft-2020-12"`).
|
|
271
|
+
- **`tools/call`** → `invoke()`; `data` → text block (JSON-stringified unless a string), `error` → `isError: true`. Unknown tool → JSON-RPC error.
|
|
272
|
+
- **`ping`** → `{}`.
|
|
273
|
+
|
|
274
|
+
## Drive the protocol yourself (tests, custom servers)
|
|
175
275
|
|
|
176
276
|
```ts
|
|
177
277
|
import { createServeHandler } from "@warlock.js/ai-tools";
|
|
278
|
+
import { ai } from "@warlock.js/ai";
|
|
178
279
|
|
|
179
|
-
const handler = createServeHandler([
|
|
280
|
+
const handler = createServeHandler([ai.tools.calculator()], { name: "utils" });
|
|
180
281
|
|
|
181
|
-
|
|
182
|
-
|
|
282
|
+
const response = await handler.handle(
|
|
283
|
+
{ jsonrpc: "2.0", id: 1, method: "tools/list" },
|
|
284
|
+
{ request: { method: "POST", path: "/mcp", headers: { "x-tenant-id": "acme" } } },
|
|
285
|
+
);
|
|
183
286
|
```
|
|
184
287
|
|
|
185
|
-
|
|
288
|
+
Mounting it in your own server means you own Origin validation, auth and body limits.
|
|
186
289
|
|
|
187
290
|
## Errors
|
|
188
291
|
|
|
189
|
-
`
|
|
292
|
+
`McpServeError.type`: `"collision"` (from `serve()`), `"config"` (unsafe/invalid options, from `start()`), `"listen"` (bind failed). Bad requests never throw — they get an HTTP status or JSON-RPC error; failing tools return `isError: true`.
|
|
190
293
|
|
|
191
294
|
## See also
|
|
192
295
|
|
|
@@ -199,7 +302,7 @@ const response = await handler.handle(jsonRpcRequest); // request → response,
|
|
|
199
302
|
|
|
200
303
|
---
|
|
201
304
|
name: use-web-and-http-tools
|
|
202
|
-
description: 'Wire the @warlock.js/ai-tools belt — web search, fetch/scrape, HTTP/REST, calculator, date-time — into an agent under their guardrails. Triggers: `ai.tools.webSearch`, `ai.tools.fetchUrl`, `ai.tools.http`, `ai.tools.calculator`, `ai.tools.dateTime`, `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `web_search`, `fetch_url`, `http_request`, `calculator`, `date_time`, `WebToolError`, `HttpPolicyError`, `CalculatorError`, `DateTimeError`, `allowHosts`, `allowMethods`, `allowPrivateNetwork`, `baseUrl`, `maxBytes`, `guardedFetch`, `SearchProvider`, `TAVILY_API_KEY`; ''give an agent web search'', ''let an agent fetch/scrape a page'', ''let an agent call a REST API'', ''restrict which hosts/methods an agent can hit'', ''add a calculator/date tool to an agent'', ''SSRF guardrail for an agent tool'', ''let a tool reach an internal/dev service''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: connecting to an external MCP server — the `connect-mcp-server` topic; exposing tools AS an MCP server — the `expose-as-mcp-server` topic; filesystem/shell tools — the `use-a-workspace` topic of the `warlock-js-ai-workspace` skill; defining your own custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.'
|
|
305
|
+
description: 'Wire the @warlock.js/ai-tools belt — web search, fetch/scrape, HTTP/REST, calculator, date-time — into an agent under their guardrails. Triggers: `ai.tools.measure`, `measureTools`, `ai.tools.webSearch`, `ai.tools.fetchUrl`, `ai.tools.http`, `ai.tools.calculator`, `ai.tools.dateTime`, `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `web_search`, `fetch_url`, `http_request`, `calculator`, `date_time`, `WebToolError`, `HttpPolicyError`, `CalculatorError`, `DateTimeError`, `allowHosts`, `allowMethods`, `allowPrivateNetwork`, `baseUrl`, `maxBytes`, `guardedFetch`, `SearchProvider`, `TAVILY_API_KEY`; ''give an agent web search'', ''let an agent fetch/scrape a page'', ''let an agent call a REST API'', ''restrict which hosts/methods an agent can hit'', ''add a calculator/date tool to an agent'', ''SSRF guardrail for an agent tool'', ''let a tool reach an internal/dev service''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: connecting to an external MCP server — the `connect-mcp-server` topic; exposing tools AS an MCP server — the `expose-as-mcp-server` topic; filesystem/shell tools — the `use-a-workspace` topic of the `warlock-js-ai-workspace` skill; defining your own custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.'
|
|
203
306
|
---
|
|
204
307
|
|
|
205
308
|
# Use the web + HTTP tool belt
|
|
@@ -227,17 +330,17 @@ const agent = ai.agent({
|
|
|
227
330
|
});
|
|
228
331
|
```
|
|
229
332
|
|
|
230
|
-
The construction `options` are the **rails** — they bound what the model may do; the model supplies the per-call inputs
|
|
333
|
+
The construction `options` are the **rails** — they bound what the model may do; the model supplies the per-call inputs _within_ those rails. Least-privilege is just array composition: hand an agent only the tools (and only the methods/hosts) it needs.
|
|
231
334
|
|
|
232
335
|
## The five tools
|
|
233
336
|
|
|
234
|
-
| Factory
|
|
235
|
-
|
|
236
|
-
| `ai.tools.webSearch(options)`
|
|
237
|
-
| `ai.tools.fetchUrl(options?)`
|
|
238
|
-
| `ai.tools.http(options?)`
|
|
239
|
-
| `ai.tools.calculator(options?)` | `calculator`
|
|
240
|
-
| `ai.tools.dateTime(options?)`
|
|
337
|
+
| Factory | Name | Model input | Returns |
|
|
338
|
+
| ------------------------------- | -------------- | -------------------------------------------------------------- | ------------------------------------------------ |
|
|
339
|
+
| `ai.tools.webSearch(options)` | `web_search` | `{ query, maxResults? }` | `{ results: { title, url, snippet, score? }[] }` |
|
|
340
|
+
| `ai.tools.fetchUrl(options?)` | `fetch_url` | `{ url }` | `{ url, status, content, truncated }` |
|
|
341
|
+
| `ai.tools.http(options?)` | `http_request` | `{ method?, url, headers?, body? }` | `{ status, headers, body, truncated }` |
|
|
342
|
+
| `ai.tools.calculator(options?)` | `calculator` | `{ expression }` | `{ result }` |
|
|
343
|
+
| `ai.tools.dateTime(options?)` | `date_time` | `{ op, iso?, from?, to?, amount?, unit?, timeZone?, format? }` | `{ value }` |
|
|
241
344
|
|
|
242
345
|
## `web_search` — pick a provider, supply a key
|
|
243
346
|
|
|
@@ -252,7 +355,12 @@ ai.tools.webSearch({ provider: "tavily", apiKey: process.env.TAVILY_API_KEY, max
|
|
|
252
355
|
## `fetch_url` — read a page you have the URL for
|
|
253
356
|
|
|
254
357
|
```ts
|
|
255
|
-
ai.tools.fetchUrl({
|
|
358
|
+
ai.tools.fetchUrl({
|
|
359
|
+
extract: "text",
|
|
360
|
+
allowHosts: ["docs.stripe.com"],
|
|
361
|
+
maxBytes: 500_000,
|
|
362
|
+
timeoutMs: 10_000,
|
|
363
|
+
});
|
|
256
364
|
```
|
|
257
365
|
|
|
258
366
|
- `extract`: `"text"` (default — readability-extracted main text), `"html"` (raw body), or `"markdown"`. **`"text"`/`"markdown"` lazily import the `@mozilla/readability` + `jsdom` optional peers** — a missing peer is a `WebToolError` of type `"missing-peer"` carrying the `npm install @mozilla/readability jsdom` string. `"html"` needs nothing.
|
|
@@ -267,7 +375,7 @@ ai.tools.fetchUrl({ extract: "text", allowHosts: ["docs.stripe.com"], maxBytes:
|
|
|
267
375
|
ai.tools.http({
|
|
268
376
|
baseUrl: "https://api.stripe.com",
|
|
269
377
|
allowHosts: ["api.stripe.com"],
|
|
270
|
-
allowMethods: ["GET", "POST"],
|
|
378
|
+
allowMethods: ["GET", "POST"], // default ["GET"] — mutating methods are opt-in
|
|
271
379
|
headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },
|
|
272
380
|
timeoutMs: 15_000,
|
|
273
381
|
maxBytes: 1_000_000,
|
|
@@ -276,6 +384,7 @@ ai.tools.http({
|
|
|
276
384
|
```
|
|
277
385
|
|
|
278
386
|
All guardrails are enforced **before the network call** and surface as a typed `HttpPolicyError`:
|
|
387
|
+
|
|
279
388
|
- a method outside `allowMethods` → `"method-not-allowed"`;
|
|
280
389
|
- a host outside `allowHosts` → `"host-not-allowed"` (SSRF guardrail);
|
|
281
390
|
- an unresolvable URL → `"invalid-url"`.
|
|
@@ -296,9 +405,15 @@ Supports `+ - * / % ^`, unary signs, parentheses, and decimal/scientific literal
|
|
|
296
405
|
|
|
297
406
|
```ts
|
|
298
407
|
await ai.tools.dateTime().invoke({ op: "now", format: "iso" });
|
|
299
|
-
await ai.tools
|
|
300
|
-
|
|
301
|
-
|
|
408
|
+
await ai.tools
|
|
409
|
+
.dateTime()
|
|
410
|
+
.invoke({ op: "add", iso: "2026-06-22T00:00:00Z", amount: 3, unit: "days" });
|
|
411
|
+
await ai.tools
|
|
412
|
+
.dateTime()
|
|
413
|
+
.invoke({ op: "diff", iso: "2026-06-22T00:00:00Z", to: "2026-06-25T00:00:00Z", unit: "days" }); // "3"
|
|
414
|
+
await ai.tools
|
|
415
|
+
.dateTime({ defaultTimeZone: "Africa/Cairo" })
|
|
416
|
+
.invoke({ op: "format", iso: "2026-06-22T12:00:00Z", format: "datetime" });
|
|
302
417
|
```
|
|
303
418
|
|
|
304
419
|
- `op`: `"now"` / `"add"` / `"diff"` / `"format"`. `format`: `"iso"` (default), `"date"`, `"time"`, `"datetime"`.
|
|
@@ -306,6 +421,26 @@ await ai.tools.dateTime({ defaultTimeZone: "Africa/Cairo" }).invoke({ op: "forma
|
|
|
306
421
|
- Units are millisecond-based: `milliseconds`, `seconds`, `minutes`, `hours`, `days`, `weeks` (plus singular/short aliases like `day`/`d`). Calendar-relative `month`/`year` are intentionally **unsupported** — they have no fixed length. An unknown unit → `DateTimeError` `"invalid-unit"`; an unknown IANA zone → `"invalid-time-zone"`.
|
|
307
422
|
- For deterministic tests, inject a clock by calling the factory directly: `import { dateTimeTool } from "@warlock.js/ai-tools"; dateTimeTool({}, { clock: () => Date.parse("2026-06-22T00:00:00Z") })`.
|
|
308
423
|
|
|
424
|
+
## `ai.tools.measure` — what the belt costs per model call
|
|
425
|
+
|
|
426
|
+
`ai.tools.measure(tools, options?)` is `measureTools` from `@warlock.js/ai`: the input tokens a tool list adds to every call, per tool and in total. Native and exact when the model has `countTokens` (`source: "native"`, `tools.length + 2` count calls, `overhead` = the provider's once-per-request tool preamble); estimated from the schema JSON otherwise (`source: "estimate"`).
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import "@warlock.js/ai-tools";
|
|
430
|
+
import { ai, type ModelContract } from "@warlock.js/ai";
|
|
431
|
+
|
|
432
|
+
export async function reportBeltCost(model: ModelContract): Promise<void> {
|
|
433
|
+
const belt = [ai.tools.calculator(), ai.tools.dateTime()];
|
|
434
|
+
|
|
435
|
+
const full = await ai.tools.measure(belt, { model });
|
|
436
|
+
const compact = await ai.tools.measure(belt, { model, mode: "compact" });
|
|
437
|
+
|
|
438
|
+
console.log(full.source, full.tokens, "→", compact.tokens, full.perTool);
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
If compact saves enough, run the agent with `toolSchemas: "compact"` (the `define-ai-tool` topic of the `warlock-js-ai` skill).
|
|
443
|
+
|
|
309
444
|
## Errors flow as data, not throws
|
|
310
445
|
|
|
311
446
|
Every failure above is thrown inside the tool handler, caught by the framework's `tool()` wrapper, and surfaced in the returned `{ error }` field — `invoke()` never throws, so the agent reads the failure as a `role: "tool"` message and self-corrects on the next trip. Each error class extends the `@warlock.js/ai` `AIError` base; branch on `.type`:
|
|
@@ -313,7 +448,9 @@ Every failure above is thrown inside the tool handler, caught by the framework's
|
|
|
313
448
|
```ts
|
|
314
449
|
import { WebToolError } from "@warlock.js/ai-tools";
|
|
315
450
|
|
|
316
|
-
const { error } = await ai.tools
|
|
451
|
+
const { error } = await ai.tools
|
|
452
|
+
.fetchUrl({ allowHosts: ["docs.stripe.com"] })
|
|
453
|
+
.invoke({ url: "http://evil.test" });
|
|
317
454
|
if (error instanceof WebToolError && error.type === "denied-host") {
|
|
318
455
|
// rejected before any network call
|
|
319
456
|
}
|
package/llms.txt
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
8
|
|
|
9
|
-
- [connect-mcp-server](@warlock.js/ai-tools/connect-mcp-server/SKILL.md): Connect a @warlock.js/ai agent to an external Model Context Protocol (MCP) server and adapt its tools as native agent tools (Direction A) with `ai.mcp(server)`. Triggers: `ai.mcp(`, `ai.mcp.serve` (the other direction), `mcp(`, `McpClient`, `McpClientOptions`, `McpTransport`, `client.tools()`, `client.close()`, `namePrefix`, `filter`, `McpTransportError`, `{ type: "stdio" }`, `{ type: "http" }`, `tools/list`, `tools/call`, `jsonSchemaToStandard`, `@modelcontextprotocol/sdk`, `ajv`; 'consume an MCP server', 'use an external MCP server's tools in my agent', 'connect to the GitHub/Slack/Filesystem MCP server', 'adapt MCP tools as agent tools', 'bridge my agent to the MCP ecosystem', 'run an MCP server over stdio/http'; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: exposing YOUR agent/tools AS an MCP server (the reverse direction) — the `expose-as-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.
|
|
10
|
-
- [expose-as-mcp-server](@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md): Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[]) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)
|
|
11
|
-
- [use-web-and-http-tools](@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md): Wire the @warlock.js/ai-tools belt — web search, fetch/scrape, HTTP/REST, calculator, date-time — into an agent under their guardrails. Triggers: `ai.tools.webSearch`, `ai.tools.fetchUrl`, `ai.tools.http`, `ai.tools.calculator`, `ai.tools.dateTime`, `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `web_search`, `fetch_url`, `http_request`, `calculator`, `date_time`, `WebToolError`, `HttpPolicyError`, `CalculatorError`, `DateTimeError`, `allowHosts`, `allowMethods`, `allowPrivateNetwork`, `baseUrl`, `maxBytes`, `guardedFetch`, `SearchProvider`, `TAVILY_API_KEY`; 'give an agent web search', 'let an agent fetch/scrape a page', 'let an agent call a REST API', 'restrict which hosts/methods an agent can hit', 'add a calculator/date tool to an agent', 'SSRF guardrail for an agent tool', 'let a tool reach an internal/dev service'; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: connecting to an external MCP server — the `connect-mcp-server` topic; exposing tools AS an MCP server — the `expose-as-mcp-server` topic; filesystem/shell tools — the `use-a-workspace` topic of the `warlock-js-ai-workspace` skill; defining your own custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.
|
|
9
|
+
- [connect-mcp-server](@warlock.js/ai-tools/connect-mcp-server/SKILL.md): Connect a @warlock.js/ai agent to an external Model Context Protocol (MCP) server and adapt its tools as native agent tools (Direction A) with `ai.mcp(server)`. Triggers: `signal`, `ai.mcp(`, `ai.mcp.serve` (the other direction), `mcp(`, `McpClient`, `McpClientOptions`, `McpTransport`, `client.tools()`, `client.close()`, `namePrefix`, `filter`, `McpTransportError`, `{ type: "stdio" }`, `{ type: "http" }`, `tools/list`, `tools/call`, `jsonSchemaToStandard`, `@modelcontextprotocol/sdk`, `ajv`; 'consume an MCP server', 'use an external MCP server's tools in my agent', 'connect to the GitHub/Slack/Filesystem MCP server', 'adapt MCP tools as agent tools', 'bridge my agent to the MCP ecosystem', 'run an MCP server over stdio/http'; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: exposing YOUR agent/tools AS an MCP server (the reverse direction) — the `expose-as-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.
|
|
10
|
+
- [expose-as-mcp-server](@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md): Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[], or named tool groups) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)` — over stdio or authenticated Streamable HTTP. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `McpHttpServeTransport`, `McpHttpAuth`, `McpServeError`, `server.start()`, `server.stop()`, `{ url, close }`, `{ type: "http", host, port, path, auth }`, `bearer`, `authorize`, `allowedOrigins`, `maxBodyBytes`, `prefixToolNames`, `groups`, `resources`, `prompts`, `ctx.request.headers`, `schemaTarget`, `tools/list`, `tools/call`, `{ type: "stdio" }`; 'expose my agent as an MCP server', 'serve my tools over HTTP', 'give each agent session its own MCP endpoint', 'protect my MCP server with a token', 'publish my tools to Cursor/Claude Desktop', 'serve MCP resources or prompts', 'tool name collision'; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.
|
|
11
|
+
- [use-web-and-http-tools](@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md): Wire the @warlock.js/ai-tools belt — web search, fetch/scrape, HTTP/REST, calculator, date-time — into an agent under their guardrails. Triggers: `ai.tools.measure`, `measureTools`, `ai.tools.webSearch`, `ai.tools.fetchUrl`, `ai.tools.http`, `ai.tools.calculator`, `ai.tools.dateTime`, `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `web_search`, `fetch_url`, `http_request`, `calculator`, `date_time`, `WebToolError`, `HttpPolicyError`, `CalculatorError`, `DateTimeError`, `allowHosts`, `allowMethods`, `allowPrivateNetwork`, `baseUrl`, `maxBytes`, `guardedFetch`, `SearchProvider`, `TAVILY_API_KEY`; 'give an agent web search', 'let an agent fetch/scrape a page', 'let an agent call a REST API', 'restrict which hosts/methods an agent can hit', 'add a calculator/date tool to an agent', 'SSRF guardrail for an agent tool', 'let a tool reach an internal/dev service'; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: connecting to an external MCP server — the `connect-mcp-server` topic; exposing tools AS an MCP server — the `expose-as-mcp-server` topic; filesystem/shell tools — the `use-a-workspace` topic of the `warlock-js-ai-workspace` skill; defining your own custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.
|
package/package.json
CHANGED
|
@@ -20,17 +20,13 @@
|
|
|
20
20
|
"directory": "packages/ai-tools"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@modelcontextprotocol/sdk": "*",
|
|
24
23
|
"@mozilla/readability": "*",
|
|
25
24
|
"@tavily/core": "*",
|
|
26
|
-
"@warlock.js/ai": "5.
|
|
25
|
+
"@warlock.js/ai": "5.31.0",
|
|
27
26
|
"ajv": "*",
|
|
28
27
|
"jsdom": "*"
|
|
29
28
|
},
|
|
30
29
|
"peerDependenciesMeta": {
|
|
31
|
-
"@modelcontextprotocol/sdk": {
|
|
32
|
-
"optional": true
|
|
33
|
-
},
|
|
34
30
|
"@mozilla/readability": {
|
|
35
31
|
"optional": true
|
|
36
32
|
},
|
|
@@ -47,7 +43,7 @@
|
|
|
47
43
|
"dependencies": {
|
|
48
44
|
"@standard-schema/spec": "^1.0.0"
|
|
49
45
|
},
|
|
50
|
-
"version": "5.
|
|
46
|
+
"version": "5.31.0",
|
|
51
47
|
"main": "./cjs/index.cjs",
|
|
52
48
|
"module": "./esm/index.mjs",
|
|
53
49
|
"types": "./esm/index.d.mts",
|