@warlock.js/ai-tools 5.2.3 → 5.3.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.
@@ -1 +1 @@
1
- {"version":3,"file":"http-request.mjs","names":[],"sources":["../../../../../../../ai-tools/src/http/http-request.ts"],"sourcesContent":["import {\n guardedFetch,\n OutboundPolicyError,\n tool,\n type ToolContract,\n} from \"@warlock.js/ai\";\nimport type {\n HttpMethod,\n HttpRequestInput,\n HttpRequestOptions,\n HttpRequestResult,\n} from \"../contracts\";\nimport { HttpPolicyError } from \"../errors\";\nimport {\n objectSchema,\n optionalStringEnumField,\n optionalStringRecordField,\n passthroughField,\n stringField,\n} from \"../schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"http_request\";\n\n/** Default per-request wall-clock timeout, in milliseconds. */\nconst DEFAULT_TIMEOUT_MS = 15_000;\n\n/** Default hard cap on response-body bytes before truncation. */\nconst DEFAULT_MAX_BYTES = 1_000_000;\n\n/** The full set of HTTP methods, in the order they appear in {@link HttpMethod}. */\nconst ALL_METHODS: readonly HttpMethod[] = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n\n/** Methods that conventionally carry no request body — `body` is dropped for these. */\nconst BODYLESS_METHODS: ReadonlySet<HttpMethod> = new Set<HttpMethod>([\"GET\"]);\n\n/**\n * Standard Schema for {@link HttpRequestInput}. `url` is required;\n * `method` is constrained to the canonical HTTP verb set (further\n * narrowed to the tool's `allowMethods` at runtime); `headers` is an\n * optional string-to-string record; `body` is an opaque passthrough the\n * handler serializes based on its runtime type.\n */\nconst httpRequestInputSchema = objectSchema<HttpRequestInput>({\n method: optionalStringEnumField<HttpMethod>(ALL_METHODS),\n url: stringField(),\n headers: optionalStringRecordField(),\n body: passthroughField(),\n});\n\n/**\n * Resolve the request target. With a `baseUrl` configured the model\n * supplies a path joined against it; otherwise the model's `url` must be\n * an absolute `http(s)` URL. Throws a typed {@link HttpPolicyError} of\n * type `\"invalid-url\"` when the result cannot be parsed or is not an\n * `http`/`https` URL — surfaced as `{ error }` data, never a crash.\n */\nfunction resolveUrl(rawUrl: string, baseUrl: string | undefined): URL {\n let resolved: URL;\n\n try {\n // `new URL(input, base)` joins relative paths against `base` and\n // ignores `base` when `input` is already absolute, which is exactly\n // the \"path vs full URL\" behavior the design specifies.\n resolved = baseUrl !== undefined ? new URL(rawUrl, baseUrl) : new URL(rawUrl);\n } catch {\n throw new HttpPolicyError(\n `http_request could not resolve a valid URL from \"${rawUrl}\"` +\n (baseUrl !== undefined ? ` against base \"${baseUrl}\".` : \".\"),\n { type: \"invalid-url\" },\n );\n }\n\n if (resolved.protocol !== \"http:\" && resolved.protocol !== \"https:\") {\n throw new HttpPolicyError(\n `http_request only permits http(s) URLs; got \"${resolved.protocol}\".`,\n { type: \"invalid-url\" },\n );\n }\n\n return resolved;\n}\n\n/**\n * Read a `Response` body, capping at `maxBytes`. Returns the decoded text\n * and whether it was cut off. Streams chunk-by-chunk so an oversized body\n * is abandoned at the cap rather than fully buffered; falls back to\n * `response.text()` (then a post-hoc byte slice) when the body is not a\n * readable stream (e.g. a stubbed `Response` in tests).\n */\nasync function readCappedBody(\n response: Response,\n maxBytes: number,\n): Promise<{ text: string; truncated: boolean }> {\n const body = response.body;\n\n if (!body) {\n return { text: \"\", truncated: false };\n }\n\n const decoder = new TextDecoder();\n const reader = body.getReader();\n let received = 0;\n let truncated = false;\n let text = \"\";\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (!value) {\n continue;\n }\n\n const remaining = maxBytes - received;\n\n if (value.byteLength > remaining) {\n text += decoder.decode(value.subarray(0, remaining), { stream: true });\n received = maxBytes;\n truncated = true;\n break;\n }\n\n text += decoder.decode(value, { stream: true });\n received += value.byteLength;\n }\n } finally {\n // Release the lock and abandon any unread remainder.\n await reader.cancel().catch(() => undefined);\n reader.releaseLock();\n }\n\n text += decoder.decode();\n\n return { text, truncated };\n}\n\n/**\n * Decide whether a response's `content-type` indicates JSON. Matches\n * `application/json` and the `+json` structured-suffix convention\n * (e.g. `application/vnd.api+json`), case-insensitively.\n */\nfunction isJsonContentType(contentType: string | undefined): boolean {\n if (!contentType) {\n return false;\n }\n\n const value = contentType.toLowerCase();\n\n return value.includes(\"application/json\") || value.includes(\"+json\");\n}\n\n/**\n * Build the agent-facing `http_request` tool — a guarded HTTP/REST client\n * over the global `fetch`. The `options` bound what the model may do; the\n * model supplies the per-call URL / method / headers / body within those\n * rails.\n *\n * **Guardrails (all enforced before the network call).**\n * - **Method allowlist** — `allowMethods` (default `[\"GET\"]`). A method\n * outside the list is rejected with a typed\n * {@link HttpPolicyError} (`type: \"method-not-allowed\"`).\n * - **Private-network deny (default).** The request — and every redirect\n * hop — goes through the framework's `guardedFetch` outbound policy,\n * which refuses private / loopback / link-local / cloud-metadata\n * addresses (and hostnames resolving to them) unless\n * `allowPrivateNetwork: true` is set. This applies even when\n * `allowHosts` is not configured, so a bare `ai.tools.http()` is not\n * an SSRF primitive (`type: \"host-not-allowed\"`).\n * - **Host allowlist** — when `allowHosts` is set, any other host is\n * rejected (`type: \"host-not-allowed\"`), an SSRF guardrail; redirect\n * targets are held to the same allowlist.\n * - **`baseUrl` join** — when configured, the model passes a path that\n * is resolved against `baseUrl`; otherwise it must pass an absolute\n * `http(s)` URL. An unresolvable URL is rejected\n * (`type: \"invalid-url\"`).\n *\n * **Request shaping.** Static `options.headers` are merged under the\n * per-call `headers` (the per-call value wins). An object `body` is\n * JSON-serialized with a `content-type: application/json` default; a\n * string `body` is sent verbatim; `body` is dropped for bodyless methods\n * (`GET`). The call is bounded by `timeoutMs` (default `15_000`) via an\n * `AbortController`, also wired to `ctx.signal` for cooperative\n * cancellation.\n *\n * **Response shaping.** Headers are returned with lower-cased keys. The\n * body is read up to `maxBytes` (default `1_000_000`) and JSON-parsed\n * when the response `content-type` is JSON, otherwise returned as text;\n * `truncated` is `true` when the body was cut off at the cap (a truncated\n * JSON body is returned as the raw partial string, since it can no longer\n * be parsed).\n *\n * **Errors flow as data.** Every guardrail rejection and network failure\n * is thrown inside `execute`; the framework's `tool()` wrapper catches it\n * and surfaces it in the returned `{ error }` field, so the agent reads\n * the failure and self-corrects rather than crashing.\n *\n * @param options - Construction-time policy bounding the tool.\n * @returns A {@link ToolContract} the agent can call as `http_request`.\n *\n * @example\n * const stripe = httpRequestTool({\n * baseUrl: \"https://api.stripe.com\",\n * allowHosts: [\"api.stripe.com\"],\n * allowMethods: [\"GET\", \"POST\"],\n * headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },\n * });\n * const { data } = await stripe.invoke({ method: \"GET\", url: \"/v1/charges\" });\n */\nexport function httpRequestTool(\n options: HttpRequestOptions = {},\n): ToolContract<HttpRequestInput, HttpRequestResult> {\n const allowMethods = options.allowMethods ?? [\"GET\"];\n const allowedMethodSet = new Set<HttpMethod>(allowMethods);\n const allowHostSet = options.allowHosts ? new Set(options.allowHosts) : undefined;\n const allowPrivateNetwork = options.allowPrivateNetwork ?? false;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n const staticHeaders = options.headers;\n\n return tool<HttpRequestInput, HttpRequestResult>({\n name: options.name ?? DEFAULT_NAME,\n description:\n \"Issue an HTTP request and return the status, response headers, and \" +\n \"parsed body. Allowed methods and hosts are restricted by the tool's \" +\n \"configuration; a request outside those rails is rejected before any \" +\n \"network call. Pass an object body to send JSON, or a string to send \" +\n \"it verbatim. The response body is JSON-parsed when the content-type \" +\n \"is JSON, otherwise returned as text, and is capped — `truncated` is \" +\n \"true when the body was cut off.\",\n action: (input) => `Requesting ${input.method ?? \"GET\"} ${input.url}`,\n input: httpRequestInputSchema,\n async execute(input, ctx) {\n const method: HttpMethod = input.method ?? \"GET\";\n\n // 1. Method allowlist — rejected before anything else.\n if (!allowedMethodSet.has(method)) {\n throw new HttpPolicyError(\n `http_request method \"${method}\" is not allowed. ` +\n `Permitted methods: ${[...allowedMethodSet].join(\", \")}.`,\n { type: \"method-not-allowed\" },\n );\n }\n\n // 2. URL resolution (baseUrl join when configured).\n const url = resolveUrl(input.url, options.baseUrl);\n\n // 3. Host allowlist — SSRF guardrail, before the fetch.\n if (allowHostSet && !allowHostSet.has(url.hostname)) {\n throw new HttpPolicyError(\n `http_request host \"${url.hostname}\" is not in the allowlist. ` +\n `Permitted hosts: ${[...allowHostSet].join(\", \")}.`,\n { type: \"host-not-allowed\" },\n );\n }\n\n // 4. Merge headers — static option headers under the per-call ones,\n // so a per-call header overrides a static default of the same name.\n const headers: Record<string, string> = { ...staticHeaders, ...input.headers };\n\n // 5. Shape the body. Dropped for bodyless methods; objects become\n // JSON (with a default content-type); strings are sent verbatim.\n let body: string | undefined;\n\n if (!BODYLESS_METHODS.has(method) && input.body !== undefined) {\n if (typeof input.body === \"string\") {\n body = input.body;\n } else {\n body = JSON.stringify(input.body);\n\n const hasContentType = Object.keys(headers).some(\n (key) => key.toLowerCase() === \"content-type\",\n );\n\n if (!hasContentType) {\n headers[\"content-type\"] = \"application/json\";\n }\n }\n }\n\n // 6. Issue the request through the shared hardened outbound path:\n // private-IP deny by default, per-hop redirect re-validation,\n // timeout, and the caller's signal — never a raw fetch.\n let response: Response;\n\n try {\n response = await guardedFetch(\n url.toString(),\n {\n allowedSchemes: [\"http\", \"https\"],\n hostAllowlist: options.allowHosts,\n denyPrivateIPsAfterDNS: !allowPrivateNetwork,\n timeoutMs,\n signal: ctx?.signal,\n },\n { method, headers, body },\n );\n } catch (cause) {\n if (cause instanceof OutboundPolicyError) {\n throw new HttpPolicyError(`http_request blocked: ${cause.message}`, {\n type: \"host-not-allowed\",\n cause,\n });\n }\n\n throw cause;\n }\n\n // 7. Collect response headers with lower-cased keys.\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key.toLowerCase()] = value;\n });\n\n // 8. Read the body up to the cap, then parse-or-pass.\n const { text, truncated } = await readCappedBody(response, maxBytes);\n\n let parsedBody: unknown = text;\n\n // A truncated body can no longer be valid JSON, so only attempt a\n // parse on a complete JSON response; otherwise hand back the raw text.\n if (!truncated && isJsonContentType(responseHeaders[\"content-type\"]) && text.length > 0) {\n try {\n parsedBody = JSON.parse(text);\n } catch {\n // Content-type claimed JSON but the body was not — fall back to\n // the raw text rather than failing the whole call.\n parsedBody = text;\n }\n }\n\n return {\n status: response.status,\n headers: responseHeaders,\n body: parsedBody,\n truncated,\n };\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"http-request.mjs","names":[],"sources":["../../../../../../../ai-tools/src/http/http-request.ts"],"sourcesContent":["import {\n guardedFetch,\n OutboundPolicyError,\n tool,\n type ToolContract,\n} from \"@warlock.js/ai\";\nimport type {\n HttpMethod,\n HttpRequestInput,\n HttpRequestOptions,\n HttpRequestResult,\n} from \"../contracts\";\nimport { HttpPolicyError } from \"../errors\";\nimport {\n objectSchema,\n optionalStringEnumField,\n optionalStringRecordField,\n passthroughField,\n stringField,\n} from \"../schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"http_request\";\n\n/** Default per-request wall-clock timeout, in milliseconds. */\nconst DEFAULT_TIMEOUT_MS = 15_000;\n\n/** Default hard cap on response-body bytes before truncation. */\nconst DEFAULT_MAX_BYTES = 1_000_000;\n\n/** The full set of HTTP methods, in the order they appear in {@link HttpMethod}. */\nconst ALL_METHODS: readonly HttpMethod[] = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n\n/** Methods that conventionally carry no request body — `body` is dropped for these. */\nconst BODYLESS_METHODS: ReadonlySet<HttpMethod> = new Set<HttpMethod>([\"GET\"]);\n\n/**\n * Standard Schema for {@link HttpRequestInput}. `url` is required;\n * `method` is constrained to the canonical HTTP verb set (further\n * narrowed to the tool's `allowMethods` at runtime); `headers` is an\n * optional string-to-string record; `body` is an opaque passthrough the\n * handler serializes based on its runtime type.\n */\nconst httpRequestInputSchema = objectSchema<HttpRequestInput>({\n method: optionalStringEnumField<HttpMethod>(ALL_METHODS),\n url: stringField(),\n headers: optionalStringRecordField(),\n body: passthroughField(),\n});\n\n/**\n * Resolve the request target. With a `baseUrl` configured the model\n * supplies a path joined against it; otherwise the model's `url` must be\n * an absolute `http(s)` URL. Throws a typed {@link HttpPolicyError} of\n * type `\"invalid-url\"` when the result cannot be parsed or is not an\n * `http`/`https` URL — surfaced as `{ error }` data, never a crash.\n */\nfunction resolveUrl(rawUrl: string, baseUrl: string | undefined): URL {\n let resolved: URL;\n\n try {\n // `new URL(input, base)` joins relative paths against `base` and\n // ignores `base` when `input` is already absolute, which is exactly\n // the \"path vs full URL\" behavior the design specifies.\n resolved = baseUrl !== undefined ? new URL(rawUrl, baseUrl) : new URL(rawUrl);\n } catch {\n throw new HttpPolicyError(\n `http_request could not resolve a valid URL from \"${rawUrl}\"` +\n (baseUrl !== undefined ? ` against base \"${baseUrl}\".` : \".\"),\n { type: \"invalid-url\" },\n );\n }\n\n if (resolved.protocol !== \"http:\" && resolved.protocol !== \"https:\") {\n throw new HttpPolicyError(\n `http_request only permits http(s) URLs; got \"${resolved.protocol}\".`,\n { type: \"invalid-url\" },\n );\n }\n\n return resolved;\n}\n\n/**\n * Read a `Response` body, capping at `maxBytes`. Returns the decoded text\n * and whether it was cut off. Streams chunk-by-chunk so an oversized body\n * is abandoned at the cap rather than fully buffered; falls back to\n * `response.text()` (then a post-hoc byte slice) when the body is not a\n * readable stream (e.g. a stubbed `Response` in tests).\n */\nasync function readCappedBody(\n response: Response,\n maxBytes: number,\n): Promise<{ text: string; truncated: boolean }> {\n const body = response.body;\n\n if (!body) {\n return { text: \"\", truncated: false };\n }\n\n const decoder = new TextDecoder();\n const reader = body.getReader();\n let received = 0;\n let truncated = false;\n let text = \"\";\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (!value) {\n continue;\n }\n\n const remaining = maxBytes - received;\n\n if (value.byteLength > remaining) {\n text += decoder.decode(value.subarray(0, remaining), { stream: true });\n received = maxBytes;\n truncated = true;\n break;\n }\n\n text += decoder.decode(value, { stream: true });\n received += value.byteLength;\n }\n } finally {\n // Release the lock and abandon any unread remainder.\n await reader.cancel().catch(() => undefined);\n reader.releaseLock();\n }\n\n text += decoder.decode();\n\n return { text, truncated };\n}\n\n/**\n * Decide whether a response's `content-type` indicates JSON. Matches\n * `application/json` and the `+json` structured-suffix convention\n * (e.g. `application/vnd.api+json`), case-insensitively.\n */\nfunction isJsonContentType(contentType: string | undefined): boolean {\n if (!contentType) {\n return false;\n }\n\n const value = contentType.toLowerCase();\n\n return value.includes(\"application/json\") || value.includes(\"+json\");\n}\n\n/**\n * Build the agent-facing `http_request` tool — a guarded HTTP/REST client\n * over the global `fetch`. The `options` bound what the model may do; the\n * model supplies the per-call URL / method / headers / body within those\n * rails.\n *\n * **Guardrails (all enforced before the network call).**\n * - **Method allowlist** — `allowMethods` (default `[\"GET\"]`). A method\n * outside the list is rejected with a typed\n * {@link HttpPolicyError} (`type: \"method-not-allowed\"`).\n * - **Private-network deny (default).** The request — and every redirect\n * hop — goes through the framework's `guardedFetch` outbound policy,\n * which refuses private / loopback / link-local / cloud-metadata\n * addresses (and hostnames resolving to them) unless\n * `allowPrivateNetwork: true` is set. This applies even when\n * `allowHosts` is not configured, so a bare `ai.tools.http()` is not\n * an SSRF primitive (`type: \"host-not-allowed\"`).\n * - **Host allowlist** — when `allowHosts` is set, any other host is\n * rejected (`type: \"host-not-allowed\"`), an SSRF guardrail; redirect\n * targets are held to the same allowlist.\n * - **`baseUrl` join** — when configured, the model passes a path that\n * is resolved against `baseUrl`; otherwise it must pass an absolute\n * `http(s)` URL. An unresolvable URL is rejected\n * (`type: \"invalid-url\"`).\n *\n * **Request shaping.** Static `options.headers` are merged under the\n * per-call `headers` (the per-call value wins). An object `body` is\n * JSON-serialized with a `content-type: application/json` default; a\n * string `body` is sent verbatim; `body` is dropped for bodyless methods\n * (`GET`). The call is bounded by `timeoutMs` (default `15_000`) via an\n * `AbortController`, also wired to `ctx.signal` for cooperative\n * cancellation.\n *\n * **Response shaping.** Headers are returned with lower-cased keys. The\n * body is read up to `maxBytes` (default `1_000_000`) and JSON-parsed\n * when the response `content-type` is JSON, otherwise returned as text;\n * `truncated` is `true` when the body was cut off at the cap (a truncated\n * JSON body is returned as the raw partial string, since it can no longer\n * be parsed).\n *\n * **Errors flow as data.** Every guardrail rejection and network failure\n * is thrown inside `execute`; the framework's `tool()` wrapper catches it\n * and surfaces it in the returned `{ error }` field, so the agent reads\n * the failure and self-corrects rather than crashing.\n *\n * @param options - Construction-time policy bounding the tool.\n * @returns A {@link ToolContract} the agent can call as `http_request`.\n *\n * @example\n * const stripe = httpRequestTool({\n * baseUrl: \"https://api.stripe.com\",\n * allowHosts: [\"api.stripe.com\"],\n * allowMethods: [\"GET\", \"POST\"],\n * headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },\n * });\n * const { data } = await stripe.invoke({ method: \"GET\", url: \"/v1/charges\" });\n */\nexport function httpRequestTool(\n options: HttpRequestOptions = {},\n): ToolContract<HttpRequestInput, HttpRequestResult> {\n const allowMethods = options.allowMethods ?? [\"GET\"];\n const allowedMethodSet = new Set<HttpMethod>(allowMethods);\n const allowHostSet = options.allowHosts ? new Set(options.allowHosts) : undefined;\n const allowPrivateNetwork = options.allowPrivateNetwork ?? false;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n const staticHeaders = options.headers;\n\n return tool<HttpRequestInput, HttpRequestResult>({\n name: options.name ?? DEFAULT_NAME,\n description:\n \"Issue an HTTP request and return the status, response headers, and \" +\n \"parsed body. Allowed methods and hosts are restricted by the tool's \" +\n \"configuration; a request outside those rails is rejected before any \" +\n \"network call. Pass an object body to send JSON, or a string to send \" +\n \"it verbatim. The response body is JSON-parsed when the content-type \" +\n \"is JSON, otherwise returned as text, and is capped — `truncated` is \" +\n \"true when the body was cut off.\",\n action: (input) => `Requesting ${input.method ?? \"GET\"} ${input.url}`,\n input: httpRequestInputSchema,\n async execute(input, ctx) {\n const method: HttpMethod = input.method ?? \"GET\";\n\n // 1. Method allowlist — rejected before anything else.\n if (!allowedMethodSet.has(method)) {\n throw new HttpPolicyError(\n `http_request method \"${method}\" is not allowed. ` +\n `Permitted methods: ${[...allowedMethodSet].join(\", \")}.`,\n { type: \"method-not-allowed\" },\n );\n }\n\n // 2. URL resolution (baseUrl join when configured).\n const url = resolveUrl(input.url, options.baseUrl);\n\n // 3. Host allowlist — SSRF guardrail, before the fetch.\n if (allowHostSet && !allowHostSet.has(url.hostname)) {\n throw new HttpPolicyError(\n `http_request host \"${url.hostname}\" is not in the allowlist. ` +\n `Permitted hosts: ${[...allowHostSet].join(\", \")}.`,\n { type: \"host-not-allowed\" },\n );\n }\n\n // 4. Merge headers — static option headers under the per-call ones,\n // so a per-call header overrides a static default of the same name.\n const headers: Record<string, string> = { ...staticHeaders, ...input.headers };\n\n // 5. Shape the body. Dropped for bodyless methods; objects become\n // JSON (with a default content-type); strings are sent verbatim.\n let body: string | undefined;\n\n if (!BODYLESS_METHODS.has(method) && input.body !== undefined) {\n if (typeof input.body === \"string\") {\n body = input.body;\n } else {\n body = JSON.stringify(input.body);\n\n const hasContentType = Object.keys(headers).some(\n (key) => key.toLowerCase() === \"content-type\",\n );\n\n if (!hasContentType) {\n headers[\"content-type\"] = \"application/json\";\n }\n }\n }\n\n // 6. Issue the request through the shared hardened outbound path:\n // private-IP deny by default, per-hop redirect re-validation,\n // timeout, and the caller's signal — never a raw fetch.\n let response: Response;\n\n try {\n response = await guardedFetch(\n url.toString(),\n {\n allowedSchemes: [\"http\", \"https\"],\n hostAllowlist: options.allowHosts,\n denyPrivateIPsAfterDNS: !allowPrivateNetwork,\n timeoutMs,\n signal: ctx?.signal,\n },\n { method, headers, body },\n );\n } catch (cause) {\n if (cause instanceof OutboundPolicyError) {\n throw new HttpPolicyError(`http_request blocked: ${cause.message}`, {\n type: \"host-not-allowed\",\n cause,\n });\n }\n\n throw cause;\n }\n\n // 7. Collect response headers with lower-cased keys.\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key.toLowerCase()] = value;\n });\n\n // 8. Read the body up to the cap, then parse-or-pass.\n const { text, truncated } = await readCappedBody(response, maxBytes);\n\n let parsedBody: unknown = text;\n\n // A truncated body can no longer be valid JSON, so only attempt a\n // parse on a complete JSON response; otherwise hand back the raw text.\n if (!truncated && isJsonContentType(responseHeaders[\"content-type\"]) && text.length > 0) {\n try {\n parsedBody = JSON.parse(text);\n } catch {\n // Content-type claimed JSON but the body was not — fall back to\n // the raw text rather than failing the whole call.\n parsedBody = text;\n }\n }\n\n return {\n status: response.status,\n headers: responseHeaders,\n body: parsedBody,\n truncated,\n };\n },\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../../../../../../../ai-tools/src/mcp/client.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n McpClient,\n McpClientOptions,\n McpContentBlock,\n McpToolCallResult,\n McpToolDescriptor,\n McpTransport,\n} from \"../contracts\";\nimport { McpTransportError } from \"../errors\";\nimport { jsonSchemaToStandard } from \"./json-schema-to-standard\";\nimport { createJsonRpcClient, type JsonRpcClientHandle } from \"./transport\";\nimport type { McpTransportClient } from \"./transport.type\";\n\n/** Default per-call timeout for `tools/call`. */\nconst DEFAULT_CALL_TIMEOUT_MS = 30_000;\n\n/** The MCP protocol version this client advertises in `initialize`. */\nconst PROTOCOL_VERSION = \"2025-06-18\";\n\n/** The `tools/list` response slice we read. */\ninterface ToolsListResult {\n tools?: McpToolDescriptor[];\n}\n\n/**\n * The internal {@link McpClient} implementation. Owns one JSON-RPC client\n * over a transport, runs the `initialize` handshake on first use, lists\n * the server's tools, and adapts each into a {@link ToolContract} whose\n * `execute` issues `tools/call`. The adapted contracts are cached after\n * the first `tools()` so repeat calls don't re-handshake.\n *\n * Constructed via {@link mcp}; the class itself is internal.\n */\nclass McpClientImpl implements McpClient {\n /** The JSON-RPC client over the transport. */\n private readonly rpc: JsonRpcClientHandle;\n\n /** Construction-time options (prefix / filter / timeout). */\n private readonly options: McpClientOptions;\n\n /** Resolved + cached adapted tools, set after the first `tools()`. */\n private cached: ToolContract[] | undefined;\n\n /** In-flight `tools()` so concurrent callers share one handshake. */\n private pending: Promise<ToolContract[]> | undefined;\n\n /** Flipped once the handshake completes so we only do it once. */\n private initialized = false;\n\n public constructor(\n source: McpTransport | McpTransportClient,\n options: McpClientOptions = {},\n ) {\n this.rpc = createJsonRpcClient(source);\n this.options = options;\n }\n\n /**\n * Run the MCP `initialize` handshake exactly once, then send the\n * `notifications/initialized` notification the protocol requires before\n * any other request. Wraps a handshake failure as a typed\n * {@link McpTransportError} of type `\"connect\"`.\n */\n private async handshake(): Promise<void> {\n if (this.initialized) {\n return;\n }\n\n try {\n await this.rpc.call(\"initialize\", {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: {},\n clientInfo: { name: \"@warlock.js/ai-tools\", version: \"4.4.0\" },\n });\n\n await this.rpc.notify(\"notifications/initialized\");\n } catch (cause) {\n if (cause instanceof McpTransportError) {\n throw cause;\n }\n\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new McpTransportError(`MCP initialize handshake failed: ${message}`, {\n type: \"connect\",\n method: \"initialize\",\n cause,\n });\n }\n\n this.initialized = true;\n }\n\n public tools(): Promise<ToolContract[]> {\n if (this.cached) {\n return Promise.resolve(this.cached);\n }\n\n if (this.pending) {\n return this.pending;\n }\n\n this.pending = this.listAndAdapt()\n .then((tools) => {\n this.cached = tools;\n\n return tools;\n })\n .finally(() => {\n this.pending = undefined;\n });\n\n return this.pending;\n }\n\n /**\n * Handshake, `tools/list`, and adapt each descriptor into a\n * {@link ToolContract}, applying the `filter` and `namePrefix` options.\n */\n private async listAndAdapt(): Promise<ToolContract[]> {\n await this.handshake();\n\n const result = await this.rpc.call<ToolsListResult>(\"tools/list\");\n const descriptors = result.tools ?? [];\n\n const filter = this.options.filter;\n const selected = filter ? descriptors.filter((d) => filter(d.name)) : descriptors;\n\n return selected.map((descriptor) => this.adapt(descriptor));\n }\n\n /**\n * Adapt one remote tool descriptor into a {@link ToolContract}: build\n * the input schema from its JSON Schema via {@link jsonSchemaToStandard},\n * prefix the name, and route `execute` through a `tools/call` that\n * honors `ctx.signal`, unwraps the content blocks, and throws on an\n * `isError` result so `tool()` wraps it.\n */\n private adapt(descriptor: McpToolDescriptor): ToolContract {\n const prefixedName = `${this.options.namePrefix ?? \"\"}${descriptor.name}`;\n const remoteName = descriptor.name;\n const timeoutMs = this.options.timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;\n const input = jsonSchemaToStandard(descriptor.inputSchema);\n\n return tool<unknown, unknown>({\n name: prefixedName,\n description:\n descriptor.description ?? `Invoke the remote MCP tool \"${remoteName}\".`,\n input,\n execute: async (args, ctx) => {\n const result = await this.rpc.call<McpToolCallResult>(\n \"tools/call\",\n { name: remoteName, arguments: args ?? {} },\n { signal: ctx?.signal, timeoutMs },\n );\n\n // An `isError` result is a tool-level failure — throw it so the\n // surrounding `tool()` wraps it as a `ToolExecutionError` and the\n // agent reads it as `{ error }` data and self-corrects.\n if (result.isError) {\n throw new McpTransportError(\n `MCP tool \"${remoteName}\" returned an error: ${unwrapContent(result.content)}`,\n { type: \"protocol\", method: \"tools/call\" },\n );\n }\n\n return unwrapResult(result.content);\n },\n });\n }\n\n public close(): Promise<void> {\n return this.rpc.close();\n }\n}\n\n/**\n * Flatten an MCP `tools/call` result's content blocks into the value a\n * tool returns. Text blocks are concatenated; a single block whose text is\n * valid JSON is parsed so structured tool output flows back as an object\n * rather than a string. Non-text blocks are preserved as `type`-tagged\n * objects (MCP's wire `type` is kept; any inbound `kind` is normalized to\n * `type`).\n */\nfunction unwrapResult(content: McpContentBlock[] | undefined): unknown {\n const blocks = content ?? [];\n\n // The overwhelmingly common case: a single text block. Parse JSON when\n // it is one, so structured results come back typed; otherwise the string.\n if (blocks.length === 1 && blocks[0].type === \"text\") {\n const text = blocks[0].text ?? \"\";\n\n return tryParseJson(text);\n }\n\n // Multiple / mixed blocks: return a normalized array, each tagged by\n // `type` (never `kind`).\n return blocks.map((block) => normalizeBlock(block));\n}\n\n/**\n * Render content blocks to a short human string for error messages — the\n * concatenated text of every text block.\n */\nfunction unwrapContent(content: McpContentBlock[] | undefined): string {\n return (content ?? [])\n .filter((block) => block.type === \"text\" && typeof block.text === \"string\")\n .map((block) => block.text)\n .join(\" \")\n .trim();\n}\n\n/**\n * Normalize one content block onto our `type`-only shape: translate an\n * inbound `kind` discriminator to `type` (and strip `kind`) so the value a\n * tool returns never carries MCP's `kind` vocabulary.\n */\nfunction normalizeBlock(block: McpContentBlock): Record<string, unknown> {\n const { kind, ...rest } = block as McpContentBlock & { kind?: string };\n const type = block.type ?? kind ?? \"unknown\";\n\n return { ...rest, type };\n}\n\n/**\n * Parse a string as JSON, returning the parsed value on success or the\n * original string when it is not JSON — so a plain-text tool result stays\n * a string while a JSON tool result becomes an object.\n */\nfunction tryParseJson(text: string): unknown {\n const trimmed = text.trim();\n\n if (!trimmed) {\n return text;\n }\n\n const first = trimmed[0];\n\n // Only attempt a parse for plausibly-structured payloads, so a bare\n // sentence isn't mangled by a lenient parse.\n if (first !== \"{\" && first !== \"[\") {\n return text;\n }\n\n try {\n return JSON.parse(trimmed);\n } catch {\n return text;\n }\n}\n\n/**\n * Connect to an external MCP server and adapt its tools as agent tools\n * (Direction A: server → local agent tools).\n *\n * Opens the transport lazily and exposes {@link McpClient.tools}, which on\n * first call runs the `initialize` handshake, lists the server's tools via\n * `tools/list`, and maps each into a {@link ToolContract}:\n * - **input schema** — the remote tool's JSON Schema is wrapped as a\n * Standard Schema via {@link jsonSchemaToStandard} (Ajv-backed, an\n * optional peer);\n * - **execute** — issues `tools/call` honoring `ctx.signal` and the\n * configured `timeoutMs`, unwraps the result content, and throws on an\n * `isError` result so the `tool()` wrapper surfaces it as `{ error }`;\n * - **name** — prefixed with `options.namePrefix` to avoid local\n * collisions; only tools passing `options.filter` are adapted.\n *\n * The adapted contracts are cached after the first `tools()` call, so the\n * handshake + list happen exactly once. The returned contracts drop\n * straight into `ai.agent({ tools: [...] })`.\n *\n * @param server - The transport config (`{ type: \"stdio\" }` /\n * `{ type: \"http\" }`). A pre-built transport client may be injected for\n * testing.\n * @param options - Prefix / filter / per-call timeout.\n * @returns An {@link McpClient} handle.\n *\n * @example\n * const github = mcp(\n * { type: \"stdio\", command: \"npx\", args: [\"-y\", \"@modelcontextprotocol/server-github\"] },\n * { namePrefix: \"github.\" },\n * );\n * const dev = ai.agent({ model, tools: [...(await github.tools())] });\n */\nexport function mcp(\n server: McpTransport | McpTransportClient,\n options?: McpClientOptions,\n): McpClient {\n return new McpClientImpl(server, options);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../../../../../../../ai-tools/src/mcp/client.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n McpClient,\n McpClientOptions,\n McpContentBlock,\n McpToolCallResult,\n McpToolDescriptor,\n McpTransport,\n} from \"../contracts\";\nimport { McpTransportError } from \"../errors\";\nimport { jsonSchemaToStandard } from \"./json-schema-to-standard\";\nimport { createJsonRpcClient, type JsonRpcClientHandle } from \"./transport\";\nimport type { McpTransportClient } from \"./transport.type\";\n\n/** Default per-call timeout for `tools/call`. */\nconst DEFAULT_CALL_TIMEOUT_MS = 30_000;\n\n/** The MCP protocol version this client advertises in `initialize`. */\nconst PROTOCOL_VERSION = \"2025-06-18\";\n\n/** The `tools/list` response slice we read. */\ninterface ToolsListResult {\n tools?: McpToolDescriptor[];\n}\n\n/**\n * The internal {@link McpClient} implementation. Owns one JSON-RPC client\n * over a transport, runs the `initialize` handshake on first use, lists\n * the server's tools, and adapts each into a {@link ToolContract} whose\n * `execute` issues `tools/call`. The adapted contracts are cached after\n * the first `tools()` so repeat calls don't re-handshake.\n *\n * Constructed via {@link mcp}; the class itself is internal.\n */\nclass McpClientImpl implements McpClient {\n /** The JSON-RPC client over the transport. */\n private readonly rpc: JsonRpcClientHandle;\n\n /** Construction-time options (prefix / filter / timeout). */\n private readonly options: McpClientOptions;\n\n /** Resolved + cached adapted tools, set after the first `tools()`. */\n private cached: ToolContract[] | undefined;\n\n /** In-flight `tools()` so concurrent callers share one handshake. */\n private pending: Promise<ToolContract[]> | undefined;\n\n /** Flipped once the handshake completes so we only do it once. */\n private initialized = false;\n\n public constructor(\n source: McpTransport | McpTransportClient,\n options: McpClientOptions = {},\n ) {\n this.rpc = createJsonRpcClient(source);\n this.options = options;\n }\n\n /**\n * Run the MCP `initialize` handshake exactly once, then send the\n * `notifications/initialized` notification the protocol requires before\n * any other request. Wraps a handshake failure as a typed\n * {@link McpTransportError} of type `\"connect\"`.\n */\n private async handshake(): Promise<void> {\n if (this.initialized) {\n return;\n }\n\n try {\n await this.rpc.call(\"initialize\", {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: {},\n clientInfo: { name: \"@warlock.js/ai-tools\", version: \"4.4.0\" },\n });\n\n await this.rpc.notify(\"notifications/initialized\");\n } catch (cause) {\n if (cause instanceof McpTransportError) {\n throw cause;\n }\n\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new McpTransportError(`MCP initialize handshake failed: ${message}`, {\n type: \"connect\",\n method: \"initialize\",\n cause,\n });\n }\n\n this.initialized = true;\n }\n\n public tools(): Promise<ToolContract[]> {\n if (this.cached) {\n return Promise.resolve(this.cached);\n }\n\n if (this.pending) {\n return this.pending;\n }\n\n this.pending = this.listAndAdapt()\n .then((tools) => {\n this.cached = tools;\n\n return tools;\n })\n .finally(() => {\n this.pending = undefined;\n });\n\n return this.pending;\n }\n\n /**\n * Handshake, `tools/list`, and adapt each descriptor into a\n * {@link ToolContract}, applying the `filter` and `namePrefix` options.\n */\n private async listAndAdapt(): Promise<ToolContract[]> {\n await this.handshake();\n\n const result = await this.rpc.call<ToolsListResult>(\"tools/list\");\n const descriptors = result.tools ?? [];\n\n const filter = this.options.filter;\n const selected = filter ? descriptors.filter((d) => filter(d.name)) : descriptors;\n\n return selected.map((descriptor) => this.adapt(descriptor));\n }\n\n /**\n * Adapt one remote tool descriptor into a {@link ToolContract}: build\n * the input schema from its JSON Schema via {@link jsonSchemaToStandard},\n * prefix the name, and route `execute` through a `tools/call` that\n * honors `ctx.signal`, unwraps the content blocks, and throws on an\n * `isError` result so `tool()` wraps it.\n */\n private adapt(descriptor: McpToolDescriptor): ToolContract {\n const prefixedName = `${this.options.namePrefix ?? \"\"}${descriptor.name}`;\n const remoteName = descriptor.name;\n const timeoutMs = this.options.timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;\n const input = jsonSchemaToStandard(descriptor.inputSchema);\n\n return tool<unknown, unknown>({\n name: prefixedName,\n description:\n descriptor.description ?? `Invoke the remote MCP tool \"${remoteName}\".`,\n input,\n execute: async (args, ctx) => {\n const result = await this.rpc.call<McpToolCallResult>(\n \"tools/call\",\n { name: remoteName, arguments: args ?? {} },\n { signal: ctx?.signal, timeoutMs },\n );\n\n // An `isError` result is a tool-level failure — throw it so the\n // surrounding `tool()` wraps it as a `ToolExecutionError` and the\n // agent reads it as `{ error }` data and self-corrects.\n if (result.isError) {\n throw new McpTransportError(\n `MCP tool \"${remoteName}\" returned an error: ${unwrapContent(result.content)}`,\n { type: \"protocol\", method: \"tools/call\" },\n );\n }\n\n return unwrapResult(result.content);\n },\n });\n }\n\n public close(): Promise<void> {\n return this.rpc.close();\n }\n}\n\n/**\n * Flatten an MCP `tools/call` result's content blocks into the value a\n * tool returns. Text blocks are concatenated; a single block whose text is\n * valid JSON is parsed so structured tool output flows back as an object\n * rather than a string. Non-text blocks are preserved as `type`-tagged\n * objects (MCP's wire `type` is kept; any inbound `kind` is normalized to\n * `type`).\n */\nfunction unwrapResult(content: McpContentBlock[] | undefined): unknown {\n const blocks = content ?? [];\n\n // The overwhelmingly common case: a single text block. Parse JSON when\n // it is one, so structured results come back typed; otherwise the string.\n if (blocks.length === 1 && blocks[0].type === \"text\") {\n const text = blocks[0].text ?? \"\";\n\n return tryParseJson(text);\n }\n\n // Multiple / mixed blocks: return a normalized array, each tagged by\n // `type` (never `kind`).\n return blocks.map((block) => normalizeBlock(block));\n}\n\n/**\n * Render content blocks to a short human string for error messages — the\n * concatenated text of every text block.\n */\nfunction unwrapContent(content: McpContentBlock[] | undefined): string {\n return (content ?? [])\n .filter((block) => block.type === \"text\" && typeof block.text === \"string\")\n .map((block) => block.text)\n .join(\" \")\n .trim();\n}\n\n/**\n * Normalize one content block onto our `type`-only shape: translate an\n * inbound `kind` discriminator to `type` (and strip `kind`) so the value a\n * tool returns never carries MCP's `kind` vocabulary.\n */\nfunction normalizeBlock(block: McpContentBlock): Record<string, unknown> {\n const { kind, ...rest } = block as McpContentBlock & { kind?: string };\n const type = block.type ?? kind ?? \"unknown\";\n\n return { ...rest, type };\n}\n\n/**\n * Parse a string as JSON, returning the parsed value on success or the\n * original string when it is not JSON — so a plain-text tool result stays\n * a string while a JSON tool result becomes an object.\n */\nfunction tryParseJson(text: string): unknown {\n const trimmed = text.trim();\n\n if (!trimmed) {\n return text;\n }\n\n const first = trimmed[0];\n\n // Only attempt a parse for plausibly-structured payloads, so a bare\n // sentence isn't mangled by a lenient parse.\n if (first !== \"{\" && first !== \"[\") {\n return text;\n }\n\n try {\n return JSON.parse(trimmed);\n } catch {\n return text;\n }\n}\n\n/**\n * Connect to an external MCP server and adapt its tools as agent tools\n * (Direction A: server → local agent tools).\n *\n * Opens the transport lazily and exposes {@link McpClient.tools}, which on\n * first call runs the `initialize` handshake, lists the server's tools via\n * `tools/list`, and maps each into a {@link ToolContract}:\n * - **input schema** — the remote tool's JSON Schema is wrapped as a\n * Standard Schema via {@link jsonSchemaToStandard} (Ajv-backed, an\n * optional peer);\n * - **execute** — issues `tools/call` honoring `ctx.signal` and the\n * configured `timeoutMs`, unwraps the result content, and throws on an\n * `isError` result so the `tool()` wrapper surfaces it as `{ error }`;\n * - **name** — prefixed with `options.namePrefix` to avoid local\n * collisions; only tools passing `options.filter` are adapted.\n *\n * The adapted contracts are cached after the first `tools()` call, so the\n * handshake + list happen exactly once. The returned contracts drop\n * straight into `ai.agent({ tools: [...] })`.\n *\n * @param server - The transport config (`{ type: \"stdio\" }` /\n * `{ type: \"http\" }`). A pre-built transport client may be injected for\n * testing.\n * @param options - Prefix / filter / per-call timeout.\n * @returns An {@link McpClient} handle.\n *\n * @example\n * const github = mcp(\n * { type: \"stdio\", command: \"npx\", args: [\"-y\", \"@modelcontextprotocol/server-github\"] },\n * { namePrefix: \"github.\" },\n * );\n * const dev = ai.agent({ model, tools: [...(await github.tools())] });\n */\nexport function mcp(\n server: McpTransport | McpTransportClient,\n options?: McpClientOptions,\n): McpClient {\n return new McpClientImpl(server, options);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"json-schema-to-standard.d.mts","names":[],"sources":["../../../../../../../ai-tools/src/mcp/json-schema-to-standard.ts"],"mappings":";;;;;AA+JA;;;;;;;;;;;;;;AAE0B;;;;;;;;;;;iBAFV,oBAAA,kBAAA,CACd,MAAA,EAAQ,MAAA,gCACP,gBAAA,CAAiB,MAAA"}
1
+ {"version":3,"file":"json-schema-to-standard.d.mts","names":[],"sources":["../../../../../../../ai-tools/src/mcp/json-schema-to-standard.ts"],"mappings":";;;;;AA+JA;;;;;;;;;;;;;;AAE0B;;;;;;;;;;;iBAFV,oBAAA,mBACd,MAAA,EAAQ,MAAA,gCACP,gBAAA,CAAiB,MAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"json-schema-to-standard.mjs","names":[],"sources":["../../../../../../../ai-tools/src/mcp/json-schema-to-standard.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * The inverse of `@warlock.js/ai`'s `extractJsonSchema` (which goes\n * Standard Schema → JSON Schema). Here we wrap a raw JSON Schema as a\n * {@link StandardSchemaV1} whose `~standard.validate` runs a lazily-imported\n * Ajv validator — so an MCP server's `inputSchema` (JSON Schema) becomes a\n * `ToolConfig.input` the `tool()` factory can validate against.\n *\n * Ajv is an OPTIONAL peer, lazy-imported on first validate following the\n * langfuse/readability pattern: a missing peer surfaces a curated install\n * string (via the returned issues), never a raw module-resolution stack.\n */\n\n/** The structural vendor template, mirroring `passthroughSchema()`. */\nconst VENDOR = \"warlock-ai\";\n\n// ============================================================\n// Lazily-loaded ajv (OPTIONAL peer)\n// ============================================================\n\n/**\n * Minimal structural view of an Ajv-compiled validator. Ajv is an optional\n * peer that may not be installed, so we model only the surface we touch\n * rather than depending on ajv's own published types. A validator is a\n * callable that returns a boolean and exposes the `errors` it collected.\n */\ninterface AjvValidateFunctionLike {\n (data: unknown): boolean;\n errors?: AjvErrorObject[] | null;\n}\n\n/** Minimal structural view of an `Ajv` instance — only `compile`. */\ninterface AjvInstanceLike {\n compile(schema: Record<string, unknown>): AjvValidateFunctionLike;\n}\n\n/** The `Ajv` constructor, as exposed by both the CJS and ESM builds. */\ntype AjvConstructorLike = new (options?: Record<string, unknown>) => AjvInstanceLike;\n\n/**\n * Minimal structural view of the dynamically imported `ajv` module. Ajv\n * ships its constructor as a `default` export under ESM interop; the older\n * CJS shape exposes the constructor as the module namespace itself, so\n * `default` is optional here and the loader falls back to the namespace.\n */\ninterface AjvModuleLike {\n default?: AjvConstructorLike;\n}\n\nlet AjvSdk: AjvModuleLike | undefined;\nlet ajvInstance: AjvInstanceLike | undefined;\nlet isAjvAvailable: boolean | undefined;\nlet loadingPromise: Promise<void> | undefined;\n\nconst AJV_INSTALL_INSTRUCTIONS = `\nThe MCP client's JSON-Schema validation requires the ajv package.\nInstall it with:\n\n npm install ajv\n\nOr with your preferred package manager:\n\n pnpm add ajv\n yarn add ajv\n`.trim();\n\n/**\n * Settle the lazy import of `ajv` once, concurrency-safe. A bare `catch`\n * flips the availability flag to `false`; the curated install string then\n * surfaces at validate time as a Standard Schema issue, never a raw\n * module-resolution error. The constructed `Ajv` instance is cached and\n * reused for every schema compile.\n */\nasync function loadAjv(): Promise<void> {\n if (isAjvAvailable !== undefined) {\n return;\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n AjvSdk = (await import(\"ajv\")) as AjvModuleLike;\n // Ajv ships as a default export under both CJS and ESM interop; the\n // older CJS shape exposes the constructor as the namespace itself.\n const AjvCtor: AjvConstructorLike =\n AjvSdk.default ?? (AjvSdk as unknown as AjvConstructorLike);\n ajvInstance = new AjvCtor({ allErrors: true, strict: false });\n isAjvAvailable = true;\n } catch {\n isAjvAvailable = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * Compile a JSON Schema with the shared Ajv instance, caching the compiled\n * validator on a closure so repeated validations don't recompile. A schema\n * Ajv itself rejects at compile time (an invalid meta-schema) degrades to\n * an accept-all validator so a malformed remote schema can't wedge the\n * tool — the server, not us, owns its schema's correctness.\n */\nfunction makeCompiler(schema: Record<string, unknown>): () => AjvValidateFunctionLike | undefined {\n let compiled: AjvValidateFunctionLike | undefined;\n let attempted = false;\n\n return () => {\n if (attempted) {\n return compiled;\n }\n\n attempted = true;\n\n if (!ajvInstance) {\n return undefined;\n }\n\n try {\n compiled = ajvInstance.compile(schema);\n } catch {\n compiled = undefined;\n }\n\n return compiled;\n };\n}\n\n/**\n * Wrap a raw JSON Schema as a {@link StandardSchemaV1} whose\n * `~standard.validate` runs Ajv. The shape mirrors `passthroughSchema()`\n * (`{ \"~standard\": { version: 1, vendor, validate } }`) so it drops into\n * `tool({ input })` exactly like a native seal schema.\n *\n * Validation behavior:\n * - **Valid input** → `{ value }` (the input is passed through unchanged;\n * Ajv validates, it does not transform).\n * - **Invalid input** → `{ issues }` carrying Ajv's `instancePath` +\n * message per failure, so `tool()` produces a `SchemaValidationError`.\n * - **Missing `ajv` peer** → a single issue carrying the curated install\n * string, surfaced the same way (a developer-facing message in logs).\n * - **No / empty schema** → an accept-all passthrough (an MCP tool may\n * advertise no `inputSchema`).\n *\n * @param schema - The JSON Schema (an MCP tool's `inputSchema`), or\n * `undefined` for a no-argument tool.\n * @returns A `StandardSchemaV1<TInput>` ready for `tool({ input })`.\n *\n * @example\n * const input = jsonSchemaToStandard<{ q: string }>({\n * type: \"object\",\n * properties: { q: { type: \"string\" } },\n * required: [\"q\"],\n * });\n */\nexport function jsonSchemaToStandard<TInput = unknown>(\n schema: Record<string, unknown> | undefined,\n): StandardSchemaV1<TInput> {\n // A tool with no schema (or an empty object schema) validates everything\n // — return an accept-all passthrough and never touch Ajv.\n if (!schema || Object.keys(schema).length === 0) {\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n validate: (value: unknown) => ({ value: value as TInput }),\n },\n };\n }\n\n const compile = makeCompiler(schema);\n\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n async validate(value: unknown): Promise<StandardSchemaV1.Result<TInput>> {\n await loadAjv();\n\n if (!isAjvAvailable) {\n return {\n issues: [{ message: AJV_INSTALL_INSTRUCTIONS }],\n };\n }\n\n const validator = compile();\n\n // A schema Ajv could not compile degrades to accept-all rather\n // than failing every call — the remote server owns its schema.\n if (!validator) {\n return { value: value as TInput };\n }\n\n const ok = validator(value);\n\n if (ok) {\n return { value: value as TInput };\n }\n\n const issues: StandardSchemaV1.Issue[] = (validator.errors ?? []).map((error) => ({\n message: formatAjvError(error),\n path: pathFromInstancePath(error.instancePath),\n }));\n\n return {\n issues: issues.length > 0 ? issues : [{ message: \"input failed JSON Schema validation\" }],\n };\n },\n },\n };\n}\n\n/** A single Ajv error object — narrowed to the fields we read. */\ninterface AjvErrorObject {\n instancePath?: string;\n message?: string;\n keyword?: string;\n}\n\n/**\n * Render one Ajv error into a human-readable issue message. Prefixes the\n * failing instance path (when present) so the model can see WHICH field\n * was wrong, e.g. `/query: must be string`.\n */\nfunction formatAjvError(error: AjvErrorObject): string {\n const where = error.instancePath ? `${error.instancePath}: ` : \"\";\n const message = error.message ?? `failed \"${error.keyword ?? \"validation\"}\"`;\n\n return `${where}${message}`;\n}\n\n/**\n * Convert an Ajv `instancePath` (a JSON-Pointer like `/items/0/name`) into\n * the Standard Schema `path` segment array (`[\"items\", \"0\", \"name\"]`).\n * Empty paths (a root-level failure) become an empty array.\n */\nfunction pathFromInstancePath(instancePath: string | undefined): string[] {\n if (!instancePath) {\n return [];\n }\n\n return instancePath\n .split(\"/\")\n .filter((segment) => segment.length > 0)\n .map((segment) => segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\"));\n}\n\n/**\n * Reset the cached lazy-import state. Test-only seam so a spec can mock\n * `ajv` as present/absent across cases without module-cache bleed.\n *\n * @internal\n */\nexport function resetAjvCacheForTests(): void {\n AjvSdk = undefined;\n ajvInstance = undefined;\n isAjvAvailable = undefined;\n loadingPromise = undefined;\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAM,SAAS;AAmCf,IAAI;AACJ,IAAI;AACJ,IAAI;AACJ,IAAI;AAEJ,MAAM,2BAA2B;;;;;;;;;;EAU/B,KAAK;;;;;;;;AASP,eAAe,UAAyB;CACtC,IAAI,mBAAmB,QACrB;CAGF,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GACF,SAAU,MAAM,OAAO;GAKvB,cAAc,KADZ,OAAO,WAAY,QACK;IAAE,WAAW;IAAM,QAAQ;GAAM,CAAC;GAC5D,iBAAiB;EACnB,QAAQ;GACN,iBAAiB;EACnB;CACF,GAAG;CAEH,OAAO;AACT;;;;;;;;AASA,SAAS,aAAa,QAA4E;CAChG,IAAI;CACJ,IAAI,YAAY;CAEhB,aAAa;EACX,IAAI,WACF,OAAO;EAGT,YAAY;EAEZ,IAAI,CAAC,aACH;EAGF,IAAI;GACF,WAAW,YAAY,QAAQ,MAAM;EACvC,QAAQ;GACN,WAAW;EACb;EAEA,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,qBACd,QAC0B;CAG1B,IAAI,CAAC,UAAU,OAAO,KAAK,MAAM,EAAE,WAAW,GAC5C,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,WAAW,WAAoB,EAAS,MAAgB;CAC1D,EACF;CAGF,MAAM,UAAU,aAAa,MAAM;CAEnC,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,MAAM,SAAS,OAA0D;GACvE,MAAM,QAAQ;GAEd,IAAI,CAAC,gBACH,OAAO,EACL,QAAQ,CAAC,EAAE,SAAS,yBAAyB,CAAC,EAChD;GAGF,MAAM,YAAY,QAAQ;GAI1B,IAAI,CAAC,WACH,OAAO,EAAS,MAAgB;GAKlC,IAFW,UAAU,KAEhB,GACH,OAAO,EAAS,MAAgB;GAGlC,MAAM,UAAoC,UAAU,UAAU,CAAC,GAAG,KAAK,WAAW;IAChF,SAAS,eAAe,KAAK;IAC7B,MAAM,qBAAqB,MAAM,YAAY;GAC/C,EAAE;GAEF,OAAO,EACL,QAAQ,OAAO,SAAS,IAAI,SAAS,CAAC,EAAE,SAAS,sCAAsC,CAAC,EAC1F;EACF;CACF,EACF;AACF;;;;;;AAcA,SAAS,eAAe,OAA+B;CAIrD,OAAO,GAHO,MAAM,eAAe,GAAG,MAAM,aAAa,MAAM,KAC/C,MAAM,WAAW,WAAW,MAAM,WAAW,aAAa;AAG5E;;;;;;AAOA,SAAS,qBAAqB,cAA4C;CACxE,IAAI,CAAC,cACH,OAAO,CAAC;CAGV,OAAO,aACJ,MAAM,GAAG,EACT,QAAQ,YAAY,QAAQ,SAAS,CAAC,EACtC,KAAK,YAAY,QAAQ,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,CAAC;AACrE"}
1
+ {"version":3,"file":"json-schema-to-standard.mjs","names":[],"sources":["../../../../../../../ai-tools/src/mcp/json-schema-to-standard.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * The inverse of `@warlock.js/ai`'s `extractJsonSchema` (which goes\n * Standard Schema → JSON Schema). Here we wrap a raw JSON Schema as a\n * {@link StandardSchemaV1} whose `~standard.validate` runs a lazily-imported\n * Ajv validator — so an MCP server's `inputSchema` (JSON Schema) becomes a\n * `ToolConfig.input` the `tool()` factory can validate against.\n *\n * Ajv is an OPTIONAL peer, lazy-imported on first validate following the\n * langfuse/readability pattern: a missing peer surfaces a curated install\n * string (via the returned issues), never a raw module-resolution stack.\n */\n\n/** The structural vendor template, mirroring `passthroughSchema()`. */\nconst VENDOR = \"warlock-ai\";\n\n// ============================================================\n// Lazily-loaded ajv (OPTIONAL peer)\n// ============================================================\n\n/**\n * Minimal structural view of an Ajv-compiled validator. Ajv is an optional\n * peer that may not be installed, so we model only the surface we touch\n * rather than depending on ajv's own published types. A validator is a\n * callable that returns a boolean and exposes the `errors` it collected.\n */\ninterface AjvValidateFunctionLike {\n (data: unknown): boolean;\n errors?: AjvErrorObject[] | null;\n}\n\n/** Minimal structural view of an `Ajv` instance — only `compile`. */\ninterface AjvInstanceLike {\n compile(schema: Record<string, unknown>): AjvValidateFunctionLike;\n}\n\n/** The `Ajv` constructor, as exposed by both the CJS and ESM builds. */\ntype AjvConstructorLike = new (options?: Record<string, unknown>) => AjvInstanceLike;\n\n/**\n * Minimal structural view of the dynamically imported `ajv` module. Ajv\n * ships its constructor as a `default` export under ESM interop; the older\n * CJS shape exposes the constructor as the module namespace itself, so\n * `default` is optional here and the loader falls back to the namespace.\n */\ninterface AjvModuleLike {\n default?: AjvConstructorLike;\n}\n\nlet AjvSdk: AjvModuleLike | undefined;\nlet ajvInstance: AjvInstanceLike | undefined;\nlet isAjvAvailable: boolean | undefined;\nlet loadingPromise: Promise<void> | undefined;\n\nconst AJV_INSTALL_INSTRUCTIONS = `\nThe MCP client's JSON-Schema validation requires the ajv package.\nInstall it with:\n\n npm install ajv\n\nOr with your preferred package manager:\n\n pnpm add ajv\n yarn add ajv\n`.trim();\n\n/**\n * Settle the lazy import of `ajv` once, concurrency-safe. A bare `catch`\n * flips the availability flag to `false`; the curated install string then\n * surfaces at validate time as a Standard Schema issue, never a raw\n * module-resolution error. The constructed `Ajv` instance is cached and\n * reused for every schema compile.\n */\nasync function loadAjv(): Promise<void> {\n if (isAjvAvailable !== undefined) {\n return;\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n AjvSdk = (await import(\"ajv\")) as AjvModuleLike;\n // Ajv ships as a default export under both CJS and ESM interop; the\n // older CJS shape exposes the constructor as the namespace itself.\n const AjvCtor: AjvConstructorLike =\n AjvSdk.default ?? (AjvSdk as unknown as AjvConstructorLike);\n ajvInstance = new AjvCtor({ allErrors: true, strict: false });\n isAjvAvailable = true;\n } catch {\n isAjvAvailable = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * Compile a JSON Schema with the shared Ajv instance, caching the compiled\n * validator on a closure so repeated validations don't recompile. A schema\n * Ajv itself rejects at compile time (an invalid meta-schema) degrades to\n * an accept-all validator so a malformed remote schema can't wedge the\n * tool — the server, not us, owns its schema's correctness.\n */\nfunction makeCompiler(schema: Record<string, unknown>): () => AjvValidateFunctionLike | undefined {\n let compiled: AjvValidateFunctionLike | undefined;\n let attempted = false;\n\n return () => {\n if (attempted) {\n return compiled;\n }\n\n attempted = true;\n\n if (!ajvInstance) {\n return undefined;\n }\n\n try {\n compiled = ajvInstance.compile(schema);\n } catch {\n compiled = undefined;\n }\n\n return compiled;\n };\n}\n\n/**\n * Wrap a raw JSON Schema as a {@link StandardSchemaV1} whose\n * `~standard.validate` runs Ajv. The shape mirrors `passthroughSchema()`\n * (`{ \"~standard\": { version: 1, vendor, validate } }`) so it drops into\n * `tool({ input })` exactly like a native seal schema.\n *\n * Validation behavior:\n * - **Valid input** → `{ value }` (the input is passed through unchanged;\n * Ajv validates, it does not transform).\n * - **Invalid input** → `{ issues }` carrying Ajv's `instancePath` +\n * message per failure, so `tool()` produces a `SchemaValidationError`.\n * - **Missing `ajv` peer** → a single issue carrying the curated install\n * string, surfaced the same way (a developer-facing message in logs).\n * - **No / empty schema** → an accept-all passthrough (an MCP tool may\n * advertise no `inputSchema`).\n *\n * @param schema - The JSON Schema (an MCP tool's `inputSchema`), or\n * `undefined` for a no-argument tool.\n * @returns A `StandardSchemaV1<TInput>` ready for `tool({ input })`.\n *\n * @example\n * const input = jsonSchemaToStandard<{ q: string }>({\n * type: \"object\",\n * properties: { q: { type: \"string\" } },\n * required: [\"q\"],\n * });\n */\nexport function jsonSchemaToStandard<TInput = unknown>(\n schema: Record<string, unknown> | undefined,\n): StandardSchemaV1<TInput> {\n // A tool with no schema (or an empty object schema) validates everything\n // — return an accept-all passthrough and never touch Ajv.\n if (!schema || Object.keys(schema).length === 0) {\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n validate: (value: unknown) => ({ value: value as TInput }),\n },\n };\n }\n\n const compile = makeCompiler(schema);\n\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n async validate(value: unknown): Promise<StandardSchemaV1.Result<TInput>> {\n await loadAjv();\n\n if (!isAjvAvailable) {\n return {\n issues: [{ message: AJV_INSTALL_INSTRUCTIONS }],\n };\n }\n\n const validator = compile();\n\n // A schema Ajv could not compile degrades to accept-all rather\n // than failing every call — the remote server owns its schema.\n if (!validator) {\n return { value: value as TInput };\n }\n\n const ok = validator(value);\n\n if (ok) {\n return { value: value as TInput };\n }\n\n const issues: StandardSchemaV1.Issue[] = (validator.errors ?? []).map((error) => ({\n message: formatAjvError(error),\n path: pathFromInstancePath(error.instancePath),\n }));\n\n return {\n issues: issues.length > 0 ? issues : [{ message: \"input failed JSON Schema validation\" }],\n };\n },\n },\n };\n}\n\n/** A single Ajv error object — narrowed to the fields we read. */\ninterface AjvErrorObject {\n instancePath?: string;\n message?: string;\n keyword?: string;\n}\n\n/**\n * Render one Ajv error into a human-readable issue message. Prefixes the\n * failing instance path (when present) so the model can see WHICH field\n * was wrong, e.g. `/query: must be string`.\n */\nfunction formatAjvError(error: AjvErrorObject): string {\n const where = error.instancePath ? `${error.instancePath}: ` : \"\";\n const message = error.message ?? `failed \"${error.keyword ?? \"validation\"}\"`;\n\n return `${where}${message}`;\n}\n\n/**\n * Convert an Ajv `instancePath` (a JSON-Pointer like `/items/0/name`) into\n * the Standard Schema `path` segment array (`[\"items\", \"0\", \"name\"]`).\n * Empty paths (a root-level failure) become an empty array.\n */\nfunction pathFromInstancePath(instancePath: string | undefined): string[] {\n if (!instancePath) {\n return [];\n }\n\n return instancePath\n .split(\"/\")\n .filter((segment) => segment.length > 0)\n .map((segment) => segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\"));\n}\n\n/**\n * Reset the cached lazy-import state. Test-only seam so a spec can mock\n * `ajv` as present/absent across cases without module-cache bleed.\n *\n * @internal\n */\nexport function resetAjvCacheForTests(): void {\n AjvSdk = undefined;\n ajvInstance = undefined;\n isAjvAvailable = undefined;\n loadingPromise = undefined;\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAM,SAAS;AAmCf,IAAI;AACJ,IAAI;AACJ,IAAI;AACJ,IAAI;AAEJ,MAAM,2BAA2B;;;;;;;;;;EAU/B,KAAK;;;;;;;;AASP,eAAe,UAAyB;CACtC,IAAI,mBAAmB,QACrB;CAGF,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GACF,SAAU,MAAM,OAAO;GAKvB,cAAc,KADZ,OAAO,WAAY,QACK;IAAE,WAAW;IAAM,QAAQ;GAAM,CAAC;GAC5D,iBAAiB;EACnB,QAAQ;GACN,iBAAiB;EACnB;CACF,EAAC,CAAE;CAEH,OAAO;AACT;;;;;;;;AASA,SAAS,aAAa,QAA4E;CAChG,IAAI;CACJ,IAAI,YAAY;CAEhB,aAAa;EACX,IAAI,WACF,OAAO;EAGT,YAAY;EAEZ,IAAI,CAAC,aACH;EAGF,IAAI;GACF,WAAW,YAAY,QAAQ,MAAM;EACvC,QAAQ;GACN,WAAW;EACb;EAEA,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,qBACd,QAC0B;CAG1B,IAAI,CAAC,UAAU,OAAO,KAAK,MAAM,CAAC,CAAC,WAAW,GAC5C,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,WAAW,WAAoB,EAAS,MAAgB;CAC1D,EACF;CAGF,MAAM,UAAU,aAAa,MAAM;CAEnC,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,MAAM,SAAS,OAA0D;GACvE,MAAM,QAAQ;GAEd,IAAI,CAAC,gBACH,OAAO,EACL,QAAQ,CAAC,EAAE,SAAS,yBAAyB,CAAC,EAChD;GAGF,MAAM,YAAY,QAAQ;GAI1B,IAAI,CAAC,WACH,OAAO,EAAS,MAAgB;GAKlC,IAFW,UAAU,KAEhB,GACH,OAAO,EAAS,MAAgB;GAGlC,MAAM,UAAoC,UAAU,UAAU,CAAC,EAAC,CAAE,KAAK,WAAW;IAChF,SAAS,eAAe,KAAK;IAC7B,MAAM,qBAAqB,MAAM,YAAY;GAC/C,EAAE;GAEF,OAAO,EACL,QAAQ,OAAO,SAAS,IAAI,SAAS,CAAC,EAAE,SAAS,sCAAsC,CAAC,EAC1F;EACF;CACF,EACF;AACF;;;;;;AAcA,SAAS,eAAe,OAA+B;CAIrD,OAAO,GAHO,MAAM,eAAe,GAAG,MAAM,aAAa,MAAM,KAC/C,MAAM,WAAW,WAAW,MAAM,WAAW,aAAa;AAG5E;;;;;;AAOA,SAAS,qBAAqB,cAA4C;CACxE,IAAI,CAAC,cACH,OAAO,CAAC;CAGV,OAAO,aACJ,MAAM,GAAG,CAAC,CACV,QAAQ,YAAY,QAAQ,SAAS,CAAC,CAAC,CACvC,KAAK,YAAY,QAAQ,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC;AACrE"}
@@ -1 +1 @@
1
- {"version":3,"file":"serve.mjs","names":[],"sources":["../../../../../../../ai-tools/src/mcp/serve.ts"],"sourcesContent":["import { createInterface, type Interface } from \"node:readline\";\nimport { extractJsonSchema, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n JsonRpcRequest,\n JsonRpcResponse,\n McpServeOptions,\n McpServeSource,\n McpServer,\n McpToolCallResult,\n McpToolDescriptor,\n} from \"../contracts\";\nimport { McpTransportError } from \"../errors\";\n\n/** JSON-RPC version literal every outbound message carries. */\nconst JSONRPC_VERSION = \"2.0\";\n\n/** Default JSON-Schema dialect emitted for each tool's `inputSchema`. */\nconst DEFAULT_SCHEMA_TARGET = \"draft-2020-12\";\n\n/** Default advertised server version when the caller omits one. */\nconst DEFAULT_VERSION = \"4.4.0\";\n\n/** The MCP protocol version this server advertises in `initialize`. */\nconst PROTOCOL_VERSION = \"2025-06-18\";\n\n/** JSON-RPC standard error codes we emit. */\nconst JSON_RPC_METHOD_NOT_FOUND = -32601;\nconst JSON_RPC_INVALID_PARAMS = -32602;\n\n/**\n * Resolve the {@link McpServeSource} (either an object exposing `tools()`\n * or a literal `ToolContract[]`) into a flat contract array.\n */\nfunction resolveTools(source: McpServeSource): ToolContract[] {\n if (Array.isArray(source)) {\n return source;\n }\n\n return source.tools();\n}\n\n/**\n * The pure protocol core of `serve` — maps one JSON-RPC request to its\n * response, with no I/O. Both the stdio and http serve-transports pump\n * their inbound requests through this, and specs can drive it directly.\n *\n * Handles exactly the MCP slice this package serves: `initialize`,\n * `tools/list`, and `tools/call`. Any other method answers with a\n * JSON-RPC `method not found` error.\n *\n * Constructed via {@link createServeHandler}.\n */\nclass McpServeHandler {\n /** The tools this server exposes (snapshotted at construction). */\n private readonly tools: ToolContract[];\n\n /** Fast lookup by tool name for `tools/call` dispatch. */\n private readonly byName: Map<string, ToolContract>;\n\n /** Construction-time serve options (name / version / schema target). */\n private readonly options: McpServeOptions;\n\n public constructor(source: McpServeSource, options: McpServeOptions) {\n this.tools = resolveTools(source);\n this.byName = new Map(this.tools.map((contract) => [contract.name, contract]));\n this.options = options;\n }\n\n /**\n * Dispatch one inbound JSON-RPC request to its handler and produce the\n * response. A handler that throws is mapped to a JSON-RPC error response\n * — the serve loop never crashes on a bad request.\n */\n public async handle(request: JsonRpcRequest): Promise<JsonRpcResponse> {\n try {\n switch (request.method) {\n case \"initialize\":\n return this.ok(request.id, this.initializeResult());\n case \"tools/list\":\n return this.ok(request.id, { tools: this.listTools() });\n case \"tools/call\":\n return this.ok(request.id, await this.callTool(request.params));\n default:\n return this.error(\n request.id,\n JSON_RPC_METHOD_NOT_FOUND,\n `Method \"${request.method}\" is not supported by this MCP server.`,\n );\n }\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n\n return this.error(request.id, JSON_RPC_INVALID_PARAMS, message);\n }\n }\n\n /** Build the `initialize` result advertising name / version / capabilities. */\n private initializeResult(): Record<string, unknown> {\n return {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: { tools: {} },\n serverInfo: {\n name: this.options.name,\n version: this.options.version ?? DEFAULT_VERSION,\n },\n };\n }\n\n /**\n * Build the `tools/list` payload: one {@link McpToolDescriptor} per\n * contract, its `inputSchema` extracted via `extractJsonSchema` at the\n * configured dialect (default `draft-2020-12` — overriding\n * `extractJsonSchema`'s own `openai-strict` default to a neutral MCP draft).\n */\n private listTools(): McpToolDescriptor[] {\n const target = this.options.schemaTarget ?? DEFAULT_SCHEMA_TARGET;\n\n return this.tools.map((contract) => {\n const inputSchema = extractJsonSchema(contract.input, { target }) ?? {\n type: \"object\",\n properties: {},\n };\n\n return {\n name: contract.name,\n description: contract.description,\n inputSchema,\n };\n });\n }\n\n /**\n * Route a `tools/call` to the named contract's `invoke()` and map the\n * never-throwing {@link import(\"@warlock.js/ai\").ToolInvokeResult}: `data`\n * → a text content block, `error` → an `isError: true` result. An unknown\n * tool name throws (mapped to a JSON-RPC error by {@link handle}).\n */\n private async callTool(params: unknown): Promise<McpToolCallResult> {\n const { name, args } = readCallParams(params);\n const contract = this.byName.get(name);\n\n if (!contract) {\n throw new McpTransportError(`Unknown tool \"${name}\".`, {\n type: \"protocol\",\n method: \"tools/call\",\n });\n }\n\n const result = await contract.invoke(args);\n\n if (result.error) {\n return {\n content: [{ type: \"text\", text: result.error.message }],\n isError: true,\n };\n }\n\n return {\n content: [{ type: \"text\", text: serializeData(result.data) }],\n isError: false,\n };\n }\n\n /** Build a JSON-RPC success response. */\n private ok(id: JsonRpcRequest[\"id\"], result: unknown): JsonRpcResponse {\n return { jsonrpc: JSONRPC_VERSION, id, result };\n }\n\n /** Build a JSON-RPC error response. */\n private error(id: JsonRpcRequest[\"id\"], code: number, message: string): JsonRpcResponse {\n return { jsonrpc: JSONRPC_VERSION, id, error: { code, message } };\n }\n}\n\n/**\n * Read and validate the `tools/call` params into `{ name, args }`. Throws\n * a typed {@link McpTransportError} when `name` is missing — mapped to a\n * JSON-RPC `invalid params` error by the handler.\n */\nfunction readCallParams(params: unknown): { name: string; args: unknown } {\n if (typeof params !== \"object\" || params === null) {\n throw new McpTransportError(\"tools/call params must be an object.\", {\n type: \"protocol\",\n method: \"tools/call\",\n });\n }\n\n const record = params as { name?: unknown; arguments?: unknown };\n\n if (typeof record.name !== \"string\") {\n throw new McpTransportError(\"tools/call requires a string `name`.\", {\n type: \"protocol\",\n method: \"tools/call\",\n });\n }\n\n return { name: record.name, args: record.arguments ?? {} };\n}\n\n/**\n * Serialize a tool's `data` for an MCP text content block — a string is\n * passed verbatim, everything else is JSON-stringified so structured\n * output crosses the wire as text the consuming client can re-parse.\n */\nfunction serializeData(data: unknown): string {\n if (typeof data === \"string\") {\n return data;\n }\n\n if (data === undefined) {\n return \"\";\n }\n\n return JSON.stringify(data);\n}\n\n/**\n * Build the pure protocol handler for a serve source. Exposed (alongside\n * {@link serve}) so callers and tests can drive the MCP protocol without an\n * actual transport — feed it a JSON-RPC request, get the response.\n *\n * @param source - The tools to expose (an object with `tools()` or a literal array).\n * @param options - Serve options (name / version / schema target).\n * @returns An object whose `handle(request)` maps a request to a response.\n */\nexport function createServeHandler(\n source: McpServeSource,\n options: McpServeOptions,\n): { handle(request: JsonRpcRequest): Promise<JsonRpcResponse> } {\n return new McpServeHandler(source, options);\n}\n\n/**\n * The internal {@link McpServer} — owns a {@link McpServeHandler} and a\n * transport pump. For `stdio` it reads newline-delimited JSON-RPC requests\n * from `process.stdin` and writes responses to `process.stdout`; the\n * `http` transport is accepted but listening is deferred to the host\n * (a serve-over-HTTP needs a server the caller owns).\n *\n * Constructed via {@link serve}; the class itself is internal.\n */\nclass McpServerImpl implements McpServer {\n /** The pure protocol handler. */\n private readonly handler: McpServeHandler;\n\n /** Serve options (transport selection lives here). */\n private readonly options: McpServeOptions;\n\n /** The stdin line reader while serving over stdio. */\n private reader: Interface | undefined;\n\n /** Flipped while the server is actively reading the transport. */\n private running = false;\n\n public constructor(source: McpServeSource, options: McpServeOptions) {\n this.handler = new McpServeHandler(source, options);\n this.options = options;\n }\n\n public async start(): Promise<void> {\n if (this.running) {\n return;\n }\n\n const transport = this.options.transport ?? { type: \"stdio\" };\n\n if (transport.type !== \"stdio\") {\n throw new McpTransportError(\n \"serve() over http requires a host-provided server; only stdio is auto-pumped.\",\n { type: \"connect\" },\n );\n }\n\n this.running = true;\n this.reader = createInterface({ input: process.stdin });\n\n this.reader.on(\"line\", (line) => {\n void this.onLine(line);\n });\n }\n\n /**\n * Parse one stdin line as a JSON-RPC request, dispatch it through the\n * handler, and write the response as a single line to stdout. Non-JSON\n * lines and notifications (no `id`) are ignored.\n */\n private async onLine(line: string): Promise<void> {\n const trimmed = line.trim();\n\n if (!trimmed) {\n return;\n }\n\n let request: JsonRpcRequest;\n\n try {\n request = JSON.parse(trimmed) as JsonRpcRequest;\n } catch {\n return;\n }\n\n if (request.id === undefined || request.id === null) {\n // A notification (e.g. notifications/initialized) — nothing to answer.\n return;\n }\n\n const response = await this.handler.handle(request);\n process.stdout.write(`${JSON.stringify(response)}\\n`);\n }\n\n public async stop(): Promise<void> {\n this.running = false;\n this.reader?.close();\n this.reader = undefined;\n }\n}\n\n/**\n * Expose a built agent / supervisor / orchestrator (or a raw\n * `ToolContract[]`) AS an MCP server (Direction B: local primitive → MCP\n * server other clients consume).\n *\n * Enumerates `source.tools()` (or the literal array) once at construction.\n * `tools/list` answers with each tool's `inputSchema` extracted via\n * `extractJsonSchema` at the configured `schemaTarget` (default\n * `draft-2020-12`). `tools/call` routes to the named contract's\n * `invoke()` and maps the never-throwing result — `data` becomes a text\n * content block, `error` becomes an `isError: true` result — so a failing\n * tool surfaces as a normal MCP tool error rather than crashing the server.\n *\n * The default transport is `stdio`, pumped over `process.stdin` /\n * `process.stdout`. Serving over HTTP is left to a host-owned server;\n * `start()` rejects an `http` transport (the protocol core is available\n * via {@link createServeHandler} for a caller's own HTTP wiring).\n *\n * @param source - The tools to expose.\n * @param options - Server name, version, transport, and schema dialect.\n * @returns An {@link McpServer} with `start()` / `stop()`.\n *\n * @example\n * serve(\n * { tools: () => ws.allTools() },\n * { name: \"warlock-workspace\", transport: { type: \"stdio\" } },\n * ).start();\n */\nexport function serve(source: McpServeSource, options: McpServeOptions): McpServer {\n return new McpServerImpl(source, options);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"serve.mjs","names":[],"sources":["../../../../../../../ai-tools/src/mcp/serve.ts"],"sourcesContent":["import { createInterface, type Interface } from \"node:readline\";\nimport { extractJsonSchema, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n JsonRpcRequest,\n JsonRpcResponse,\n McpServeOptions,\n McpServeSource,\n McpServer,\n McpToolCallResult,\n McpToolDescriptor,\n} from \"../contracts\";\nimport { McpTransportError } from \"../errors\";\n\n/** JSON-RPC version literal every outbound message carries. */\nconst JSONRPC_VERSION = \"2.0\";\n\n/** Default JSON-Schema dialect emitted for each tool's `inputSchema`. */\nconst DEFAULT_SCHEMA_TARGET = \"draft-2020-12\";\n\n/** Default advertised server version when the caller omits one. */\nconst DEFAULT_VERSION = \"4.4.0\";\n\n/** The MCP protocol version this server advertises in `initialize`. */\nconst PROTOCOL_VERSION = \"2025-06-18\";\n\n/** JSON-RPC standard error codes we emit. */\nconst JSON_RPC_METHOD_NOT_FOUND = -32601;\nconst JSON_RPC_INVALID_PARAMS = -32602;\n\n/**\n * Resolve the {@link McpServeSource} (either an object exposing `tools()`\n * or a literal `ToolContract[]`) into a flat contract array.\n */\nfunction resolveTools(source: McpServeSource): ToolContract[] {\n if (Array.isArray(source)) {\n return source;\n }\n\n return source.tools();\n}\n\n/**\n * The pure protocol core of `serve` — maps one JSON-RPC request to its\n * response, with no I/O. Both the stdio and http serve-transports pump\n * their inbound requests through this, and specs can drive it directly.\n *\n * Handles exactly the MCP slice this package serves: `initialize`,\n * `tools/list`, and `tools/call`. Any other method answers with a\n * JSON-RPC `method not found` error.\n *\n * Constructed via {@link createServeHandler}.\n */\nclass McpServeHandler {\n /** The tools this server exposes (snapshotted at construction). */\n private readonly tools: ToolContract[];\n\n /** Fast lookup by tool name for `tools/call` dispatch. */\n private readonly byName: Map<string, ToolContract>;\n\n /** Construction-time serve options (name / version / schema target). */\n private readonly options: McpServeOptions;\n\n public constructor(source: McpServeSource, options: McpServeOptions) {\n this.tools = resolveTools(source);\n this.byName = new Map(this.tools.map((contract) => [contract.name, contract]));\n this.options = options;\n }\n\n /**\n * Dispatch one inbound JSON-RPC request to its handler and produce the\n * response. A handler that throws is mapped to a JSON-RPC error response\n * — the serve loop never crashes on a bad request.\n */\n public async handle(request: JsonRpcRequest): Promise<JsonRpcResponse> {\n try {\n switch (request.method) {\n case \"initialize\":\n return this.ok(request.id, this.initializeResult());\n case \"tools/list\":\n return this.ok(request.id, { tools: this.listTools() });\n case \"tools/call\":\n return this.ok(request.id, await this.callTool(request.params));\n default:\n return this.error(\n request.id,\n JSON_RPC_METHOD_NOT_FOUND,\n `Method \"${request.method}\" is not supported by this MCP server.`,\n );\n }\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n\n return this.error(request.id, JSON_RPC_INVALID_PARAMS, message);\n }\n }\n\n /** Build the `initialize` result advertising name / version / capabilities. */\n private initializeResult(): Record<string, unknown> {\n return {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: { tools: {} },\n serverInfo: {\n name: this.options.name,\n version: this.options.version ?? DEFAULT_VERSION,\n },\n };\n }\n\n /**\n * Build the `tools/list` payload: one {@link McpToolDescriptor} per\n * contract, its `inputSchema` extracted via `extractJsonSchema` at the\n * configured dialect (default `draft-2020-12` — overriding\n * `extractJsonSchema`'s own `openai-strict` default to a neutral MCP draft).\n */\n private listTools(): McpToolDescriptor[] {\n const target = this.options.schemaTarget ?? DEFAULT_SCHEMA_TARGET;\n\n return this.tools.map((contract) => {\n const inputSchema = extractJsonSchema(contract.input, { target }) ?? {\n type: \"object\",\n properties: {},\n };\n\n return {\n name: contract.name,\n description: contract.description,\n inputSchema,\n };\n });\n }\n\n /**\n * Route a `tools/call` to the named contract's `invoke()` and map the\n * never-throwing {@link import(\"@warlock.js/ai\").ToolInvokeResult}: `data`\n * → a text content block, `error` → an `isError: true` result. An unknown\n * tool name throws (mapped to a JSON-RPC error by {@link handle}).\n */\n private async callTool(params: unknown): Promise<McpToolCallResult> {\n const { name, args } = readCallParams(params);\n const contract = this.byName.get(name);\n\n if (!contract) {\n throw new McpTransportError(`Unknown tool \"${name}\".`, {\n type: \"protocol\",\n method: \"tools/call\",\n });\n }\n\n const result = await contract.invoke(args);\n\n if (result.error) {\n return {\n content: [{ type: \"text\", text: result.error.message }],\n isError: true,\n };\n }\n\n return {\n content: [{ type: \"text\", text: serializeData(result.data) }],\n isError: false,\n };\n }\n\n /** Build a JSON-RPC success response. */\n private ok(id: JsonRpcRequest[\"id\"], result: unknown): JsonRpcResponse {\n return { jsonrpc: JSONRPC_VERSION, id, result };\n }\n\n /** Build a JSON-RPC error response. */\n private error(id: JsonRpcRequest[\"id\"], code: number, message: string): JsonRpcResponse {\n return { jsonrpc: JSONRPC_VERSION, id, error: { code, message } };\n }\n}\n\n/**\n * Read and validate the `tools/call` params into `{ name, args }`. Throws\n * a typed {@link McpTransportError} when `name` is missing — mapped to a\n * JSON-RPC `invalid params` error by the handler.\n */\nfunction readCallParams(params: unknown): { name: string; args: unknown } {\n if (typeof params !== \"object\" || params === null) {\n throw new McpTransportError(\"tools/call params must be an object.\", {\n type: \"protocol\",\n method: \"tools/call\",\n });\n }\n\n const record = params as { name?: unknown; arguments?: unknown };\n\n if (typeof record.name !== \"string\") {\n throw new McpTransportError(\"tools/call requires a string `name`.\", {\n type: \"protocol\",\n method: \"tools/call\",\n });\n }\n\n return { name: record.name, args: record.arguments ?? {} };\n}\n\n/**\n * Serialize a tool's `data` for an MCP text content block — a string is\n * passed verbatim, everything else is JSON-stringified so structured\n * output crosses the wire as text the consuming client can re-parse.\n */\nfunction serializeData(data: unknown): string {\n if (typeof data === \"string\") {\n return data;\n }\n\n if (data === undefined) {\n return \"\";\n }\n\n return JSON.stringify(data);\n}\n\n/**\n * Build the pure protocol handler for a serve source. Exposed (alongside\n * {@link serve}) so callers and tests can drive the MCP protocol without an\n * actual transport — feed it a JSON-RPC request, get the response.\n *\n * @param source - The tools to expose (an object with `tools()` or a literal array).\n * @param options - Serve options (name / version / schema target).\n * @returns An object whose `handle(request)` maps a request to a response.\n */\nexport function createServeHandler(\n source: McpServeSource,\n options: McpServeOptions,\n): { handle(request: JsonRpcRequest): Promise<JsonRpcResponse> } {\n return new McpServeHandler(source, options);\n}\n\n/**\n * The internal {@link McpServer} — owns a {@link McpServeHandler} and a\n * transport pump. For `stdio` it reads newline-delimited JSON-RPC requests\n * from `process.stdin` and writes responses to `process.stdout`; the\n * `http` transport is accepted but listening is deferred to the host\n * (a serve-over-HTTP needs a server the caller owns).\n *\n * Constructed via {@link serve}; the class itself is internal.\n */\nclass McpServerImpl implements McpServer {\n /** The pure protocol handler. */\n private readonly handler: McpServeHandler;\n\n /** Serve options (transport selection lives here). */\n private readonly options: McpServeOptions;\n\n /** The stdin line reader while serving over stdio. */\n private reader: Interface | undefined;\n\n /** Flipped while the server is actively reading the transport. */\n private running = false;\n\n public constructor(source: McpServeSource, options: McpServeOptions) {\n this.handler = new McpServeHandler(source, options);\n this.options = options;\n }\n\n public async start(): Promise<void> {\n if (this.running) {\n return;\n }\n\n const transport = this.options.transport ?? { type: \"stdio\" };\n\n if (transport.type !== \"stdio\") {\n throw new McpTransportError(\n \"serve() over http requires a host-provided server; only stdio is auto-pumped.\",\n { type: \"connect\" },\n );\n }\n\n this.running = true;\n this.reader = createInterface({ input: process.stdin });\n\n this.reader.on(\"line\", (line) => {\n void this.onLine(line);\n });\n }\n\n /**\n * Parse one stdin line as a JSON-RPC request, dispatch it through the\n * handler, and write the response as a single line to stdout. Non-JSON\n * lines and notifications (no `id`) are ignored.\n */\n private async onLine(line: string): Promise<void> {\n const trimmed = line.trim();\n\n if (!trimmed) {\n return;\n }\n\n let request: JsonRpcRequest;\n\n try {\n request = JSON.parse(trimmed) as JsonRpcRequest;\n } catch {\n return;\n }\n\n if (request.id === undefined || request.id === null) {\n // A notification (e.g. notifications/initialized) — nothing to answer.\n return;\n }\n\n const response = await this.handler.handle(request);\n process.stdout.write(`${JSON.stringify(response)}\\n`);\n }\n\n public async stop(): Promise<void> {\n this.running = false;\n this.reader?.close();\n this.reader = undefined;\n }\n}\n\n/**\n * Expose a built agent / supervisor / orchestrator (or a raw\n * `ToolContract[]`) AS an MCP server (Direction B: local primitive → MCP\n * server other clients consume).\n *\n * Enumerates `source.tools()` (or the literal array) once at construction.\n * `tools/list` answers with each tool's `inputSchema` extracted via\n * `extractJsonSchema` at the configured `schemaTarget` (default\n * `draft-2020-12`). `tools/call` routes to the named contract's\n * `invoke()` and maps the never-throwing result — `data` becomes a text\n * content block, `error` becomes an `isError: true` result — so a failing\n * tool surfaces as a normal MCP tool error rather than crashing the server.\n *\n * The default transport is `stdio`, pumped over `process.stdin` /\n * `process.stdout`. Serving over HTTP is left to a host-owned server;\n * `start()` rejects an `http` transport (the protocol core is available\n * via {@link createServeHandler} for a caller's own HTTP wiring).\n *\n * @param source - The tools to expose.\n * @param options - Server name, version, transport, and schema dialect.\n * @returns An {@link McpServer} with `start()` / `stop()`.\n *\n * @example\n * serve(\n * { tools: () => ws.allTools() },\n * { name: \"warlock-workspace\", transport: { type: \"stdio\" } },\n * ).start();\n */\nexport function serve(source: McpServeSource, options: McpServeOptions): McpServer {\n return new McpServerImpl(source, options);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"transport.mjs","names":[],"sources":["../../../../../../../ai-tools/src/mcp/transport.ts"],"sourcesContent":["import { spawn, type ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport { createInterface, type Interface } from \"node:readline\";\nimport type {\n JsonRpcId,\n JsonRpcRequest,\n JsonRpcResponse,\n McpTransport,\n} from \"../contracts\";\nimport { McpTransportError } from \"../errors\";\nimport type { McpTransportClient } from \"./transport.type\";\n\n/** Default per-request wait before a transport call is abandoned. */\nconst DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n\n/** The JSON-RPC version literal every outbound message carries. */\nconst JSONRPC_VERSION = \"2.0\";\n\n/**\n * A pending in-flight request awaiting its correlated response, keyed by\n * the JSON-RPC `id`. The stdio transport multiplexes many requests over\n * one line-framed pipe, so each resolve/reject is parked here until the\n * line whose `id` matches arrives.\n */\ninterface PendingCall {\n resolve(response: JsonRpcResponse): void;\n reject(error: McpTransportError): void;\n /** Clears the per-call timeout + abort wiring when the call settles. */\n cleanup(): void;\n}\n\n/**\n * Wire a per-call timeout and an optional caller `AbortSignal` onto a\n * pending request, returning a `cleanup()` that tears both down. The\n * `onSettle` callback removes the pending entry from whatever registry the\n * transport keeps so a late response can't double-settle.\n */\nfunction armCall(\n reject: (error: McpTransportError) => void,\n method: string,\n options: { signal?: AbortSignal; timeoutMs?: number } | undefined,\n onSettle: () => void,\n): () => void {\n const timeoutMs = options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\n const timer = setTimeout(() => {\n onSettle();\n reject(\n new McpTransportError(\n `MCP request \"${method}\" timed out after ${timeoutMs}ms.`,\n { type: \"timeout\", method },\n ),\n );\n }, timeoutMs);\n\n const onAbort = () => {\n cleanup();\n reject(\n new McpTransportError(`MCP request \"${method}\" was aborted.`, {\n type: \"closed\",\n method,\n }),\n );\n };\n\n const signal = options?.signal;\n\n if (signal) {\n if (signal.aborted) {\n // Defer so the caller has the rejection wired before it fires.\n queueMicrotask(onAbort);\n } else {\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n\n function cleanup(): void {\n clearTimeout(timer);\n\n if (signal) {\n signal.removeEventListener(\"abort\", onAbort);\n }\n }\n\n return cleanup;\n}\n\n/**\n * The stdio transport — spawns a child process and speaks JSON-RPC over\n * its stdin/stdout, one JSON object per line (newline-delimited framing).\n * Uses only Node built-ins (`node:child_process` + `node:readline`); no\n * dependency.\n *\n * Constructed via {@link createStdioTransport}; the class itself is\n * internal.\n */\nclass StdioTransport implements McpTransportClient {\n /** The spawned server process. */\n private readonly child: ChildProcessWithoutNullStreams;\n\n /** Line reader over the child's stdout — one JSON-RPC message per line. */\n private readonly reader: Interface;\n\n /** In-flight requests awaiting a correlated response, keyed by id. */\n private readonly pending = new Map<JsonRpcId, PendingCall>();\n\n /** Monotonic id source for outbound requests. */\n private nextId = 1;\n\n /** Flipped once {@link close} runs (or the child exits) so reuse rejects. */\n private closed = false;\n\n public constructor(transport: Extract<McpTransport, { type: \"stdio\" }>) {\n let child: ChildProcessWithoutNullStreams;\n\n try {\n child = spawn(transport.command, transport.args ?? [], {\n // process.env is NOT inherited unless the caller opts in — pass\n // what the server needs explicitly, mirroring the workspace shell\n // policy. `undefined` lets Node default to an empty-ish env.\n env: transport.env,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }) as ChildProcessWithoutNullStreams;\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new McpTransportError(\n `MCP stdio transport could not spawn \"${transport.command}\": ${message}`,\n { type: \"connect\", cause },\n );\n }\n\n this.child = child;\n this.reader = createInterface({ input: child.stdout });\n\n this.reader.on(\"line\", (line) => this.onLine(line));\n\n // A child that dies takes every in-flight (and future) call with it.\n child.on(\"exit\", (code) => this.failAll(\"connect\", `child exited with code ${code ?? \"null\"}`));\n child.on(\"error\", (error) => this.failAll(\"connect\", error.message));\n }\n\n /**\n * Parse one stdout line and route it to its pending request. Non-JSON\n * lines (a server logging to stdout) and messages with no matching `id`\n * (notifications, stray responses) are ignored — robustness over strictness.\n */\n private onLine(line: string): void {\n const trimmed = line.trim();\n\n if (!trimmed) {\n return;\n }\n\n let message: JsonRpcResponse;\n\n try {\n message = JSON.parse(trimmed) as JsonRpcResponse;\n } catch {\n // Not a JSON-RPC line (server diagnostics on stdout) — ignore.\n return;\n }\n\n if (message.id === undefined || message.id === null) {\n // A notification or a malformed response — nothing to correlate.\n return;\n }\n\n const call = this.pending.get(message.id);\n\n if (!call) {\n return;\n }\n\n this.pending.delete(message.id);\n call.cleanup();\n call.resolve(message);\n }\n\n /**\n * Reject every pending call (and mark the transport unusable) when the\n * child dies or errors — so a hung server can never leave a caller\n * waiting forever.\n */\n private failAll(type: \"connect\" | \"closed\", reason: string): void {\n this.closed = true;\n\n for (const [id, call] of this.pending) {\n this.pending.delete(id);\n call.cleanup();\n call.reject(\n new McpTransportError(`MCP stdio transport failed: ${reason}.`, { type }),\n );\n }\n }\n\n public request<TResult = unknown>(\n request: JsonRpcRequest,\n options?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<JsonRpcResponse<TResult>> {\n if (this.closed) {\n return Promise.reject(\n new McpTransportError(\"MCP stdio transport is closed.\", {\n type: \"closed\",\n method: request.method,\n }),\n );\n }\n\n const id = request.id;\n\n return new Promise<JsonRpcResponse<TResult>>((resolve, reject) => {\n const cleanup = armCall(reject, request.method, options, () =>\n this.pending.delete(id),\n );\n\n this.pending.set(id, {\n resolve: (response) => resolve(response as JsonRpcResponse<TResult>),\n reject,\n cleanup,\n });\n\n try {\n this.child.stdin.write(`${JSON.stringify(request)}\\n`);\n } catch (cause) {\n this.pending.delete(id);\n cleanup();\n\n const message = cause instanceof Error ? cause.message : String(cause);\n\n reject(\n new McpTransportError(\n `MCP stdio transport failed to write request \"${request.method}\": ${message}`,\n { type: \"closed\", method: request.method, cause },\n ),\n );\n }\n });\n }\n\n public async notify(method: string, params?: unknown): Promise<void> {\n if (this.closed) {\n throw new McpTransportError(\"MCP stdio transport is closed.\", {\n type: \"closed\",\n method,\n });\n }\n\n const notification = { jsonrpc: JSONRPC_VERSION, method, params };\n this.child.stdin.write(`${JSON.stringify(notification)}\\n`);\n }\n\n /** Allocate the next outbound request id. */\n public allocateId(): number {\n return this.nextId++;\n }\n\n public async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n\n this.closed = true;\n this.reader.close();\n this.failAll(\"closed\", \"transport closed by caller\");\n this.child.kill();\n }\n}\n\n/**\n * The Streamable HTTP transport — POSTs each JSON-RPC request to the\n * server endpoint over the global `fetch` (Node 18+) and reads the single\n * JSON response. No SSE-legacy, no WebSocket. Static `headers` (e.g. auth)\n * are sent with every request.\n *\n * Constructed via {@link createHttpTransport}; the class itself is internal.\n */\nclass HttpTransport implements McpTransportClient {\n /** The server endpoint POST target. */\n private readonly url: string;\n\n /** Static headers merged into every request (auth, etc.). */\n private readonly headers: Record<string, string>;\n\n /** Monotonic id source for outbound requests. */\n private nextId = 1;\n\n public constructor(transport: Extract<McpTransport, { type: \"http\" }>) {\n this.url = transport.url;\n this.headers = {\n \"content-type\": \"application/json\",\n accept: \"application/json, text/event-stream\",\n ...transport.headers,\n };\n }\n\n public async request<TResult = unknown>(\n request: JsonRpcRequest,\n options?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<JsonRpcResponse<TResult>> {\n const timeoutMs = options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const onAbort = () => controller.abort();\n const signal = options?.signal;\n\n if (signal) {\n if (signal.aborted) {\n controller.abort();\n } else {\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n\n let response: Response;\n\n try {\n response = await fetch(this.url, {\n method: \"POST\",\n headers: this.headers,\n body: JSON.stringify(request),\n signal: controller.signal,\n });\n } catch (cause) {\n const aborted = controller.signal.aborted;\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new McpTransportError(\n aborted\n ? `MCP request \"${request.method}\" timed out or was aborted after ${timeoutMs}ms.`\n : `MCP http transport request \"${request.method}\" failed: ${message}`,\n { type: aborted ? \"timeout\" : \"connect\", method: request.method, cause },\n );\n } finally {\n clearTimeout(timer);\n\n if (signal) {\n signal.removeEventListener(\"abort\", onAbort);\n }\n }\n\n if (!response.ok) {\n throw new McpTransportError(\n `MCP http transport request \"${request.method}\" returned HTTP ${response.status}.`,\n { type: \"connect\", method: request.method, context: { status: response.status } },\n );\n }\n\n return this.parseBody<TResult>(response, request.method);\n }\n\n /**\n * Parse the HTTP response body into a JSON-RPC response. Streamable HTTP\n * may answer with either `application/json` (a single response object)\n * or `text/event-stream` (SSE frames); we read the body as text and\n * extract the first JSON object, supporting the common `data: {...}`\n * SSE line shape without a streaming parser.\n */\n private async parseBody<TResult>(\n response: Response,\n method: string,\n ): Promise<JsonRpcResponse<TResult>> {\n const raw = await response.text();\n const contentType = response.headers.get(\"content-type\")?.toLowerCase() ?? \"\";\n\n const jsonText = contentType.includes(\"text/event-stream\")\n ? extractSseData(raw)\n : raw;\n\n if (!jsonText) {\n throw new McpTransportError(\n `MCP http transport got an empty response for \"${method}\".`,\n { type: \"protocol\", method },\n );\n }\n\n try {\n return JSON.parse(jsonText) as JsonRpcResponse<TResult>;\n } catch (cause) {\n throw new McpTransportError(\n `MCP http transport got a non-JSON response for \"${method}\".`,\n { type: \"protocol\", method, cause },\n );\n }\n }\n\n public async notify(method: string, params?: unknown): Promise<void> {\n const notification = { jsonrpc: JSONRPC_VERSION, method, params };\n\n // A notification expects no response; fire-and-forget but surface a\n // connect failure so a dead endpoint is not silently ignored.\n try {\n await fetch(this.url, {\n method: \"POST\",\n headers: this.headers,\n body: JSON.stringify(notification),\n });\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new McpTransportError(\n `MCP http transport notification \"${method}\" failed: ${message}`,\n { type: \"connect\", method, cause },\n );\n }\n }\n\n /** Allocate the next outbound request id. */\n public allocateId(): number {\n return this.nextId++;\n }\n\n public async close(): Promise<void> {\n // Streamable HTTP is stateless per request — nothing persistent to\n // release.\n }\n}\n\n/**\n * Pull the first `data:` JSON payload out of an SSE response body. MCP's\n * Streamable HTTP transport answers a single request with one SSE frame\n * carrying the JSON-RPC response; we take the first non-empty `data:`\n * line. Returns an empty string when none is found.\n */\nfunction extractSseData(body: string): string {\n for (const line of body.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n\n if (trimmed.startsWith(\"data:\")) {\n const payload = trimmed.slice(\"data:\".length).trim();\n\n if (payload && payload !== \"[DONE]\") {\n return payload;\n }\n }\n }\n\n return \"\";\n}\n\n/**\n * Build the concrete {@link McpTransportClient} for an {@link McpTransport}\n * config — a {@link StdioTransport} for `type: \"stdio\"`, an\n * {@link HttpTransport} for `type: \"http\"`. The returned client also\n * carries an `allocateId()` for the JSON-RPC client to mint request ids.\n *\n * @param transport - The transport config (discriminated by `type`).\n * @returns A transport client paired with its id allocator.\n */\nexport function createTransport(\n transport: McpTransport,\n): McpTransportClient & { allocateId(): number } {\n if (transport.type === \"stdio\") {\n return new StdioTransport(transport);\n }\n\n return new HttpTransport(transport);\n}\n\n/**\n * A minimal JSON-RPC 2.0 request/response client over any\n * {@link McpTransportClient}. Mints monotonic ids, frames the\n * `{ jsonrpc, id, method, params }` envelope, and unwraps the response —\n * translating a JSON-RPC `error` member into a typed\n * {@link McpTransportError} so callers branch on `error.type` rather than\n * parsing the wire.\n *\n * Constructed via {@link createJsonRpcClient}; the class itself is internal.\n */\nclass JsonRpcClient {\n /** The underlying framing transport. */\n private readonly transport: McpTransportClient & { allocateId(): number };\n\n public constructor(transport: McpTransportClient & { allocateId(): number }) {\n this.transport = transport;\n }\n\n /**\n * Issue a JSON-RPC `method` call and resolve with its `result`,\n * throwing a typed {@link McpTransportError} on a JSON-RPC error member\n * or a malformed response (neither `result` nor `error`).\n */\n public async call<TResult = unknown>(\n method: string,\n params?: unknown,\n options?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<TResult> {\n const request: JsonRpcRequest = {\n jsonrpc: JSONRPC_VERSION,\n id: this.transport.allocateId(),\n method,\n params,\n };\n\n const response = await this.transport.request<TResult>(request, options);\n\n if (response.error) {\n throw new McpTransportError(\n `MCP \"${method}\" failed: ${response.error.message} (code ${response.error.code}).`,\n { type: \"protocol\", method, context: { code: response.error.code }, cause: response.error.data },\n );\n }\n\n if (response.result === undefined) {\n throw new McpTransportError(\n `MCP \"${method}\" returned a response with neither result nor error.`,\n { type: \"protocol\", method },\n );\n }\n\n return response.result;\n }\n\n /** Send a one-way JSON-RPC notification (no response awaited). */\n public notify(method: string, params?: unknown): Promise<void> {\n return this.transport.notify(method, params);\n }\n\n /** Close the underlying transport. */\n public close(): Promise<void> {\n return this.transport.close();\n }\n}\n\n/**\n * A JSON-RPC client over an MCP transport. Either pass an already-built\n * transport client (tests inject a fake) or an {@link McpTransport} config\n * to spawn/connect a real one.\n */\nexport interface JsonRpcClientHandle {\n /** Issue a request and resolve with its `result` (throws on error). */\n call<TResult = unknown>(\n method: string,\n params?: unknown,\n options?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<TResult>;\n /** Send a one-way notification. */\n notify(method: string, params?: unknown): Promise<void>;\n /** Close the underlying transport. */\n close(): Promise<void>;\n}\n\n/**\n * Build a {@link JsonRpcClientHandle} over a transport. Accepts either a\n * pre-built {@link McpTransportClient} (the test seam — inject a scripted\n * fake) or an {@link McpTransport} config, in which case the concrete\n * transport is constructed via {@link createTransport}.\n *\n * When a bare {@link McpTransportClient} (without an `allocateId`) is\n * injected, the client supplies its own monotonic id source.\n *\n * @param source - A transport client or an `McpTransport` config.\n * @returns A JSON-RPC client handle.\n */\nexport function createJsonRpcClient(\n source: McpTransport | McpTransportClient,\n): JsonRpcClientHandle {\n const transport: McpTransportClient & { allocateId(): number } = isTransportConfig(source)\n ? createTransport(source)\n : withIdAllocator(source);\n\n return new JsonRpcClient(transport);\n}\n\n/**\n * Distinguish an {@link McpTransport} config (a plain object with a `type`\n * discriminator and no `request` method) from a built\n * {@link McpTransportClient} (which exposes `request`).\n */\nfunction isTransportConfig(\n source: McpTransport | McpTransportClient,\n): source is McpTransport {\n return typeof (source as McpTransportClient).request !== \"function\";\n}\n\n/**\n * Wrap an injected {@link McpTransportClient} that lacks its own\n * `allocateId` with a monotonic id source, so the JSON-RPC client can mint\n * request ids uniformly regardless of whether the transport was built here\n * or supplied by a test.\n */\nfunction withIdAllocator(\n client: McpTransportClient,\n): McpTransportClient & { allocateId(): number } {\n const candidate = client as McpTransportClient & { allocateId?(): number };\n\n if (typeof candidate.allocateId === \"function\") {\n return candidate as McpTransportClient & { allocateId(): number };\n }\n\n let nextId = 1;\n\n return Object.assign(client, { allocateId: () => nextId++ });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"transport.mjs","names":[],"sources":["../../../../../../../ai-tools/src/mcp/transport.ts"],"sourcesContent":["import { spawn, type ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport { createInterface, type Interface } from \"node:readline\";\nimport type {\n JsonRpcId,\n JsonRpcRequest,\n JsonRpcResponse,\n McpTransport,\n} from \"../contracts\";\nimport { McpTransportError } from \"../errors\";\nimport type { McpTransportClient } from \"./transport.type\";\n\n/** Default per-request wait before a transport call is abandoned. */\nconst DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n\n/** The JSON-RPC version literal every outbound message carries. */\nconst JSONRPC_VERSION = \"2.0\";\n\n/**\n * A pending in-flight request awaiting its correlated response, keyed by\n * the JSON-RPC `id`. The stdio transport multiplexes many requests over\n * one line-framed pipe, so each resolve/reject is parked here until the\n * line whose `id` matches arrives.\n */\ninterface PendingCall {\n resolve(response: JsonRpcResponse): void;\n reject(error: McpTransportError): void;\n /** Clears the per-call timeout + abort wiring when the call settles. */\n cleanup(): void;\n}\n\n/**\n * Wire a per-call timeout and an optional caller `AbortSignal` onto a\n * pending request, returning a `cleanup()` that tears both down. The\n * `onSettle` callback removes the pending entry from whatever registry the\n * transport keeps so a late response can't double-settle.\n */\nfunction armCall(\n reject: (error: McpTransportError) => void,\n method: string,\n options: { signal?: AbortSignal; timeoutMs?: number } | undefined,\n onSettle: () => void,\n): () => void {\n const timeoutMs = options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\n const timer = setTimeout(() => {\n onSettle();\n reject(\n new McpTransportError(\n `MCP request \"${method}\" timed out after ${timeoutMs}ms.`,\n { type: \"timeout\", method },\n ),\n );\n }, timeoutMs);\n\n const onAbort = () => {\n cleanup();\n reject(\n new McpTransportError(`MCP request \"${method}\" was aborted.`, {\n type: \"closed\",\n method,\n }),\n );\n };\n\n const signal = options?.signal;\n\n if (signal) {\n if (signal.aborted) {\n // Defer so the caller has the rejection wired before it fires.\n queueMicrotask(onAbort);\n } else {\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n\n function cleanup(): void {\n clearTimeout(timer);\n\n if (signal) {\n signal.removeEventListener(\"abort\", onAbort);\n }\n }\n\n return cleanup;\n}\n\n/**\n * The stdio transport — spawns a child process and speaks JSON-RPC over\n * its stdin/stdout, one JSON object per line (newline-delimited framing).\n * Uses only Node built-ins (`node:child_process` + `node:readline`); no\n * dependency.\n *\n * Constructed via {@link createStdioTransport}; the class itself is\n * internal.\n */\nclass StdioTransport implements McpTransportClient {\n /** The spawned server process. */\n private readonly child: ChildProcessWithoutNullStreams;\n\n /** Line reader over the child's stdout — one JSON-RPC message per line. */\n private readonly reader: Interface;\n\n /** In-flight requests awaiting a correlated response, keyed by id. */\n private readonly pending = new Map<JsonRpcId, PendingCall>();\n\n /** Monotonic id source for outbound requests. */\n private nextId = 1;\n\n /** Flipped once {@link close} runs (or the child exits) so reuse rejects. */\n private closed = false;\n\n public constructor(transport: Extract<McpTransport, { type: \"stdio\" }>) {\n let child: ChildProcessWithoutNullStreams;\n\n try {\n child = spawn(transport.command, transport.args ?? [], {\n // process.env is NOT inherited unless the caller opts in — pass\n // what the server needs explicitly, mirroring the workspace shell\n // policy. `undefined` lets Node default to an empty-ish env.\n env: transport.env,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }) as ChildProcessWithoutNullStreams;\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new McpTransportError(\n `MCP stdio transport could not spawn \"${transport.command}\": ${message}`,\n { type: \"connect\", cause },\n );\n }\n\n this.child = child;\n this.reader = createInterface({ input: child.stdout });\n\n this.reader.on(\"line\", (line) => this.onLine(line));\n\n // A child that dies takes every in-flight (and future) call with it.\n child.on(\"exit\", (code) => this.failAll(\"connect\", `child exited with code ${code ?? \"null\"}`));\n child.on(\"error\", (error) => this.failAll(\"connect\", error.message));\n }\n\n /**\n * Parse one stdout line and route it to its pending request. Non-JSON\n * lines (a server logging to stdout) and messages with no matching `id`\n * (notifications, stray responses) are ignored — robustness over strictness.\n */\n private onLine(line: string): void {\n const trimmed = line.trim();\n\n if (!trimmed) {\n return;\n }\n\n let message: JsonRpcResponse;\n\n try {\n message = JSON.parse(trimmed) as JsonRpcResponse;\n } catch {\n // Not a JSON-RPC line (server diagnostics on stdout) — ignore.\n return;\n }\n\n if (message.id === undefined || message.id === null) {\n // A notification or a malformed response — nothing to correlate.\n return;\n }\n\n const call = this.pending.get(message.id);\n\n if (!call) {\n return;\n }\n\n this.pending.delete(message.id);\n call.cleanup();\n call.resolve(message);\n }\n\n /**\n * Reject every pending call (and mark the transport unusable) when the\n * child dies or errors — so a hung server can never leave a caller\n * waiting forever.\n */\n private failAll(type: \"connect\" | \"closed\", reason: string): void {\n this.closed = true;\n\n for (const [id, call] of this.pending) {\n this.pending.delete(id);\n call.cleanup();\n call.reject(\n new McpTransportError(`MCP stdio transport failed: ${reason}.`, { type }),\n );\n }\n }\n\n public request<TResult = unknown>(\n request: JsonRpcRequest,\n options?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<JsonRpcResponse<TResult>> {\n if (this.closed) {\n return Promise.reject(\n new McpTransportError(\"MCP stdio transport is closed.\", {\n type: \"closed\",\n method: request.method,\n }),\n );\n }\n\n const id = request.id;\n\n return new Promise<JsonRpcResponse<TResult>>((resolve, reject) => {\n const cleanup = armCall(reject, request.method, options, () =>\n this.pending.delete(id),\n );\n\n this.pending.set(id, {\n resolve: (response) => resolve(response as JsonRpcResponse<TResult>),\n reject,\n cleanup,\n });\n\n try {\n this.child.stdin.write(`${JSON.stringify(request)}\\n`);\n } catch (cause) {\n this.pending.delete(id);\n cleanup();\n\n const message = cause instanceof Error ? cause.message : String(cause);\n\n reject(\n new McpTransportError(\n `MCP stdio transport failed to write request \"${request.method}\": ${message}`,\n { type: \"closed\", method: request.method, cause },\n ),\n );\n }\n });\n }\n\n public async notify(method: string, params?: unknown): Promise<void> {\n if (this.closed) {\n throw new McpTransportError(\"MCP stdio transport is closed.\", {\n type: \"closed\",\n method,\n });\n }\n\n const notification = { jsonrpc: JSONRPC_VERSION, method, params };\n this.child.stdin.write(`${JSON.stringify(notification)}\\n`);\n }\n\n /** Allocate the next outbound request id. */\n public allocateId(): number {\n return this.nextId++;\n }\n\n public async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n\n this.closed = true;\n this.reader.close();\n this.failAll(\"closed\", \"transport closed by caller\");\n this.child.kill();\n }\n}\n\n/**\n * The Streamable HTTP transport — POSTs each JSON-RPC request to the\n * server endpoint over the global `fetch` (Node 18+) and reads the single\n * JSON response. No SSE-legacy, no WebSocket. Static `headers` (e.g. auth)\n * are sent with every request.\n *\n * Constructed via {@link createHttpTransport}; the class itself is internal.\n */\nclass HttpTransport implements McpTransportClient {\n /** The server endpoint POST target. */\n private readonly url: string;\n\n /** Static headers merged into every request (auth, etc.). */\n private readonly headers: Record<string, string>;\n\n /** Monotonic id source for outbound requests. */\n private nextId = 1;\n\n public constructor(transport: Extract<McpTransport, { type: \"http\" }>) {\n this.url = transport.url;\n this.headers = {\n \"content-type\": \"application/json\",\n accept: \"application/json, text/event-stream\",\n ...transport.headers,\n };\n }\n\n public async request<TResult = unknown>(\n request: JsonRpcRequest,\n options?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<JsonRpcResponse<TResult>> {\n const timeoutMs = options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const onAbort = () => controller.abort();\n const signal = options?.signal;\n\n if (signal) {\n if (signal.aborted) {\n controller.abort();\n } else {\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n\n let response: Response;\n\n try {\n response = await fetch(this.url, {\n method: \"POST\",\n headers: this.headers,\n body: JSON.stringify(request),\n signal: controller.signal,\n });\n } catch (cause) {\n const aborted = controller.signal.aborted;\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new McpTransportError(\n aborted\n ? `MCP request \"${request.method}\" timed out or was aborted after ${timeoutMs}ms.`\n : `MCP http transport request \"${request.method}\" failed: ${message}`,\n { type: aborted ? \"timeout\" : \"connect\", method: request.method, cause },\n );\n } finally {\n clearTimeout(timer);\n\n if (signal) {\n signal.removeEventListener(\"abort\", onAbort);\n }\n }\n\n if (!response.ok) {\n throw new McpTransportError(\n `MCP http transport request \"${request.method}\" returned HTTP ${response.status}.`,\n { type: \"connect\", method: request.method, context: { status: response.status } },\n );\n }\n\n return this.parseBody<TResult>(response, request.method);\n }\n\n /**\n * Parse the HTTP response body into a JSON-RPC response. Streamable HTTP\n * may answer with either `application/json` (a single response object)\n * or `text/event-stream` (SSE frames); we read the body as text and\n * extract the first JSON object, supporting the common `data: {...}`\n * SSE line shape without a streaming parser.\n */\n private async parseBody<TResult>(\n response: Response,\n method: string,\n ): Promise<JsonRpcResponse<TResult>> {\n const raw = await response.text();\n const contentType = response.headers.get(\"content-type\")?.toLowerCase() ?? \"\";\n\n const jsonText = contentType.includes(\"text/event-stream\")\n ? extractSseData(raw)\n : raw;\n\n if (!jsonText) {\n throw new McpTransportError(\n `MCP http transport got an empty response for \"${method}\".`,\n { type: \"protocol\", method },\n );\n }\n\n try {\n return JSON.parse(jsonText) as JsonRpcResponse<TResult>;\n } catch (cause) {\n throw new McpTransportError(\n `MCP http transport got a non-JSON response for \"${method}\".`,\n { type: \"protocol\", method, cause },\n );\n }\n }\n\n public async notify(method: string, params?: unknown): Promise<void> {\n const notification = { jsonrpc: JSONRPC_VERSION, method, params };\n\n // A notification expects no response; fire-and-forget but surface a\n // connect failure so a dead endpoint is not silently ignored.\n try {\n await fetch(this.url, {\n method: \"POST\",\n headers: this.headers,\n body: JSON.stringify(notification),\n });\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new McpTransportError(\n `MCP http transport notification \"${method}\" failed: ${message}`,\n { type: \"connect\", method, cause },\n );\n }\n }\n\n /** Allocate the next outbound request id. */\n public allocateId(): number {\n return this.nextId++;\n }\n\n public async close(): Promise<void> {\n // Streamable HTTP is stateless per request — nothing persistent to\n // release.\n }\n}\n\n/**\n * Pull the first `data:` JSON payload out of an SSE response body. MCP's\n * Streamable HTTP transport answers a single request with one SSE frame\n * carrying the JSON-RPC response; we take the first non-empty `data:`\n * line. Returns an empty string when none is found.\n */\nfunction extractSseData(body: string): string {\n for (const line of body.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n\n if (trimmed.startsWith(\"data:\")) {\n const payload = trimmed.slice(\"data:\".length).trim();\n\n if (payload && payload !== \"[DONE]\") {\n return payload;\n }\n }\n }\n\n return \"\";\n}\n\n/**\n * Build the concrete {@link McpTransportClient} for an {@link McpTransport}\n * config — a {@link StdioTransport} for `type: \"stdio\"`, an\n * {@link HttpTransport} for `type: \"http\"`. The returned client also\n * carries an `allocateId()` for the JSON-RPC client to mint request ids.\n *\n * @param transport - The transport config (discriminated by `type`).\n * @returns A transport client paired with its id allocator.\n */\nexport function createTransport(\n transport: McpTransport,\n): McpTransportClient & { allocateId(): number } {\n if (transport.type === \"stdio\") {\n return new StdioTransport(transport);\n }\n\n return new HttpTransport(transport);\n}\n\n/**\n * A minimal JSON-RPC 2.0 request/response client over any\n * {@link McpTransportClient}. Mints monotonic ids, frames the\n * `{ jsonrpc, id, method, params }` envelope, and unwraps the response —\n * translating a JSON-RPC `error` member into a typed\n * {@link McpTransportError} so callers branch on `error.type` rather than\n * parsing the wire.\n *\n * Constructed via {@link createJsonRpcClient}; the class itself is internal.\n */\nclass JsonRpcClient {\n /** The underlying framing transport. */\n private readonly transport: McpTransportClient & { allocateId(): number };\n\n public constructor(transport: McpTransportClient & { allocateId(): number }) {\n this.transport = transport;\n }\n\n /**\n * Issue a JSON-RPC `method` call and resolve with its `result`,\n * throwing a typed {@link McpTransportError} on a JSON-RPC error member\n * or a malformed response (neither `result` nor `error`).\n */\n public async call<TResult = unknown>(\n method: string,\n params?: unknown,\n options?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<TResult> {\n const request: JsonRpcRequest = {\n jsonrpc: JSONRPC_VERSION,\n id: this.transport.allocateId(),\n method,\n params,\n };\n\n const response = await this.transport.request<TResult>(request, options);\n\n if (response.error) {\n throw new McpTransportError(\n `MCP \"${method}\" failed: ${response.error.message} (code ${response.error.code}).`,\n { type: \"protocol\", method, context: { code: response.error.code }, cause: response.error.data },\n );\n }\n\n if (response.result === undefined) {\n throw new McpTransportError(\n `MCP \"${method}\" returned a response with neither result nor error.`,\n { type: \"protocol\", method },\n );\n }\n\n return response.result;\n }\n\n /** Send a one-way JSON-RPC notification (no response awaited). */\n public notify(method: string, params?: unknown): Promise<void> {\n return this.transport.notify(method, params);\n }\n\n /** Close the underlying transport. */\n public close(): Promise<void> {\n return this.transport.close();\n }\n}\n\n/**\n * A JSON-RPC client over an MCP transport. Either pass an already-built\n * transport client (tests inject a fake) or an {@link McpTransport} config\n * to spawn/connect a real one.\n */\nexport interface JsonRpcClientHandle {\n /** Issue a request and resolve with its `result` (throws on error). */\n call<TResult = unknown>(\n method: string,\n params?: unknown,\n options?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<TResult>;\n /** Send a one-way notification. */\n notify(method: string, params?: unknown): Promise<void>;\n /** Close the underlying transport. */\n close(): Promise<void>;\n}\n\n/**\n * Build a {@link JsonRpcClientHandle} over a transport. Accepts either a\n * pre-built {@link McpTransportClient} (the test seam — inject a scripted\n * fake) or an {@link McpTransport} config, in which case the concrete\n * transport is constructed via {@link createTransport}.\n *\n * When a bare {@link McpTransportClient} (without an `allocateId`) is\n * injected, the client supplies its own monotonic id source.\n *\n * @param source - A transport client or an `McpTransport` config.\n * @returns A JSON-RPC client handle.\n */\nexport function createJsonRpcClient(\n source: McpTransport | McpTransportClient,\n): JsonRpcClientHandle {\n const transport: McpTransportClient & { allocateId(): number } = isTransportConfig(source)\n ? createTransport(source)\n : withIdAllocator(source);\n\n return new JsonRpcClient(transport);\n}\n\n/**\n * Distinguish an {@link McpTransport} config (a plain object with a `type`\n * discriminator and no `request` method) from a built\n * {@link McpTransportClient} (which exposes `request`).\n */\nfunction isTransportConfig(\n source: McpTransport | McpTransportClient,\n): source is McpTransport {\n return typeof (source as McpTransportClient).request !== \"function\";\n}\n\n/**\n * Wrap an injected {@link McpTransportClient} that lacks its own\n * `allocateId` with a monotonic id source, so the JSON-RPC client can mint\n * request ids uniformly regardless of whether the transport was built here\n * or supplied by a test.\n */\nfunction withIdAllocator(\n client: McpTransportClient,\n): McpTransportClient & { allocateId(): number } {\n const candidate = client as McpTransportClient & { allocateId?(): number };\n\n if (typeof candidate.allocateId === \"function\") {\n return candidate as McpTransportClient & { allocateId(): number };\n }\n\n let nextId = 1;\n\n return Object.assign(client, { allocateId: () => nextId++ });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"calculator.mjs","names":[],"sources":["../../../../../../../ai-tools/src/utility/calculator.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { CalculatorError } from \"../errors\";\nimport { objectSchema, stringField } from \"./schema\";\nimport type {\n CalculatorInput,\n CalculatorOptions,\n CalculatorResult,\n} from \"../contracts/utility.type\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"calculator\";\n\n/** Input schema for the `calculator` tool: a single `expression` string. */\nconst inputSchema = objectSchema<CalculatorInput>({\n expression: stringField(),\n});\n\n/**\n * A binary operator the evaluator understands, with its precedence and\n * associativity. Higher `precedence` binds tighter; `^` is the only\n * right-associative operator (so `2 ^ 3 ^ 2` is `2 ^ (3 ^ 2)`).\n */\ninterface OperatorSpec {\n precedence: number;\n associativity: \"left\" | \"right\";\n /** Apply the operator to its two operands. May throw {@link CalculatorError}. */\n apply(left: number, right: number): number;\n}\n\n/** The closed set of supported binary operators. */\nconst OPERATORS: Record<string, OperatorSpec> = {\n \"+\": { precedence: 1, associativity: \"left\", apply: (a, b) => a + b },\n \"-\": { precedence: 1, associativity: \"left\", apply: (a, b) => a - b },\n \"*\": { precedence: 2, associativity: \"left\", apply: (a, b) => a * b },\n \"/\": {\n precedence: 2,\n associativity: \"left\",\n apply: (a, b) => {\n if (b === 0) {\n throw new CalculatorError(\"Division by zero.\", { type: \"divide-by-zero\" });\n }\n\n return a / b;\n },\n },\n \"%\": {\n precedence: 2,\n associativity: \"left\",\n apply: (a, b) => {\n if (b === 0) {\n throw new CalculatorError(\"Modulo by zero.\", { type: \"divide-by-zero\" });\n }\n\n return a % b;\n },\n },\n \"^\": { precedence: 3, associativity: \"right\", apply: (a, b) => a ** b },\n};\n\n/** A lexed token: a number literal, an operator, or a parenthesis. */\ntype Token =\n | { type: \"number\"; value: number }\n | { type: \"operator\"; value: string }\n | { type: \"paren\"; value: \"(\" | \")\" };\n\n/**\n * Tokenize an arithmetic expression into {@link Token}s. Recognizes\n * decimal and scientific-notation numbers (`3`, `4.5`, `1e3`, `2.5E-2`),\n * the operators in {@link OPERATORS}, and parentheses; whitespace is\n * skipped. Any other character is a syntax error — there is no path to\n * an identifier, function call, or property access, so nothing\n * code-like can be smuggled in.\n *\n * @throws CalculatorError `type: \"syntax\"` on an unrecognized character\n * or a malformed number.\n */\nfunction tokenize(expression: string): Token[] {\n const tokens: Token[] = [];\n let index = 0;\n\n while (index < expression.length) {\n const char = expression[index];\n\n if (char === \" \" || char === \"\\t\" || char === \"\\n\" || char === \"\\r\") {\n index += 1;\n\n continue;\n }\n\n if (char === \"(\" || char === \")\") {\n tokens.push({ type: \"paren\", value: char });\n index += 1;\n\n continue;\n }\n\n if (char in OPERATORS) {\n tokens.push({ type: \"operator\", value: char });\n index += 1;\n\n continue;\n }\n\n if (isDigit(char) || char === \".\") {\n const { value, nextIndex } = readNumber(expression, index);\n tokens.push({ type: \"number\", value });\n index = nextIndex;\n\n continue;\n }\n\n throw new CalculatorError(\n `Unexpected character \"${char}\" at position ${index}. Only numbers, parentheses, and the operators + - * / % ^ are allowed.`,\n { type: \"syntax\" },\n );\n }\n\n return tokens;\n}\n\n/** True for an ASCII digit `0`–`9`. */\nfunction isDigit(char: string): boolean {\n return char >= \"0\" && char <= \"9\";\n}\n\n/**\n * Read a single number literal starting at `start`. Consumes an optional\n * integer part, optional fraction, and optional exponent\n * (`e`/`E` with an optional sign). Returns the parsed value and the index\n * just past the literal.\n *\n * @throws CalculatorError `type: \"syntax\"` if the consumed run is not a\n * valid finite number (e.g. a lone `.` or `1e` with no exponent).\n */\nfunction readNumber(\n expression: string,\n start: number,\n): { value: number; nextIndex: number } {\n let index = start;\n\n while (index < expression.length && isDigit(expression[index])) {\n index += 1;\n }\n\n if (expression[index] === \".\") {\n index += 1;\n\n while (index < expression.length && isDigit(expression[index])) {\n index += 1;\n }\n }\n\n if (expression[index] === \"e\" || expression[index] === \"E\") {\n index += 1;\n\n if (expression[index] === \"+\" || expression[index] === \"-\") {\n index += 1;\n }\n\n while (index < expression.length && isDigit(expression[index])) {\n index += 1;\n }\n }\n\n const literal = expression.slice(start, index);\n const value = Number(literal);\n\n if (!Number.isFinite(value)) {\n throw new CalculatorError(`Invalid number literal \"${literal}\".`, {\n type: \"syntax\",\n });\n }\n\n return { value, nextIndex: index };\n}\n\n/**\n * Evaluate a token stream with a single left-to-right pass that resolves\n * unary signs, then a shunting-yard conversion that interleaves operator\n * application — so the result is produced without ever building an AST or\n * calling `eval`/`Function`.\n *\n * Unary `+`/`-` are detected positionally: a `+`/`-` is unary when it\n * starts the expression or directly follows another operator or an\n * opening paren. A unary `-` folds into the following number literal\n * (and a unary `+` is a no-op), which keeps the operator stack purely\n * binary.\n *\n * @throws CalculatorError on malformed structure (`type: \"syntax\"`),\n * division/modulo by zero (`type: \"divide-by-zero\"`), or a non-finite\n * result (`type: \"overflow\"`).\n */\nfunction evaluate(tokens: Token[]): number {\n const values: number[] = [];\n const operators: string[] = [];\n\n /** Pop the top operator and apply it to the top two values. */\n const applyTop = (): void => {\n const operator = operators.pop();\n\n if (operator === undefined) {\n throw new CalculatorError(\"Malformed expression.\", { type: \"syntax\" });\n }\n\n const right = values.pop();\n const left = values.pop();\n\n if (left === undefined || right === undefined) {\n throw new CalculatorError(\n `Operator \"${operator}\" is missing an operand.`,\n { type: \"syntax\" },\n );\n }\n\n values.push(OPERATORS[operator].apply(left, right));\n };\n\n // `expectOperand` tracks the positional state machine: at the start, and\n // immediately after an operator or \"(\", we expect an operand (a number,\n // a \"(\", or a unary sign). After a number or \")\", we expect a binary\n // operator or \")\".\n let expectOperand = true;\n // Pending unary sign multiplier folded into the next numeric operand.\n let pendingSign = 1;\n\n for (let i = 0; i < tokens.length; i += 1) {\n const token = tokens[i];\n\n if (token.type === \"number\") {\n if (!expectOperand) {\n throw new CalculatorError(\n \"Unexpected number — a number cannot directly follow another value.\",\n { type: \"syntax\" },\n );\n }\n\n values.push(pendingSign * token.value);\n pendingSign = 1;\n expectOperand = false;\n\n continue;\n }\n\n if (token.type === \"paren\") {\n if (token.value === \"(\") {\n if (!expectOperand) {\n throw new CalculatorError(\n 'Unexpected \"(\" — it cannot directly follow a value.',\n { type: \"syntax\" },\n );\n }\n\n // A unary sign in front of a parenthesized group is modeled by\n // pushing the sign as a multiplication: `-(…)` becomes `-1 * (…)`.\n if (pendingSign === -1) {\n values.push(-1);\n operators.push(\"*\");\n pendingSign = 1;\n }\n\n operators.push(\"(\");\n expectOperand = true;\n\n continue;\n }\n\n // token.value === \")\"\n if (expectOperand) {\n throw new CalculatorError(\n 'Unexpected \")\" — an operand was expected.',\n { type: \"syntax\" },\n );\n }\n\n let foundOpen = false;\n\n while (operators.length > 0) {\n if (operators[operators.length - 1] === \"(\") {\n operators.pop();\n foundOpen = true;\n\n break;\n }\n\n applyTop();\n }\n\n if (!foundOpen) {\n throw new CalculatorError(\"Unbalanced parentheses.\", { type: \"syntax\" });\n }\n\n expectOperand = false;\n\n continue;\n }\n\n // token.type === \"operator\"\n if (expectOperand) {\n // A `+`/`-` in operand position is a unary sign; anything else is a\n // misplaced binary operator.\n if (token.value === \"-\") {\n pendingSign = -pendingSign;\n\n continue;\n }\n\n if (token.value === \"+\") {\n continue;\n }\n\n throw new CalculatorError(\n `Operator \"${token.value}\" has no left-hand operand.`,\n { type: \"syntax\" },\n );\n }\n\n const incoming = OPERATORS[token.value];\n\n while (operators.length > 0) {\n const top = operators[operators.length - 1];\n\n if (top === \"(\") {\n break;\n }\n\n const topSpec = OPERATORS[top];\n const higher = topSpec.precedence > incoming.precedence;\n const equalLeft =\n topSpec.precedence === incoming.precedence &&\n incoming.associativity === \"left\";\n\n if (higher || equalLeft) {\n applyTop();\n\n continue;\n }\n\n break;\n }\n\n operators.push(token.value);\n expectOperand = true;\n }\n\n if (expectOperand) {\n throw new CalculatorError(\n \"Expression ends with an operator or is empty.\",\n { type: \"syntax\" },\n );\n }\n\n while (operators.length > 0) {\n if (operators[operators.length - 1] === \"(\") {\n throw new CalculatorError(\"Unbalanced parentheses.\", { type: \"syntax\" });\n }\n\n applyTop();\n }\n\n const result = values.pop();\n\n if (result === undefined || values.length > 0) {\n throw new CalculatorError(\"Malformed expression.\", { type: \"syntax\" });\n }\n\n if (!Number.isFinite(result)) {\n throw new CalculatorError(\"Result is not a finite number.\", {\n type: \"overflow\",\n });\n }\n\n return result;\n}\n\n/**\n * Build the `calculator` tool — a SAFE arithmetic evaluator the agent can\n * call to compute a numeric expression. It supports `+ - * / % ^`, unary\n * signs, parentheses, and decimal/scientific-notation literals, with the\n * usual precedence (`^` highest and right-associative, then `* / %`, then\n * `+ -`).\n *\n * **Safety.** The expression is tokenized and evaluated with a\n * shunting-yard pass — it NEVER calls `eval` or `new Function`. The\n * lexer only recognizes numbers, parentheses, and the fixed operator set,\n * so there is no path to an identifier, function call, or property\n * access; any other character is a syntax error returned as data.\n *\n * **Errors flow as data.** A malformed expression, division/modulo by\n * zero, or a non-finite result throws a {@link CalculatorError} inside\n * the handler; `tool()` catches it and surfaces it in the returned\n * `{ error }` field (the LLM-visible message is preserved), so the agent\n * reads the failure and self-corrects instead of crashing.\n *\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n * @returns A `ToolContract<{ expression }, { result }>` ready to drop into `tools: []`.\n *\n * @example\n * const calc = calculatorTool();\n * const { data } = await calc.invoke({ expression: \"(3 + 4) * 2\" });\n * console.log(data?.result); // 14\n */\nexport function calculatorTool(\n options?: CalculatorOptions,\n): ToolContract<CalculatorInput, CalculatorResult> {\n return tool<CalculatorInput, CalculatorResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Evaluate an arithmetic expression and return the numeric result. \" +\n \"Supports + - * / % ^, parentheses, unary +/-, and decimal or \" +\n \"scientific-notation numbers. Does not support variables or \" +\n \"functions — pass a literal expression like \\\"(3 + 4) * 2\\\".\",\n input: inputSchema,\n async execute(input) {\n const tokens = tokenize(input.expression);\n\n if (tokens.length === 0) {\n throw new CalculatorError(\"Expression is empty.\", { type: \"syntax\" });\n }\n\n return { result: evaluate(tokens) };\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"calculator.mjs","names":[],"sources":["../../../../../../../ai-tools/src/utility/calculator.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { CalculatorError } from \"../errors\";\nimport { objectSchema, stringField } from \"./schema\";\nimport type {\n CalculatorInput,\n CalculatorOptions,\n CalculatorResult,\n} from \"../contracts/utility.type\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"calculator\";\n\n/** Input schema for the `calculator` tool: a single `expression` string. */\nconst inputSchema = objectSchema<CalculatorInput>({\n expression: stringField(),\n});\n\n/**\n * A binary operator the evaluator understands, with its precedence and\n * associativity. Higher `precedence` binds tighter; `^` is the only\n * right-associative operator (so `2 ^ 3 ^ 2` is `2 ^ (3 ^ 2)`).\n */\ninterface OperatorSpec {\n precedence: number;\n associativity: \"left\" | \"right\";\n /** Apply the operator to its two operands. May throw {@link CalculatorError}. */\n apply(left: number, right: number): number;\n}\n\n/** The closed set of supported binary operators. */\nconst OPERATORS: Record<string, OperatorSpec> = {\n \"+\": { precedence: 1, associativity: \"left\", apply: (a, b) => a + b },\n \"-\": { precedence: 1, associativity: \"left\", apply: (a, b) => a - b },\n \"*\": { precedence: 2, associativity: \"left\", apply: (a, b) => a * b },\n \"/\": {\n precedence: 2,\n associativity: \"left\",\n apply: (a, b) => {\n if (b === 0) {\n throw new CalculatorError(\"Division by zero.\", { type: \"divide-by-zero\" });\n }\n\n return a / b;\n },\n },\n \"%\": {\n precedence: 2,\n associativity: \"left\",\n apply: (a, b) => {\n if (b === 0) {\n throw new CalculatorError(\"Modulo by zero.\", { type: \"divide-by-zero\" });\n }\n\n return a % b;\n },\n },\n \"^\": { precedence: 3, associativity: \"right\", apply: (a, b) => a ** b },\n};\n\n/** A lexed token: a number literal, an operator, or a parenthesis. */\ntype Token =\n | { type: \"number\"; value: number }\n | { type: \"operator\"; value: string }\n | { type: \"paren\"; value: \"(\" | \")\" };\n\n/**\n * Tokenize an arithmetic expression into {@link Token}s. Recognizes\n * decimal and scientific-notation numbers (`3`, `4.5`, `1e3`, `2.5E-2`),\n * the operators in {@link OPERATORS}, and parentheses; whitespace is\n * skipped. Any other character is a syntax error — there is no path to\n * an identifier, function call, or property access, so nothing\n * code-like can be smuggled in.\n *\n * @throws CalculatorError `type: \"syntax\"` on an unrecognized character\n * or a malformed number.\n */\nfunction tokenize(expression: string): Token[] {\n const tokens: Token[] = [];\n let index = 0;\n\n while (index < expression.length) {\n const char = expression[index];\n\n if (char === \" \" || char === \"\\t\" || char === \"\\n\" || char === \"\\r\") {\n index += 1;\n\n continue;\n }\n\n if (char === \"(\" || char === \")\") {\n tokens.push({ type: \"paren\", value: char });\n index += 1;\n\n continue;\n }\n\n if (char in OPERATORS) {\n tokens.push({ type: \"operator\", value: char });\n index += 1;\n\n continue;\n }\n\n if (isDigit(char) || char === \".\") {\n const { value, nextIndex } = readNumber(expression, index);\n tokens.push({ type: \"number\", value });\n index = nextIndex;\n\n continue;\n }\n\n throw new CalculatorError(\n `Unexpected character \"${char}\" at position ${index}. Only numbers, parentheses, and the operators + - * / % ^ are allowed.`,\n { type: \"syntax\" },\n );\n }\n\n return tokens;\n}\n\n/** True for an ASCII digit `0`–`9`. */\nfunction isDigit(char: string): boolean {\n return char >= \"0\" && char <= \"9\";\n}\n\n/**\n * Read a single number literal starting at `start`. Consumes an optional\n * integer part, optional fraction, and optional exponent\n * (`e`/`E` with an optional sign). Returns the parsed value and the index\n * just past the literal.\n *\n * @throws CalculatorError `type: \"syntax\"` if the consumed run is not a\n * valid finite number (e.g. a lone `.` or `1e` with no exponent).\n */\nfunction readNumber(\n expression: string,\n start: number,\n): { value: number; nextIndex: number } {\n let index = start;\n\n while (index < expression.length && isDigit(expression[index])) {\n index += 1;\n }\n\n if (expression[index] === \".\") {\n index += 1;\n\n while (index < expression.length && isDigit(expression[index])) {\n index += 1;\n }\n }\n\n if (expression[index] === \"e\" || expression[index] === \"E\") {\n index += 1;\n\n if (expression[index] === \"+\" || expression[index] === \"-\") {\n index += 1;\n }\n\n while (index < expression.length && isDigit(expression[index])) {\n index += 1;\n }\n }\n\n const literal = expression.slice(start, index);\n const value = Number(literal);\n\n if (!Number.isFinite(value)) {\n throw new CalculatorError(`Invalid number literal \"${literal}\".`, {\n type: \"syntax\",\n });\n }\n\n return { value, nextIndex: index };\n}\n\n/**\n * Evaluate a token stream with a single left-to-right pass that resolves\n * unary signs, then a shunting-yard conversion that interleaves operator\n * application — so the result is produced without ever building an AST or\n * calling `eval`/`Function`.\n *\n * Unary `+`/`-` are detected positionally: a `+`/`-` is unary when it\n * starts the expression or directly follows another operator or an\n * opening paren. A unary `-` folds into the following number literal\n * (and a unary `+` is a no-op), which keeps the operator stack purely\n * binary.\n *\n * @throws CalculatorError on malformed structure (`type: \"syntax\"`),\n * division/modulo by zero (`type: \"divide-by-zero\"`), or a non-finite\n * result (`type: \"overflow\"`).\n */\nfunction evaluate(tokens: Token[]): number {\n const values: number[] = [];\n const operators: string[] = [];\n\n /** Pop the top operator and apply it to the top two values. */\n const applyTop = (): void => {\n const operator = operators.pop();\n\n if (operator === undefined) {\n throw new CalculatorError(\"Malformed expression.\", { type: \"syntax\" });\n }\n\n const right = values.pop();\n const left = values.pop();\n\n if (left === undefined || right === undefined) {\n throw new CalculatorError(\n `Operator \"${operator}\" is missing an operand.`,\n { type: \"syntax\" },\n );\n }\n\n values.push(OPERATORS[operator].apply(left, right));\n };\n\n // `expectOperand` tracks the positional state machine: at the start, and\n // immediately after an operator or \"(\", we expect an operand (a number,\n // a \"(\", or a unary sign). After a number or \")\", we expect a binary\n // operator or \")\".\n let expectOperand = true;\n // Pending unary sign multiplier folded into the next numeric operand.\n let pendingSign = 1;\n\n for (let i = 0; i < tokens.length; i += 1) {\n const token = tokens[i];\n\n if (token.type === \"number\") {\n if (!expectOperand) {\n throw new CalculatorError(\n \"Unexpected number — a number cannot directly follow another value.\",\n { type: \"syntax\" },\n );\n }\n\n values.push(pendingSign * token.value);\n pendingSign = 1;\n expectOperand = false;\n\n continue;\n }\n\n if (token.type === \"paren\") {\n if (token.value === \"(\") {\n if (!expectOperand) {\n throw new CalculatorError(\n 'Unexpected \"(\" — it cannot directly follow a value.',\n { type: \"syntax\" },\n );\n }\n\n // A unary sign in front of a parenthesized group is modeled by\n // pushing the sign as a multiplication: `-(…)` becomes `-1 * (…)`.\n if (pendingSign === -1) {\n values.push(-1);\n operators.push(\"*\");\n pendingSign = 1;\n }\n\n operators.push(\"(\");\n expectOperand = true;\n\n continue;\n }\n\n // token.value === \")\"\n if (expectOperand) {\n throw new CalculatorError(\n 'Unexpected \")\" — an operand was expected.',\n { type: \"syntax\" },\n );\n }\n\n let foundOpen = false;\n\n while (operators.length > 0) {\n if (operators[operators.length - 1] === \"(\") {\n operators.pop();\n foundOpen = true;\n\n break;\n }\n\n applyTop();\n }\n\n if (!foundOpen) {\n throw new CalculatorError(\"Unbalanced parentheses.\", { type: \"syntax\" });\n }\n\n expectOperand = false;\n\n continue;\n }\n\n // token.type === \"operator\"\n if (expectOperand) {\n // A `+`/`-` in operand position is a unary sign; anything else is a\n // misplaced binary operator.\n if (token.value === \"-\") {\n pendingSign = -pendingSign;\n\n continue;\n }\n\n if (token.value === \"+\") {\n continue;\n }\n\n throw new CalculatorError(\n `Operator \"${token.value}\" has no left-hand operand.`,\n { type: \"syntax\" },\n );\n }\n\n const incoming = OPERATORS[token.value];\n\n while (operators.length > 0) {\n const top = operators[operators.length - 1];\n\n if (top === \"(\") {\n break;\n }\n\n const topSpec = OPERATORS[top];\n const higher = topSpec.precedence > incoming.precedence;\n const equalLeft =\n topSpec.precedence === incoming.precedence &&\n incoming.associativity === \"left\";\n\n if (higher || equalLeft) {\n applyTop();\n\n continue;\n }\n\n break;\n }\n\n operators.push(token.value);\n expectOperand = true;\n }\n\n if (expectOperand) {\n throw new CalculatorError(\n \"Expression ends with an operator or is empty.\",\n { type: \"syntax\" },\n );\n }\n\n while (operators.length > 0) {\n if (operators[operators.length - 1] === \"(\") {\n throw new CalculatorError(\"Unbalanced parentheses.\", { type: \"syntax\" });\n }\n\n applyTop();\n }\n\n const result = values.pop();\n\n if (result === undefined || values.length > 0) {\n throw new CalculatorError(\"Malformed expression.\", { type: \"syntax\" });\n }\n\n if (!Number.isFinite(result)) {\n throw new CalculatorError(\"Result is not a finite number.\", {\n type: \"overflow\",\n });\n }\n\n return result;\n}\n\n/**\n * Build the `calculator` tool — a SAFE arithmetic evaluator the agent can\n * call to compute a numeric expression. It supports `+ - * / % ^`, unary\n * signs, parentheses, and decimal/scientific-notation literals, with the\n * usual precedence (`^` highest and right-associative, then `* / %`, then\n * `+ -`).\n *\n * **Safety.** The expression is tokenized and evaluated with a\n * shunting-yard pass — it NEVER calls `eval` or `new Function`. The\n * lexer only recognizes numbers, parentheses, and the fixed operator set,\n * so there is no path to an identifier, function call, or property\n * access; any other character is a syntax error returned as data.\n *\n * **Errors flow as data.** A malformed expression, division/modulo by\n * zero, or a non-finite result throws a {@link CalculatorError} inside\n * the handler; `tool()` catches it and surfaces it in the returned\n * `{ error }` field (the LLM-visible message is preserved), so the agent\n * reads the failure and self-corrects instead of crashing.\n *\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n * @returns A `ToolContract<{ expression }, { result }>` ready to drop into `tools: []`.\n *\n * @example\n * const calc = calculatorTool();\n * const { data } = await calc.invoke({ expression: \"(3 + 4) * 2\" });\n * console.log(data?.result); // 14\n */\nexport function calculatorTool(\n options?: CalculatorOptions,\n): ToolContract<CalculatorInput, CalculatorResult> {\n return tool<CalculatorInput, CalculatorResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Evaluate an arithmetic expression and return the numeric result. \" +\n \"Supports + - * / % ^, parentheses, unary +/-, and decimal or \" +\n \"scientific-notation numbers. Does not support variables or \" +\n \"functions — pass a literal expression like \\\"(3 + 4) * 2\\\".\",\n input: inputSchema,\n async execute(input) {\n const tokens = tokenize(input.expression);\n\n if (tokens.length === 0) {\n throw new CalculatorError(\"Expression is empty.\", { type: \"syntax\" });\n }\n\n return { result: evaluate(tokens) };\n },\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"date-time.mjs","names":[],"sources":["../../../../../../../ai-tools/src/utility/date-time.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { DateTimeError } from \"../errors\";\nimport { literalField, objectSchema, optionalNumberField, optionalStringField } from \"./schema\";\nimport type {\n DateTimeInput,\n DateTimeOp,\n DateTimeOptions,\n DateTimeResult,\n} from \"../contracts/utility.type\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"date_time\";\n\n/** The closed set of operations the tool implements. */\nconst OPS: readonly DateTimeOp[] = [\"now\", \"add\", \"diff\", \"format\"];\n\n/**\n * A clock seam so tests are deterministic. Defaults to the real\n * {@link Date}; a test injects a fixed instant. Returns milliseconds\n * since the Unix epoch — the same contract as `Date.now()`.\n */\nexport type Clock = () => number;\n\n/** Construction dependencies for {@link dateTimeTool}, all optional. */\nexport interface DateTimeToolDeps {\n /**\n * The clock used by the `now` op. Defaults to `Date.now`. Injected in\n * tests for deterministic output.\n */\n clock?: Clock;\n}\n\n/** Input schema for the `date_time` tool. */\nconst inputSchema = objectSchema<DateTimeInput>({\n op: literalField<DateTimeOp>(OPS),\n iso: optionalStringField(),\n from: optionalStringField(),\n to: optionalStringField(),\n amount: optionalNumberField(),\n unit: optionalStringField(),\n timeZone: optionalStringField(),\n format: optionalStringField(),\n});\n\n/**\n * The time units `add` and `diff` understand, each as its length in\n * milliseconds. Calendar-unaware on purpose: a \"month\" / \"year\" has no\n * fixed millisecond length, so they are deliberately excluded — adding\n * `30 days` is unambiguous in a way adding `1 month` is not.\n */\nconst UNIT_MS: Record<string, number> = {\n milliseconds: 1,\n seconds: 1_000,\n minutes: 60_000,\n hours: 3_600_000,\n days: 86_400_000,\n weeks: 604_800_000,\n};\n\n/** Singular aliases mapped to their canonical plural unit key. */\nconst UNIT_ALIASES: Record<string, string> = {\n millisecond: \"milliseconds\",\n ms: \"milliseconds\",\n second: \"seconds\",\n sec: \"seconds\",\n s: \"seconds\",\n minute: \"minutes\",\n min: \"minutes\",\n m: \"minutes\",\n hour: \"hours\",\n hr: \"hours\",\n h: \"hours\",\n day: \"days\",\n d: \"days\",\n week: \"weeks\",\n w: \"weeks\",\n};\n\n/** Resolve a (possibly aliased / singular) unit token to its ms length. */\nfunction unitToMs(unit: string): number {\n const canonical = UNIT_ALIASES[unit] ?? unit;\n const ms = UNIT_MS[canonical];\n\n if (ms === undefined) {\n throw new DateTimeError(\n `Unknown unit \"${unit}\". Supported units: ${Object.keys(UNIT_MS).join(\", \")}.`,\n { type: \"invalid-unit\" },\n );\n }\n\n return ms;\n}\n\n/**\n * Parse an ISO-8601 instant into a {@link Date}, or throw a typed\n * {@link DateTimeError} when the string is missing or unparseable.\n *\n * @param iso - The ISO string from the model (may be undefined).\n * @param field - The input field name, for the error message.\n */\nfunction parseIso(iso: string | undefined, field: string): Date {\n if (iso === undefined) {\n throw new DateTimeError(`\"${field}\" is required for this operation.`, {\n type: \"invalid-input\",\n });\n }\n\n const date = new Date(iso);\n\n if (Number.isNaN(date.getTime())) {\n throw new DateTimeError(`\"${field}\" is not a valid ISO-8601 instant: \"${iso}\".`, {\n type: \"invalid-input\",\n });\n }\n\n return date;\n}\n\n/**\n * Render a {@link Date} in a target time zone using `Intl`. The\n * `\"iso\"` format (the default) returns the instant's UTC ISO string;\n * any other `format` value is treated as an `Intl.DateTimeFormat`\n * locale-style rendering in the given `timeZone`.\n *\n * @throws DateTimeError `type: \"invalid-time-zone\"` when `timeZone` is\n * not a recognized IANA zone.\n */\nfunction render(date: Date, format: string | undefined, timeZone: string | undefined): string {\n if (format === undefined || format === \"iso\") {\n // `timeZone` is irrelevant to a UTC ISO string, but validate it when\n // supplied so a bad zone is reported rather than silently ignored.\n if (timeZone !== undefined) {\n assertTimeZone(timeZone);\n }\n\n return date.toISOString();\n }\n\n try {\n return new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n dateStyle: format === \"date\" ? \"medium\" : undefined,\n timeStyle: format === \"time\" ? \"medium\" : undefined,\n ...(format === \"datetime\" ? { dateStyle: \"medium\", timeStyle: \"medium\" } : {}),\n }).format(date);\n } catch (error) {\n throw new DateTimeError(\n `Could not render with format \"${format}\"${\n timeZone ? ` in time zone \"${timeZone}\"` : \"\"\n }.`,\n { type: \"invalid-time-zone\", cause: error },\n );\n }\n}\n\n/**\n * Validate an IANA time zone by attempting to construct a formatter for\n * it; an unrecognized zone makes `Intl` throw a `RangeError`.\n *\n * @throws DateTimeError `type: \"invalid-time-zone\"` for an unknown zone.\n */\nfunction assertTimeZone(timeZone: string): void {\n try {\n new Intl.DateTimeFormat(\"en-US\", { timeZone });\n } catch (error) {\n throw new DateTimeError(`Unknown time zone \"${timeZone}\".`, {\n type: \"invalid-time-zone\",\n cause: error,\n });\n }\n}\n\n/**\n * Build the `date_time` tool — a small clock/calendar utility the agent\n * can call to read the current instant, shift an instant, measure the\n * gap between two instants, or render one. The `op` discriminator selects\n * the operation:\n *\n * - **`now`** — the current instant, rendered per `format` / `timeZone`\n * (defaults to a UTC ISO string). Reads the injectable {@link Clock}.\n * - **`add`** — `iso` shifted by `amount` of `unit` (e.g.\n * `+3 days`); a negative `amount` shifts backward.\n * - **`diff`** — the signed difference `to − iso`, expressed in `unit`.\n * - **`format`** — `iso` rendered per `format` / `timeZone`.\n *\n * Units are millisecond-based (`milliseconds`…`weeks`, plus common\n * aliases); calendar-relative `month`/`year` are intentionally\n * unsupported because they have no fixed length.\n *\n * **Deterministic in tests.** The clock backing `now` is injectable via\n * `deps.clock`; production defaults to `Date.now`.\n *\n * **Errors flow as data.** A missing/invalid field, an unknown unit, or\n * an unrecognized time zone throws a {@link DateTimeError} inside the\n * handler; `tool()` surfaces it in `{ error }` so the agent self-corrects.\n *\n * @param options - Optional overrides; `name` renames the tool,\n * `defaultTimeZone` applies when a call omits `timeZone`.\n * @param deps - Injectable dependencies (the {@link Clock}); defaults to real time.\n * @returns A `ToolContract<DateTimeInput, { value }>` ready for `tools: []`.\n *\n * @example\n * const clock = () => Date.parse(\"2026-06-22T00:00:00Z\");\n * const dt = dateTimeTool({}, { clock });\n * const { data } = await dt.invoke({ op: \"now\" });\n * console.log(data?.value); // \"2026-06-22T00:00:00.000Z\"\n */\nexport function dateTimeTool(\n options?: DateTimeOptions,\n deps?: DateTimeToolDeps,\n): ToolContract<DateTimeInput, DateTimeResult> {\n const clock: Clock = deps?.clock ?? Date.now;\n const defaultTimeZone = options?.defaultTimeZone;\n\n return tool<DateTimeInput, DateTimeResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Work with dates and times. Set op to: \\\"now\\\" (current instant), \" +\n '\"add\" (shift iso by amount of unit), \"diff\" (signed to − from in unit; ' +\n \"pass the start instant as iso or from), \" +\n 'or \"format\" (render iso). Units are milliseconds, seconds, minutes, ' +\n \"hours, days, or weeks (month/year are not supported). Pass instants as \" +\n 'ISO-8601 strings; set format to \"iso\" (default), \"date\", \"time\", or ' +\n '\"datetime\", and timeZone as an IANA zone like \"Africa/Cairo\".',\n input: inputSchema,\n async execute(input) {\n const timeZone = input.timeZone ?? defaultTimeZone;\n\n switch (input.op) {\n case \"now\": {\n const date = new Date(clock());\n\n return { value: render(date, input.format, timeZone) };\n }\n\n case \"format\": {\n const date = parseIso(input.iso, \"iso\");\n\n return { value: render(date, input.format, timeZone) };\n }\n\n case \"add\": {\n const date = parseIso(input.iso, \"iso\");\n\n if (input.amount === undefined) {\n throw new DateTimeError('\"amount\" is required for the \"add\" operation.', {\n type: \"invalid-input\",\n });\n }\n\n if (input.unit === undefined) {\n throw new DateTimeError('\"unit\" is required for the \"add\" operation.', {\n type: \"invalid-input\",\n });\n }\n\n const shifted = new Date(date.getTime() + input.amount * unitToMs(input.unit));\n\n return { value: render(shifted, input.format, timeZone) };\n }\n\n case \"diff\": {\n // Accept `from` as an alias for `iso` — models naturally pass\n // `from` / `to` for a difference. `iso` wins when both are set.\n const from = parseIso(input.iso ?? input.from, \"iso (or from)\");\n const to = parseIso(input.to, \"to\");\n\n if (input.unit === undefined) {\n throw new DateTimeError('\"unit\" is required for the \"diff\" operation.', {\n type: \"invalid-input\",\n });\n }\n\n const diff = (to.getTime() - from.getTime()) / unitToMs(input.unit);\n\n return { value: String(diff) };\n }\n\n default: {\n // Exhaustiveness guard — the schema's literal union should make\n // this unreachable, but a future op added to the union without a\n // case here surfaces as typed data rather than silent fallthrough.\n const unreachable: never = input.op;\n\n throw new DateTimeError(`Unsupported operation \"${String(unreachable)}\".`, {\n type: \"unsupported-op\",\n });\n }\n }\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"date-time.mjs","names":[],"sources":["../../../../../../../ai-tools/src/utility/date-time.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { DateTimeError } from \"../errors\";\nimport { literalField, objectSchema, optionalNumberField, optionalStringField } from \"./schema\";\nimport type {\n DateTimeInput,\n DateTimeOp,\n DateTimeOptions,\n DateTimeResult,\n} from \"../contracts/utility.type\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"date_time\";\n\n/** The closed set of operations the tool implements. */\nconst OPS: readonly DateTimeOp[] = [\"now\", \"add\", \"diff\", \"format\"];\n\n/**\n * A clock seam so tests are deterministic. Defaults to the real\n * {@link Date}; a test injects a fixed instant. Returns milliseconds\n * since the Unix epoch — the same contract as `Date.now()`.\n */\nexport type Clock = () => number;\n\n/** Construction dependencies for {@link dateTimeTool}, all optional. */\nexport interface DateTimeToolDeps {\n /**\n * The clock used by the `now` op. Defaults to `Date.now`. Injected in\n * tests for deterministic output.\n */\n clock?: Clock;\n}\n\n/** Input schema for the `date_time` tool. */\nconst inputSchema = objectSchema<DateTimeInput>({\n op: literalField<DateTimeOp>(OPS),\n iso: optionalStringField(),\n from: optionalStringField(),\n to: optionalStringField(),\n amount: optionalNumberField(),\n unit: optionalStringField(),\n timeZone: optionalStringField(),\n format: optionalStringField(),\n});\n\n/**\n * The time units `add` and `diff` understand, each as its length in\n * milliseconds. Calendar-unaware on purpose: a \"month\" / \"year\" has no\n * fixed millisecond length, so they are deliberately excluded — adding\n * `30 days` is unambiguous in a way adding `1 month` is not.\n */\nconst UNIT_MS: Record<string, number> = {\n milliseconds: 1,\n seconds: 1_000,\n minutes: 60_000,\n hours: 3_600_000,\n days: 86_400_000,\n weeks: 604_800_000,\n};\n\n/** Singular aliases mapped to their canonical plural unit key. */\nconst UNIT_ALIASES: Record<string, string> = {\n millisecond: \"milliseconds\",\n ms: \"milliseconds\",\n second: \"seconds\",\n sec: \"seconds\",\n s: \"seconds\",\n minute: \"minutes\",\n min: \"minutes\",\n m: \"minutes\",\n hour: \"hours\",\n hr: \"hours\",\n h: \"hours\",\n day: \"days\",\n d: \"days\",\n week: \"weeks\",\n w: \"weeks\",\n};\n\n/** Resolve a (possibly aliased / singular) unit token to its ms length. */\nfunction unitToMs(unit: string): number {\n const canonical = UNIT_ALIASES[unit] ?? unit;\n const ms = UNIT_MS[canonical];\n\n if (ms === undefined) {\n throw new DateTimeError(\n `Unknown unit \"${unit}\". Supported units: ${Object.keys(UNIT_MS).join(\", \")}.`,\n { type: \"invalid-unit\" },\n );\n }\n\n return ms;\n}\n\n/**\n * Parse an ISO-8601 instant into a {@link Date}, or throw a typed\n * {@link DateTimeError} when the string is missing or unparseable.\n *\n * @param iso - The ISO string from the model (may be undefined).\n * @param field - The input field name, for the error message.\n */\nfunction parseIso(iso: string | undefined, field: string): Date {\n if (iso === undefined) {\n throw new DateTimeError(`\"${field}\" is required for this operation.`, {\n type: \"invalid-input\",\n });\n }\n\n const date = new Date(iso);\n\n if (Number.isNaN(date.getTime())) {\n throw new DateTimeError(`\"${field}\" is not a valid ISO-8601 instant: \"${iso}\".`, {\n type: \"invalid-input\",\n });\n }\n\n return date;\n}\n\n/**\n * Render a {@link Date} in a target time zone using `Intl`. The\n * `\"iso\"` format (the default) returns the instant's UTC ISO string;\n * any other `format` value is treated as an `Intl.DateTimeFormat`\n * locale-style rendering in the given `timeZone`.\n *\n * @throws DateTimeError `type: \"invalid-time-zone\"` when `timeZone` is\n * not a recognized IANA zone.\n */\nfunction render(date: Date, format: string | undefined, timeZone: string | undefined): string {\n if (format === undefined || format === \"iso\") {\n // `timeZone` is irrelevant to a UTC ISO string, but validate it when\n // supplied so a bad zone is reported rather than silently ignored.\n if (timeZone !== undefined) {\n assertTimeZone(timeZone);\n }\n\n return date.toISOString();\n }\n\n try {\n return new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n dateStyle: format === \"date\" ? \"medium\" : undefined,\n timeStyle: format === \"time\" ? \"medium\" : undefined,\n ...(format === \"datetime\" ? { dateStyle: \"medium\", timeStyle: \"medium\" } : {}),\n }).format(date);\n } catch (error) {\n throw new DateTimeError(\n `Could not render with format \"${format}\"${\n timeZone ? ` in time zone \"${timeZone}\"` : \"\"\n }.`,\n { type: \"invalid-time-zone\", cause: error },\n );\n }\n}\n\n/**\n * Validate an IANA time zone by attempting to construct a formatter for\n * it; an unrecognized zone makes `Intl` throw a `RangeError`.\n *\n * @throws DateTimeError `type: \"invalid-time-zone\"` for an unknown zone.\n */\nfunction assertTimeZone(timeZone: string): void {\n try {\n new Intl.DateTimeFormat(\"en-US\", { timeZone });\n } catch (error) {\n throw new DateTimeError(`Unknown time zone \"${timeZone}\".`, {\n type: \"invalid-time-zone\",\n cause: error,\n });\n }\n}\n\n/**\n * Build the `date_time` tool — a small clock/calendar utility the agent\n * can call to read the current instant, shift an instant, measure the\n * gap between two instants, or render one. The `op` discriminator selects\n * the operation:\n *\n * - **`now`** — the current instant, rendered per `format` / `timeZone`\n * (defaults to a UTC ISO string). Reads the injectable {@link Clock}.\n * - **`add`** — `iso` shifted by `amount` of `unit` (e.g.\n * `+3 days`); a negative `amount` shifts backward.\n * - **`diff`** — the signed difference `to − iso`, expressed in `unit`.\n * - **`format`** — `iso` rendered per `format` / `timeZone`.\n *\n * Units are millisecond-based (`milliseconds`…`weeks`, plus common\n * aliases); calendar-relative `month`/`year` are intentionally\n * unsupported because they have no fixed length.\n *\n * **Deterministic in tests.** The clock backing `now` is injectable via\n * `deps.clock`; production defaults to `Date.now`.\n *\n * **Errors flow as data.** A missing/invalid field, an unknown unit, or\n * an unrecognized time zone throws a {@link DateTimeError} inside the\n * handler; `tool()` surfaces it in `{ error }` so the agent self-corrects.\n *\n * @param options - Optional overrides; `name` renames the tool,\n * `defaultTimeZone` applies when a call omits `timeZone`.\n * @param deps - Injectable dependencies (the {@link Clock}); defaults to real time.\n * @returns A `ToolContract<DateTimeInput, { value }>` ready for `tools: []`.\n *\n * @example\n * const clock = () => Date.parse(\"2026-06-22T00:00:00Z\");\n * const dt = dateTimeTool({}, { clock });\n * const { data } = await dt.invoke({ op: \"now\" });\n * console.log(data?.value); // \"2026-06-22T00:00:00.000Z\"\n */\nexport function dateTimeTool(\n options?: DateTimeOptions,\n deps?: DateTimeToolDeps,\n): ToolContract<DateTimeInput, DateTimeResult> {\n const clock: Clock = deps?.clock ?? Date.now;\n const defaultTimeZone = options?.defaultTimeZone;\n\n return tool<DateTimeInput, DateTimeResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Work with dates and times. Set op to: \\\"now\\\" (current instant), \" +\n '\"add\" (shift iso by amount of unit), \"diff\" (signed to − from in unit; ' +\n \"pass the start instant as iso or from), \" +\n 'or \"format\" (render iso). Units are milliseconds, seconds, minutes, ' +\n \"hours, days, or weeks (month/year are not supported). Pass instants as \" +\n 'ISO-8601 strings; set format to \"iso\" (default), \"date\", \"time\", or ' +\n '\"datetime\", and timeZone as an IANA zone like \"Africa/Cairo\".',\n input: inputSchema,\n async execute(input) {\n const timeZone = input.timeZone ?? defaultTimeZone;\n\n switch (input.op) {\n case \"now\": {\n const date = new Date(clock());\n\n return { value: render(date, input.format, timeZone) };\n }\n\n case \"format\": {\n const date = parseIso(input.iso, \"iso\");\n\n return { value: render(date, input.format, timeZone) };\n }\n\n case \"add\": {\n const date = parseIso(input.iso, \"iso\");\n\n if (input.amount === undefined) {\n throw new DateTimeError('\"amount\" is required for the \"add\" operation.', {\n type: \"invalid-input\",\n });\n }\n\n if (input.unit === undefined) {\n throw new DateTimeError('\"unit\" is required for the \"add\" operation.', {\n type: \"invalid-input\",\n });\n }\n\n const shifted = new Date(date.getTime() + input.amount * unitToMs(input.unit));\n\n return { value: render(shifted, input.format, timeZone) };\n }\n\n case \"diff\": {\n // Accept `from` as an alias for `iso` — models naturally pass\n // `from` / `to` for a difference. `iso` wins when both are set.\n const from = parseIso(input.iso ?? input.from, \"iso (or from)\");\n const to = parseIso(input.to, \"to\");\n\n if (input.unit === undefined) {\n throw new DateTimeError('\"unit\" is required for the \"diff\" operation.', {\n type: \"invalid-input\",\n });\n }\n\n const diff = (to.getTime() - from.getTime()) / unitToMs(input.unit);\n\n return { value: String(diff) };\n }\n\n default: {\n // Exhaustiveness guard — the schema's literal union should make\n // this unreachable, but a future op added to the union without a\n // case here surfaces as typed data rather than silent fallthrough.\n const unreachable: never = input.op;\n\n throw new DateTimeError(`Unsupported operation \"${String(unreachable)}\".`, {\n type: \"unsupported-op\",\n });\n }\n }\n },\n });\n}\n"],"mappings":"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"}