@homeflare/seat-runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/index.d.ts +20 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +508 -0
  6. package/dist/index.js.map +21 -0
  7. package/dist/mcp-connect.d.ts +72 -0
  8. package/dist/mcp-connect.d.ts.map +1 -0
  9. package/dist/mcp-error.d.ts +77 -0
  10. package/dist/mcp-error.d.ts.map +1 -0
  11. package/dist/mcp-pages.d.ts +18 -0
  12. package/dist/mcp-pages.d.ts.map +1 -0
  13. package/dist/mcp-render.d.ts +25 -0
  14. package/dist/mcp-render.d.ts.map +1 -0
  15. package/dist/mcp-tool.d.ts +61 -0
  16. package/dist/mcp-tool.d.ts.map +1 -0
  17. package/dist/mcp-toolkit.d.ts +61 -0
  18. package/dist/mcp-toolkit.d.ts.map +1 -0
  19. package/dist/mcp-toolset.d.ts +33 -0
  20. package/dist/mcp-toolset.d.ts.map +1 -0
  21. package/dist/rounds.d.ts +108 -0
  22. package/dist/rounds.d.ts.map +1 -0
  23. package/dist/seat-model.d.ts +59 -0
  24. package/dist/seat-model.d.ts.map +1 -0
  25. package/dist/seat-obs.d.ts +23 -0
  26. package/dist/seat-obs.d.ts.map +1 -0
  27. package/dist/seat-state.d.ts +33 -0
  28. package/dist/seat-state.d.ts.map +1 -0
  29. package/dist/stamp.d.ts +23 -0
  30. package/dist/stamp.d.ts.map +1 -0
  31. package/dist/state-dsn.d.ts +41 -0
  32. package/dist/state-dsn.d.ts.map +1 -0
  33. package/dist/state-postgres.d.ts +58 -0
  34. package/dist/state-postgres.d.ts.map +1 -0
  35. package/dist/state-valkey-connection.d.ts +49 -0
  36. package/dist/state-valkey-connection.d.ts.map +1 -0
  37. package/dist/state-valkey-scrub.d.ts +37 -0
  38. package/dist/state-valkey-scrub.d.ts.map +1 -0
  39. package/dist/state-valkey-send.d.ts +35 -0
  40. package/dist/state-valkey-send.d.ts.map +1 -0
  41. package/dist/state-valkey.d.ts +83 -0
  42. package/dist/state-valkey.d.ts.map +1 -0
  43. package/dist/state.d.ts +9 -0
  44. package/dist/state.d.ts.map +1 -0
  45. package/dist/state.js +327 -0
  46. package/dist/state.js.map +16 -0
  47. package/dist/version.d.ts +2 -0
  48. package/dist/version.d.ts.map +1 -0
  49. package/docs/mcp.md +40 -0
  50. package/docs/pairing.md +39 -0
  51. package/docs/state.md +174 -0
  52. package/package.json +45 -0
  53. package/src/index.ts +25 -0
  54. package/src/mcp-connect.ts +172 -0
  55. package/src/mcp-error.ts +150 -0
  56. package/src/mcp-pages.ts +37 -0
  57. package/src/mcp-render.ts +67 -0
  58. package/src/mcp-tool.ts +101 -0
  59. package/src/mcp-toolkit.ts +183 -0
  60. package/src/mcp-toolset.ts +91 -0
  61. package/src/rounds.ts +211 -0
  62. package/src/seat-model.ts +94 -0
  63. package/src/seat-obs.ts +103 -0
  64. package/src/seat-state.ts +53 -0
  65. package/src/stamp.ts +88 -0
  66. package/src/state-dsn.ts +112 -0
  67. package/src/state-postgres.ts +114 -0
  68. package/src/state-valkey-connection.ts +113 -0
  69. package/src/state-valkey-scrub.ts +60 -0
  70. package/src/state-valkey-send.ts +67 -0
  71. package/src/state-valkey.ts +193 -0
  72. package/src/state.ts +8 -0
  73. package/src/version.ts +2 -0
