@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
package/dist/index.js ADDED
@@ -0,0 +1,508 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
17
+
18
+ // src/seat-model.ts
19
+ var exports_seat_model = {};
20
+ __export(exports_seat_model, {
21
+ clientLayer: () => clientLayer,
22
+ embeddingLayer: () => embeddingLayer,
23
+ layer: () => layer2
24
+ });
25
+ import { OpenAiClient, OpenAiEmbeddingModel, OpenAiLanguageModel } from "@effect/ai-openai-compat";
26
+ import * as Layer from "effect/Layer";
27
+ import * as Redacted from "effect/Redacted";
28
+ import * as EmbeddingModel from "effect/unstable/ai/EmbeddingModel";
29
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
30
+ import * as HttpClient from "effect/unstable/http/HttpClient";
31
+
32
+ // src/stamp.ts
33
+ import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
34
+ var TAG = /^[\x21-\x2b\x2d-\x7e]{1,128}$/;
35
+ function joinTags(tags) {
36
+ const list = typeof tags === "string" ? tags.split(",") : [...tags];
37
+ if (list.length === 0)
38
+ throw new TypeError("seat tags: at least one tag is required");
39
+ for (const tag of list) {
40
+ if (!TAG.test(tag)) {
41
+ throw new TypeError(`seat tags: ${JSON.stringify(tag)} is not a tag (1-128 printable ASCII, no comma or space)`);
42
+ }
43
+ }
44
+ return list.join(",");
45
+ }
46
+ function jsonObject(request) {
47
+ const body = request.body;
48
+ if (body._tag !== "Uint8Array" || !body.contentType.includes("json"))
49
+ return;
50
+ const text = body.text ?? new TextDecoder().decode(body.body);
51
+ let parsed;
52
+ try {
53
+ parsed = JSON.parse(text);
54
+ } catch {
55
+ return;
56
+ }
57
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
58
+ return;
59
+ return parsed;
60
+ }
61
+ function stampRequest(request, stamp) {
62
+ const headers = stamp.noCache ? { "x-litellm-tags": stamp.tags, "cache-control": "no-cache, no-store" } : { "x-litellm-tags": stamp.tags };
63
+ const tagged = HttpClientRequest.setHeaders(request, headers);
64
+ const body = jsonObject(request);
65
+ if (body === undefined)
66
+ return tagged;
67
+ return HttpClientRequest.bodyJsonUnsafe(tagged, {
68
+ ...body,
69
+ ...stamp.noCache ? { cache: { "no-cache": true, "no-store": true } } : {},
70
+ num_retries: 0,
71
+ ...stamp.metadata !== undefined && !("metadata" in body) ? { metadata: stamp.metadata } : {}
72
+ });
73
+ }
74
+
75
+ // src/seat-model.ts
76
+ function clientLayer(options) {
77
+ const stamp = {
78
+ tags: joinTags(options.tags),
79
+ noCache: options.noCache ?? true,
80
+ metadata: options.metadata
81
+ };
82
+ return OpenAiClient.layer({
83
+ apiKey: Redacted.isRedacted(options.apiKey) ? options.apiKey : Redacted.make(options.apiKey),
84
+ apiUrl: options.apiUrl.replace(/\/$/, ""),
85
+ transformClient: (http) => HttpClient.mapRequest(http, (request) => stampRequest(request, stamp))
86
+ }).pipe(Layer.provide(FetchHttpClient.layer));
87
+ }
88
+ function layer2(options) {
89
+ return OpenAiLanguageModel.layer({
90
+ model: options.model,
91
+ config: { strictJsonSchema: false, ...options.config }
92
+ }).pipe(Layer.provide(clientLayer(options)));
93
+ }
94
+ function embeddingLayer(options) {
95
+ return Layer.merge(OpenAiEmbeddingModel.layer({ model: options.model, config: options.config }), Layer.succeed(EmbeddingModel.Dimensions, options.dimensions)).pipe(Layer.provide(clientLayer(options)));
96
+ }
97
+ // src/seat-obs.ts
98
+ var exports_seat_obs = {};
99
+ __export(exports_seat_obs, {
100
+ CT100_ENDPOINTS: () => CT100_ENDPOINTS,
101
+ DEFAULT_SERVICE_NAME: () => DEFAULT_SERVICE_NAME,
102
+ layer: () => layer4
103
+ });
104
+ import * as Config from "effect/Config";
105
+ import * as ConfigProvider from "effect/ConfigProvider";
106
+ import * as Effect from "effect/Effect";
107
+ import * as Layer2 from "effect/Layer";
108
+ import * as Schema from "effect/Schema";
109
+ import * as FetchHttpClient2 from "effect/unstable/http/FetchHttpClient";
110
+ import {
111
+ OtlpLogger,
112
+ OtlpMetrics,
113
+ OtlpSerialization,
114
+ OtlpTracer
115
+ } from "effect/unstable/observability";
116
+ var CT100_ENDPOINTS = {
117
+ traces: "http://10.100.1.4:10428/insert/opentelemetry/v1/traces",
118
+ logs: "http://10.100.1.4:9428/insert/opentelemetry/v1/logs",
119
+ metrics: "http://10.100.1.4:8428/opentelemetry/v1/metrics"
120
+ };
121
+ var DEFAULT_SERVICE_NAME = "seat-runtime";
122
+ var resourceAttributes = Config.Record(Schema.StringFromUriComponent, Schema.StringFromUriComponent, "OTEL_RESOURCE_ATTRIBUTES").pipe(Config.withDefault(undefined));
123
+ var defaults = Effect.gen(function* () {
124
+ const current = yield* ConfigProvider.ConfigProvider;
125
+ const base = yield* current.load(["OTEL_EXPORTER_OTLP_ENDPOINT"]);
126
+ const attributes = yield* resourceAttributes.parse(current);
127
+ const named = attributes?.["service.name"] !== undefined;
128
+ return ConfigProvider.fromUnknown({
129
+ OTEL_TRACES_EXPORTER: "otlp",
130
+ OTEL_LOGS_EXPORTER: "otlp",
131
+ OTEL_METRICS_EXPORTER: "otlp",
132
+ ...named ? {} : { OTEL_SERVICE_NAME: DEFAULT_SERVICE_NAME },
133
+ ...base === undefined ? {
134
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: CT100_ENDPOINTS.traces,
135
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: CT100_ENDPOINTS.logs,
136
+ OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: CT100_ENDPOINTS.metrics
137
+ } : {}
138
+ });
139
+ }).pipe(Effect.orDie);
140
+ var layer4 = Layer2.mergeAll(OtlpTracer.layerFromConfig(), OtlpLogger.layerFromConfig(), OtlpMetrics.layerFromConfig()).pipe(Layer2.provide(OtlpSerialization.layerProtobuf), Layer2.provide(FetchHttpClient2.layer), Layer2.provide(ConfigProvider.layerAdd(defaults)));
141
+ // src/rounds.ts
142
+ import * as Effect2 from "effect/Effect";
143
+ import * as Metric from "effect/Metric";
144
+ import * as Prompt from "effect/unstable/ai/Prompt";
145
+ var isToolRefusal = (error) => error.reason._tag === "ToolNotFoundError" || error.reason._tag === "InvalidOutputError" && error.module === "LanguageModel";
146
+ var roundsTotal = Metric.counter("seat_rounds_total", {
147
+ description: "Model turns taken by runRounds that returned; a refused forced final turn is not counted."
148
+ });
149
+ var roundsCapped = Metric.counter("seat_rounds_capped_total", {
150
+ description: "runRounds runs that hit maxRounds and were given a forced final turn."
151
+ });
152
+ var roundsUnanswered = Metric.counter("seat_rounds_unanswered_total", {
153
+ description: "runRounds runs whose forced final turn came back as a tool call, not an answer."
154
+ });
155
+ function runRounds(options) {
156
+ const { chat, toolkit, maxRounds, onRound } = options;
157
+ return Effect2.gen(function* () {
158
+ if (!Number.isInteger(maxRounds) || maxRounds < 1) {
159
+ return yield* Effect2.die(new RangeError(`runRounds: maxRounds must be a positive integer, got ${String(maxRounds)}`));
160
+ }
161
+ const finish = (round2, forced2) => Effect2.fnUntraced(function* (response2) {
162
+ yield* Metric.update(roundsTotal, 1);
163
+ yield* Effect2.annotateCurrentSpan({ "seat.tool_calls": response2.toolCalls.length });
164
+ if (onRound !== undefined)
165
+ yield* onRound({ round: round2, forced: forced2, response: response2 });
166
+ return response2;
167
+ });
168
+ const span = (round2, forced2) => Effect2.withSpan("seat.round", {
169
+ attributes: { "seat.round": round2, "seat.round.forced": forced2 }
170
+ });
171
+ const turn = (round2) => chat.generateText({ prompt: Prompt.empty, toolkit }).pipe(Effect2.flatMap(finish(round2, false)), span(round2, false));
172
+ let round = 1;
173
+ let response = yield* turn(round);
174
+ while (response.toolCalls.length > 0 && round < maxRounds) {
175
+ round += 1;
176
+ response = yield* turn(round);
177
+ }
178
+ if (response.toolCalls.length === 0) {
179
+ return { response, rounds: round, capped: false, unanswered: false };
180
+ }
181
+ yield* Metric.update(roundsCapped, 1);
182
+ yield* Effect2.logWarning("seat round cap reached; forcing a final turn without tools", {
183
+ maxRounds
184
+ });
185
+ const forcedRound = maxRounds + 1;
186
+ const refused = () => Effect2.as(Effect2.all([
187
+ Metric.update(roundsUnanswered, 1),
188
+ Effect2.logWarning("seat forced final turn refused: the model still asked for a tool", {
189
+ maxRounds
190
+ })
191
+ ]), undefined);
192
+ const forced = yield* chat.generateText({ prompt: Prompt.empty, toolChoice: "none" }).pipe(Effect2.flatMap(finish(forcedRound, true)), span(forcedRound, true), Effect2.catchTag("AiError", (error) => isToolRefusal(error) ? refused() : Effect2.fail(error)));
193
+ return forced === undefined ? { response, rounds: maxRounds, capped: true, unanswered: true } : { response: forced, rounds: forcedRound, capped: true, unanswered: false };
194
+ });
195
+ }
196
+ // src/mcp-toolkit.ts
197
+ import * as Effect6 from "effect/Effect";
198
+
199
+ // src/mcp-connect.ts
200
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
201
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
202
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
203
+ import * as Effect3 from "effect/Effect";
204
+ import * as Redacted2 from "effect/Redacted";
205
+
206
+ // src/mcp-error.ts
207
+ function serverLabel(url) {
208
+ return `${url.origin}${url.pathname}`;
209
+ }
210
+ var MIN_SECRET_LENGTH = 6;
211
+ function queryValues(url) {
212
+ const raw = url.search.slice(1).split("&").map((pair) => pair.slice(pair.indexOf("=") + 1));
213
+ return [...raw, ...url.searchParams.values()];
214
+ }
215
+ function headerParts(value) {
216
+ const token = value.trim().split(/\s+/).at(-1) ?? "";
217
+ return [value, token];
218
+ }
219
+ function redactor(url, headerValues = []) {
220
+ const label = serverLabel(url);
221
+ const whole = [
222
+ url.search.length > 1 ? url.search : "",
223
+ url.hash.length > 1 ? url.hash : "",
224
+ url.password
225
+ ];
226
+ const parts = [...queryValues(url), ...headerValues.flatMap(headerParts)];
227
+ const secrets = [
228
+ ...whole,
229
+ ...[...new Set(parts)].filter((part) => part.length >= MIN_SECRET_LENGTH).sort((a, b) => b.length - a.length)
230
+ ].filter((secret) => secret !== "");
231
+ return (text) => {
232
+ let out = text.split(url.href).join(label);
233
+ for (const secret of secrets)
234
+ out = out.split(secret).join("[redacted]");
235
+ return out;
236
+ };
237
+ }
238
+ var MAX_CAUSE_DEPTH = 5;
239
+ function scrubCause(cause, redact, depth = 0) {
240
+ if (!(cause instanceof Error))
241
+ return redact(String(cause));
242
+ const inner = depth < MAX_CAUSE_DEPTH && cause.cause !== undefined;
243
+ const copy = new Error(redact(cause.message), inner ? { cause: scrubCause(cause.cause, redact, depth + 1) } : undefined);
244
+ copy.name = redact(cause.name);
245
+ if (typeof cause.stack === "string")
246
+ copy.stack = redact(cause.stack);
247
+ const code = cause.code;
248
+ if (typeof code === "string")
249
+ Object.assign(copy, { code: redact(code) });
250
+ else if (typeof code === "number")
251
+ Object.assign(copy, { code });
252
+ return copy;
253
+ }
254
+ function describeCause(cause) {
255
+ return cause instanceof Error ? cause.message : String(cause);
256
+ }
257
+
258
+ class McpToolkitError extends Error {
259
+ _tag = "McpToolkitError";
260
+ operation;
261
+ server;
262
+ constructor(fields) {
263
+ const redact = fields.redact ?? ((text) => text);
264
+ super(`MCP ${fields.operation} against ${fields.server} failed: ${redact(describeCause(fields.cause))}`, { cause: scrubCause(fields.cause, redact) });
265
+ this.name = "McpToolkitError";
266
+ this.operation = fields.operation;
267
+ this.server = fields.server;
268
+ }
269
+ }
270
+
271
+ // src/version.ts
272
+ var VERSION = "0.1.0";
273
+
274
+ // src/mcp-connect.ts
275
+ var DEFAULT_CONNECT_TIMEOUT_MS = 15000;
276
+ var MAX_TIMEOUT_MS = 2 ** 31 - 1;
277
+ var validateTimeout = (timeout) => Number.isFinite(timeout) && timeout > 0 && timeout <= MAX_TIMEOUT_MS ? Effect3.succeed(timeout) : Effect3.die(new RangeError(`connectTimeoutMs must be a finite number of milliseconds above 0 and at most ${String(MAX_TIMEOUT_MS)}, got ${String(timeout)}`));
278
+ var plainHeaders = (headers) => Object.fromEntries(Object.entries(headers ?? {}).map(([name, value2]) => [
279
+ name,
280
+ Redacted2.isRedacted(value2) ? Redacted2.value(value2) : value2
281
+ ]));
282
+ function headerValues(headers) {
283
+ return Object.values(plainHeaders(headers));
284
+ }
285
+ async function handshake(client, url, headers, timeout, signal) {
286
+ const transport = new StreamableHTTPClientTransport(url, {
287
+ requestInit: { headers: plainHeaders(headers) }
288
+ });
289
+ const budget = new AbortController;
290
+ const stop = AbortSignal.any([signal, budget.signal]);
291
+ const timer = setTimeout(() => budget.abort(), timeout);
292
+ const close = () => void client.close().catch(() => {
293
+ return;
294
+ });
295
+ stop.addEventListener("abort", close, { once: true });
296
+ try {
297
+ await client.connect(transport, { signal: stop, timeout });
298
+ } catch (error) {
299
+ await client.close().catch(() => {
300
+ return;
301
+ });
302
+ const spent = budget.signal.aborted;
303
+ const sdkTimeout = error instanceof McpError && error.code === ErrorCode.RequestTimeout;
304
+ throw (spent || sdkTimeout) && !signal.aborted ? new Error(`the MCP handshake did not finish within ${String(timeout)} ms`) : error;
305
+ } finally {
306
+ clearTimeout(timer);
307
+ stop.removeEventListener("abort", close);
308
+ }
309
+ }
310
+ var connectAndBuild = (url, headers, server, timeout, redact, build) => Effect3.uninterruptibleMask((restore) => Effect3.gen(function* () {
311
+ const client = new Client({ name: "@homeflare/seat-runtime", version: VERSION });
312
+ const close = Effect3.ignore(Effect3.tryPromise(() => client.close()));
313
+ yield* restore(Effect3.tryPromise({
314
+ try: (signal) => handshake(client, url, headers, timeout, signal),
315
+ catch: (cause) => new McpToolkitError({ operation: "connect", server, cause, redact })
316
+ }).pipe(Effect3.withSpan("seat.mcp.connect", { attributes: { "server.address": server } }))).pipe(Effect3.onError(() => close));
317
+ const built = yield* restore(build(client)).pipe(Effect3.onError(() => close));
318
+ yield* Effect3.addFinalizer(() => close);
319
+ return built;
320
+ }));
321
+
322
+ // src/mcp-pages.ts
323
+ import * as Effect4 from "effect/Effect";
324
+ var MAX_PAGES = 100;
325
+ function collect(operation, server, page, redact) {
326
+ return Effect4.tryPromise({
327
+ try: async (signal) => {
328
+ const items = [];
329
+ let cursor;
330
+ for (let pages = 0;pages < MAX_PAGES; pages += 1) {
331
+ const next = await page(cursor, signal);
332
+ items.push(...next.items);
333
+ if (next.next === undefined)
334
+ return items;
335
+ cursor = next.next;
336
+ }
337
+ throw new Error(`the server returned more than ${String(MAX_PAGES)} pages`);
338
+ },
339
+ catch: (cause) => new McpToolkitError({ operation, server, cause, redact })
340
+ });
341
+ }
342
+
343
+ // src/mcp-toolset.ts
344
+ import * as Effect5 from "effect/Effect";
345
+ import * as Toolkit from "effect/unstable/ai/Toolkit";
346
+
347
+ // src/mcp-render.ts
348
+ var EMPTY_RESULT = "(no content)";
349
+ var isBlock = (value2) => typeof value2 === "object" && value2 !== null;
350
+ var str = (value2) => typeof value2 === "string" ? value2 : undefined;
351
+ function renderBlock(block) {
352
+ const type = str(block["type"]) ?? "unknown";
353
+ switch (type) {
354
+ case "text":
355
+ return str(block["text"]) ?? "";
356
+ case "image":
357
+ case "audio":
358
+ return `[${type}: ${str(block["mimeType"]) ?? "unknown type"}, not shown]`;
359
+ case "resource_link":
360
+ return `[resource link: ${str(block["uri"]) ?? "no uri"}]`;
361
+ case "resource": {
362
+ const resource = isBlock(block["resource"]) ? block["resource"] : {};
363
+ return str(resource["text"]) ?? `[resource: ${str(resource["uri"]) ?? "no uri"}, not shown]`;
364
+ }
365
+ default:
366
+ return `[unsupported content block: ${type}]`;
367
+ }
368
+ }
369
+ function renderContent(content) {
370
+ if (!Array.isArray(content)) {
371
+ return content === undefined ? EMPTY_RESULT : JSON.stringify(content);
372
+ }
373
+ const lines = content.map((block) => isBlock(block) ? renderBlock(block) : JSON.stringify(block));
374
+ return lines.length === 0 ? EMPTY_RESULT : lines.join(`
375
+ `);
376
+ }
377
+ function renderResult(result) {
378
+ const content = "content" in result ? result["content"] : result["toolResult"];
379
+ if (Array.isArray(content) && content.length === 0 && result["structuredContent"] !== undefined) {
380
+ return JSON.stringify(result["structuredContent"]);
381
+ }
382
+ return renderContent(content);
383
+ }
384
+
385
+ // src/mcp-tool.ts
386
+ import * as Schema2 from "effect/Schema";
387
+ import * as SchemaGetter from "effect/SchemaGetter";
388
+ import * as Tool from "effect/unstable/ai/Tool";
389
+ function declaredParameters(inputSchema) {
390
+ const properties = inputSchema["properties"];
391
+ const names = typeof properties === "object" && properties !== null ? Object.keys(properties) : [];
392
+ if (names.length === 0)
393
+ return Tool.EmptyParams;
394
+ const required = new Set(Array.isArray(inputSchema["required"]) ? inputSchema["required"] : []);
395
+ const declared = Schema2.Struct(Object.fromEntries(names.map((name) => [
396
+ name,
397
+ required.has(name) ? Schema2.Unknown : Schema2.optional(Schema2.Unknown)
398
+ ])));
399
+ return declared.pipe(Schema2.decodeTo(Schema2.Unknown, {
400
+ decode: SchemaGetter.passthrough(),
401
+ encode: SchemaGetter.forbiddenEncoding
402
+ }));
403
+ }
404
+ function mcpTool(spec) {
405
+ const dynamic2 = Tool.dynamic(spec.name, {
406
+ description: spec.description,
407
+ parameters: spec.inputSchema,
408
+ success: Schema2.String,
409
+ failure: Schema2.String,
410
+ failureMode: "return"
411
+ });
412
+ return Object.assign(Object.create(Object.getPrototypeOf(dynamic2)), dynamic2, {
413
+ parametersSchema: declaredParameters(spec.inputSchema)
414
+ });
415
+ }
416
+
417
+ // src/mcp-toolset.ts
418
+ var isRecord = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
419
+ var call = (client, server, redact, name) => (params) => Effect5.gen(function* () {
420
+ if (!isRecord(params)) {
421
+ return yield* Effect5.fail(`${name}: the arguments must be a JSON object`);
422
+ }
423
+ const result = yield* Effect5.tryPromise({
424
+ try: (signal) => client.callTool({ name, arguments: params }, undefined, { signal }),
425
+ catch: (cause) => `MCP call to ${name} failed: ${redact(describeCause(cause))}`
426
+ });
427
+ const text = renderResult(result);
428
+ return result.isError === true ? yield* Effect5.fail(redact(text)) : text;
429
+ }).pipe(Effect5.withSpan("seat.mcp.call_tool", {
430
+ attributes: { "mcp.tool": name, "server.address": server }
431
+ }));
432
+ var toolset = (client, server, timeout, redact) => Effect5.gen(function* () {
433
+ const listed = client.getServerCapabilities()?.tools === undefined ? [] : yield* collect("listTools", server, async (cursor, signal) => {
434
+ const page = await client.listTools(cursor === undefined ? undefined : { cursor }, {
435
+ signal,
436
+ timeout
437
+ });
438
+ return { items: page.tools, next: page.nextCursor };
439
+ }, redact);
440
+ const tools = listed.map((tool) => mcpTool({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }));
441
+ const definition = Toolkit.make(...tools);
442
+ const handlers = Object.fromEntries(tools.map((tool) => [tool.name, call(client, server, redact, tool.name)]));
443
+ const context = yield* definition.toHandlers(handlers);
444
+ return yield* definition.pipe(Effect5.provideContext(context));
445
+ });
446
+
447
+ // src/mcp-toolkit.ts
448
+ function resourcesOf(client, server, redact) {
449
+ const listResources = Effect6.suspend(() => client.getServerCapabilities()?.resources === undefined ? Effect6.succeed([]) : collect("listResources", server, async (cursor, signal) => {
450
+ const page = await client.listResources(cursor === undefined ? undefined : { cursor }, {
451
+ signal
452
+ });
453
+ return {
454
+ items: page.resources.map((resource) => ({
455
+ uri: resource.uri,
456
+ name: resource.name,
457
+ description: resource.description,
458
+ mimeType: resource.mimeType
459
+ })),
460
+ next: page.nextCursor
461
+ };
462
+ }, redact)).pipe(Effect6.withSpan("seat.mcp.list_resources", { attributes: { "server.address": server } }));
463
+ const readResource = (uri) => Effect6.tryPromise({
464
+ try: async (signal) => {
465
+ const read = await client.readResource({ uri }, { signal });
466
+ return read.contents.map((entry) => ({
467
+ uri: entry.uri,
468
+ mimeType: entry.mimeType,
469
+ text: "text" in entry ? entry.text : undefined,
470
+ blob: "blob" in entry ? entry.blob : undefined
471
+ }));
472
+ },
473
+ catch: (cause) => new McpToolkitError({ operation: "readResource", server, cause, redact })
474
+ }).pipe(Effect6.withSpan("seat.mcp.read_resource", {
475
+ attributes: { "mcp.resource": uri, "server.address": server }
476
+ }));
477
+ return { listResources, readResource };
478
+ }
479
+ function mcpToolkit(url, headers, options) {
480
+ return Effect6.gen(function* () {
481
+ const timeout = yield* validateTimeout(options?.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS);
482
+ const target = yield* Effect6.try({
483
+ try: () => new URL(url),
484
+ catch: () => new McpToolkitError({
485
+ operation: "connect",
486
+ server: "(unparseable URL)",
487
+ cause: "the URL did not parse"
488
+ })
489
+ });
490
+ const server = serverLabel(target);
491
+ const redact = redactor(target, headerValues(headers));
492
+ return yield* connectAndBuild(target, headers, server, timeout, redact, (client) => Effect6.gen(function* () {
493
+ const toolkit = yield* toolset(client, server, timeout, redact);
494
+ return { toolkit, ...resourcesOf(client, server, redact) };
495
+ }));
496
+ });
497
+ }
498
+ export {
499
+ McpToolkitError,
500
+ exports_seat_model as SeatModel,
501
+ exports_seat_obs as SeatObs,
502
+ VERSION,
503
+ mcpToolkit,
504
+ runRounds
505
+ };
506
+
507
+ //# debugId=61F68CACADDB04EE64756E2164756E21
508
+ //# sourceMappingURL=index.js.map