@@ -0,0 +1,21 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/seat-model.ts", "../src/stamp.ts", "../src/seat-obs.ts", "../src/rounds.ts", "../src/mcp-toolkit.ts", "../src/mcp-connect.ts", "../src/mcp-error.ts", "../src/version.ts", "../src/mcp-pages.ts", "../src/mcp-toolset.ts", "../src/mcp-render.ts", "../src/mcp-tool.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * LiteLLM as an Effect `LanguageModel` and `EmbeddingModel`, on the chat-completions wire.\n *\n * ★ ONE CLIENT SHAPE FOR EVERY SEAT. `@effect/ai-openai-compat` builds the HTTP client;\n * this file adds only what LiteLLM needs to attribute and control a seat's calls (tags,\n * the response cache, retries, metadata — see stamp.ts) and pins `strictJsonSchema: false`.\n * ⛔ THE COMPAT PACKAGE POSTS `/chat/completions` AND `/embeddings`, NOT `/responses`. The\n * responses wire fails on cf-code with `missing field sequence_number` (landscape PR 165),\n * so nothing here points at it.\n */\nimport { OpenAiClient, OpenAiEmbeddingModel, OpenAiLanguageModel } from '@effect/ai-openai-compat';\nimport * as Layer from 'effect/Layer';\nimport * as Redacted from 'effect/Redacted';\nimport * as EmbeddingModel from 'effect/unstable/ai/EmbeddingModel';\nimport type * as LanguageModel from 'effect/unstable/ai/LanguageModel';\nimport * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';\nimport * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { joinTags, stampRequest } from './stamp.ts';\n\nexport type SeatClientOptions = {\n /** LiteLLM's OpenAI-compatible base, e.g. `http://127.0.0.1:4100/v1`. Required: no host default. */\n readonly apiUrl: string;\n /** A per-seat LiteLLM virtual key. Held `Redacted`; this package never logs or reads it from disk. */\n readonly apiKey: string | Redacted.Redacted<string>;\n /** Spend-log attribution, e.g. `['host:ct100', 'lane:cfcode', 'seat:cf-coding']`. */\n readonly tags: string | ReadonlyArray<string>;\n /**\n * Skip LiteLLM's response cache, read and write. Defaults to TRUE.\n * ⚠️ LiteLLM caches every completion for every key, so a seat that repeats a call gets the\n * old answer back at a tenth of the latency — an agent loop would replay itself.\n */\n readonly noCache?: boolean | undefined;\n /** Written into the request body only when the body carries none (compat drops it). */\n readonly metadata?: Readonly<Record<string, string>> | undefined;\n};\n\n/** Compat's own per-model config (`temperature`, `max_output_tokens`, custom body keys). */\ntype ModelConfig = NonNullable<Parameters<typeof OpenAiLanguageModel.layer>[0]['config']>;\ntype EmbeddingConfig = NonNullable<Parameters<typeof OpenAiEmbeddingModel.layer>[0]['config']>;\n\nexport type SeatModelOptions = SeatClientOptions & {\n /** The LiteLLM alias, e.g. `cf-code`. */\n readonly model: string;\n readonly config?: ModelConfig | undefined;\n};\n\nexport type SeatEmbeddingOptions = SeatClientOptions & {\n /** The LiteLLM alias, e.g. `embeddings`. */\n readonly model: string;\n /**\n * What `EmbeddingModel.Dimensions` reports to a vector store. NOT sent to LiteLLM.\n * ⚠️ compat's own `OpenAiEmbeddingModel.model(name, { dimensions })` puts the number in the\n * request body too, and a provider that has no `dimensions` parameter (a llama.cpp bge-m3\n * behind LiteLLM) can refuse it. Pass `config: { dimensions }` to send it deliberately.\n */\n readonly dimensions: number;\n readonly config?: EmbeddingConfig | undefined;\n};\n\n/** The client: bearer key, tag header, cache and retry fields, over `fetch`. */\nexport function clientLayer(options: SeatClientOptions): Layer.Layer<OpenAiClient.OpenAiClient> {\n const stamp = {\n tags: joinTags(options.tags),\n noCache: options.noCache ?? true,\n metadata: options.metadata,\n };\n return OpenAiClient.layer({\n apiKey: Redacted.isRedacted(options.apiKey) ? options.apiKey : Redacted.make(options.apiKey),\n apiUrl: options.apiUrl.replace(/\\/$/, ''),\n transformClient: (http) =>\n HttpClient.mapRequest(http, (request) => stampRequest(request, stamp)),\n }).pipe(Layer.provide(FetchHttpClient.layer));\n}\n\n/** `LanguageModel` for one LiteLLM alias. */\nexport function layer(options: SeatModelOptions): Layer.Layer<LanguageModel.LanguageModel> {\n return OpenAiLanguageModel.layer({\n model: options.model,\n // ★ cf-review's default, and measured here 2026-09-29: compat sends `strict: true` on every\n // tool schema unless told otherwise, which the seats' previous client never did; this\n // sends `strict: false`. A caller's `config` wins.\n config: { strictJsonSchema: false, ...options.config },\n }).pipe(Layer.provide(clientLayer(options)));\n}\n\n/** `EmbeddingModel` (and its `Dimensions`) for one LiteLLM alias. */\nexport function embeddingLayer(\n options: SeatEmbeddingOptions,\n): Layer.Layer<EmbeddingModel.EmbeddingModel | EmbeddingModel.Dimensions> {\n return Layer.merge(\n OpenAiEmbeddingModel.layer({ model: options.model, config: options.config }),\n Layer.succeed(EmbeddingModel.Dimensions, options.dimensions),\n ).pipe(Layer.provide(clientLayer(options)));\n}\n",
6
+ "/**\n * The per-request marks a seat puts on every LiteLLM call, applied as an `HttpClient`\n * transform because `@effect/ai-openai-compat` gives no other place to set them.\n *\n * ★ WHY A TRANSFORM AND NOT MODEL CONFIG. The compat mapper forwards unknown config keys\n * into the body but DROPS `metadata` (it is a known Responses-API field with no\n * chat-completions place), and no config key can set a header. Measured in\n * packages/seat-runtime/tests/stamp.test.ts against the installed rc.115.\n * ★ IT IS THE SAME TRANSFORM AS cf-harness (landscape PR 165, cf-harness/src/request.ts),\n * which is where the header and the body fields were measured against LiteLLM.\n */\nimport * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';\n\nexport type SeatStamp = {\n /** Already validated and joined: LiteLLM reads one comma-separated header. */\n readonly tags: string;\n readonly noCache: boolean;\n readonly metadata: Readonly<Record<string, string>> | undefined;\n};\n\n/**\n * ⛔ A TAG IS A TOKEN, NOT A SENTENCE. LiteLLM splits `x-litellm-tags` on commas and this\n * package joins with them, so a comma inside a tag would silently become two tags, and a\n * control character in a header value is header injection. Printable ASCII, no comma, no\n * space; `host:ct100` and `seat:cf-coding` are the shape.\n */\nconst TAG = /^[\\x21-\\x2b\\x2d-\\x7e]{1,128}$/;\n\n/** Validate and join. Throws at layer construction, where the mistake is one line away. */\nexport function joinTags(tags: string | ReadonlyArray<string>): string {\n const list = typeof tags === 'string' ? tags.split(',') : [...tags];\n if (list.length === 0) throw new TypeError('seat tags: at least one tag is required');\n for (const tag of list) {\n if (!TAG.test(tag)) {\n throw new TypeError(\n `seat tags: ${JSON.stringify(tag)} is not a tag (1-128 printable ASCII, no comma or space)`,\n );\n }\n }\n return list.join(',');\n}\n\n/** The JSON body as an object, or undefined for anything this must leave alone. */\nfunction jsonObject(\n request: HttpClientRequest.HttpClientRequest,\n): Record<string, unknown> | undefined {\n const body = request.body;\n if (body._tag !== 'Uint8Array' || !body.contentType.includes('json')) return undefined;\n // ⚠️ `text` is only \"the original text retained for adapters that can skip encoding\"\n // (HttpBody.d.ts): a body built from bytes has none. Decoding the bytes keeps the stamp\n // from becoming a silent no-op, which is the failure that would leave the LiteLLM cache\n // ON for a seat that asked for it off.\n const text = body.text ?? new TextDecoder().decode(body.body);\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return undefined;\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;\n return parsed as Record<string, unknown>;\n}\n\n/** Headers and body fields for one request. Pure: the same request in, a new one out. */\nexport function stampRequest(\n request: HttpClientRequest.HttpClientRequest,\n stamp: SeatStamp,\n): HttpClientRequest.HttpClientRequest {\n const headers = stamp.noCache\n ? { 'x-litellm-tags': stamp.tags, 'cache-control': 'no-cache, no-store' }\n : { 'x-litellm-tags': stamp.tags };\n const tagged = HttpClientRequest.setHeaders(request, headers);\n const body = jsonObject(request);\n if (body === undefined) return tagged;\n return HttpClientRequest.bodyJsonUnsafe(tagged, {\n ...body,\n // ⚠️ THE HEADER ALONE DOES NOT TURN THE CACHE OFF. Measured 2026-09-25 (cf-review PR 22,\n // LiteLLM 1.100.0): `cache: {\"no-cache\": true}` skips the read and `\"no-store\": true`\n // skips the write; `caching: false` in the body is a no-op, and a repeat answered from\n // the cache reads as agreement. The Cache-Control header is sent as well, for the\n // Cloudflare AI Gateway below LiteLLM, and is not what LiteLLM honours.\n ...(stamp.noCache ? { cache: { 'no-cache': true, 'no-store': true } } : {}),\n // ★ ZERO, ALWAYS. A seat retries in Effect, where the attempt is a span; LiteLLM retrying\n // behind it would multiply attempts invisibly (and bill each one).\n num_retries: 0,\n ...(stamp.metadata !== undefined && !('metadata' in body) ? { metadata: stamp.metadata } : {}),\n });\n}\n",
7
+ "/**\n * Traces, logs and metrics from any Effect seat to VictoriaMetrics on CT100, from one\n * environment block.\n *\n * ★ `layerFromConfig`, NOT `Otlp.layer`. The combined layer takes ONE base URL and appends\n * `/v1/traces` and friends, and the three Victoria services each mount OTLP at a different\n * path (measured 2026-09-29, README). The per-signal layers read the per-signal env the\n * Claude Code seats already carry, so one block wires every seat.\n * ⛔ WITH NO ENVIRONMENT `layerFromConfig` EXPORTS NOTHING, SILENTLY. It returns a bare\n * flusher unless `OTEL_<SIGNAL>_EXPORTER` names `otlp` and an endpoint is set. So the CT100\n * defaults below are not a convenience, they are what makes this layer emit at all.\n */\nimport * as Config from 'effect/Config';\nimport * as ConfigProvider from 'effect/ConfigProvider';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Schema from 'effect/Schema';\nimport * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';\nimport {\n type OtlpExporter,\n OtlpLogger,\n OtlpMetrics,\n OtlpSerialization,\n OtlpTracer,\n} from 'effect/unstable/observability';\n\n/**\n * CT100's Victoria services, one path each. Verified 2026-09-29 by GET against the live\n * ports (no payload sent): a mounted path answers, an unmounted sibling answers\n * `unsupported path requested`; the counters carry `format=\"protobuf\"` for traces and logs.\n */\nexport const CT100_ENDPOINTS: {\n readonly traces: string;\n readonly logs: string;\n readonly metrics: string;\n} = {\n traces: 'http://10.100.1.4:10428/insert/opentelemetry/v1/traces',\n logs: 'http://10.100.1.4:9428/insert/opentelemetry/v1/logs',\n metrics: 'http://10.100.1.4:8428/opentelemetry/v1/metrics',\n};\n\n/**\n * Shown when a process names itself neither with `OTEL_SERVICE_NAME` nor with a `service.name`\n * in `OTEL_RESOURCE_ATTRIBUTES`; set one per seat.\n */\nexport const DEFAULT_SERVICE_NAME = 'seat-runtime';\n\n/**\n * `OTEL_RESOURCE_ATTRIBUTES` read the way Effect reads it (`OtlpResource.fromConfig`, rc.115:\n * `key=value` pairs, both sides URI-decoded), so \"has a `service.name`\" means what Effect means.\n */\nconst resourceAttributes = Config.Record(\n Schema.StringFromUriComponent,\n Schema.StringFromUriComponent,\n 'OTEL_RESOURCE_ATTRIBUTES',\n).pipe(Config.withDefault(undefined));\n\n/**\n * The fallback config source, computed against the CURRENT provider.\n *\n * ⛔ EVERYTHING HERE IS A FALLBACK. The environment is tried first, so `OTEL_SDK_DISABLED=true`,\n * `OTEL_TRACES_EXPORTER=none` and every endpoint the operator sets win.\n * ⚠️ THE PER-SIGNAL DEFAULTS STEP ASIDE FOR A BASE ENDPOINT. Effect reads\n * `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` and only then `OTEL_EXPORTER_OTLP_ENDPOINT`, so a\n * per-signal default would shadow an operator's base URL and send their traces to CT100.\n * ⚠️ THE SERVICE NAME STEPS ASIDE FOR `service.name` IN `OTEL_RESOURCE_ATTRIBUTES` FOR THE SAME\n * REASON. Effect resolves `OTEL_SERVICE_NAME`, then that attribute, then fails, so an injected\n * `OTEL_SERVICE_NAME` would win over the operator's attribute and Effect then drops the\n * attribute, leaving their seat mislabelled `seat-runtime` in Victoria with nothing to say why.\n */\nconst defaults: Effect.Effect<ConfigProvider.ConfigProvider> = Effect.gen(function* () {\n const current = yield* ConfigProvider.ConfigProvider;\n const base = yield* current.load(['OTEL_EXPORTER_OTLP_ENDPOINT']);\n const attributes = yield* resourceAttributes.parse(current);\n const named = attributes?.['service.name'] !== undefined;\n return ConfigProvider.fromUnknown({\n OTEL_TRACES_EXPORTER: 'otlp',\n OTEL_LOGS_EXPORTER: 'otlp',\n OTEL_METRICS_EXPORTER: 'otlp',\n ...(named ? {} : { OTEL_SERVICE_NAME: DEFAULT_SERVICE_NAME }),\n ...(base === undefined\n ? {\n OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: CT100_ENDPOINTS.traces,\n OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: CT100_ENDPOINTS.logs,\n OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: CT100_ENDPOINTS.metrics,\n }\n : {}),\n });\n}).pipe(Effect.orDie);\n\n/**\n * OTLP tracer, logger and metrics over `fetch`, protobuf on the wire (what VictoriaTraces,\n * VictoriaLogs and VictoriaMetrics ingest, and what the Claude Code seats already send).\n */\nexport const layer: Layer.Layer<OtlpExporter.Flusher> = Layer.mergeAll(\n OtlpTracer.layerFromConfig(),\n OtlpLogger.layerFromConfig(),\n OtlpMetrics.layerFromConfig(),\n).pipe(\n Layer.provide(OtlpSerialization.layerProtobuf),\n Layer.provide(FetchHttpClient.layer),\n Layer.provide(ConfigProvider.layerAdd(defaults)),\n);\n",
8
+ "/**\n * The one round loop every seat shares: model turn, tool results, model turn, until the model\n * stops asking for tools or the cap fires — and a cap that fires is followed by ONE more turn\n * with the toolkit taken away, so a run ends with an answer instead of a truncated transcript.\n *\n * ★ WHAT THE SDK DOES AND DOES NOT DO. `Chat.generateText` with a toolkit resolves the tool\n * calls of ONE model turn and returns; it never re-prompts, and Effect AI ships no\n * `maxRounds` or `stopWhen` (grep of LanguageModel.d.ts and Chat.d.ts, 2026-09-29). The\n * documented multi-round agent is a `while` over `session.generateText({ prompt: [], toolkit })`\n * (compat's ai-docs, 30_chat.ts). This file is that `while`, once, with the cap.\n * ⛔ THE CAP IS HARD AND FINITE. `maxRounds` must be a positive integer: `Infinity`, `0`, a\n * fraction or `NaN` is a programming error and dies with a `RangeError` before any model\n * call, because a loop a bad config can un-cap is a loop that spends until something else\n * stops it (LiteLLM's budget hit the claude2 fleet that way, 2026-09-28).\n * ⚠️ THE FORCED TURN SENDS NO TOOLS. No toolkit, and `toolChoice: 'none'` stated anyway: the SDK\n * already defaults `toolChoice` to `'none'` for a call with no toolkit (LanguageModel.js,\n * `providerOptions`, measured by mutation 2026-09-29: dropping the option changed no test), so\n * it is kept for what it says and for the `toolChoice` attribute on the turn's span. compat\n * then sends neither `tools` nor `tool_choice` (prepareTools returns both undefined for an\n * empty tool list, rc.115), because an OpenAI-shaped API refuses a `tool_choice` with no\n * `tools`. The history still carries the earlier tool calls and results, and cf-code accepted\n * exactly that: one live call through CT100's LiteLLM (:4100), 2026-09-29, a history of\n * system, user, assistant tool call and tool result, no `tools`, `toolChoice: 'none'`, came\n * back `finishReason: 'stop'` with text and no tool call. One sample, one alias.\n * ⚠️ `capped` MEANS THE MODEL STILL WANTED TOOLS after `maxRounds` tool rounds. A model that\n * answers on round `maxRounds` exactly is `capped: false`: the cap did not fire.\n * 🔴 THE FORCED TURN CAN BE REFUSED, AND THEN THE RUN DOES NOT DIE OF IT. A provider that asks for\n * a tool although none was offered (a gateway that adds a dummy tool for a history full of\n * tool calls does exactly that) answers a turn the SDK cannot use, and the SDK says so with one\n * of TWO `AiError` reasons, depending on WHO rejects the tool call:\n * - `ToolNotFoundError`: @effect/ai-openai-compat (`SeatModel`'s provider) rejects it first,\n * while it maps the reply (`transformToolCallParams`: \"Tool ... not found. Available tools:\n * none\"). Measured 2026-09-29 (review of PR 328, of the round 2 fix), `SeatModel` against a loopback\n * LiteLLM stand-in that answered every call with a tool call: 3 model calls, tools offered\n * [true, true, false], then the whole run failed with every round already spent. Round 2's\n * fix caught only the reason below and so did nothing for this provider.\n * - `InvalidOutputError` raised by `LanguageModel` (module `LanguageModel`): the SDK's own decode\n * of the provider's parts (\"Expected ... text | reasoning ...\"), reached by a provider that hands\n * the SDK a tool-call part itself (the scripted model in tests/fake-model.ts). The same reason\n * raised by `OpenAiClient` (an empty, truncated or non-completion body, a dropped stream) is NOT\n * a refusal and fails the run (tests/seat-broken-forced.test.ts).\n * Either is caught: the result is `capped` and `unanswered`, its `response` is the LAST TOOL\n * ROUND's (whose calls did run), `rounds` stays `maxRounds`, and a warning, a span error and\n * `seat_rounds_unanswered_total` say what happened. Any OTHER failure of the forced turn\n * (network, rate limit, a handler) still fails the run.\n */\nimport * as Effect from 'effect/Effect';\nimport * as Metric from 'effect/Metric';\nimport type * as Chat from 'effect/unstable/ai/Chat';\nimport type * as LanguageModel from 'effect/unstable/ai/LanguageModel';\nimport * as Prompt from 'effect/unstable/ai/Prompt';\nimport type * as Tool from 'effect/unstable/ai/Tool';\nimport type { AiError } from 'effect/unstable/ai/AiError';\nimport type * as Toolkit from 'effect/unstable/ai/Toolkit';\n\n/**\n * A tool call the forced turn cannot use. `ToolNotFoundError` is compat's own rejection (its reply\n * mapping); `InvalidOutputError` counts ONLY when `LanguageModel` raised it (the SDK's decode of a\n * provider that handed it a tool-call part). compat raises the same reason, from `OpenAiClient`, for\n * a 200 with an empty, non-JSON, truncated or non-completion body and for a body stream that dies\n * mid-read: those are gateway or network failures and must fail the run, not read as a refusal\n * (review of PR 328, round 4; tests/seat-broken-forced.test.ts).\n */\nconst isToolRefusal = (error: AiError): boolean =>\n error.reason._tag === 'ToolNotFoundError' ||\n (error.reason._tag === 'InvalidOutputError' && error.module === 'LanguageModel');\n\n/** Every model turn, forced or not. */\nconst roundsTotal = Metric.counter('seat_rounds_total', {\n description:\n 'Model turns taken by runRounds that returned; a refused forced final turn is not counted.',\n});\n/** Runs whose cap fired: the interesting number, because each one is an agent that did not finish. */\nconst roundsCapped = Metric.counter('seat_rounds_capped_total', {\n description: 'runRounds runs that hit maxRounds and were given a forced final turn.',\n});\n\n/** Runs whose forced final turn was refused: the model still asked for a tool. */\nconst roundsUnanswered = Metric.counter('seat_rounds_unanswered_total', {\n description: 'runRounds runs whose forced final turn came back as a tool call, not an answer.',\n});\n\n/**\n * A turn's response: with the toolkit, or the forced one without.\n * ⚠️ The two modes differ because the SDK's own types do: a call with a toolkit answers\n * `'opaque'` tool parameters, a call without one answers the default `'decoded'`.\n */\ntype Response<Tools extends Record<string, Tool.Any>> =\n | LanguageModel.GenerateTextResponse<Tools, 'opaque'>\n | LanguageModel.GenerateTextResponse<{}, 'decoded'>;\n\n/** One model turn, as `onRound` and the result see it. A refused forced final turn is not reported. */\nexport type Round<Tools extends Record<string, Tool.Any>> = {\n /** 1-based. The forced final turn, when there is one, is `maxRounds + 1`. */\n readonly round: number;\n /** True only for the forced final turn: no toolkit, `toolChoice: 'none'`. */\n readonly forced: boolean;\n readonly response: Response<Tools>;\n};\n\nexport type RoundsOptions<Tools extends Record<string, Tool.Any>> = {\n /**\n * The conversation, already holding the opening prompt (`Chat.fromPrompt`) or a resumed\n * history that ends in a user message or tool results. Every round sends an EMPTY prompt:\n * `Chat` appends the model's turn and its tool results to `history` itself.\n */\n readonly chat: Chat.Chat;\n /**\n * Tools WITH their handlers. A `Toolkit.make(...)` is an Effect that needs its handlers from\n * context: `yield*` it where they are provided, and pass the result. (Typing it as the\n * Effect would hide its requirements, which `generateText` itself cannot track either.)\n */\n readonly toolkit: Toolkit.WithHandler<Tools>;\n /** The most tool rounds allowed. A positive integer; see the header. */\n readonly maxRounds: number;\n /** After every model turn, the forced one included. Runs inside that turn's span. */\n readonly onRound?: ((round: Round<Tools>) => Effect.Effect<void>) | undefined;\n};\n\nexport type RoundsResult<Tools extends Record<string, Tool.Any>> = {\n /** The last model turn: the answer, or the forced final turn when the cap fired. */\n readonly response: Response<Tools>;\n /** Model turns that returned, the forced one included when it did. */\n readonly rounds: number;\n /** The cap fired and `response` is the forced final turn (or see `unanswered`). */\n readonly capped: boolean;\n /**\n * The forced turn was refused (a provider that still asks for a tool) and `response` is the\n * last TOOL round's: it holds no answer, and its tool calls ran. Only ever true with `capped`;\n * `rounds` then counts the turns that returned, so it is `maxRounds`. See the header.\n */\n readonly unanswered: boolean;\n};\n\n/**\n * Run the loop. Fails with whatever a model turn fails with (`AiError`, a tool handler's own\n * failure); nothing is retried here — the caller wraps `Effect.retry` where it wants one.\n */\nexport function runRounds<Tools extends Record<string, Tool.Any>>(\n options: RoundsOptions<Tools>,\n): Effect.Effect<\n RoundsResult<Tools>,\n LanguageModel.ExtractError<{ readonly toolkit: Toolkit.WithHandler<Tools> }>,\n | LanguageModel.LanguageModel\n | LanguageModel.ExtractServices<{ readonly toolkit: Toolkit.WithHandler<Tools> }>\n> {\n const { chat, toolkit, maxRounds, onRound } = options;\n return Effect.gen(function* () {\n if (!Number.isInteger(maxRounds) || maxRounds < 1) {\n return yield* Effect.die(\n new RangeError(`runRounds: maxRounds must be a positive integer, got ${String(maxRounds)}`),\n );\n }\n const finish = (round: number, forced: boolean) =>\n Effect.fnUntraced(function* (response: Response<Tools>) {\n yield* Metric.update(roundsTotal, 1);\n yield* Effect.annotateCurrentSpan({ 'seat.tool_calls': response.toolCalls.length });\n if (onRound !== undefined) yield* onRound({ round, forced, response });\n return response;\n });\n const span = (round: number, forced: boolean) =>\n Effect.withSpan('seat.round', {\n attributes: { 'seat.round': round, 'seat.round.forced': forced },\n });\n\n const turn = (round: number) =>\n chat\n .generateText({ prompt: Prompt.empty, toolkit })\n .pipe(Effect.flatMap(finish(round, false)), span(round, false));\n\n let round = 1;\n let response: Response<Tools> = yield* turn(round);\n // ★ The exit test is the model's: no tool calls means it has answered.\n while (response.toolCalls.length > 0 && round < maxRounds) {\n round += 1;\n response = yield* turn(round);\n }\n if (response.toolCalls.length === 0) {\n return { response, rounds: round, capped: false, unanswered: false };\n }\n\n yield* Metric.update(roundsCapped, 1);\n yield* Effect.logWarning('seat round cap reached; forcing a final turn without tools', {\n maxRounds,\n });\n const forcedRound = maxRounds + 1;\n /** The forced turn came back as a tool call: counted and logged, and the run goes on. */\n const refused = () =>\n Effect.as(\n Effect.all([\n Metric.update(roundsUnanswered, 1),\n Effect.logWarning('seat forced final turn refused: the model still asked for a tool', {\n maxRounds,\n }),\n ]),\n undefined,\n );\n const forced = yield* chat.generateText({ prompt: Prompt.empty, toolChoice: 'none' }).pipe(\n Effect.flatMap(finish(forcedRound, true)),\n span(forcedRound, true),\n // ⚠️ Only a tool call nobody offered: see the header for who raises which. Every other AiError\n // (a dropped connection, an empty or truncated body, a rate limit) still fails the run.\n Effect.catchTag('AiError', (error) =>\n isToolRefusal(error) ? refused() : Effect.fail(error),\n ),\n );\n return forced === undefined\n ? { response, rounds: maxRounds, capped: true, unanswered: true }\n : { response: forced, rounds: forcedRound, capped: true, unanswered: false };\n });\n}\n",
9
+ "/**\n * An MCP server's tools as an Effect AI `Toolkit`, and its resources as two Effects, through\n * the official MCP TypeScript SDK (`Client` over `StreamableHTTPClientTransport`).\n *\n * ★ THE SDK'S CLIENT IS THE CLIENT. Effect AI ships the SERVER half of MCP (`McpServer`,\n * `McpSchema`); no client (walked down 2026-09-29, docs/plans/2026-09-29-cf-seat-sdk-spike.md\n * open item 2). Wrapping the official client as dynamic tools is that plan's smallest option,\n * and the scout measured the pairing: list, call and resource read against an Effect\n * `McpServer` (pair115/mcp.ts).\n * ★ EACH MCP TOOL IS A `Tool.dynamic` WITH THE SERVER'S OWN JSON SCHEMA (mcp-tool.ts). The schema\n * goes to the model as the server wrote it, and no Effect Schema is built from its types: the\n * only client-side check is that the declared `required` arguments are present, and the server\n * validates the rest and its complaint comes back to the model.\n * ⚠️ A TOOL FAILURE IS THE MODEL'S TO SEE, NOT THE RUN'S TO DIE OF. Every tool is\n * `failureMode: 'return'`: an `isError` result, a JSON-RPC error (bad arguments, unknown\n * tool) and a call that never completed all come back to the model as the tool's result, so\n * it can correct itself or answer without the tool. MCP specifies errors-in-results for\n * exactly this. Connecting and listing are different: nothing works without them, so they\n * fail with `McpToolkitError`.\n * ⚠️ THE TOOL LIST IS A SNAPSHOT taken at connect time; a server's `listChanged` notification is\n * not followed. Reconnect for a fresh list.\n * ⛔ HEADERS ARE CREDENTIALS. They ride `requestInit` to the server and nowhere else: not in an\n * error, a span or a log (mcp-error.ts). Pass a bearer value as `Redacted` to keep it out of\n * an accidental print of the argument, as `SeatModel` does with the API key.\n */\nimport type { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport * as Effect from 'effect/Effect';\nimport type * as Scope from 'effect/Scope';\nimport type * as Toolkit from 'effect/unstable/ai/Toolkit';\nimport {\n DEFAULT_CONNECT_TIMEOUT_MS,\n type McpHeaders,\n connectAndBuild,\n headerValues,\n validateTimeout,\n} from './mcp-connect.ts';\nimport { McpToolkitError, type Redact, redactor, serverLabel } from './mcp-error.ts';\nimport { collect } from './mcp-pages.ts';\nimport { type McpTools, toolset } from './mcp-toolset.ts';\n\nexport type { McpTools } from './mcp-toolset.ts';\n\nexport type McpResource = {\n readonly uri: string;\n readonly name: string;\n readonly description?: string | undefined;\n readonly mimeType?: string | undefined;\n};\n\n/** One entry of a resource read: `text` for text, `blob` (base64) for binary. */\nexport type McpResourceContent = {\n readonly uri: string;\n readonly mimeType?: string | undefined;\n readonly text?: string | undefined;\n readonly blob?: string | undefined;\n};\n\nexport type McpToolkit = {\n /** Every tool the server listed, with handlers that call it: hand this to `runRounds`. */\n readonly toolkit: Toolkit.WithHandler<McpTools>;\n /** The server's resources; empty when it does not advertise the resources capability. */\n readonly listResources: Effect.Effect<ReadonlyArray<McpResource>, McpToolkitError>;\n /** The contents of one resource; fails with `McpToolkitError` for an unknown URI. */\n readonly readResource: (\n uri: string,\n ) => Effect.Effect<ReadonlyArray<McpResourceContent>, McpToolkitError>;\n};\n\nexport type McpToolkitOptions = {\n /**\n * The budget for STARTING UP, in milliseconds; default 15 000. A finite number above 0 and at\n * most 2 ** 31 - 1, or a `RangeError` defect before any request (mcp-connect.ts).\n * It covers, each with the full budget:\n * - the WHOLE handshake: `initialize` and the `notifications/initialized` that follows it;\n * - every `tools/list` page read after it (a server holding one fails the call as\n * `McpToolkitError` with `operation: 'listTools'`).\n * ⚠️ It is a budget PER REQUEST, not a total: a server that answers each of its (up to 100)\n * tool pages in just under the budget can still take that many budgets. Wrap the whole call\n * in `Effect.timeout` for a total.\n * ⚠️ The SDK bounds only the `initialize` request (its default is 60 s) and leaves the\n * notification unbounded, so a server that answers `initialize` and then stalls would hold a\n * seat at startup indefinitely. Here the budget covers both, and the handshake and the\n * listing are interruptible, so a caller's `Effect.timeout` or a shutdown also ends them, and\n * either way the client is closed and nothing is left in your scope (mcp-connect.ts).\n * ⚠️ NOT COVERED: `listResources`, `readResource` and tool calls, which are requests you make\n * after startup and keep the SDK's 60 s default per request. They are interruptible too.\n */\n readonly connectTimeoutMs?: number | undefined;\n};\n\n/** The two resource operations against a connected client; neither runs at connect time. */\nfunction resourcesOf(client: Client, server: string, redact: Redact) {\n const listResources = Effect.suspend(() =>\n // ★ A server that does not advertise resources is not asked: a strict one answers\n // \"method not found\", and \"no resources\" is the truthful reading of that.\n client.getServerCapabilities()?.resources === undefined\n ? Effect.succeed<ReadonlyArray<McpResource>>([])\n : collect(\n 'listResources',\n server,\n async (cursor, signal) => {\n const page = await client.listResources(cursor === undefined ? undefined : { cursor }, {\n signal,\n });\n return {\n items: page.resources.map((resource): McpResource => ({\n uri: resource.uri,\n name: resource.name,\n description: resource.description,\n mimeType: resource.mimeType,\n })),\n next: page.nextCursor,\n };\n },\n redact,\n ),\n ).pipe(Effect.withSpan('seat.mcp.list_resources', { attributes: { 'server.address': server } }));\n\n const readResource = (uri: string) =>\n Effect.tryPromise({\n try: async (signal): Promise<ReadonlyArray<McpResourceContent>> => {\n const read = await client.readResource({ uri }, { signal });\n return read.contents.map((entry) => ({\n uri: entry.uri,\n mimeType: entry.mimeType,\n text: 'text' in entry ? entry.text : undefined,\n blob: 'blob' in entry ? entry.blob : undefined,\n }));\n },\n catch: (cause) => new McpToolkitError({ operation: 'readResource', server, cause, redact }),\n }).pipe(\n Effect.withSpan('seat.mcp.read_resource', {\n attributes: { 'mcp.resource': uri, 'server.address': server },\n }),\n );\n\n return { listResources, readResource };\n}\n\n/**\n * Connect to a Streamable HTTP MCP server, list its tools, and return them as a toolkit.\n * The connection lives as long as the surrounding `Scope`.\n *\n * ★ A FAILURE AT ANY POINT LEAVES THE CALLER'S SCOPE EMPTY: the handshake, the `tools/list`\n * (a JSON-RPC error, a page held past `connectTimeoutMs`) and building the toolkit all run\n * before the client's finalizer is registered, and each closes the client if it fails or is\n * interrupted (`connectAndBuild`, mcp-connect.ts). So `Effect.retry` around `mcpToolkit`\n * accumulates no clients, sessions or sockets.\n */\nexport function mcpToolkit(\n url: string | URL,\n headers?: McpHeaders,\n options?: McpToolkitOptions,\n): Effect.Effect<McpToolkit, McpToolkitError, Scope.Scope> {\n return Effect.gen(function* () {\n const timeout = yield* validateTimeout(options?.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS);\n // ⚠️ Parsed INSIDE the Effect: `new URL` throws, and a throw at call time would skip the typed\n // error. The message never repeats the string: a URL is where a token may sit.\n const target = yield* Effect.try({\n try: () => new URL(url),\n catch: () =>\n new McpToolkitError({\n operation: 'connect',\n server: '(unparseable URL)',\n cause: 'the URL did not parse',\n }),\n });\n const server = serverLabel(target);\n // ⛔ Every error and every failure text below goes through this: a server echoes its address,\n // a query value or a header value, and a fetch failure prints the address, query string and\n // all (mcp-error.ts).\n const redact = redactor(target, headerValues(headers));\n\n // The connection lives in the surrounding scope; it is registered there only once the whole\n // toolkit below has been built (see the header of `mcpToolkit`).\n return yield* connectAndBuild(target, headers, server, timeout, redact, (client) =>\n Effect.gen(function* () {\n const toolkit = yield* toolset(client, server, timeout, redact);\n return { toolkit, ...resourcesOf(client, server, redact) };\n }),\n );\n });\n}\n",
10
+ "/**\n * Connect to a Streamable HTTP MCP server with the official SDK client: the transport, the\n * caller's headers on every request, and the WHOLE handshake (`initialize` and the\n * `notifications/initialized` that follows it) bounded in time and interruptible.\n *\n * ⛔ HEADERS ARE CREDENTIALS. They ride `requestInit` to the server and nowhere else: not in an\n * error, a span or a log (mcp-error.ts). A `Redacted` value is unwrapped only here, at the\n * moment the transport is built.\n */\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';\nimport * as Effect from 'effect/Effect';\nimport * as Redacted from 'effect/Redacted';\nimport type * as Scope from 'effect/Scope';\nimport { McpToolkitError, type Redact } from './mcp-error.ts';\nimport { VERSION } from './version.ts';\n\n/** Header values; a `Redacted` is unwrapped only at the moment the transport is built. */\nexport type McpHeaders = Readonly<Record<string, string | Redacted.Redacted<string>>>;\n\nexport const DEFAULT_CONNECT_TIMEOUT_MS = 15_000;\n\n/**\n * The longest a timer can run: `setTimeout` takes a 32-bit signed delay, and a larger one (like\n * one below 1) is set to 1 ms. Node documents that; Bun does the same (measured, see below).\n */\nexport const MAX_TIMEOUT_MS: number = 2 ** 31 - 1;\n\n/**\n * ⛔ A timeout that is not a finite number of milliseconds in `(0, MAX_TIMEOUT_MS]` is a\n * programming error, and dies with a `RangeError` before any request, as `maxRounds` does.\n * The budget feeds two timers (ours and the SDK's per request), and an out-of-range value made\n * them disagree: measured 2026-09-29 (review of PR 328) against a healthy local stub,\n * `Infinity` failed at once (\"did not finish within Infinity ms\") while `0`, `-1`, `NaN` and\n * `2 ** 31` each happened to succeed, because the timer clamped to 1 ms and the handshake won\n * the race. A real handshake takes longer than that 1 ms (not measured remotely), so those\n * would lose it.\n */\nexport const validateTimeout = (timeout: number): Effect.Effect<number> =>\n Number.isFinite(timeout) && timeout > 0 && timeout <= MAX_TIMEOUT_MS\n ? Effect.succeed(timeout)\n : Effect.die(\n new RangeError(\n `connectTimeoutMs must be a finite number of milliseconds above 0 and at most ${String(MAX_TIMEOUT_MS)}, got ${String(timeout)}`,\n ),\n );\n\nconst plainHeaders = (headers: McpHeaders | undefined): Record<string, string> =>\n Object.fromEntries(\n Object.entries(headers ?? {}).map(([name, value]) => [\n name,\n Redacted.isRedacted(value) ? Redacted.value(value) : value,\n ]),\n );\n\n/**\n * The header values as plain strings, for `redactor` and for nothing else: a value an error\n * message must never repeat is exactly a value the redactor has to know. Kept here so that a\n * `Redacted` is unwrapped in this file only.\n */\nexport function headerValues(headers: McpHeaders | undefined): string[] {\n return Object.values(plainHeaders(headers));\n}\n\n/**\n * ★ THE BUDGET COVERS THE WHOLE HANDSHAKE, NOT ONE REQUEST. SDK 1.31.0 `Client.connect` sends\n * `initialize` (bounded by its `timeout` option) and then AWAITS a `notifications/initialized`\n * POST that carries no timeout at all: only the transport's own abort signal ends it. Measured\n * 2026-09-29 (review of PR 328): a server that answered `initialize` and held the second POST\n * left a caller's `Effect.timeout('2 seconds')` unsettled at 8 s. So a timer here closes the\n * client when the budget is spent, and `close` aborts the transport's fetch, which is the only\n * thing that cancels that pending POST.\n * ★ THE CALLER'S `signal` CLOSES IT TOO. It fires when the fiber is interrupted (a seat shutting\n * down, an `Effect.timeout`), which only means something because `connectAndBuild` runs the handshake\n * in the `restore`d, interruptible part of its mask: a handshake inside `acquireRelease`'s\n * uninterruptible acquire would ignore it (that was the first version of this timeout).\n * ⚠️ Whatever the SDK rejects with after the budget is spent (an `AbortError` from the cancelled\n * fetch, or its own `Request timed out`) is an artefact of the close, so it is replaced by one\n * message that says what happened.\n */\nasync function handshake(\n client: Client,\n url: URL,\n headers: McpHeaders | undefined,\n timeout: number,\n signal: AbortSignal,\n): Promise<void> {\n const transport = new StreamableHTTPClientTransport(url, {\n requestInit: { headers: plainHeaders(headers) },\n });\n const budget = new AbortController();\n const stop = AbortSignal.any([signal, budget.signal]);\n const timer = setTimeout(() => budget.abort(), timeout);\n const close = (): void => void client.close().catch(() => undefined);\n stop.addEventListener('abort', close, { once: true });\n try {\n // ⚠️ The SDK's own `sessionId?: string` reads as `string | undefined` against its own\n // `Transport` under `exactOptionalPropertyTypes` (this repo's baseline), so the two\n // of its types do not line up here. Upstream's typing, not a wrong argument: the\n // class IS the transport, and nothing of it reaches this package's declarations.\n await client.connect(transport as Transport, { signal: stop, timeout });\n } catch (error) {\n // A failure AFTER the transport starts (a refused `initialize`) leaves the client holding\n // an open transport: close it here rather than wait for the scope.\n await client.close().catch(() => undefined);\n const spent = budget.signal.aborted;\n const sdkTimeout = error instanceof McpError && error.code === ErrorCode.RequestTimeout;\n throw (spent || sdkTimeout) && !signal.aborted\n ? new Error(`the MCP handshake did not finish within ${String(timeout)} ms`)\n : error;\n } finally {\n clearTimeout(timer);\n stop.removeEventListener('abort', close);\n }\n}\n\n/**\n * Connect, handshake and BUILD whatever the caller needs from the client (`build`: for\n * `mcpToolkit`, listing the tools and making the toolkit). The client lives as long as the\n * surrounding `Scope`, and only when ALL of that worked.\n *\n * ★ THE FINALIZER IS REGISTERED LAST: after the handshake AND after `build`. A scoped constructor\n * that fails must leave nothing in the caller's scope, and this one has been wrong twice:\n * - it used to register the `Client`'s `close` before the handshake. Each `Client` carries its\n * own JSON Schema validator (SDK client/index.js), so a seat retrying `mcpToolkit` through a\n * gateway outage held one per failed attempt until its scope closed: measured 2026-09-29\n * (review of PR 328), 64.7 MB against 10.6 MB after 3 001 refused attempts in one scope,\n * about 18 KB each. `handshake` closes the client on every failure, so a refused handshake\n * has nothing left to release.\n * - it then registered it right after the handshake, BEFORE the `tools/list` that `build` does,\n * and that listing can fail too: a JSON-RPC error, a page held past `connectTimeoutMs`, a\n * caller who interrupts. Measured 2026-09-29 (review of PR 328, later round), a loopback\n * server that completed the handshake: 20 attempts whose listing errored left 20 finalizers\n * and 20 open SSE streams in one scope; 10 whose listing was held left 10 finalizers, 10\n * streams and 10 `tools/list` POSTs still open (the SDK's per-request timeout rejects the\n * promise and does not abort the fetch). A seat retrying once a second against a slow gateway\n * is about 3 600 sessions and sockets an hour, held for the life of the seat.\n * `close` aborts the transport's fetches (the SSE stream and a held POST alike), which is why\n * closing the client is what releases them.\n * ⚠️ `uninterruptibleMask` keeps the gaps between the steps from being interruption points (a\n * client opened and never closed); only the handshake and `build` are restored to\n * interruptible, and each has an `onError` that closes the client when it fails OR is\n * interrupted, so the SDK's own close-on-abort is not the only line of defence. `onError` runs\n * uninterruptibly, so the close itself cannot be cut off.\n * ★ THE `seat.mcp.connect` SPAN IS THE HANDSHAKE ONLY, not `build`: it is what a slow or refused\n * connect looks like in a trace, and the listing is not that.\n */\nexport const connectAndBuild = <A>(\n url: URL,\n headers: McpHeaders | undefined,\n server: string,\n timeout: number,\n redact: Redact,\n build: (client: Client) => Effect.Effect<A, McpToolkitError>,\n): Effect.Effect<A, McpToolkitError, Scope.Scope> =>\n Effect.uninterruptibleMask((restore) =>\n Effect.gen(function* () {\n const client = new Client({ name: '@homeflare/seat-runtime', version: VERSION });\n const close = Effect.ignore(Effect.tryPromise(() => client.close()));\n yield* restore(\n Effect.tryPromise({\n try: (signal) => handshake(client, url, headers, timeout, signal),\n catch: (cause) => new McpToolkitError({ operation: 'connect', server, cause, redact }),\n }).pipe(Effect.withSpan('seat.mcp.connect', { attributes: { 'server.address': server } })),\n ).pipe(Effect.onError(() => close));\n const built = yield* restore(build(client)).pipe(Effect.onError(() => close));\n yield* Effect.addFinalizer(() => close);\n return built;\n }),\n );\n",
11
+ "/**\n * `mcpToolkit`'s typed failure, and the one place an MCP server's address is spelled for a\n * message or a span.\n *\n * ⛔ THE ADDRESS IS ORIGIN AND PATH ONLY. A query string is where a token lands when a server\n * wants one there, and the headers a caller passes are bearer credentials: neither may reach\n * an error message, a log line or a span attribute. Header NAMES and the header map are never\n * read here; a header VALUE is read only to be redacted (`redactor`).\n * 🔴 A SERVER ECHOES A VALUE ON ITS OWN. Redacting the query STRING and the address is not\n * enough: a server that answers \"rejected key QVALUE\" prints the value alone, and one that says\n * \"bad credential Bearer HVALUE\" prints a header's. Measured 2026-09-29 (review of PR 328,\n * round 2): both reached `McpToolkitError.message`. So `redactor` also takes out each query\n * value and each header value the caller passed (`MIN_SECRET_LENGTH` and up). What it cannot\n * catch: a value the server TRANSFORMS (hashed, base64, truncated), and one under the minimum.\n * 🔴 THE `cause` IS A COPY, NEVER THE ORIGINAL. Measured 2026-09-29 (review of PR 328): under Bun a\n * refused fetch is a `TypeError` whose own `path` field is the FULL URL, query string included,\n * so attaching it as `cause` printed the token through `Bun.inspect`, `console.error`, an\n * uncaught rejection and the default `Effect.logError` — while `error.message` was clean, which\n * is all the first tests looked at. `scrubCause` rebuilds the chain from name, message, stack\n * and a string or numeric `code` only, so no other field (`path`, `url`, `data`, `body`) is\n * carried, and `redactor` takes the query string, fragment and password out of what remains.\n * The price: `cause instanceof McpError` no longer holds; read `cause.code` instead.\n */\n/** Which call failed. `connect` covers the transport and the whole handshake (mcp-connect.ts). */\nexport type McpOperation = 'connect' | 'listTools' | 'listResources' | 'readResource';\n\n/** A server address safe to print: `https://host:port/path`, no credentials, query or fragment. */\nexport function serverLabel(url: URL): string {\n return `${url.origin}${url.pathname}`;\n}\n\n/** Text with a server's query string, fragment and password taken out of it. */\nexport type Redact = (text: string) => string;\n\n/**\n * The shortest value `redactor` treats as a secret ON ITS OWN. Redacting every occurrence of a\n * value costs legible messages (a `?v=1` would blank every \"1\"), and a value this short is not\n * a credential anyone relies on. The query STRING, fragment and password are redacted whole\n * whatever their length.\n */\nexport const MIN_SECRET_LENGTH = 6;\n\n/** The values in a raw query string, as written (percent-encoded) and as decoded. */\nfunction queryValues(url: URL): string[] {\n const raw = url.search\n .slice(1)\n .split('&')\n .map((pair) => pair.slice(pair.indexOf('=') + 1));\n return [...raw, ...url.searchParams.values()];\n}\n\n/**\n * A header value, and its credential when it has a scheme: `Bearer abc` is echoed whole or as\n * `abc`, and \"Bearer\" alone is not a secret.\n */\nfunction headerParts(value: string): string[] {\n const token = value.trim().split(/\\s+/).at(-1) ?? '';\n return [value, token];\n}\n\n/**\n * A `Redact` for one server address, plus any header values the caller passes. The whole URL\n * becomes `serverLabel`; a bare query string, fragment or password left over, each query value\n * and each header value (`MIN_SECRET_LENGTH` and up, longest first) becomes `[redacted]`: a\n * server can echo the address or a value back in an error body, and a fetch failure can print it.\n */\nexport function redactor(url: URL, headerValues: ReadonlyArray<string> = []): Redact {\n const label = serverLabel(url);\n const whole = [\n url.search.length > 1 ? url.search : '',\n url.hash.length > 1 ? url.hash : '',\n url.password,\n ];\n const parts = [...queryValues(url), ...headerValues.flatMap(headerParts)];\n const secrets = [\n ...whole,\n ...[...new Set(parts)]\n .filter((part) => part.length >= MIN_SECRET_LENGTH)\n .sort((a, b) => b.length - a.length),\n ].filter((secret) => secret !== '');\n return (text) => {\n let out = text.split(url.href).join(label);\n for (const secret of secrets) out = out.split(secret).join('[redacted]');\n return out;\n };\n}\n\n/** How many `cause` links `scrubCause` follows: a cycle or a deep chain ends here. */\nconst MAX_CAUSE_DEPTH = 5;\n\n/**\n * A copy of `cause` that is safe to print. ⚠️ An allowlist, not a denylist: an Error is rebuilt\n * from its name, message, stack and `code` (a string or number) and its own `cause`, each run\n * through `redact`; every other own field is left behind, whatever it is called. See the header.\n */\nexport function scrubCause(cause: unknown, redact: Redact, depth = 0): unknown {\n if (!(cause instanceof Error)) return redact(String(cause));\n const inner = depth < MAX_CAUSE_DEPTH && cause.cause !== undefined;\n const copy = new Error(\n redact(cause.message),\n inner ? { cause: scrubCause(cause.cause, redact, depth + 1) } : undefined,\n );\n copy.name = redact(cause.name);\n if (typeof cause.stack === 'string') copy.stack = redact(cause.stack);\n const code: unknown = (cause as { code?: unknown }).code;\n if (typeof code === 'string') Object.assign(copy, { code: redact(code) });\n else if (typeof code === 'number') Object.assign(copy, { code });\n return copy;\n}\n\n/** The message of whatever a promise rejected with; an SDK `McpError` carries the JSON-RPC text. */\nexport function describeCause(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n\n/**\n * The connection, a listing or a resource read failed.\n *\n * ★ A PLAIN `Error` WITH A `_tag`, NOT `Data.TaggedError`. `class X extends Data.TaggedError(…)<…>`\n * is TS9021 under `isolatedDeclarations` (\"extends clause can't contain an expression\"; measured\n * 2026-09-15, see packages/alchemy/tsconfig.json), and this package keeps that flag on. The\n * `_tag` is all `Effect.catchTag('McpToolkitError', …)` reads.\n */\nexport class McpToolkitError extends Error {\n readonly _tag = 'McpToolkitError' as const;\n readonly operation: McpOperation;\n /** `serverLabel` of the server: never a header, never a query string. */\n readonly server: string;\n\n constructor(fields: {\n readonly operation: McpOperation;\n readonly server: string;\n /**\n * What the SDK threw: an `McpError` (a JSON-RPC error), a `StreamableHTTPError`, a fetch\n * failure. Never stored as given: `cause` on the error is `scrubCause` of it.\n */\n readonly cause: unknown;\n /** `redactor` of the server's URL. Left out, only the structural scrub of `cause` applies. */\n readonly redact?: Redact | undefined;\n }) {\n const redact = fields.redact ?? ((text: string) => text);\n super(\n `MCP ${fields.operation} against ${fields.server} failed: ${redact(describeCause(fields.cause))}`,\n { cause: scrubCause(fields.cause, redact) },\n );\n this.name = 'McpToolkitError';\n this.operation = fields.operation;\n this.server = fields.server;\n }\n}\n",
12
+ "// ⛔ GENERATED by scripts/sync-versions.ts — do not edit. Source: package.json.\nexport const VERSION: string = '0.1.0';\n",
13
+ "/**\n * MCP's cursor pagination, followed to the end — but not forever.\n *\n * ⛔ THE PAGE COUNT IS CAPPED. A server that answers every page with another `nextCursor` (a\n * bug, or a hostile one) would otherwise hold a seat in a listing loop before its first\n * model call. A hundred pages is far past any estate server's tool list; the cap fails the\n * listing loudly instead of truncating it quietly.\n */\nimport * as Effect from 'effect/Effect';\nimport { type McpOperation, McpToolkitError, type Redact } from './mcp-error.ts';\n\nexport const MAX_PAGES = 100;\n\nexport type Page<T> = { readonly items: ReadonlyArray<T>; readonly next: string | undefined };\n\n/** Every item of a paged listing, or an `McpToolkitError` for `operation`. Interruption aborts the request in flight. */\nexport function collect<T>(\n operation: McpOperation,\n server: string,\n page: (cursor: string | undefined, signal: AbortSignal) => Promise<Page<T>>,\n redact?: Redact | undefined,\n): Effect.Effect<ReadonlyArray<T>, McpToolkitError> {\n return Effect.tryPromise({\n try: async (signal) => {\n const items: T[] = [];\n let cursor: string | undefined;\n for (let pages = 0; pages < MAX_PAGES; pages += 1) {\n const next = await page(cursor, signal);\n items.push(...next.items);\n if (next.next === undefined) return items;\n cursor = next.next;\n }\n throw new Error(`the server returned more than ${String(MAX_PAGES)} pages`);\n },\n catch: (cause) => new McpToolkitError({ operation, server, cause, redact }),\n });\n}\n",
14
+ "/**\n * An MCP server's tools as a ready-to-run Effect AI toolkit: list them (bounded by the startup\n * budget), wrap each as a `Tool.dynamic` (mcp-tool.ts), and give each a handler that calls the\n * server. Split from mcp-toolkit.ts so that `connectAndBuild` can run all of this BEFORE the\n * client's finalizer is registered (mcp-connect.ts): listing can fail, and a failed listing must\n * leave nothing in the caller's scope.\n *\n * ⚠️ A TOOL FAILURE IS THE MODEL'S TO SEE, NOT THE RUN'S TO DIE OF (see mcp-toolkit.ts). Every\n * failure below comes back as a string.\n * ⛔ A FAILURE'S TEXT IS REDACTED, A SUCCESS'S IS NOT. What the model reads for an `isError`\n * result, a JSON-RPC error or a dropped call goes through `redact`: a server that rejects a\n * credential typically prints it (\"bad credential Bearer ...\"), and that text goes on to a\n * cloud model. Measured 2026-09-29 (review of PR 328, later round): an `isError` result that\n * echoed a query value and a bearer token delivered both to the model, while the same echo in\n * an `McpToolkitError` was already redacted. A SUCCESSFUL result is the tool's payload and is\n * delivered verbatim: rewriting it would corrupt legitimate data that happens to contain a\n * value, and a server that returns a credential as data is not a failure this can tell apart.\n */\nimport type { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport * as Effect from 'effect/Effect';\nimport * as Toolkit from 'effect/unstable/ai/Toolkit';\nimport { type McpToolkitError, type Redact, describeCause } from './mcp-error.ts';\nimport { collect } from './mcp-pages.ts';\nimport { renderResult } from './mcp-render.ts';\nimport { type McpTool, mcpTool } from './mcp-tool.ts';\n\nexport type McpTools = Readonly<Record<string, McpTool>>;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/** Everything the model can do wrong or the server can refuse comes back as a string. */\nconst call = (client: Client, server: string, redact: Redact, name: string) => (params: unknown) =>\n Effect.gen(function* () {\n // The Toolkit's decode has already rejected a non-object (mcp-tool.ts), so this is the\n // type guard the `unknown` argument needs and not a second line of defence.\n if (!isRecord(params)) {\n return yield* Effect.fail(`${name}: the arguments must be a JSON object`);\n }\n const result = yield* Effect.tryPromise({\n try: (signal) => client.callTool({ name, arguments: params }, undefined, { signal }),\n catch: (cause) => `MCP call to ${name} failed: ${redact(describeCause(cause))}`,\n });\n const text = renderResult(result);\n // ⛔ Only a FAILURE's text is redacted: see the header.\n return result.isError === true ? yield* Effect.fail(redact(text)) : text;\n }).pipe(\n Effect.withSpan('seat.mcp.call_tool', {\n attributes: { 'mcp.tool': name, 'server.address': server },\n }),\n );\n\n/**\n * List the server's tools and return them as a toolkit with handlers.\n * ★ A server that does not advertise tools is not asked for them: a resources-only server\n * answers `tools/list` with \"method not found\", and one such server would otherwise make the\n * whole connection unusable for its resources.\n * ★ `timeout` is the startup budget: without it the SDK waits its 60 s per page.\n */\nexport const toolset = (\n client: Client,\n server: string,\n timeout: number,\n redact: Redact,\n): Effect.Effect<Toolkit.WithHandler<McpTools>, McpToolkitError> =>\n Effect.gen(function* () {\n const listed =\n client.getServerCapabilities()?.tools === undefined\n ? []\n : yield* collect(\n 'listTools',\n server,\n async (cursor, signal) => {\n const page = await client.listTools(cursor === undefined ? undefined : { cursor }, {\n signal,\n timeout,\n });\n return { items: page.tools, next: page.nextCursor };\n },\n redact,\n );\n const tools = listed.map((tool) =>\n mcpTool({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }),\n );\n const definition = Toolkit.make(...tools);\n const handlers = Object.fromEntries(\n tools.map((tool) => [tool.name, call(client, server, redact, tool.name)]),\n );\n const context = yield* definition.toHandlers(handlers);\n return yield* definition.pipe(Effect.provideContext(context));\n });\n",
15
+ "/**\n * An MCP tool result as the text a model reads.\n *\n * ★ ALWAYS A STRING. A result is a list of content blocks (text, image, audio, a resource, a\n * link), and what goes into the model's context is one message. Text is passed through\n * verbatim, joined by newlines; everything else becomes a one-line marker naming what it was.\n * ⛔ A BINARY BLOCK NEVER REACHES THE MODEL AS ITS BYTES. An image's base64 is tens of\n * kilobytes of context that says nothing to a text model and costs every later round; the\n * marker keeps its type and MIME type so the model knows something was there.\n * ⚠️ AN EMPTY RESULT IS NOT AN EMPTY STRING. Some providers refuse a tool message with empty\n * content (not measured against cf-code), and a refused request ends the whole run over a\n * tool that merely had nothing to say.\n */\n\n/** What the model reads for a tool that returned no content blocks. */\nexport const EMPTY_RESULT = '(no content)';\n\ntype Block = Readonly<Record<string, unknown>>;\n\nconst isBlock = (value: unknown): value is Block => typeof value === 'object' && value !== null;\nconst str = (value: unknown): string | undefined => (typeof value === 'string' ? value : undefined);\n\n/** One block. The shapes are MCP's `TextContent`, `ImageContent`, `AudioContent`, `ResourceLink`, `EmbeddedResource`. */\nfunction renderBlock(block: Block): string {\n const type = str(block['type']) ?? 'unknown';\n switch (type) {\n case 'text':\n return str(block['text']) ?? '';\n case 'image':\n case 'audio':\n return `[${type}: ${str(block['mimeType']) ?? 'unknown type'}, not shown]`;\n case 'resource_link':\n return `[resource link: ${str(block['uri']) ?? 'no uri'}]`;\n case 'resource': {\n const resource = isBlock(block['resource']) ? block['resource'] : {};\n // An embedded resource is either text (shown) or a base64 blob (not).\n return str(resource['text']) ?? `[resource: ${str(resource['uri']) ?? 'no uri'}, not shown]`;\n }\n default:\n return `[unsupported content block: ${type}]`;\n }\n}\n\n/** The text of a `CallToolResult.content`. Anything that is not a block list is JSON, not dropped. */\nexport function renderContent(content: unknown): string {\n if (!Array.isArray(content)) {\n return content === undefined ? EMPTY_RESULT : JSON.stringify(content);\n }\n const lines = content.map((block: unknown) =>\n isBlock(block) ? renderBlock(block) : JSON.stringify(block),\n );\n return lines.length === 0 ? EMPTY_RESULT : lines.join('\\n');\n}\n\n/**\n * The text of a whole `callTool` result. ★ The `content` blocks are what MCP asks a server to\n * always send; a server that sends only `structuredContent` (an `outputSchema` tool that skipped\n * the text copy) would otherwise read as empty, so its JSON is shown instead. The legacy\n * `toolResult` shape (the SDK's compatibility result) is read as content.\n */\nexport function renderResult(result: Readonly<Record<string, unknown>>): string {\n const content = 'content' in result ? result['content'] : result['toolResult'];\n if (Array.isArray(content) && content.length === 0 && result['structuredContent'] !== undefined) {\n return JSON.stringify(result['structuredContent']);\n }\n return renderContent(content);\n}\n",
16
+ "/**\n * One MCP tool as an Effect AI dynamic tool: the server's own JSON Schema goes to the model,\n * and the tool call the model sends back decodes without losing its arguments.\n *\n * ★ `Tool.dynamic` WITH THE SERVER'S JSON SCHEMA IS THE RIGHT SHAPE (its docs name MCP tools\n * discovered at runtime), and the model sees that schema verbatim.\n * 🔴 BUT @effect/ai-openai-compat rc.115 CANNOT DECODE A TOOL CALL FOR IT. Measured 2026-09-29\n * (tests/seat-loop.test.ts, first run): the request goes out, and when the model's reply asks\n * for the tool, `transformToolCallParams` runs the OpenAI structured-output codec over the\n * tool's `parametersSchema` — `Schema.Unknown` in JSON-Schema mode — and fails the whole turn\n * with `UnsupportedSchemaError: Root JSON Schema must have type \"object\" and must not use\n * \"anyOf\"`. rc.118 fixed it upstream: it returns the raw params for a dynamic tool that has a\n * `jsonSchema`. The estate is pinned at rc.115 and rc.118 drops the `unstable/` import prefix,\n * so this file carries the workaround until the pin moves.\n * ★ THE WORKAROUND: keep `jsonSchema` (what the model is sent, and what `Tool.getJsonSchema`\n * returns), and replace `parametersSchema` — what compat's codec and the Toolkit decode with —\n * by an object schema of the DECLARED property names, each `Unknown`, decoded to `Unknown`\n * with an ENCODE THAT IS FORBIDDEN. Every declared value passes through untouched and\n * `required` is honoured, so a missing argument fails as a tool result the model can read\n * instead of reaching the server.\n * 🔴 WHY THE ENCODE IS FORBIDDEN. compat's `transformToolCallParams` (OpenAiLanguageModel.js)\n * decodes the model's params through the OpenAI structured-output codec and re-encodes them\n * with `parametersSchema`, falling back to the params AS SENT when either step fails. That\n * codec rewrites every optional property as nullable and reads `null` as ABSENT, so with a\n * plain object schema an explicit `null` on an optional argument was deleted before the call.\n * Measured 2026-09-29 (review of PR 328), end to end through `SeatModel` and `mcpToolkit` into\n * an Effect `McpServer`: `update_issue {id, assignee: null}` (null unassigns, omitted leaves\n * alone) reached the server as `{id}`, and the run ended 'done'. That normalisation exists for\n * the codec's own JSON Schema, and the model here was sent the server's, where `null` is a\n * value. Forbidding the encode makes the re-encode fail, so compat forwards the params as the\n * model sent them: `null` arrives as `null` (pinned in tests/mcp-arguments.test.ts).\n * ⚠️ WHAT IT COSTS: a property the schema does not declare is DROPPED before the call\n * (Effect's decoder ignores excess keys; v4 has no \"preserve\"), so a server that declares\n * `additionalProperties: true` and relies on undeclared keys gets fewer than the model sent.\n * And a tool that declares no properties takes no arguments: `Tool.EmptyParams` is the only\n * root the codec accepts for it, and it rejects any key.\n * ⛔ REMOVE THIS WHEN THE PIN REACHES rc.118 or later: build the tool from `Tool.dynamic` alone.\n * The clone below copies what `Tool`'s own `setParameters` copies (its prototype and own\n * fields), so it depends on Effect's tool object layout; the end-to-end test is what fails\n * first if a bump changes that.\n */\nimport type * as JsonSchema from 'effect/JsonSchema';\nimport * as Schema from 'effect/Schema';\nimport * as SchemaGetter from 'effect/SchemaGetter';\nimport * as Tool from 'effect/unstable/ai/Tool';\n\n/** One dynamic tool per MCP tool: the server's JSON Schema in, text out, failures returned. */\nexport type McpTool = Tool.Dynamic<\n string,\n {\n readonly parameters: JsonSchema.JsonSchema;\n readonly success: typeof Schema.String;\n readonly failure: typeof Schema.String;\n readonly failureMode: 'return';\n }\n>;\n\n/** What `client.listTools()` gives for one tool, narrowed to what is used here. */\nexport type McpToolSpec = {\n readonly name: string;\n readonly description?: string | undefined;\n readonly inputSchema: JsonSchema.JsonSchema;\n};\n\n/** The workaround's `parametersSchema`: the declared property names, each passed through as-is, decode-only. */\nexport function declaredParameters(inputSchema: JsonSchema.JsonSchema): Schema.Constraint {\n const properties = inputSchema['properties'];\n const names =\n typeof properties === 'object' && properties !== null ? Object.keys(properties) : [];\n if (names.length === 0) return Tool.EmptyParams;\n const required = new Set(\n Array.isArray(inputSchema['required']) ? (inputSchema['required'] as unknown[]) : [],\n );\n const declared = Schema.Struct(\n Object.fromEntries(\n names.map((name) => [\n name,\n required.has(name) ? Schema.Unknown : Schema.optional(Schema.Unknown),\n ]),\n ),\n );\n return declared.pipe(\n Schema.decodeTo(Schema.Unknown, {\n decode: SchemaGetter.passthrough(),\n encode: SchemaGetter.forbiddenEncoding,\n }),\n );\n}\n\nexport function mcpTool(spec: McpToolSpec): McpTool {\n const dynamic = Tool.dynamic(spec.name, {\n description: spec.description,\n parameters: spec.inputSchema,\n success: Schema.String,\n failure: Schema.String,\n failureMode: 'return',\n });\n return Object.assign(Object.create(Object.getPrototypeOf(dynamic)), dynamic, {\n parametersSchema: declaredParameters(spec.inputSchema),\n }) as McpTool;\n}\n"
17
+ ],
18
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;AAUA;AACA;AACA;AACA;AAEA;AACA;;;ACLA;AAeA,IAAM,MAAM;AAGL,SAAS,QAAQ,CAAC,MAA8C;AAAA,EACrE,MAAM,OAAO,OAAO,SAAS,WAAW,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI;AAAA,EAClE,IAAI,KAAK,WAAW;AAAA,IAAG,MAAM,IAAI,UAAU,yCAAyC;AAAA,EACpF,WAAW,OAAO,MAAM;AAAA,IACtB,IAAI,CAAC,IAAI,KAAK,GAAG,GAAG;AAAA,MAClB,MAAM,IAAI,UACR,cAAc,KAAK,UAAU,GAAG,2DAClC;AAAA,IACF;AAAA,EACF;AAAA,EACA,OAAO,KAAK,KAAK,GAAG;AAAA;AAItB,SAAS,UAAU,CACjB,SACqC;AAAA,EACrC,MAAM,OAAO,QAAQ;AAAA,EACrB,IAAI,KAAK,SAAS,gBAAgB,CAAC,KAAK,YAAY,SAAS,MAAM;AAAA,IAAG;AAAA,EAKtE,MAAM,OAAO,KAAK,QAAQ,IAAI,YAAY,EAAE,OAAO,KAAK,IAAI;AAAA,EAC5D,IAAI;AAAA,EACJ,IAAI;AAAA,IACF,SAAS,KAAK,MAAM,IAAI;AAAA,IACxB,MAAM;AAAA,IACN;AAAA;AAAA,EAEF,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM;AAAA,IAAG;AAAA,EAC5E,OAAO;AAAA;AAIF,SAAS,YAAY,CAC1B,SACA,OACqC;AAAA,EACrC,MAAM,UAAU,MAAM,UAClB,EAAE,kBAAkB,MAAM,MAAM,iBAAiB,qBAAqB,IACtE,EAAE,kBAAkB,MAAM,KAAK;AAAA,EACnC,MAAM,SAA2B,6BAAW,SAAS,OAAO;AAAA,EAC5D,MAAM,OAAO,WAAW,OAAO;AAAA,EAC/B,IAAI,SAAS;AAAA,IAAW,OAAO;AAAA,EAC/B,OAAyB,iCAAe,QAAQ;AAAA,OAC3C;AAAA,OAMC,MAAM,UAAU,EAAE,OAAO,EAAE,YAAY,MAAM,YAAY,KAAK,EAAE,IAAI,CAAC;AAAA,IAGzE,aAAa;AAAA,OACT,MAAM,aAAa,aAAa,EAAE,cAAc,QAAQ,EAAE,UAAU,MAAM,SAAS,IAAI,CAAC;AAAA,EAC9F,CAAC;AAAA;;;AD1BI,SAAS,WAAW,CAAC,SAAoE;AAAA,EAC9F,MAAM,QAAQ;AAAA,IACZ,MAAM,SAAS,QAAQ,IAAI;AAAA,IAC3B,SAAS,QAAQ,WAAW;AAAA,IAC5B,UAAU,QAAQ;AAAA,EACpB;AAAA,EACA,OAAO,aAAa,MAAM;AAAA,IACxB,QAAiB,oBAAW,QAAQ,MAAM,IAAI,QAAQ,SAAkB,cAAK,QAAQ,MAAM;AAAA,IAC3F,QAAQ,QAAQ,OAAO,QAAQ,OAAO,EAAE;AAAA,IACxC,iBAAiB,CAAC,SACL,sBAAW,MAAM,CAAC,YAAY,aAAa,SAAS,KAAK,CAAC;AAAA,EACzE,CAAC,EAAE,KAAW,cAAwB,qBAAK,CAAC;AAAA;AAIvC,SAAS,MAAK,CAAC,SAAqE;AAAA,EACzF,OAAO,oBAAoB,MAAM;AAAA,IAC/B,OAAO,QAAQ;AAAA,IAIf,QAAQ,EAAE,kBAAkB,UAAU,QAAQ,OAAO;AAAA,EACvD,CAAC,EAAE,KAAW,cAAQ,YAAY,OAAO,CAAC,CAAC;AAAA;AAItC,SAAS,cAAc,CAC5B,SACwE;AAAA,EACxE,OAAa,YACX,qBAAqB,MAAM,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC,GACrE,cAAuB,2BAAY,QAAQ,UAAU,CAC7D,EAAE,KAAW,cAAQ,YAAY,OAAO,CAAC,CAAC;AAAA;;;;;;;;AEhF5C;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaO,IAAM,kBAIT;AAAA,EACF,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,SAAS;AACX;AAMO,IAAM,uBAAuB;AAMpC,IAAM,qBAA4B,cACzB,+BACA,+BACP,0BACF,EAAE,KAAY,mBAAY,SAAS,CAAC;AAepC,IAAM,WAAgE,WAAI,UAAU,GAAG;AAAA,EACrF,MAAM,UAAU,OAAsB;AAAA,EACtC,MAAM,OAAO,OAAO,QAAQ,KAAK,CAAC,6BAA6B,CAAC;AAAA,EAChE,MAAM,aAAa,OAAO,mBAAmB,MAAM,OAAO;AAAA,EAC1D,MAAM,QAAQ,aAAa,oBAAoB;AAAA,EAC/C,OAAsB,2BAAY;AAAA,IAChC,sBAAsB;AAAA,IACtB,oBAAoB;AAAA,IACpB,uBAAuB;AAAA,OACnB,QAAQ,CAAC,IAAI,EAAE,mBAAmB,qBAAqB;AAAA,OACvD,SAAS,YACT;AAAA,MACE,oCAAoC,gBAAgB;AAAA,MACpD,kCAAkC,gBAAgB;AAAA,MAClD,qCAAqC,gBAAgB;AAAA,IACvD,IACA,CAAC;AAAA,EACP,CAAC;AAAA,CACF,EAAE,KAAY,YAAK;AAMb,IAAM,SAAiD,gBAC5D,WAAW,gBAAgB,GAC3B,WAAW,gBAAgB,GAC3B,YAAY,gBAAgB,CAC9B,EAAE,KACM,eAAQ,kBAAkB,aAAa,GACvC,eAAwB,sBAAK,GAC7B,eAAuB,wBAAS,QAAQ,CAAC,CACjD;;ACxDA;AACA;AAGA;AAaA,IAAM,gBAAgB,CAAC,UACrB,MAAM,OAAO,SAAS,uBACrB,MAAM,OAAO,SAAS,wBAAwB,MAAM,WAAW;AAGlE,IAAM,cAAqB,eAAQ,qBAAqB;AAAA,EACtD,aACE;AACJ,CAAC;AAED,IAAM,eAAsB,eAAQ,4BAA4B;AAAA,EAC9D,aAAa;AACf,CAAC;AAGD,IAAM,mBAA0B,eAAQ,gCAAgC;AAAA,EACtE,aAAa;AACf,CAAC;AA0DM,SAAS,SAAiD,CAC/D,SAMA;AAAA,EACA,QAAQ,MAAM,SAAS,WAAW,YAAY;AAAA,EAC9C,OAAc,YAAI,UAAU,GAAG;AAAA,IAC7B,IAAI,CAAC,OAAO,UAAU,SAAS,KAAK,YAAY,GAAG;AAAA,MACjD,OAAO,OAAc,YACnB,IAAI,WAAW,wDAAwD,OAAO,SAAS,GAAG,CAC5F;AAAA,IACF;AAAA,IACA,MAAM,SAAS,CAAC,QAAe,YACtB,mBAAW,UAAU,CAAC,WAA2B;AAAA,MACtD,OAAc,cAAO,aAAa,CAAC;AAAA,MACnC,OAAc,4BAAoB,EAAE,mBAAmB,UAAS,UAAU,OAAO,CAAC;AAAA,MAClF,IAAI,YAAY;AAAA,QAAW,OAAO,QAAQ,EAAE,eAAO,iBAAQ,oBAAS,CAAC;AAAA,MACrE,OAAO;AAAA,KACR;AAAA,IACH,MAAM,OAAO,CAAC,QAAe,YACpB,iBAAS,cAAc;AAAA,MAC5B,YAAY,EAAE,cAAc,QAAO,qBAAqB,QAAO;AAAA,IACjE,CAAC;AAAA,IAEH,MAAM,OAAO,CAAC,WACZ,KACG,aAAa,EAAE,QAAe,cAAO,QAAQ,CAAC,EAC9C,KAAY,gBAAQ,OAAO,QAAO,KAAK,CAAC,GAAG,KAAK,QAAO,KAAK,CAAC;AAAA,IAElE,IAAI,QAAQ;AAAA,IACZ,IAAI,WAA4B,OAAO,KAAK,KAAK;AAAA,IAEjD,OAAO,SAAS,UAAU,SAAS,KAAK,QAAQ,WAAW;AAAA,MACzD,SAAS;AAAA,MACT,WAAW,OAAO,KAAK,KAAK;AAAA,IAC9B;AAAA,IACA,IAAI,SAAS,UAAU,WAAW,GAAG;AAAA,MACnC,OAAO,EAAE,UAAU,QAAQ,OAAO,QAAQ,OAAO,YAAY,MAAM;AAAA,IACrE;AAAA,IAEA,OAAc,cAAO,cAAc,CAAC;AAAA,IACpC,OAAc,mBAAW,8DAA8D;AAAA,MACrF;AAAA,IACF,CAAC;AAAA,IACD,MAAM,cAAc,YAAY;AAAA,IAEhC,MAAM,UAAU,MACP,WACE,YAAI;AAAA,MACF,cAAO,kBAAkB,CAAC;AAAA,MAC1B,mBAAW,oEAAoE;AAAA,QACpF;AAAA,MACF,CAAC;AAAA,IACH,CAAC,GACD,SACF;AAAA,IACF,MAAM,SAAS,OAAO,KAAK,aAAa,EAAE,QAAe,cAAO,YAAY,OAAO,CAAC,EAAE,KAC7E,gBAAQ,OAAO,aAAa,IAAI,CAAC,GACxC,KAAK,aAAa,IAAI,GAGf,iBAAS,WAAW,CAAC,UAC1B,cAAc,KAAK,IAAI,QAAQ,IAAW,aAAK,KAAK,CACtD,CACF;AAAA,IACA,OAAO,WAAW,YACd,EAAE,UAAU,QAAQ,WAAW,QAAQ,MAAM,YAAY,KAAK,IAC9D,EAAE,UAAU,QAAQ,QAAQ,aAAa,QAAQ,MAAM,YAAY,MAAM;AAAA,GAC9E;AAAA;;ACvLH;;;ACjBA;AACA;AAEA;AACA;AACA;;;ACaO,SAAS,WAAW,CAAC,KAAkB;AAAA,EAC5C,OAAO,GAAG,IAAI,SAAS,IAAI;AAAA;AAYtB,IAAM,oBAAoB;AAGjC,SAAS,WAAW,CAAC,KAAoB;AAAA,EACvC,MAAM,MAAM,IAAI,OACb,MAAM,CAAC,EACP,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,MAAM,KAAK,QAAQ,GAAG,IAAI,CAAC,CAAC;AAAA,EAClD,OAAO,CAAC,GAAG,KAAK,GAAG,IAAI,aAAa,OAAO,CAAC;AAAA;AAO9C,SAAS,WAAW,CAAC,OAAyB;AAAA,EAC5C,MAAM,QAAQ,MAAM,KAAK,EAAE,MAAM,KAAK,EAAE,GAAG,EAAE,KAAK;AAAA,EAClD,OAAO,CAAC,OAAO,KAAK;AAAA;AASf,SAAS,QAAQ,CAAC,KAAU,eAAsC,CAAC,GAAW;AAAA,EACnF,MAAM,QAAQ,YAAY,GAAG;AAAA,EAC7B,MAAM,QAAQ;AAAA,IACZ,IAAI,OAAO,SAAS,IAAI,IAAI,SAAS;AAAA,IACrC,IAAI,KAAK,SAAS,IAAI,IAAI,OAAO;AAAA,IACjC,IAAI;AAAA,EACN;AAAA,EACA,MAAM,QAAQ,CAAC,GAAG,YAAY,GAAG,GAAG,GAAG,aAAa,QAAQ,WAAW,CAAC;AAAA,EACxE,MAAM,UAAU;AAAA,IACd,GAAG;AAAA,IACH,GAAG,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,EAClB,OAAO,CAAC,SAAS,KAAK,UAAU,iBAAiB,EACjD,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;AAAA,EACvC,EAAE,OAAO,CAAC,WAAW,WAAW,EAAE;AAAA,EAClC,OAAO,CAAC,SAAS;AAAA,IACf,IAAI,MAAM,KAAK,MAAM,IAAI,IAAI,EAAE,KAAK,KAAK;AAAA,IACzC,WAAW,UAAU;AAAA,MAAS,MAAM,IAAI,MAAM,MAAM,EAAE,KAAK,YAAY;AAAA,IACvE,OAAO;AAAA;AAAA;AAKX,IAAM,kBAAkB;AAOjB,SAAS,UAAU,CAAC,OAAgB,QAAgB,QAAQ,GAAY;AAAA,EAC7E,IAAI,EAAE,iBAAiB;AAAA,IAAQ,OAAO,OAAO,OAAO,KAAK,CAAC;AAAA,EAC1D,MAAM,QAAQ,QAAQ,mBAAmB,MAAM,UAAU;AAAA,EACzD,MAAM,OAAO,IAAI,MACf,OAAO,MAAM,OAAO,GACpB,QAAQ,EAAE,OAAO,WAAW,MAAM,OAAO,QAAQ,QAAQ,CAAC,EAAE,IAAI,SAClE;AAAA,EACA,KAAK,OAAO,OAAO,MAAM,IAAI;AAAA,EAC7B,IAAI,OAAO,MAAM,UAAU;AAAA,IAAU,KAAK,QAAQ,OAAO,MAAM,KAAK;AAAA,EACpE,MAAM,OAAiB,MAA6B;AAAA,EACpD,IAAI,OAAO,SAAS;AAAA,IAAU,OAAO,OAAO,MAAM,EAAE,MAAM,OAAO,IAAI,EAAE,CAAC;AAAA,EACnE,SAAI,OAAO,SAAS;AAAA,IAAU,OAAO,OAAO,MAAM,EAAE,KAAK,CAAC;AAAA,EAC/D,OAAO;AAAA;AAIF,SAAS,aAAa,CAAC,OAAwB;AAAA,EACpD,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA;AAAA;AAWvD,MAAM,wBAAwB,MAAM;AAAA,EAChC,OAAO;AAAA,EACP;AAAA,EAEA;AAAA,EAET,WAAW,CAAC,QAUT;AAAA,IACD,MAAM,SAAS,OAAO,WAAW,CAAC,SAAiB;AAAA,IACnD,MACE,OAAO,OAAO,qBAAqB,OAAO,kBAAkB,OAAO,cAAc,OAAO,KAAK,CAAC,KAC9F,EAAE,OAAO,WAAW,OAAO,OAAO,MAAM,EAAE,CAC5C;AAAA,IACA,KAAK,OAAO;AAAA,IACZ,KAAK,YAAY,OAAO;AAAA,IACxB,KAAK,SAAS,OAAO;AAAA;AAEzB;;;ACpJO,IAAM,UAAkB;;;AFqBxB,IAAM,6BAA6B;AAMnC,IAAM,iBAAyB,KAAK,KAAK;AAYzC,IAAM,kBAAkB,CAAC,YAC9B,OAAO,SAAS,OAAO,KAAK,UAAU,KAAK,WAAW,iBAC3C,gBAAQ,OAAO,IACf,YACL,IAAI,WACF,gFAAgF,OAAO,cAAc,UAAU,OAAO,OAAO,GAC/H,CACF;AAEN,IAAM,eAAe,CAAC,YACpB,OAAO,YACL,OAAO,QAAQ,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,YAAW;AAAA,EACnD;AAAA,EACS,qBAAW,MAAK,IAAa,gBAAM,MAAK,IAAI;AACvD,CAAC,CACH;AAOK,SAAS,YAAY,CAAC,SAA2C;AAAA,EACtE,OAAO,OAAO,OAAO,aAAa,OAAO,CAAC;AAAA;AAmB5C,eAAe,SAAS,CACtB,QACA,KACA,SACA,SACA,QACe;AAAA,EACf,MAAM,YAAY,IAAI,8BAA8B,KAAK;AAAA,IACvD,aAAa,EAAE,SAAS,aAAa,OAAO,EAAE;AAAA,EAChD,CAAC;AAAA,EACD,MAAM,SAAS,IAAI;AAAA,EACnB,MAAM,OAAO,YAAY,IAAI,CAAC,QAAQ,OAAO,MAAM,CAAC;AAAA,EACpD,MAAM,QAAQ,WAAW,MAAM,OAAO,MAAM,GAAG,OAAO;AAAA,EACtD,MAAM,QAAQ,MAAY,KAAK,OAAO,MAAM,EAAE,MAAM,MAAG;AAAA,IAAG;AAAA,GAAS;AAAA,EACnE,KAAK,iBAAiB,SAAS,OAAO,EAAE,MAAM,KAAK,CAAC;AAAA,EACpD,IAAI;AAAA,IAKF,MAAM,OAAO,QAAQ,WAAwB,EAAE,QAAQ,MAAM,QAAQ,CAAC;AAAA,IACtE,OAAO,OAAO;AAAA,IAGd,MAAM,OAAO,MAAM,EAAE,MAAM,MAAG;AAAA,MAAG;AAAA,KAAS;AAAA,IAC1C,MAAM,QAAQ,OAAO,OAAO;AAAA,IAC5B,MAAM,aAAa,iBAAiB,YAAY,MAAM,SAAS,UAAU;AAAA,IACzE,OAAO,SAAS,eAAe,CAAC,OAAO,UACnC,IAAI,MAAM,2CAA2C,OAAO,OAAO,MAAM,IACzE;AAAA,YACJ;AAAA,IACA,aAAa,KAAK;AAAA,IAClB,KAAK,oBAAoB,SAAS,KAAK;AAAA;AAAA;AAmCpC,IAAM,kBAAkB,CAC7B,KACA,SACA,QACA,SACA,QACA,UAEO,4BAAoB,CAAC,YACnB,YAAI,UAAU,GAAG;AAAA,EACtB,MAAM,SAAS,IAAI,OAAO,EAAE,MAAM,2BAA2B,SAAS,QAAQ,CAAC;AAAA,EAC/E,MAAM,QAAe,eAAc,mBAAW,MAAM,OAAO,MAAM,CAAC,CAAC;AAAA,EACnE,OAAO,QACE,mBAAW;AAAA,IAChB,KAAK,CAAC,WAAW,UAAU,QAAQ,KAAK,SAAS,SAAS,MAAM;AAAA,IAChE,OAAO,CAAC,UAAU,IAAI,gBAAgB,EAAE,WAAW,WAAW,QAAQ,OAAO,OAAO,CAAC;AAAA,EACvF,CAAC,EAAE,KAAY,iBAAS,oBAAoB,EAAE,YAAY,EAAE,kBAAkB,OAAO,EAAE,CAAC,CAAC,CAC3F,EAAE,KAAY,gBAAQ,MAAM,KAAK,CAAC;AAAA,EAClC,MAAM,QAAQ,OAAO,QAAQ,MAAM,MAAM,CAAC,EAAE,KAAY,gBAAQ,MAAM,KAAK,CAAC;AAAA,EAC5E,OAAc,qBAAa,MAAM,KAAK;AAAA,EACtC,OAAO;AAAA,CACR,CACH;;;AGnKF;AAGO,IAAM,YAAY;AAKlB,SAAS,OAAU,CACxB,WACA,QACA,MACA,QACkD;AAAA,EAClD,OAAc,mBAAW;AAAA,IACvB,KAAK,OAAO,WAAW;AAAA,MACrB,MAAM,QAAa,CAAC;AAAA,MACpB,IAAI;AAAA,MACJ,SAAS,QAAQ,EAAG,QAAQ,WAAW,SAAS,GAAG;AAAA,QACjD,MAAM,OAAO,MAAM,KAAK,QAAQ,MAAM;AAAA,QACtC,MAAM,KAAK,GAAG,KAAK,KAAK;AAAA,QACxB,IAAI,KAAK,SAAS;AAAA,UAAW,OAAO;AAAA,QACpC,SAAS,KAAK;AAAA,MAChB;AAAA,MACA,MAAM,IAAI,MAAM,iCAAiC,OAAO,SAAS,SAAS;AAAA;AAAA,IAE5E,OAAO,CAAC,UAAU,IAAI,gBAAgB,EAAE,WAAW,QAAQ,OAAO,OAAO,CAAC;AAAA,EAC5E,CAAC;AAAA;;;AChBH;AACA;;;ACLO,IAAM,eAAe;AAI5B,IAAM,UAAU,CAAC,WAAmC,OAAO,WAAU,YAAY,WAAU;AAC3F,IAAM,MAAM,CAAC,WAAwC,OAAO,WAAU,WAAW,SAAQ;AAGzF,SAAS,WAAW,CAAC,OAAsB;AAAA,EACzC,MAAM,OAAO,IAAI,MAAM,OAAO,KAAK;AAAA,EACnC,QAAQ;AAAA,SACD;AAAA,MACH,OAAO,IAAI,MAAM,OAAO,KAAK;AAAA,SAC1B;AAAA,SACA;AAAA,MACH,OAAO,IAAI,SAAS,IAAI,MAAM,WAAW,KAAK;AAAA,SAC3C;AAAA,MACH,OAAO,mBAAmB,IAAI,MAAM,MAAM,KAAK;AAAA,SAC5C,YAAY;AAAA,MACf,MAAM,WAAW,QAAQ,MAAM,WAAW,IAAI,MAAM,cAAc,CAAC;AAAA,MAEnE,OAAO,IAAI,SAAS,OAAO,KAAK,cAAc,IAAI,SAAS,MAAM,KAAK;AAAA,IACxE;AAAA;AAAA,MAEE,OAAO,+BAA+B;AAAA;AAAA;AAKrC,SAAS,aAAa,CAAC,SAA0B;AAAA,EACtD,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG;AAAA,IAC3B,OAAO,YAAY,YAAY,eAAe,KAAK,UAAU,OAAO;AAAA,EACtE;AAAA,EACA,MAAM,QAAQ,QAAQ,IAAI,CAAC,UACzB,QAAQ,KAAK,IAAI,YAAY,KAAK,IAAI,KAAK,UAAU,KAAK,CAC5D;AAAA,EACA,OAAO,MAAM,WAAW,IAAI,eAAe,MAAM,KAAK;AAAA,CAAI;AAAA;AASrD,SAAS,YAAY,CAAC,QAAmD;AAAA,EAC9E,MAAM,UAAU,aAAa,SAAS,OAAO,aAAa,OAAO;AAAA,EACjE,IAAI,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,KAAK,OAAO,yBAAyB,WAAW;AAAA,IAC/F,OAAO,KAAK,UAAU,OAAO,oBAAoB;AAAA,EACnD;AAAA,EACA,OAAO,cAAc,OAAO;AAAA;;;ACvB9B;AACA;AACA;AAqBO,SAAS,kBAAkB,CAAC,aAAuD;AAAA,EACxF,MAAM,aAAa,YAAY;AAAA,EAC/B,MAAM,QACJ,OAAO,eAAe,YAAY,eAAe,OAAO,OAAO,KAAK,UAAU,IAAI,CAAC;AAAA,EACrF,IAAI,MAAM,WAAW;AAAA,IAAG,OAAY;AAAA,EACpC,MAAM,WAAW,IAAI,IACnB,MAAM,QAAQ,YAAY,WAAW,IAAK,YAAY,cAA4B,CAAC,CACrF;AAAA,EACA,MAAM,WAAkB,eACtB,OAAO,YACL,MAAM,IAAI,CAAC,SAAS;AAAA,IAClB;AAAA,IACA,SAAS,IAAI,IAAI,IAAW,kBAAiB,iBAAgB,eAAO;AAAA,EACtE,CAAC,CACH,CACF;AAAA,EACA,OAAO,SAAS,KACP,iBAAgB,iBAAS;AAAA,IAC9B,QAAqB,yBAAY;AAAA,IACjC,QAAqB;AAAA,EACvB,CAAC,CACH;AAAA;AAGK,SAAS,OAAO,CAAC,MAA4B;AAAA,EAClD,MAAM,WAAe,aAAQ,KAAK,MAAM;AAAA,IACtC,aAAa,KAAK;AAAA,IAClB,YAAY,KAAK;AAAA,IACjB,SAAgB;AAAA,IAChB,SAAgB;AAAA,IAChB,aAAa;AAAA,EACf,CAAC;AAAA,EACD,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,eAAe,QAAO,CAAC,GAAG,UAAS;AAAA,IAC3E,kBAAkB,mBAAmB,KAAK,WAAW;AAAA,EACvD,CAAC;AAAA;;;AFvEH,IAAM,WAAW,CAAC,WAChB,OAAO,WAAU,YAAY,WAAU,QAAQ,CAAC,MAAM,QAAQ,MAAK;AAGrE,IAAM,OAAO,CAAC,QAAgB,QAAgB,QAAgB,SAAiB,CAAC,WACvE,YAAI,UAAU,GAAG;AAAA,EAGtB,IAAI,CAAC,SAAS,MAAM,GAAG;AAAA,IACrB,OAAO,OAAc,aAAK,GAAG,2CAA2C;AAAA,EAC1E;AAAA,EACA,MAAM,SAAS,OAAc,mBAAW;AAAA,IACtC,KAAK,CAAC,WAAW,OAAO,SAAS,EAAE,MAAM,WAAW,OAAO,GAAG,WAAW,EAAE,OAAO,CAAC;AAAA,IACnF,OAAO,CAAC,UAAU,eAAe,gBAAgB,OAAO,cAAc,KAAK,CAAC;AAAA,EAC9E,CAAC;AAAA,EACD,MAAM,OAAO,aAAa,MAAM;AAAA,EAEhC,OAAO,OAAO,YAAY,OAAO,OAAc,aAAK,OAAO,IAAI,CAAC,IAAI;AAAA,CACrE,EAAE,KACM,iBAAS,sBAAsB;AAAA,EACpC,YAAY,EAAE,YAAY,MAAM,kBAAkB,OAAO;AAC3D,CAAC,CACH;AASK,IAAM,UAAU,CACrB,QACA,QACA,SACA,WAEO,YAAI,UAAU,GAAG;AAAA,EACtB,MAAM,SACJ,OAAO,sBAAsB,GAAG,UAAU,YACtC,CAAC,IACD,OAAO,QACL,aACA,QACA,OAAO,QAAQ,WAAW;AAAA,IACxB,MAAM,OAAO,MAAM,OAAO,UAAU,WAAW,YAAY,YAAY,EAAE,OAAO,GAAG;AAAA,MACjF;AAAA,MACA;AAAA,IACF,CAAC;AAAA,IACD,OAAO,EAAE,OAAO,KAAK,OAAO,MAAM,KAAK,WAAW;AAAA,KAEpD,MACF;AAAA,EACN,MAAM,QAAQ,OAAO,IAAI,CAAC,SACxB,QAAQ,EAAE,MAAM,KAAK,MAAM,aAAa,KAAK,aAAa,aAAa,KAAK,YAAY,CAAC,CAC3F;AAAA,EACA,MAAM,aAAqB,aAAK,GAAG,KAAK;AAAA,EACxC,MAAM,WAAW,OAAO,YACtB,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,MAAM,KAAK,QAAQ,QAAQ,QAAQ,KAAK,IAAI,CAAC,CAAC,CAC1E;AAAA,EACA,MAAM,UAAU,OAAO,WAAW,WAAW,QAAQ;AAAA,EACrD,OAAO,OAAO,WAAW,KAAY,uBAAe,OAAO,CAAC;AAAA,CAC7D;;;ALCH,SAAS,WAAW,CAAC,QAAgB,QAAgB,QAAgB;AAAA,EACnE,MAAM,gBAAuB,gBAAQ,MAGnC,OAAO,sBAAsB,GAAG,cAAc,YACnC,gBAAoC,CAAC,CAAC,IAC7C,QACE,iBACA,QACA,OAAO,QAAQ,WAAW;AAAA,IACxB,MAAM,OAAO,MAAM,OAAO,cAAc,WAAW,YAAY,YAAY,EAAE,OAAO,GAAG;AAAA,MACrF;AAAA,IACF,CAAC;AAAA,IACD,OAAO;AAAA,MACL,OAAO,KAAK,UAAU,IAAI,CAAC,cAA2B;AAAA,QACpD,KAAK,SAAS;AAAA,QACd,MAAM,SAAS;AAAA,QACf,aAAa,SAAS;AAAA,QACtB,UAAU,SAAS;AAAA,MACrB,EAAE;AAAA,MACF,MAAM,KAAK;AAAA,IACb;AAAA,KAEF,MACF,CACN,EAAE,KAAY,iBAAS,2BAA2B,EAAE,YAAY,EAAE,kBAAkB,OAAO,EAAE,CAAC,CAAC;AAAA,EAE/F,MAAM,eAAe,CAAC,QACb,mBAAW;AAAA,IAChB,KAAK,OAAO,WAAuD;AAAA,MACjE,MAAM,OAAO,MAAM,OAAO,aAAa,EAAE,IAAI,GAAG,EAAE,OAAO,CAAC;AAAA,MAC1D,OAAO,KAAK,SAAS,IAAI,CAAC,WAAW;AAAA,QACnC,KAAK,MAAM;AAAA,QACX,UAAU,MAAM;AAAA,QAChB,MAAM,UAAU,QAAQ,MAAM,OAAO;AAAA,QACrC,MAAM,UAAU,QAAQ,MAAM,OAAO;AAAA,MACvC,EAAE;AAAA;AAAA,IAEJ,OAAO,CAAC,UAAU,IAAI,gBAAgB,EAAE,WAAW,gBAAgB,QAAQ,OAAO,OAAO,CAAC;AAAA,EAC5F,CAAC,EAAE,KACM,iBAAS,0BAA0B;AAAA,IACxC,YAAY,EAAE,gBAAgB,KAAK,kBAAkB,OAAO;AAAA,EAC9D,CAAC,CACH;AAAA,EAEF,OAAO,EAAE,eAAe,aAAa;AAAA;AAahC,SAAS,UAAU,CACxB,KACA,SACA,SACyD;AAAA,EACzD,OAAc,YAAI,UAAU,GAAG;AAAA,IAC7B,MAAM,UAAU,OAAO,gBAAgB,SAAS,oBAAoB,0BAA0B;AAAA,IAG9F,MAAM,SAAS,OAAc,YAAI;AAAA,MAC/B,KAAK,MAAM,IAAI,IAAI,GAAG;AAAA,MACtB,OAAO,MACL,IAAI,gBAAgB;AAAA,QAClB,WAAW;AAAA,QACX,QAAQ;AAAA,QACR,OAAO;AAAA,MACT,CAAC;AAAA,IACL,CAAC;AAAA,IACD,MAAM,SAAS,YAAY,MAAM;AAAA,IAIjC,MAAM,SAAS,SAAS,QAAQ,aAAa,OAAO,CAAC;AAAA,IAIrD,OAAO,OAAO,gBAAgB,QAAQ,SAAS,QAAQ,SAAS,QAAQ,CAAC,WAChE,YAAI,UAAU,GAAG;AAAA,MACtB,MAAM,UAAU,OAAO,QAAQ,QAAQ,QAAQ,SAAS,MAAM;AAAA,MAC9D,OAAO,EAAE,YAAY,YAAY,QAAQ,QAAQ,MAAM,EAAE;AAAA,KAC1D,CACH;AAAA,GACD;AAAA;",
19
+ "debugId": "61F68CACADDB04EE64756E2164756E21",
20
+ "names": []
21
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Connect to a Streamable HTTP MCP server with the official SDK client: the transport, the
3
+ * caller's headers on every request, and the WHOLE handshake (`initialize` and the
4
+ * `notifications/initialized` that follows it) bounded in time and interruptible.
5
+ *
6
+ * ⛔ HEADERS ARE CREDENTIALS. They ride `requestInit` to the server and nowhere else: not in an
7
+ * error, a span or a log (mcp-error.ts). A `Redacted` value is unwrapped only here, at the
8
+ * moment the transport is built.
9
+ */
10
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
11
+ import * as Effect from 'effect/Effect';
12
+ import * as Redacted from 'effect/Redacted';
13
+ import type * as Scope from 'effect/Scope';
14
+ import { McpToolkitError, type Redact } from './mcp-error.ts';
15
+ /** Header values; a `Redacted` is unwrapped only at the moment the transport is built. */
16
+ export type McpHeaders = Readonly<Record<string, string | Redacted.Redacted<string>>>;
17
+ export declare const DEFAULT_CONNECT_TIMEOUT_MS = 15000;
18
+ /**
19
+ * The longest a timer can run: `setTimeout` takes a 32-bit signed delay, and a larger one (like
20
+ * one below 1) is set to 1 ms. Node documents that; Bun does the same (measured, see below).
21
+ */
22
+ export declare const MAX_TIMEOUT_MS: number;
23
+ /**
24
+ * ⛔ A timeout that is not a finite number of milliseconds in `(0, MAX_TIMEOUT_MS]` is a
25
+ * programming error, and dies with a `RangeError` before any request, as `maxRounds` does.
26
+ * The budget feeds two timers (ours and the SDK's per request), and an out-of-range value made
27
+ * them disagree: measured 2026-09-29 (review of PR 328) against a healthy local stub,
28
+ * `Infinity` failed at once ("did not finish within Infinity ms") while `0`, `-1`, `NaN` and
29
+ * `2 ** 31` each happened to succeed, because the timer clamped to 1 ms and the handshake won
30
+ * the race. A real handshake takes longer than that 1 ms (not measured remotely), so those
31
+ * would lose it.
32
+ */
33
+ export declare const validateTimeout: (timeout: number) => Effect.Effect<number>;
34
+ /**
35
+ * The header values as plain strings, for `redactor` and for nothing else: a value an error
36
+ * message must never repeat is exactly a value the redactor has to know. Kept here so that a
37
+ * `Redacted` is unwrapped in this file only.
38
+ */
39
+ export declare function headerValues(headers: McpHeaders | undefined): string[];
40
+ /**
41
+ * Connect, handshake and BUILD whatever the caller needs from the client (`build`: for
42
+ * `mcpToolkit`, listing the tools and making the toolkit). The client lives as long as the
43
+ * surrounding `Scope`, and only when ALL of that worked.
44
+ *
45
+ * ★ THE FINALIZER IS REGISTERED LAST: after the handshake AND after `build`. A scoped constructor
46
+ * that fails must leave nothing in the caller's scope, and this one has been wrong twice:
47
+ * - it used to register the `Client`'s `close` before the handshake. Each `Client` carries its
48
+ * own JSON Schema validator (SDK client/index.js), so a seat retrying `mcpToolkit` through a
49
+ * gateway outage held one per failed attempt until its scope closed: measured 2026-09-29
50
+ * (review of PR 328), 64.7 MB against 10.6 MB after 3 001 refused attempts in one scope,
51
+ * about 18 KB each. `handshake` closes the client on every failure, so a refused handshake
52
+ * has nothing left to release.
53
+ * - it then registered it right after the handshake, BEFORE the `tools/list` that `build` does,
54
+ * and that listing can fail too: a JSON-RPC error, a page held past `connectTimeoutMs`, a
55
+ * caller who interrupts. Measured 2026-09-29 (review of PR 328, later round), a loopback
56
+ * server that completed the handshake: 20 attempts whose listing errored left 20 finalizers
57
+ * and 20 open SSE streams in one scope; 10 whose listing was held left 10 finalizers, 10
58
+ * streams and 10 `tools/list` POSTs still open (the SDK's per-request timeout rejects the
59
+ * promise and does not abort the fetch). A seat retrying once a second against a slow gateway
60
+ * is about 3 600 sessions and sockets an hour, held for the life of the seat.
61
+ * `close` aborts the transport's fetches (the SSE stream and a held POST alike), which is why
62
+ * closing the client is what releases them.
63
+ * ⚠️ `uninterruptibleMask` keeps the gaps between the steps from being interruption points (a
64
+ * client opened and never closed); only the handshake and `build` are restored to
65
+ * interruptible, and each has an `onError` that closes the client when it fails OR is
66
+ * interrupted, so the SDK's own close-on-abort is not the only line of defence. `onError` runs
67
+ * uninterruptibly, so the close itself cannot be cut off.
68
+ * ★ THE `seat.mcp.connect` SPAN IS THE HANDSHAKE ONLY, not `build`: it is what a slow or refused
69
+ * connect looks like in a trace, and the listing is not that.
70
+ */
71
+ export declare const connectAndBuild: <A>(url: URL, headers: McpHeaders | undefined, server: string, timeout: number, redact: Redact, build: (client: Client) => Effect.Effect<A, McpToolkitError>) => Effect.Effect<A, McpToolkitError, Scope.Scope>;
72
+ //# sourceMappingURL=mcp-connect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-connect.d.ts","sourceRoot":"","sources":["../src/mcp-connect.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAInE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAG9D,0FAA0F;AAC1F,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAEtF,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,MAAoB,CAAC;AAElD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,YAAa,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAO/D,CAAC;AAUR;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,EAAE,CAEtE;AAsDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,OAC1B,GAAG,WACC,UAAU,GAAG,SAAS,UACvB,MAAM,WACL,MAAM,UACP,MAAM,SACP,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,eAAe,CAAC,KAC3D,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,eAAe,EAAE,KAAK,CAAC,KAAK,CAe7C,CAAC"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `mcpToolkit`'s typed failure, and the one place an MCP server's address is spelled for a
3
+ * message or a span.
4
+ *
5
+ * ⛔ THE ADDRESS IS ORIGIN AND PATH ONLY. A query string is where a token lands when a server
6
+ * wants one there, and the headers a caller passes are bearer credentials: neither may reach
7
+ * an error message, a log line or a span attribute. Header NAMES and the header map are never
8
+ * read here; a header VALUE is read only to be redacted (`redactor`).
9
+ * 🔴 A SERVER ECHOES A VALUE ON ITS OWN. Redacting the query STRING and the address is not
10
+ * enough: a server that answers "rejected key QVALUE" prints the value alone, and one that says
11
+ * "bad credential Bearer HVALUE" prints a header's. Measured 2026-09-29 (review of PR 328,
12
+ * round 2): both reached `McpToolkitError.message`. So `redactor` also takes out each query
13
+ * value and each header value the caller passed (`MIN_SECRET_LENGTH` and up). What it cannot
14
+ * catch: a value the server TRANSFORMS (hashed, base64, truncated), and one under the minimum.
15
+ * 🔴 THE `cause` IS A COPY, NEVER THE ORIGINAL. Measured 2026-09-29 (review of PR 328): under Bun a
16
+ * refused fetch is a `TypeError` whose own `path` field is the FULL URL, query string included,
17
+ * so attaching it as `cause` printed the token through `Bun.inspect`, `console.error`, an
18
+ * uncaught rejection and the default `Effect.logError` — while `error.message` was clean, which
19
+ * is all the first tests looked at. `scrubCause` rebuilds the chain from name, message, stack
20
+ * and a string or numeric `code` only, so no other field (`path`, `url`, `data`, `body`) is
21
+ * carried, and `redactor` takes the query string, fragment and password out of what remains.
22
+ * The price: `cause instanceof McpError` no longer holds; read `cause.code` instead.
23
+ */
24
+ /** Which call failed. `connect` covers the transport and the whole handshake (mcp-connect.ts). */
25
+ export type McpOperation = 'connect' | 'listTools' | 'listResources' | 'readResource';
26
+ /** A server address safe to print: `https://host:port/path`, no credentials, query or fragment. */
27
+ export declare function serverLabel(url: URL): string;
28
+ /** Text with a server's query string, fragment and password taken out of it. */
29
+ export type Redact = (text: string) => string;
30
+ /**
31
+ * The shortest value `redactor` treats as a secret ON ITS OWN. Redacting every occurrence of a
32
+ * value costs legible messages (a `?v=1` would blank every "1"), and a value this short is not
33
+ * a credential anyone relies on. The query STRING, fragment and password are redacted whole
34
+ * whatever their length.
35
+ */
36
+ export declare const MIN_SECRET_LENGTH = 6;
37
+ /**
38
+ * A `Redact` for one server address, plus any header values the caller passes. The whole URL
39
+ * becomes `serverLabel`; a bare query string, fragment or password left over, each query value
40
+ * and each header value (`MIN_SECRET_LENGTH` and up, longest first) becomes `[redacted]`: a
41
+ * server can echo the address or a value back in an error body, and a fetch failure can print it.
42
+ */
43
+ export declare function redactor(url: URL, headerValues?: ReadonlyArray<string>): Redact;
44
+ /**
45
+ * A copy of `cause` that is safe to print. ⚠️ An allowlist, not a denylist: an Error is rebuilt
46
+ * from its name, message, stack and `code` (a string or number) and its own `cause`, each run
47
+ * through `redact`; every other own field is left behind, whatever it is called. See the header.
48
+ */
49
+ export declare function scrubCause(cause: unknown, redact: Redact, depth?: number): unknown;
50
+ /** The message of whatever a promise rejected with; an SDK `McpError` carries the JSON-RPC text. */
51
+ export declare function describeCause(cause: unknown): string;
52
+ /**
53
+ * The connection, a listing or a resource read failed.
54
+ *
55
+ * ★ A PLAIN `Error` WITH A `_tag`, NOT `Data.TaggedError`. `class X extends Data.TaggedError(…)<…>`
56
+ * is TS9021 under `isolatedDeclarations` ("extends clause can't contain an expression"; measured
57
+ * 2026-09-15, see packages/alchemy/tsconfig.json), and this package keeps that flag on. The
58
+ * `_tag` is all `Effect.catchTag('McpToolkitError', …)` reads.
59
+ */
60
+ export declare class McpToolkitError extends Error {
61
+ readonly _tag: 'McpToolkitError';
62
+ readonly operation: McpOperation;
63
+ /** `serverLabel` of the server: never a header, never a query string. */
64
+ readonly server: string;
65
+ constructor(fields: {
66
+ readonly operation: McpOperation;
67
+ readonly server: string;
68
+ /**
69
+ * What the SDK threw: an `McpError` (a JSON-RPC error), a `StreamableHTTPError`, a fetch
70
+ * failure. Never stored as given: `cause` on the error is `scrubCause` of it.
71
+ */
72
+ readonly cause: unknown;
73
+ /** `redactor` of the server's URL. Left out, only the structural scrub of `cause` applies. */
74
+ readonly redact?: Redact | undefined;
75
+ });
76
+ }
77
+ //# sourceMappingURL=mcp-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-error.d.ts","sourceRoot":"","sources":["../src/mcp-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,kGAAkG;AAClG,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,WAAW,GAAG,eAAe,GAAG,cAAc,CAAC;AAEtF,mGAAmG;AACnG,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAE5C;AAED,gFAAgF;AAChF,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;AAE9C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAoBnC;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,YAAY,GAAE,aAAa,CAAC,MAAM,CAAM,GAAG,MAAM,CAmBnF;AAKD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,OAAO,CAa7E;AAED,oGAAoG;AACpG,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED;;;;;;;GAOG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,EAAG,iBAAiB,CAAU;IAC3C,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,YAAY,MAAM,EAAE;QAClB,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;QACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB;;;WAGG;QACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QACxB,8FAA8F;QAC9F,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KACtC,EASA;CACF"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * MCP's cursor pagination, followed to the end — but not forever.
3
+ *
4
+ * ⛔ THE PAGE COUNT IS CAPPED. A server that answers every page with another `nextCursor` (a
5
+ * bug, or a hostile one) would otherwise hold a seat in a listing loop before its first
6
+ * model call. A hundred pages is far past any estate server's tool list; the cap fails the
7
+ * listing loudly instead of truncating it quietly.
8
+ */
9
+ import * as Effect from 'effect/Effect';
10
+ import { type McpOperation, McpToolkitError, type Redact } from './mcp-error.ts';
11
+ export declare const MAX_PAGES = 100;
12
+ export type Page<T> = {
13
+ readonly items: ReadonlyArray<T>;
14
+ readonly next: string | undefined;
15
+ };
16
+ /** Every item of a paged listing, or an `McpToolkitError` for `operation`. Interruption aborts the request in flight. */
17
+ export declare function collect<T>(operation: McpOperation, server: string, page: (cursor: string | undefined, signal: AbortSignal) => Promise<Page<T>>, redact?: Redact | undefined): Effect.Effect<ReadonlyArray<T>, McpToolkitError>;
18
+ //# sourceMappingURL=mcp-pages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-pages.d.ts","sourceRoot":"","sources":["../src/mcp-pages.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,KAAK,YAAY,EAAE,eAAe,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAEjF,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI;IAAE,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAE9F,yHAAyH;AACzH,wBAAgB,OAAO,CAAC,CAAC,EACvB,SAAS,EAAE,YAAY,EACvB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAC3E,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,GAC1B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC,CAelD"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * An MCP tool result as the text a model reads.
3
+ *
4
+ * ★ ALWAYS A STRING. A result is a list of content blocks (text, image, audio, a resource, a
5
+ * link), and what goes into the model's context is one message. Text is passed through
6
+ * verbatim, joined by newlines; everything else becomes a one-line marker naming what it was.
7
+ * ⛔ A BINARY BLOCK NEVER REACHES THE MODEL AS ITS BYTES. An image's base64 is tens of
8
+ * kilobytes of context that says nothing to a text model and costs every later round; the
9
+ * marker keeps its type and MIME type so the model knows something was there.
10
+ * ⚠️ AN EMPTY RESULT IS NOT AN EMPTY STRING. Some providers refuse a tool message with empty
11
+ * content (not measured against cf-code), and a refused request ends the whole run over a
12
+ * tool that merely had nothing to say.
13
+ */
14
+ /** What the model reads for a tool that returned no content blocks. */
15
+ export declare const EMPTY_RESULT = "(no content)";
16
+ /** The text of a `CallToolResult.content`. Anything that is not a block list is JSON, not dropped. */
17
+ export declare function renderContent(content: unknown): string;
18
+ /**
19
+ * The text of a whole `callTool` result. ★ The `content` blocks are what MCP asks a server to
20
+ * always send; a server that sends only `structuredContent` (an `outputSchema` tool that skipped
21
+ * the text copy) would otherwise read as empty, so its JSON is shown instead. The legacy
22
+ * `toolResult` shape (the SDK's compatibility result) is read as content.
23
+ */
24
+ export declare function renderResult(result: Readonly<Record<string, unknown>>): string;
25
+ //# sourceMappingURL=mcp-render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-render.d.ts","sourceRoot":"","sources":["../src/mcp-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,uEAAuE;AACvE,eAAO,MAAM,YAAY,iBAAiB,CAAC;AA4B3C,sGAAsG;AACtG,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAQtD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAM9E"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * One MCP tool as an Effect AI dynamic tool: the server's own JSON Schema goes to the model,
3
+ * and the tool call the model sends back decodes without losing its arguments.
4
+ *
5
+ * ★ `Tool.dynamic` WITH THE SERVER'S JSON SCHEMA IS THE RIGHT SHAPE (its docs name MCP tools
6
+ * discovered at runtime), and the model sees that schema verbatim.
7
+ * 🔴 BUT @effect/ai-openai-compat rc.115 CANNOT DECODE A TOOL CALL FOR IT. Measured 2026-09-29
8
+ * (tests/seat-loop.test.ts, first run): the request goes out, and when the model's reply asks
9
+ * for the tool, `transformToolCallParams` runs the OpenAI structured-output codec over the
10
+ * tool's `parametersSchema` — `Schema.Unknown` in JSON-Schema mode — and fails the whole turn
11
+ * with `UnsupportedSchemaError: Root JSON Schema must have type "object" and must not use
12
+ * "anyOf"`. rc.118 fixed it upstream: it returns the raw params for a dynamic tool that has a
13
+ * `jsonSchema`. The estate is pinned at rc.115 and rc.118 drops the `unstable/` import prefix,
14
+ * so this file carries the workaround until the pin moves.
15
+ * ★ THE WORKAROUND: keep `jsonSchema` (what the model is sent, and what `Tool.getJsonSchema`
16
+ * returns), and replace `parametersSchema` — what compat's codec and the Toolkit decode with —
17
+ * by an object schema of the DECLARED property names, each `Unknown`, decoded to `Unknown`
18
+ * with an ENCODE THAT IS FORBIDDEN. Every declared value passes through untouched and
19
+ * `required` is honoured, so a missing argument fails as a tool result the model can read
20
+ * instead of reaching the server.
21
+ * 🔴 WHY THE ENCODE IS FORBIDDEN. compat's `transformToolCallParams` (OpenAiLanguageModel.js)
22
+ * decodes the model's params through the OpenAI structured-output codec and re-encodes them
23
+ * with `parametersSchema`, falling back to the params AS SENT when either step fails. That
24
+ * codec rewrites every optional property as nullable and reads `null` as ABSENT, so with a
25
+ * plain object schema an explicit `null` on an optional argument was deleted before the call.
26
+ * Measured 2026-09-29 (review of PR 328), end to end through `SeatModel` and `mcpToolkit` into
27
+ * an Effect `McpServer`: `update_issue {id, assignee: null}` (null unassigns, omitted leaves
28
+ * alone) reached the server as `{id}`, and the run ended 'done'. That normalisation exists for
29
+ * the codec's own JSON Schema, and the model here was sent the server's, where `null` is a
30
+ * value. Forbidding the encode makes the re-encode fail, so compat forwards the params as the
31
+ * model sent them: `null` arrives as `null` (pinned in tests/mcp-arguments.test.ts).
32
+ * ⚠️ WHAT IT COSTS: a property the schema does not declare is DROPPED before the call
33
+ * (Effect's decoder ignores excess keys; v4 has no "preserve"), so a server that declares
34
+ * `additionalProperties: true` and relies on undeclared keys gets fewer than the model sent.
35
+ * And a tool that declares no properties takes no arguments: `Tool.EmptyParams` is the only
36
+ * root the codec accepts for it, and it rejects any key.
37
+ * ⛔ REMOVE THIS WHEN THE PIN REACHES rc.118 or later: build the tool from `Tool.dynamic` alone.
38
+ * The clone below copies what `Tool`'s own `setParameters` copies (its prototype and own
39
+ * fields), so it depends on Effect's tool object layout; the end-to-end test is what fails
40
+ * first if a bump changes that.
41
+ */
42
+ import type * as JsonSchema from 'effect/JsonSchema';
43
+ import * as Schema from 'effect/Schema';
44
+ import * as Tool from 'effect/unstable/ai/Tool';
45
+ /** One dynamic tool per MCP tool: the server's JSON Schema in, text out, failures returned. */
46
+ export type McpTool = Tool.Dynamic<string, {
47
+ readonly parameters: JsonSchema.JsonSchema;
48
+ readonly success: typeof Schema.String;
49
+ readonly failure: typeof Schema.String;
50
+ readonly failureMode: 'return';
51
+ }>;
52
+ /** What `client.listTools()` gives for one tool, narrowed to what is used here. */
53
+ export type McpToolSpec = {
54
+ readonly name: string;
55
+ readonly description?: string | undefined;
56
+ readonly inputSchema: JsonSchema.JsonSchema;
57
+ };
58
+ /** The workaround's `parametersSchema`: the declared property names, each passed through as-is, decode-only. */
59
+ export declare function declaredParameters(inputSchema: JsonSchema.JsonSchema): Schema.Constraint;
60
+ export declare function mcpTool(spec: McpToolSpec): McpTool;
61
+ //# sourceMappingURL=mcp-tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-tool.d.ts","sourceRoot":"","sources":["../src/mcp-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,KAAK,KAAK,UAAU,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,IAAI,MAAM,yBAAyB,CAAC;AAEhD,+FAA+F;AAC/F,MAAM,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAChC,MAAM,EACN;IACE,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC,UAAU,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,OAAO,MAAM,CAAC,MAAM,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,OAAO,MAAM,CAAC,MAAM,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC;CAChC,CACF,CAAC;AAEF,mFAAmF;AACnF,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,UAAU,CAAC;CAC7C,CAAC;AAEF,gHAAgH;AAChH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,UAAU,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAsBxF;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAWlD"}
@@ -0,0 +1,61 @@
1
+ import * as Effect from 'effect/Effect';
2
+ import type * as Scope from 'effect/Scope';
3
+ import type * as Toolkit from 'effect/unstable/ai/Toolkit';
4
+ import { type McpHeaders } from './mcp-connect.ts';
5
+ import { McpToolkitError } from './mcp-error.ts';
6
+ import { type McpTools } from './mcp-toolset.ts';
7
+ export type { McpTools } from './mcp-toolset.ts';
8
+ export type McpResource = {
9
+ readonly uri: string;
10
+ readonly name: string;
11
+ readonly description?: string | undefined;
12
+ readonly mimeType?: string | undefined;
13
+ };
14
+ /** One entry of a resource read: `text` for text, `blob` (base64) for binary. */
15
+ export type McpResourceContent = {
16
+ readonly uri: string;
17
+ readonly mimeType?: string | undefined;
18
+ readonly text?: string | undefined;
19
+ readonly blob?: string | undefined;
20
+ };
21
+ export type McpToolkit = {
22
+ /** Every tool the server listed, with handlers that call it: hand this to `runRounds`. */
23
+ readonly toolkit: Toolkit.WithHandler<McpTools>;
24
+ /** The server's resources; empty when it does not advertise the resources capability. */
25
+ readonly listResources: Effect.Effect<ReadonlyArray<McpResource>, McpToolkitError>;
26
+ /** The contents of one resource; fails with `McpToolkitError` for an unknown URI. */
27
+ readonly readResource: (uri: string) => Effect.Effect<ReadonlyArray<McpResourceContent>, McpToolkitError>;
28
+ };
29
+ export type McpToolkitOptions = {
30
+ /**
31
+ * The budget for STARTING UP, in milliseconds; default 15 000. A finite number above 0 and at
32
+ * most 2 ** 31 - 1, or a `RangeError` defect before any request (mcp-connect.ts).
33
+ * It covers, each with the full budget:
34
+ * - the WHOLE handshake: `initialize` and the `notifications/initialized` that follows it;
35
+ * - every `tools/list` page read after it (a server holding one fails the call as
36
+ * `McpToolkitError` with `operation: 'listTools'`).
37
+ * ⚠️ It is a budget PER REQUEST, not a total: a server that answers each of its (up to 100)
38
+ * tool pages in just under the budget can still take that many budgets. Wrap the whole call
39
+ * in `Effect.timeout` for a total.
40
+ * ⚠️ The SDK bounds only the `initialize` request (its default is 60 s) and leaves the
41
+ * notification unbounded, so a server that answers `initialize` and then stalls would hold a
42
+ * seat at startup indefinitely. Here the budget covers both, and the handshake and the
43
+ * listing are interruptible, so a caller's `Effect.timeout` or a shutdown also ends them, and
44
+ * either way the client is closed and nothing is left in your scope (mcp-connect.ts).
45
+ * ⚠️ NOT COVERED: `listResources`, `readResource` and tool calls, which are requests you make
46
+ * after startup and keep the SDK's 60 s default per request. They are interruptible too.
47
+ */
48
+ readonly connectTimeoutMs?: number | undefined;
49
+ };
50
+ /**
51
+ * Connect to a Streamable HTTP MCP server, list its tools, and return them as a toolkit.
52
+ * The connection lives as long as the surrounding `Scope`.
53
+ *
54
+ * ★ A FAILURE AT ANY POINT LEAVES THE CALLER'S SCOPE EMPTY: the handshake, the `tools/list`
55
+ * (a JSON-RPC error, a page held past `connectTimeoutMs`) and building the toolkit all run
56
+ * before the client's finalizer is registered, and each closes the client if it fails or is
57
+ * interrupted (`connectAndBuild`, mcp-connect.ts). So `Effect.retry` around `mcpToolkit`
58
+ * accumulates no clients, sessions or sockets.
59
+ */
60
+ export declare function mcpToolkit(url: string | URL, headers?: McpHeaders, options?: McpToolkitOptions): Effect.Effect<McpToolkit, McpToolkitError, Scope.Scope>;
61
+ //# sourceMappingURL=mcp-toolkit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-toolkit.d.ts","sourceRoot":"","sources":["../src/mcp-toolkit.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,KAAK,OAAO,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAEL,KAAK,UAAU,EAIhB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAsC,MAAM,gBAAgB,CAAC;AAErF,OAAO,EAAE,KAAK,QAAQ,EAAW,MAAM,kBAAkB,CAAC;AAE1D,YAAY,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC,CAAC;AAEF,iFAAiF;AACjF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,0FAA0F;IAC1F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IAChD,yFAAyF;IACzF,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,EAAE,eAAe,CAAC,CAAC;IACnF,qFAAqF;IACrF,QAAQ,CAAC,YAAY,EAAE,CACrB,GAAG,EAAE,MAAM,KACR,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,kBAAkB,CAAC,EAAE,eAAe,CAAC,CAAC;CACxE,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChD,CAAC;AAmDF;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,CAAC,EAAE,UAAU,EACpB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,EAAE,KAAK,CAAC,KAAK,CAAC,CA6BzD"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * An MCP server's tools as a ready-to-run Effect AI toolkit: list them (bounded by the startup
3
+ * budget), wrap each as a `Tool.dynamic` (mcp-tool.ts), and give each a handler that calls the
4
+ * server. Split from mcp-toolkit.ts so that `connectAndBuild` can run all of this BEFORE the
5
+ * client's finalizer is registered (mcp-connect.ts): listing can fail, and a failed listing must
6
+ * leave nothing in the caller's scope.
7
+ *
8
+ * ⚠️ A TOOL FAILURE IS THE MODEL'S TO SEE, NOT THE RUN'S TO DIE OF (see mcp-toolkit.ts). Every
9
+ * failure below comes back as a string.
10
+ * ⛔ A FAILURE'S TEXT IS REDACTED, A SUCCESS'S IS NOT. What the model reads for an `isError`
11
+ * result, a JSON-RPC error or a dropped call goes through `redact`: a server that rejects a
12
+ * credential typically prints it ("bad credential Bearer ..."), and that text goes on to a
13
+ * cloud model. Measured 2026-09-29 (review of PR 328, later round): an `isError` result that
14
+ * echoed a query value and a bearer token delivered both to the model, while the same echo in
15
+ * an `McpToolkitError` was already redacted. A SUCCESSFUL result is the tool's payload and is
16
+ * delivered verbatim: rewriting it would corrupt legitimate data that happens to contain a
17
+ * value, and a server that returns a credential as data is not a failure this can tell apart.
18
+ */
19
+ import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
20
+ import * as Effect from 'effect/Effect';
21
+ import * as Toolkit from 'effect/unstable/ai/Toolkit';
22
+ import { type McpToolkitError, type Redact } from './mcp-error.ts';
23
+ import { type McpTool } from './mcp-tool.ts';
24
+ export type McpTools = Readonly<Record<string, McpTool>>;
25
+ /**
26
+ * List the server's tools and return them as a toolkit with handlers.
27
+ * ★ A server that does not advertise tools is not asked for them: a resources-only server
28
+ * answers `tools/list` with "method not found", and one such server would otherwise make the
29
+ * whole connection unusable for its resources.
30
+ * ★ `timeout` is the startup budget: without it the SDK waits its 60 s per page.
31
+ */
32
+ export declare const toolset: (client: Client, server: string, timeout: number, redact: Redact) => Effect.Effect<Toolkit.WithHandler<McpTools>, McpToolkitError>;
33
+ //# sourceMappingURL=mcp-toolset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-toolset.d.ts","sourceRoot":"","sources":["../src/mcp-toolset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,OAAO,MAAM,4BAA4B,CAAC;AACtD,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,MAAM,EAAiB,MAAM,gBAAgB,CAAC;AAGlF,OAAO,EAAE,KAAK,OAAO,EAAW,MAAM,eAAe,CAAC;AAEtD,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AA0BzD;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,WACV,MAAM,UACN,MAAM,WACL,MAAM,UACP,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,eAAe,CA0B3D,CAAC"}