@effected/mcp-kit 0.0.0-stage → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts ADDED
@@ -0,0 +1,1048 @@
1
+ import { Array, Cause, Context, Effect, JsonSchema, Layer, Option, Runtime, Schema, Scope, Stdio, Struct } from "effect";
2
+ import { McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
3
+ import { Remediation } from "@effected/kit";
4
+ import { KeyValueStore } from "effect/persistence";
5
+ /**
6
+ * Options for {@link McpStdio.layer}.
7
+ *
8
+ * @public
9
+ */
10
+ interface McpStdioOptions<RA = never, RE = never, RR = never> {
11
+ /** The server name reported to the client. */
12
+ readonly name: string;
13
+ /** The server version reported to the client. */
14
+ readonly version: string;
15
+ /** Surfaced in `initialize` and `server/discover`; tell the agent how to use the tools. */
16
+ readonly instructions?: string | undefined;
17
+ /** A one-line description of the server, reported to the client. */
18
+ readonly description?: string | undefined;
19
+ /** The server's website, reported in `server/discover` and `initialize`. */
20
+ readonly websiteUrl?: string | undefined;
21
+ /** Icons for the server, reported to the client. */
22
+ readonly icons?: ReadonlyArray<McpSchema.Icon> | undefined;
23
+ /** Capability extensions the server advertises, keyed `vendor/name`. */
24
+ readonly extensions?: (typeof McpSchema.ServerCapabilities)["Type"]["extensions"] | undefined;
25
+ /**
26
+ * How long a client may treat each list result as fresh, per result kind.
27
+ *
28
+ * @remarks
29
+ * Core's `cacheHints`, forwarded unchanged. Only the stateless `2026-07-28` revision carries them; earlier
30
+ * revisions ignore the option. Omitted kinds keep a zero `ttl`. A positive `ttl` lets a client keep showing a list
31
+ * after registrations change, until it expires: raise it only for a list that is fixed for the server's lifetime.
32
+ */
33
+ readonly cacheHints?: McpServer.CacheHints | undefined;
34
+ /** Defaults to {@link McpStdio.protocols}. Read its ordering rules before overriding. */
35
+ readonly protocols?: Array.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter> | undefined;
36
+ /**
37
+ * The server's registration layers (toolkits, resources, prompts), built
38
+ * before the server reads its first request and under `LogToStderr`.
39
+ *
40
+ * @remarks
41
+ * Core's `registrations` option of `McpServer.layerStdio`. The transport
42
+ * discharges each `McpServer.McpRequest<"Requires", Scope>` marker; any
43
+ * other request-level marker stays in the layer's requirements, to be met
44
+ * by MCP middleware. A registration that needs `Stdio` when it is built
45
+ * gets the guarded one this layer serves over.
46
+ */
47
+ readonly registrations?: Layer.Layer<RA, RE, RR> | undefined;
48
+ }
49
+ /**
50
+ * The services a registration layer needs that {@link McpStdio.layer} does
51
+ * not supply: everything but `McpServer` and `Stdio` (which the layer
52
+ * requires itself), with each request-level marker but
53
+ * `McpServer.McpRequest<"Requires", Scope>`, which the transport discharges.
54
+ *
55
+ * @public
56
+ */
57
+ type McpStdioRegistrationServices<RR> = Exclude<McpServer.McpRequest.Without<RR>, McpServer.McpServer> | McpServer.McpRequest.From<"Requires", Exclude<McpServer.McpRequest.Only<"Requires", RR>, Scope.Scope>>;
58
+ /**
59
+ * Options for {@link McpStdio.launch}.
60
+ *
61
+ * @public
62
+ */
63
+ interface McpLaunchOptions {
64
+ /**
65
+ * Runs once the whole layer has built, and before the launch waits
66
+ * forever. By then the stdio protocol is reading stdin: the server is
67
+ * serving, although no client may have sent `initialize` yet.
68
+ *
69
+ * @remarks
70
+ * `McpGuard.run` (`@effected/mcp-kit/guard`) uses it as its "connected"
71
+ * signal. A layer built after the server layer, as in
72
+ * `Layer.effectDiscard(ready).pipe(Layer.provide(Main))`, gives the same
73
+ * signal, but only if every consumer re-derives that `Layer.provide`
74
+ * builds its dependency first.
75
+ */
76
+ readonly onReady?: Effect.Effect<void> | undefined;
77
+ }
78
+ /**
79
+ * Serve an MCP server over stdio without ever writing a log line or a failure
80
+ * report onto stdout, which is the JSON-RPC wire.
81
+ *
82
+ * @remarks
83
+ * The one-line `main.ts`:
84
+ *
85
+ * ```ts
86
+ * NodeRuntime.runMain(McpStdio.launch(Main), { teardown: McpStdio.teardown })
87
+ * ```
88
+ *
89
+ * - {@link McpStdio.layer} is `McpServer.layerStdio` with `LogToStderr`
90
+ * provided and merged into its output, and a bad `protocols` list
91
+ * turned into a defect. It answers a
92
+ * stdin line that is not JSON with a JSON-RPC `-32700` parse error, and
93
+ * JSON that is not a JSON-RPC message with `-32600`, and keeps serving.
94
+ * - {@link McpStdio.launch} reports a launch failure itself, on stderr, and
95
+ * hides it from `runMain`, whose own report is written outside anything
96
+ * the program can provide.
97
+ * - {@link McpStdio.teardown} maps stdin EOF, the normal end of every
98
+ * session, to exit 0 instead of 130.
99
+ *
100
+ * Never `NodeRuntime.runMain(Layer.launch(Main).pipe(Effect.provideService(References.LogToStderr, true)))`:
101
+ * it typechecks and serves, but a launch failure is still reported by
102
+ * `runMain` through `console.log`, straight onto the wire, where the client
103
+ * reads it as a malformed frame instead of an error.
104
+ *
105
+ * @public
106
+ */
107
+ export declare class McpStdio {
108
+ private constructor();
109
+ /**
110
+ * `[v2026_07_28, v2025_11_25, v2025_06_18]` — the stateless revision
111
+ * first, then the two newest stateful ones.
112
+ *
113
+ * @remarks
114
+ * Real clients open with either handshake: some (Claude Code, for one)
115
+ * open a stdio server with the stateless `server/discover`, while others
116
+ * still send `initialize`. The order is load-bearing:
117
+ *
118
+ * - Stateless first: a request with no session and no `_meta` falls to
119
+ * `protocols[0]`.
120
+ * - Never a single entry: `initialize` only matches stateful adapters, so
121
+ * a stateless-only list refuses every client that opens with it.
122
+ * - At most one stateless adapter; a second fails the layer.
123
+ */
124
+ static readonly protocols: Array.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>;
125
+ /**
126
+ * `McpServer.layerStdio` with `LogToStderr` provided and merged into the
127
+ * output, so every layer it provides logs to stderr too. A bad
128
+ * `protocols` list dies: it is the implementer's defect. So does a
129
+ * registration that fails with `Cause.IllegalArgumentError`, which core
130
+ * reports for the same class of mistake; any other registration error
131
+ * stays in the error channel.
132
+ *
133
+ * @remarks
134
+ * Pass the server's registration layers as `options.registrations`: they
135
+ * are built before the server reads its first request, under
136
+ * `LogToStderr`. Providing this layer to them, as
137
+ * `Registrations.pipe(Layer.provideMerge(McpStdio.layer(...)))`, also
138
+ * serves them, and they inherit `LogToStderr` the same way. A sibling
139
+ * merged beside it with `Layer.mergeAll` is neither: its build logs still
140
+ * go through `console.log`, onto the wire.
141
+ *
142
+ * The output is `McpServer` and the `McpServer.McpRequest<"Requires", Scope>`
143
+ * marker the transport discharges. `McpServerClient` is not in it: the
144
+ * stateless `v2026_07_28` revision has no client session, so a handler
145
+ * reads the client's details from `McpSchema.McpRequestContext` instead.
146
+ *
147
+ * `LogToStderr` only reaches a logger that reads it: Effect's default
148
+ * logger and `Logger.consolePretty`. `Logger.consoleJson`,
149
+ * `Logger.consoleLogFmt` and `Logger.consoleStructured` are built with
150
+ * `Logger.withConsoleLog`, which writes through `console.log` whatever
151
+ * `LogToStderr` says, so a server that installs one puts every log line on
152
+ * the wire. Never install them under this layer; wrap the formatter in
153
+ * `Logger.withConsoleError` instead, as in
154
+ * `Logger.layer([Logger.withConsoleError(Logger.formatJson)])`.
155
+ *
156
+ * The server reads stdin through a guard that frames it exactly as core's
157
+ * decoder does: one streaming UTF-8 decoder, a byte-order mark stripped
158
+ * only at the start of the stream, lines split on `\n`. Core itself skips
159
+ * a line it cannot use and keeps serving, but sends no reply, where
160
+ * JSON-RPC 2.0 requires one; the guard supplies it. A line that is not
161
+ * JSON is answered on stdout with a parse error, code `-32700` and
162
+ * `id: null`, and never reaches core. So is a line longer than core's cap
163
+ * of 16 Mi UTF-16 code units: it is answered once, as soon as it passes
164
+ * the cap, and the rest of it is discarded up to its newline (core would
165
+ * log it on stderr and drop it). A line of JSON whitespace (space, tab,
166
+ * carriage return) is ignored.
167
+ *
168
+ * A line that is JSON but no JSON-RPC message core can handle is answered
169
+ * with an Invalid Request, code `-32600` and `id: null`, and never reaches
170
+ * core: a value that is neither an object nor an array, an object whose
171
+ * `method` is not a string and whose `id` is absent or `null`, and an
172
+ * object with neither `method` nor `id`, which is neither a request nor a
173
+ * response.
174
+ *
175
+ * Everything else goes to core: an array, which core answers `-32600`
176
+ * itself because it serves no batches; an object with an `id` and no
177
+ * `method`, which is a response and gets no reply; and a request with an
178
+ * `id`, which core answers even when its `method` is malformed.
179
+ *
180
+ * Give each server a fresh layer memo map. Core's stdio protocol layer
181
+ * is a shared constant, so a second `McpStdio.layer` server whose build
182
+ * sees the first one's memo map shares its protocol: only the first
183
+ * server reads stdin, and the second never answers. Merging both into
184
+ * one graph does that, and so does building or providing the second
185
+ * anywhere under the first one's `Effect.provide`: nested `Layer.build`
186
+ * and `Effect.provide` fork the ambient memo map rather than starting a
187
+ * new one. The tool registry is shared the same way. Isolate each server
188
+ * by wrapping its whole bundle (this layer with its `registrations`, or its
189
+ * toolkit layers together with this layer) in `Layer.fresh`, or with its own `ManagedRuntime`,
190
+ * `Effect.provide(layer, { local: true })` or its own process. Never put
191
+ * the `Layer.fresh` boundary between a toolkit and this layer: the server
192
+ * would serve an empty registry.
193
+ *
194
+ * Code after a completed `Effect.provide` of a stdio server never runs:
195
+ * core's stdio protocol interrupts the fiber that built it when its stdin
196
+ * loop ends, which closing the provide's scope does. In a test, serve the
197
+ * layer through `McpHarness` from `@effected/mcp-kit/testing` instead.
198
+ *
199
+ * Each call mints a fresh layer; bind the result to a `const` or the
200
+ * server builds twice.
201
+ */
202
+ static readonly layer: <RA = never, RE = never, RR = never>(options: McpStdioOptions<RA, RE, RR>) => Layer.Layer<McpServer.McpServer | McpServer.McpRequest<"Requires", Scope.Scope>, Exclude<RE, Cause.IllegalArgumentError>, Stdio.Stdio | McpStdioRegistrationServices<RR>>;
203
+ /**
204
+ * `Layer.launch`, with any failure other than an interrupt logged on
205
+ * stderr here and re-raised marked as already reported, keeping its exit
206
+ * code.
207
+ *
208
+ * @remarks
209
+ * `runMain` logs an unhandled failure from outside the program, where no
210
+ * `Effect.provideService` reaches, and Effect's default logger writes
211
+ * through `console.log` unless `LogToStderr` is set. For an MCP server
212
+ * that is stdout, which is the wire.
213
+ *
214
+ * `options.onReady` runs after the layer has built and before the launch
215
+ * waits forever, as {@link McpLaunchOptions.onReady} describes.
216
+ */
217
+ static readonly launch: <ROut, E, R>(layer: Layer.Layer<ROut, E, R>, options?: McpLaunchOptions) => Effect.Effect<never, Error, R>;
218
+ /**
219
+ * Success or an interrupt-only exit maps to 0. Anything else goes to
220
+ * `Runtime.defaultTeardown`.
221
+ *
222
+ * @remarks
223
+ * Stdin EOF, the normal end of every session, ends in an interrupt-only
224
+ * exit, and so do SIGINT and SIGTERM: `runMain` interrupts the program on
225
+ * either signal. All three exit 0 here, not the 130 a default teardown
226
+ * reports for an interrupt.
227
+ */
228
+ static readonly teardown: Runtime.Teardown;
229
+ }
230
+ /**
231
+ * The unknown keys found at one object level of a tool-call payload.
232
+ *
233
+ * @public
234
+ */
235
+ interface UnknownKeysLevel {
236
+ /** Keys from the payload root to this object; empty at the root. Array positions are decimal strings. */
237
+ readonly path: ReadonlyArray<string>;
238
+ /** Keys the payload carries that this level does not accept, in payload order. */
239
+ readonly unknown: ReadonlyArray<string>;
240
+ /** Keys this level declares, in schema order. */
241
+ readonly accepted: ReadonlyArray<string>;
242
+ /**
243
+ * The `patternProperties` patterns this level also accepts keys by, in
244
+ * schema order. Present only when the level has at least one.
245
+ */
246
+ readonly acceptedPatterns?: ReadonlyArray<string>;
247
+ }
248
+ /**
249
+ * Options for {@link ToolInputSchema.formatUnknownKeys}.
250
+ *
251
+ * @public
252
+ */
253
+ interface FormatUnknownKeysOptions {
254
+ /** The cap on each echoed key path, in UTF-16 code units. Defaults to `ToolFailure.ECHO_LIMIT`. */
255
+ readonly echoLimit?: number | undefined;
256
+ }
257
+ /**
258
+ * Pure walkers over a tool's served JSON Schema: every unknown key at every
259
+ * depth, a message naming them all, and an object-rooted form of a union.
260
+ *
261
+ * @remarks
262
+ * For a `Tool.make` tool, core's strict decode already names every unknown
263
+ * key, with every missing or invalid field, in one response. Core never
264
+ * validates a `Tool.dynamic` tool's raw JSON Schema, so these walkers are
265
+ * for that case: a handler runs them against the schema it registered.
266
+ * They run over the schema as served, so what they accept matches what the
267
+ * tool advertises:
268
+ *
269
+ * - Only `additionalProperties: false` closes a node, as in JSON Schema; core
270
+ * emits it on every object node of a strict tool and `true` otherwise.
271
+ * - `allOf` members' properties merge; `oneOf`/`anyOf` members are chosen by an
272
+ * `action`, `kind`, `_tag` or `type` literal (tried in that order), or by
273
+ * being the only object member. A payload whose discriminant matches no
274
+ * member, or is missing, is skipped and left for decoding.
275
+ * - `items`, `prefixItems`, `$ref` into `$defs` (JSON Pointer escapes
276
+ * honoured), and `patternProperties` are followed; a key matched by a
277
+ * pattern is accepted and walked with the first matching pattern's schema.
278
+ * Effect serves a pattern-keyed `Schema.Record` with `patternProperties`
279
+ * only when the key pattern's RegExp has the `u` flag; without it the
280
+ * record is served open, and every key is accepted.
281
+ * - The payload is untrusted: the walk stops descending at depth 256 rather
282
+ * than overflowing the stack, so a deeper unknown key goes unreported and
283
+ * is left for decoding. Every hop counts toward that cap: each property or
284
+ * array position, and also each union member selection, which adds a hop
285
+ * without adding a path segment.
286
+ *
287
+ * @public
288
+ */
289
+ export declare class ToolInputSchema {
290
+ private constructor();
291
+ /** Every level of `payload` that carries a key `schema` does not accept. */
292
+ static readonly unknownKeys: (payload: unknown, schema: JsonSchema.JsonSchema) => ReadonlyArray<UnknownKeysLevel>;
293
+ /**
294
+ * `Unrecognized parameter(s): a, b.c. Accepted params: x, y.` per level,
295
+ * followed by `Accepted keys matching: ^x-.` when the level also accepts
296
+ * keys by pattern. A level that accepts keys only by pattern names its
297
+ * patterns alone, never `Accepted params: (none).` Each echoed key and
298
+ * pattern is truncated, and at most 20 keys, 20 patterns and 20 levels are
299
+ * named; a trailing `(and N more levels).` counts the levels left out.
300
+ */
301
+ static readonly formatUnknownKeys: (levels: ReadonlyArray<UnknownKeysLevel>, options?: FormatUnknownKeysOptions) => string;
302
+ /**
303
+ * `schema` with a root `$ref` inlined and a top-level union of objects
304
+ * sharing an `action`, `kind`, `_tag` or `type` literal rewritten to an
305
+ * object root carrying `type: "object"`, `oneOf` and `"x-discriminator"` —
306
+ * MCP requires an object root, and core dies at boot on a union root.
307
+ * Returned as the same value when nothing changes.
308
+ */
309
+ static readonly objectRooted: (schema: JsonSchema.JsonSchema) => JsonSchema.JsonSchema;
310
+ }
311
+ /**
312
+ * Options for {@link McpToolkit.layer}.
313
+ *
314
+ * @public
315
+ */
316
+ interface McpToolkitOptions {
317
+ /**
318
+ * `"all"` (the default): every tool without its own `Tool.Strict`
319
+ * annotation is served and decoded strict. `"annotated"`: only tools
320
+ * annotated `Tool.Strict` true are. In both modes an explicit annotation,
321
+ * true or false, always wins, and a `Tool.dynamic` tool is never
322
+ * re-annotated (core dies at registration on a strict dynamic tool).
323
+ */
324
+ readonly strict?: "all" | "annotated" | undefined;
325
+ /**
326
+ * Ignored. Core's strict decode reports every excess key at every depth,
327
+ * together with every missing or invalid field, in one `InvalidParams`,
328
+ * and the layer appends a fixed `Accepted params at <path>: …` line per
329
+ * level that carries an unknown key; this option changes neither.
330
+ *
331
+ * @deprecated A no-op, scheduled for removal in a later minor.
332
+ */
333
+ readonly unknownKeyMessage?: ((levels: ReadonlyArray<UnknownKeysLevel>) => string) | undefined;
334
+ }
335
+ /**
336
+ * A `Tool.dynamic` made by {@link McpToolkit.unionTool}: served with the
337
+ * strict, object-rooted JSON Schema of a `Schema.Union` of objects, and
338
+ * carrying that union so {@link McpToolkit.unionHandler} can decode it.
339
+ *
340
+ * @remarks
341
+ * `annotate` and `addDependency` keep the type, so a chained
342
+ * `.annotate(Tool.Title, …)` still hands `unionHandler` the union. The
343
+ * declared failure is `failure` together with `McpSchema.InvalidParams`.
344
+ *
345
+ * @public
346
+ */
347
+ interface UnionTool<Name extends string, P extends Schema.Decoder<unknown>, S extends Schema.Constraint, F extends Schema.Constraint, R = never> extends Tool.Dynamic<Name, {
348
+ readonly parameters: JsonSchema.JsonSchema;
349
+ readonly success: S;
350
+ readonly failure: Schema.Union<readonly [F, typeof McpSchema.InvalidParams]>;
351
+ readonly failureMode: "error";
352
+ }, R> {
353
+ /** The union the tool decodes its arguments with. */
354
+ readonly unionParameters: P;
355
+ /** Add an annotation, keeping the union. */
356
+ annotate<I, V>(tag: Context.Key<I, V>, value: V): UnionTool<Name, P, S, F, R>;
357
+ /** Add a request-level dependency, keeping the union. */
358
+ addDependency<Identifier, Service>(tag: Context.Key<Identifier, Service>): UnionTool<Name, P, S, F, Identifier | R>;
359
+ }
360
+ /**
361
+ * Options for {@link McpToolkit.unionTool}.
362
+ *
363
+ * @public
364
+ */
365
+ interface UnionToolOptions<P extends Schema.Decoder<unknown>, S extends Schema.Constraint, F extends Schema.Constraint, Dependencies extends ReadonlyArray<Context.Key<unknown, unknown>>> {
366
+ /** What the tool does, for the agent. */
367
+ readonly description?: string | undefined;
368
+ /** A `Schema.Union` of object schemas, discriminated by an `action`, `kind`, `_tag` or `type` literal. */
369
+ readonly parameters: P;
370
+ /** The success schema. Defaults to `Schema.Void`; object-root a union whose members are all objects with `ToolOutputSchema.objectRooted`. */
371
+ readonly success?: S | undefined;
372
+ /** Declared failures besides `InvalidParams`, such as `ToolRefusal`. Defaults to `Schema.Never`. */
373
+ readonly failure?: F | undefined;
374
+ /** Services the handler needs, as with `Tool.make`. */
375
+ readonly dependencies?: Dependencies | undefined;
376
+ }
377
+ /**
378
+ * Options for {@link McpToolkit.unionHandler}.
379
+ *
380
+ * @public
381
+ */
382
+ interface UnionHandlerOptions {
383
+ /**
384
+ * Ignored. The union decode reports every excess, missing and invalid
385
+ * field in one `InvalidParams`, followed by the same fixed
386
+ * `Accepted params at <path>: …` lines {@link McpToolkit.layer} appends;
387
+ * this option changes neither.
388
+ *
389
+ * @deprecated A no-op, scheduled for removal in a later minor.
390
+ */
391
+ readonly unknownKeyMessage?: ((levels: ReadonlyArray<UnknownKeysLevel>) => string) | undefined;
392
+ }
393
+ /**
394
+ * Register a toolkit exactly as core's `McpServer.toolkit` does, except that
395
+ * every tool is strict by default and a {@link McpToolkit.unionTool} gets its
396
+ * union decoded.
397
+ *
398
+ * @remarks
399
+ * Rejection and reporting are core's: a tool annotated `Tool.Strict` is
400
+ * served with `additionalProperties: false` on every object node and decoded
401
+ * with `onExcessProperty: "error"` and `errors: "all"`, so one
402
+ * `InvalidParams` names every excess key at every depth together with every
403
+ * missing or invalid field. The layer appends to that report, never in place
404
+ * of it: for every object level of the payload that carries a key the served
405
+ * input schema does not accept, one line naming what that level does accept,
406
+ * `Accepted params at the root: query, filter.` or
407
+ * `Accepted params at ["filter"]: kind, tag.` (the path as core writes it),
408
+ * with `keys matching <pattern>` for keys a `patternProperties` level
409
+ * accepts, and `This tool accepts no params.` for a zero-parameter tool. An
410
+ * agent can fix the call from the reply alone. A failure with no unknown key
411
+ * (a missing or mistyped field only) is core's report unchanged. The lines
412
+ * are appended to core's `InvalidParams` as it leaves the tool's registered
413
+ * handler, the one failure that handler has for bad parameters.
414
+ *
415
+ * On top of that is the policy: under
416
+ * `strict: "all"` (the default) it annotates every tool that carries no
417
+ * `Tool.Strict` annotation of its own as strict, skipping `Tool.dynamic`
418
+ * tools (core dies at registration on a strict dynamic tool). It runs core's
419
+ * `registerToolkit` unchanged under a registration-scoped `McpServer` whose
420
+ * `addTool` puts the union decode in front of a union tool's handler and
421
+ * passes every other registration through untouched. It patches no core
422
+ * internals: it uses only `McpServer.registerToolkit` and `addTool`.
423
+ *
424
+ * Registration goes through core's module-level `McpServer.McpServer.layer`,
425
+ * shared by reference with `McpStdio.layer`'s own copy — provide both into
426
+ * the same graph, as with `McpServer.toolkit`. Never wrap that layer in
427
+ * `Layer.fresh`: tools would register into a second registry nobody serves.
428
+ * To give two servers separate registries, wrap each whole bundle (this
429
+ * layer together with `McpStdio.layer`) in `Layer.fresh` instead.
430
+ *
431
+ * Each call mints a fresh layer; bind the result to a `const` or the
432
+ * registration runs twice.
433
+ *
434
+ * @example
435
+ * ```ts
436
+ * import { Layer } from "effect";
437
+ * import { McpStdio, McpToolkit } from "@effected/mcp-kit";
438
+ *
439
+ * const ToolsLayer = McpToolkit.layer(MyToolkit).pipe(Layer.provide(MyHandlers));
440
+ * const ServerLayer = ToolsLayer.pipe(Layer.provideMerge(McpStdio.layer({ name: "my-server", version: "1.0.0" })));
441
+ * ```
442
+ *
443
+ * @public
444
+ */
445
+ export declare class McpToolkit {
446
+ private constructor();
447
+ /**
448
+ * A tool whose parameters are a `Schema.Union` of objects, which
449
+ * `Tool.make` cannot take: core dies at registration on a non-object
450
+ * `parameters` root.
451
+ *
452
+ * @remarks
453
+ * The tool is a `Tool.dynamic` served with Effect's strict JSON Schema
454
+ * document for `parameters` (`additionalProperties: false` on every
455
+ * object node, definitions as `$defs`), rewritten by
456
+ * `ToolInputSchema.objectRooted` to an object root carrying `type`,
457
+ * `oneOf` and `x-discriminator`. Write its handler with
458
+ * {@link McpToolkit.unionHandler}, which receives the decoded member.
459
+ *
460
+ * Registered through {@link McpToolkit.layer}, a union tool gets the same
461
+ * treatment as a strict `Tool.make` tool: a strict decode with
462
+ * `errors: "all"` before the handler runs, reporting every excess, missing
463
+ * and invalid field of the matched member in one `InvalidParams`, with the
464
+ * same `Accepted params at <path>: …` lines appended, so bad
465
+ * arguments answer JSON-RPC `-32602` on `2025-06-18` and an `isError`
466
+ * result on the later revisions, exactly as a `Tool.make` decode failure
467
+ * does. Registered through core's `McpServer.toolkit` it still decodes
468
+ * strictly, in the handler, but the `InvalidParams` is then a declared
469
+ * failure: an `isError` result on every revision.
470
+ *
471
+ * Never annotate it `Tool.Strict` true: core dies at registration on a
472
+ * strict tool with a raw JSON Schema.
473
+ *
474
+ * @example
475
+ * ```ts
476
+ * import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp-kit";
477
+ * import { Schema } from "effect";
478
+ * import { Tool } from "effect/ai";
479
+ *
480
+ * // `AddNote`, `ListNotes`, `Added`, `Listed` are object schemas; `Kit` is a Toolkit that includes `Note`.
481
+ * const Note = McpToolkit.unionTool("note", {
482
+ * parameters: Schema.Union([AddNote, ListNotes]),
483
+ * success: ToolOutputSchema.objectRooted(Schema.Union([Added, Listed])),
484
+ * failure: ToolRefusal,
485
+ * }).annotate(Tool.Title, "Note")
486
+ * const handlers = Kit.toLayer({ note: McpToolkit.unionHandler(Note, (params) => handleNote(params)) })
487
+ * ```
488
+ */
489
+ static readonly unionTool: <const Name extends string, P extends Schema.Decoder<unknown>, S extends Schema.Constraint = typeof Schema.Void, F extends Schema.Constraint = typeof Schema.Never, const Dependencies extends ReadonlyArray<Context.Key<unknown, unknown>> = []>(name: Name, options: UnionToolOptions<P, S, F, Dependencies>) => UnionTool<Name, P, S, F, Context.Service.Identifier<Dependencies[number]>>;
490
+ /**
491
+ * The handler for a {@link McpToolkit.unionTool}: decodes the raw payload
492
+ * exactly as {@link McpToolkit.layer} does, and passes the decoded member
493
+ * to `handler`.
494
+ *
495
+ * @remarks
496
+ * A payload that does not decode strictly fails with one
497
+ * `McpSchema.InvalidParams`, worded as core words a `Tool.make` decode
498
+ * failure, naming every excess key at every depth together with every
499
+ * missing or invalid field of the matched member, then one
500
+ * `Accepted params at <path>: …` line per level with an unknown key. It
501
+ * is the same
502
+ * implementation the layer runs, so a handler called directly (a test
503
+ * helper, or a toolkit registered through core's `McpServer.toolkit`)
504
+ * reports what a client of the layer sees. Under {@link McpToolkit.layer}
505
+ * the layer has already rejected a bad call before the handler runs.
506
+ * `options.unknownKeyMessage` is deprecated and ignored.
507
+ */
508
+ static readonly unionHandler: <Name extends string, P extends Schema.Decoder<unknown>, S extends Schema.Constraint, F extends Schema.Constraint, R, A, E, RH>(tool: UnionTool<Name, P, S, F, R>, handler: (params: P["Type"]) => Effect.Effect<A, E, RH>, _options?: UnionHandlerOptions) => ((payload: unknown) => Effect.Effect<A, E | McpSchema.InvalidParams, RH>);
509
+ /**
510
+ * `McpServer.toolkit`'s registration layer, strict by default.
511
+ * `options.unknownKeyMessage` is deprecated and ignored.
512
+ *
513
+ * @remarks
514
+ * Its requirements are core's `McpServer.ToolkitRequirements` less
515
+ * `McpServer`, which the layer provides itself: the handlers, any service
516
+ * a parameter or success schema needs, and each service a tool declares
517
+ * with `dependencies` or `addDependency` as an
518
+ * `McpServer.McpRequest<"Requires", S>` marker. A marker is met per
519
+ * request by MCP middleware (`McpServer.middleware`, its `layer` provided
520
+ * to this one), never by providing `S`. A service the handlers only need
521
+ * when they are built is captured by the effect form of
522
+ * `Toolkit.toLayer` instead, and declared nowhere.
523
+ */
524
+ static readonly layer: <Tools extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<Tools>, options?: McpToolkitOptions) => Layer.Layer<never, never, Exclude<McpServer.ToolkitRequirements<Tools>, McpServer.McpServer>>;
525
+ /**
526
+ * Meet the request-level dependencies a tool declares with `dependencies`
527
+ * or `addDependency` from a layer built once for the server's lifetime.
528
+ * Provide the result to {@link McpToolkit.layer}.
529
+ *
530
+ * @remarks
531
+ * The layer builds with the server's graph, so a failure fails the server
532
+ * build rather than a request, and its finalizers run at server shutdown.
533
+ * It builds in that graph's memo map: a layer the graph also builds is
534
+ * shared, and two servers each wrapped whole in `Layer.fresh` build their
535
+ * own.
536
+ *
537
+ * It captures what the layer outputs and nothing else, so a key the build
538
+ * merely ran under (a span, a request-level reference) never shadows the
539
+ * request's own, which capturing `Effect.context` in hand-written
540
+ * middleware does. The outputs are taken as given: a layer that outputs
541
+ * `Effect.context()` wholesale, through `Layer.effectContext`, still leaks
542
+ * everything it read.
543
+ *
544
+ * A service that varies per request still needs its own
545
+ * `McpServer.middleware`. A service only the handlers' construction needs
546
+ * is captured by the effect form of `Toolkit.toLayer` and declared nowhere.
547
+ *
548
+ * Each call mints a fresh layer; bind the result to a `const` or the
549
+ * middleware builds again.
550
+ *
551
+ * @example
552
+ * ```ts
553
+ * import { McpToolkit } from "@effected/mcp-kit";
554
+ * import { Layer } from "effect";
555
+ *
556
+ * // `Kit` declares `dependencies: [Inspector]`; `InspectorLive` builds it once.
557
+ * const Lifetime = McpToolkit.provideLifetime(InspectorLive);
558
+ * const ToolsLayer = McpToolkit.layer(Kit).pipe(Layer.provide(Lifetime), Layer.provide(KitHandlers));
559
+ * ```
560
+ */
561
+ static readonly provideLifetime: <S, E, R>(layer: Layer.Layer<S, E, R>) => Layer.Layer<McpServer.McpRequest.From<"Requires", S>, E, R>;
562
+ }
563
+ /**
564
+ * How a tool's declared failure should read on the wire.
565
+ *
566
+ * @remarks
567
+ * Core sends a declared failure that is an `Error` instance — every
568
+ * `Schema.TaggedError` is — as `isError: true` with `error.message` as the only
569
+ * text and no `structuredContent` (effect `ai/McpServer.ts`,
570
+ * `declaredFailureResult`). So the remediation must be folded into `message`
571
+ * when the error is constructed: whatever is not in the message never reaches
572
+ * the agent.
573
+ *
574
+ * Spread {@link ToolFailure.fields} into your tool's `Schema.TaggedError`, build
575
+ * `message` with {@link ToolFailure.message}, and pass every caller-supplied
576
+ * value through {@link ToolFailure.truncate} before echoing it. For a plain
577
+ * refusal with no fields of its own, `ToolRefusal` is this shape ready-made.
578
+ *
579
+ * Only a declared failure keeps its message: core scrubs an undeclared
580
+ * failure or a defect to "Tool execution failed due to an internal server
581
+ * error.", so audit every throw path a tool can take and declare each one the
582
+ * agent should read.
583
+ *
584
+ * @example
585
+ * ```ts
586
+ * import { ToolFailure } from "@effected/mcp-kit";
587
+ * import { Schema } from "effect";
588
+ *
589
+ * class NotFound extends Schema.TaggedError<NotFound>()("NotFound", { ...ToolFailure.fields, id: Schema.String }) {}
590
+ * const remediation = { hint: "List the ids first.", suggestedTool: "list_things" }
591
+ * new NotFound({ id, remediation, message: ToolFailure.message(`No thing "${ToolFailure.truncate(id)}".`, remediation) })
592
+ * ```
593
+ *
594
+ * @public
595
+ */
596
+ export declare class ToolFailure {
597
+ private constructor();
598
+ /** The cap, in UTF-16 code units, on a caller-supplied value echoed into a message. */
599
+ static readonly ECHO_LIMIT: number;
600
+ /** The larger cap for a value the engine produced (a path, a diagnostic), not one a caller typed. */
601
+ static readonly ENGINE_ECHO_LIMIT: number;
602
+ /**
603
+ * The `message` and `remediation` fields to spread into a consumer's
604
+ * `Schema.TaggedError`, each carrying a default `description` that a served
605
+ * JSON Schema (a tool's `outputSchema`) shows the agent.
606
+ */
607
+ static readonly fields: {
608
+ readonly message: Schema.String;
609
+ readonly remediation: Schema.Struct<{
610
+ readonly hint: Schema.String;
611
+ readonly suggestedTool: Schema.optionalKey<Schema.String>;
612
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
613
+ }>;
614
+ };
615
+ /**
616
+ * `"<raw> <hint> Try <suggestedTool>."`, dropping any empty part, so a
617
+ * missing tool or an empty hint never leaves a double space.
618
+ */
619
+ static readonly message: (raw: string, remediation: Remediation) => string;
620
+ /**
621
+ * `value` cut to `limit` code units with a trailing ellipsis, backing off one
622
+ * unit rather than splitting a surrogate pair. A hostile multi-megabyte
623
+ * argument is otherwise echoed into the agent's context whole.
624
+ */
625
+ static readonly truncate: (value: string, limit?: number) => string;
626
+ }
627
+ /**
628
+ * What a {@link ToolOutcome.catchAsEnvelope} case returns for one caught error:
629
+ * the same `message` and `remediation` a {@link ToolRefusal} carries, plus any
630
+ * structured fields the agent branches on.
631
+ *
632
+ * @remarks
633
+ * `message` is the plain sentence. The hint stays in `remediation`, where it
634
+ * reaches the agent as data; unlike a refusal's message, nothing is folded in.
635
+ *
636
+ * @public
637
+ */
638
+ interface ToolOutcomeErrorInput {
639
+ readonly message: string;
640
+ readonly remediation: Remediation;
641
+ readonly [field: string]: unknown;
642
+ }
643
+ /**
644
+ * The `ok: false` member of an envelope: `{ ok: false, error: { _tag, message, remediation, ...fields } }`.
645
+ *
646
+ * @public
647
+ */
648
+ interface ToolOutcomeFailure<Tag extends string, I extends ToolOutcomeErrorInput> {
649
+ readonly ok: false;
650
+ readonly error: {
651
+ readonly _tag: Tag;
652
+ } & I;
653
+ }
654
+ /**
655
+ * The failure values {@link ToolOutcome.catchAsEnvelope} produces for a set of
656
+ * cases: one {@link ToolOutcomeFailure} per handled tag.
657
+ *
658
+ * @public
659
+ */
660
+ type ToolOutcomeFailures<Cases> = { [K in keyof Cases]: Cases[K] extends ((...args: ReadonlyArray<never>) => infer I) ? I extends ToolOutcomeErrorInput ? ToolOutcomeFailure<K & string, I> : never : never; }[keyof Cases];
661
+ /**
662
+ * {@link ToolOutcome.envelope}'s options: annotations for the `ok: false`
663
+ * member, so a served `outputSchema` can name and describe it.
664
+ *
665
+ * @public
666
+ */
667
+ interface ToolOutcomeEnvelopeOptions {
668
+ /**
669
+ * The failure member's schema identifier. Core's JSON Schema output then
670
+ * emits it once under `$defs` by this name and refers to it with `$ref`.
671
+ */
672
+ readonly identifier?: string;
673
+ /**
674
+ * The failure member's description, replacing the default one (which says
675
+ * that `ok: false` carries an expected error to branch on).
676
+ */
677
+ readonly description?: string;
678
+ }
679
+ /**
680
+ * The `ok: false` structured-outcome envelope: an expected error an agent
681
+ * should branch on, returned on a tool's success channel so it reaches the
682
+ * client as `structuredContent` instead of message text.
683
+ *
684
+ * @remarks
685
+ * Two channels, two jobs, one error shape:
686
+ *
687
+ * - {@link ToolRefusal} is a declared failure: the call itself was wrong, and
688
+ * the agent reads one sentence (`isError: true`, message text only; core
689
+ * never sends a failure's `structuredContent`).
690
+ * - `ToolOutcome` is an expected domain outcome: an id that does not exist, a
691
+ * transition that is not allowed. The agent branches on its `_tag` and
692
+ * fields (`isError: false`, `structuredContent` carries the envelope).
693
+ *
694
+ * Both carry {@link ToolFailure.fields} (`message`, `remediation`), so an error
695
+ * can move between channels without reshaping.
696
+ *
697
+ * Success members are not reshaped. Spread {@link ToolOutcome.ok} into each
698
+ * success struct so a client can discriminate on `ok`; a member that
699
+ * discriminates another way (`found: true | false`) stays legal.
700
+ *
701
+ * @example
702
+ * ```ts
703
+ * import { ToolOutcome } from "@effected/mcp-kit"
704
+ * import { Effect, Schema } from "effect"
705
+ * import { Tool } from "effect/ai"
706
+ *
707
+ * const NotFound = ToolOutcome.error("GoalNotFound", { id: Schema.Number })
708
+ * const Lookup = Tool.make("goal", {
709
+ * parameters: Schema.Struct({ id: Schema.Number }),
710
+ * success: ToolOutcome.envelope(Schema.Struct({ ...ToolOutcome.ok, goal: Goal }), [NotFound]),
711
+ * })
712
+ * // in the handler:
713
+ * findGoal(id).pipe(
714
+ * Effect.map((goal) => ({ ok: true as const, goal })),
715
+ * ToolOutcome.catchAsEnvelope({
716
+ * GoalNotFound: (e) => ({ id: e.id, message: `No goal ${e.id}.`, remediation: { hint: "List goals first.", suggestedTool: "goal" } }),
717
+ * }),
718
+ * )
719
+ * ```
720
+ *
721
+ * @public
722
+ */
723
+ export declare class ToolOutcome {
724
+ private constructor();
725
+ /** `{ ok: true }` as schema fields, to spread into each success member. */
726
+ static readonly ok: {
727
+ readonly ok: Schema.Literal<true>;
728
+ };
729
+ /**
730
+ * The schema of one envelope error: `_tag`, {@link ToolFailure.fields} and
731
+ * `fields`. A field named `_tag`, `message` or `remediation` is rejected by
732
+ * the type, because it would replace the shared one.
733
+ */
734
+ static readonly error: <const Tag extends string, const Fields extends Schema.Struct.Fields = Record<never, never>>(tag: Tag, fields?: Fields & { readonly [K in "_tag" | "message" | "remediation"]?: never; }) => Schema.Struct<{
735
+ readonly _tag: Schema.Literal<Tag>;
736
+ } & typeof ToolFailure.fields & Fields>;
737
+ /**
738
+ * A tool's success schema: `success`, or `{ ok: false, error }` where `error`
739
+ * is one of `errors`. Object-rooted with {@link ToolOutputSchema.objectRooted},
740
+ * so core serves its `outputSchema` on every protocol revision.
741
+ *
742
+ * @remarks
743
+ * The failure member, its `ok` and its `error` carry default descriptions.
744
+ * `options` names the member (`identifier`, served once under `$defs`) or
745
+ * replaces its description.
746
+ */
747
+ static readonly envelope: <S extends Schema.Top, const Errors extends ReadonlyArray<Schema.Top>>(success: S, errors: Errors, options?: ToolOutcomeEnvelopeOptions) => Schema.Union<readonly [S, Schema.Struct<{
748
+ readonly ok: Schema.Literal<false>;
749
+ readonly error: Schema.Union<Errors>;
750
+ }>]>;
751
+ /** One `ok: false` value, for a handler that decides on a failure without catching an error. */
752
+ static readonly failure: <const Tag extends string, const I extends ToolOutcomeErrorInput>(tag: Tag, error: I) => ToolOutcomeFailure<Tag, I>;
753
+ /**
754
+ * Turns each handled tagged error into its `ok: false` value on the success
755
+ * channel; every other error stays in the error channel. Typed like core's
756
+ * `Effect.catchTags`: a case for a tag the effect cannot fail with is a type
757
+ * error, and the handled tags leave `E`.
758
+ *
759
+ * @remarks
760
+ * Write it inline in the effect's `pipe`. `E` is inferred from the effect
761
+ * being piped, so `effect.pipe(ToolOutcome.catchAsEnvelope({ … }))` checks
762
+ * every case name against the effect's real tags. Bound to a `const` first,
763
+ * the combinator never sees that effect: `E` infers as `unknown`, the
764
+ * unknown-tag check switches off, and a misspelled tag compiles and never
765
+ * matches. Core's `catchTags` looks a case up only among `cases`' own keys,
766
+ * so an error tagged `"toString"` or `"constructor"` never hits an inherited
767
+ * `Object.prototype` member and stays in the error channel.
768
+ */
769
+ static readonly catchAsEnvelope: <E, Cases extends { [K in Extract<E, {
770
+ readonly _tag: string;
771
+ }>["_tag"]]+?: (error: Extract<E, {
772
+ readonly _tag: K;
773
+ }>) => ToolOutcomeErrorInput; } & (unknown extends E ? Record<never, never> : { [K in Exclude<keyof Cases, Extract<E, {
774
+ readonly _tag: string;
775
+ }>["_tag"]>]: never; })>(cases: Cases) => <A, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A | ToolOutcomeFailures<Cases>, Exclude<E, {
776
+ readonly _tag: keyof Cases;
777
+ }>, R>;
778
+ /**
779
+ * Whether `value` is an `ok: false` envelope member: an object whose `ok` is
780
+ * exactly `false`. A success member that discriminates some other way, or a
781
+ * non-object, is not a failure. Narrows to `{ ok: false }` only: it checks
782
+ * the discriminant, not the `error` payload.
783
+ */
784
+ static readonly isFailure: (value: unknown) => value is {
785
+ readonly ok: false;
786
+ };
787
+ }
788
+ /**
789
+ * Shaping for a tool's `success` schema, so the `outputSchema` core serves
790
+ * is one every client accepts.
791
+ *
792
+ * @remarks
793
+ * MCP requires a tool's `outputSchema` to be rooted in an object, and a
794
+ * top-level `Schema.Union` emits a bare `anyOf`. Core drops such an
795
+ * `outputSchema` from `tools/list` on the stateful revisions and serves it
796
+ * verbatim on the stateless one, where strict clients reject it.
797
+ * `McpToolAudit`'s `objectRootedOutput` check reports it either way.
798
+ *
799
+ * {@link ToolOutputSchema.objectRooted} is the fix when every member of the
800
+ * union is an object shape; never apply it to a union with a primitive or
801
+ * array member. It works on the Effect
802
+ * schema, not on JSON Schema — unlike `ToolInputSchema.objectRooted`, which
803
+ * rewrites the served input document of a `Tool.dynamic`.
804
+ *
805
+ * @public
806
+ */
807
+ export declare class ToolOutputSchema {
808
+ private constructor();
809
+ /**
810
+ * `schema` with `type: "object"` added beside the `anyOf` at its JSON
811
+ * Schema root, through a check that always passes. Decoding, encoding and
812
+ * the schema's type are unchanged.
813
+ *
814
+ * @remarks
815
+ * Order against `.annotate({ identifier })` does not matter. A check added
816
+ * after an identifier starts a new node that would drop the definition
817
+ * name, so the identifier the schema already resolves is carried onto the
818
+ * new check, and the served documents are equal either way (key order
819
+ * can differ when the schema carries a title or description). Applying it
820
+ * twice is a no-op.
821
+ *
822
+ * Every union member must be an object shape: the added `type: "object"`
823
+ * is a claim about the whole union, and nothing checks it. Do not use it
824
+ * on a union with a primitive or array member — the served schema would
825
+ * contradict the values the tool returns; give such a tool an object
826
+ * envelope instead.
827
+ *
828
+ * @example
829
+ * ```ts
830
+ * const Result = ToolOutputSchema.objectRooted(Schema.Union([Found, Missing])).annotate({ identifier: "Result" })
831
+ * Tool.make("lookup", { parameters, success: Result })
832
+ * ```
833
+ */
834
+ static readonly objectRooted: <S extends Schema.Top>(schema: S) => S;
835
+ }
836
+ declare const ToolRefusal_base: Schema.Class<ToolRefusal, Schema.TaggedStruct<"ToolRefusal", {
837
+ readonly message: Schema.String;
838
+ readonly remediation: Schema.Struct<{
839
+ readonly hint: Schema.String;
840
+ readonly suggestedTool: Schema.optionalKey<Schema.String>;
841
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
842
+ }>;
843
+ }>, import("effect/Cause").YieldableError>;
844
+ /**
845
+ * A tool call refused for a reason the caller can fix: the kit's ready-made
846
+ * declared failure, built on {@link ToolFailure.fields}.
847
+ *
848
+ * @remarks
849
+ * Core scrubs every undeclared failure and every defect to one generic
850
+ * internal-error sentence, so a refusal the agent must act on (an unknown id,
851
+ * a missing argument, a precondition) reaches it only when the tool declares
852
+ * it. Declare `ToolRefusal` in the tool's `failure` schema and fail with
853
+ * {@link ToolRefusal.refuse}; its message already carries the remediation,
854
+ * because a declared `Error` failure sends `message` and nothing else.
855
+ *
856
+ * Audit every throw path of a tool: any failure left undeclared loses its
857
+ * message on the wire, however carefully it was worded.
858
+ *
859
+ * **Structured data on a refusal is deliberately unreachable: put it in the
860
+ * message.** Core's `McpServer.registerToolkit` builds every failed call's
861
+ * result with `structuredContent` unset and no `_meta`, on every protocol
862
+ * revision. Under the default `failureMode: "error"` a declared `Error`
863
+ * failure sends `message` as the only text; under `failureMode: "return"`
864
+ * it sends the whole encoded failure JSON-stringified into the text, still
865
+ * with no `structuredContent`, which buries the human message in JSON a
866
+ * client must parse. So `refuse` takes no data argument: a list of valid
867
+ * names, the root that was searched, the id that was not found all belong
868
+ * in `reason`, truncated with {@link ToolFailure.truncate}. When an agent
869
+ * must branch on the data itself, return it on the success channel as an
870
+ * `ok: false` envelope, where `structuredContent` does reach the client.
871
+ *
872
+ * @example
873
+ * ```ts
874
+ * import { ToolFailure, ToolRefusal } from "@effected/mcp-kit";
875
+ * import { Tool } from "effect/ai";
876
+ *
877
+ * const Lookup = Tool.make("lookup", { parameters, success, failure: ToolRefusal })
878
+ * // in the handler:
879
+ * return yield* ToolRefusal.refuse(`No run "${ToolFailure.truncate(id)}".`, { hint: "List runs first.", suggestedTool: "list_runs" })
880
+ * ```
881
+ *
882
+ * @public
883
+ */
884
+ export declare class ToolRefusal extends ToolRefusal_base {
885
+ /**
886
+ * A refusal for `reason`, with `remediation` folded into its message by
887
+ * {@link ToolFailure.message}. `reason` must already truncate any
888
+ * caller-supplied value it echoes, with {@link ToolFailure.truncate}.
889
+ */
890
+ static readonly refuse: (reason: string, remediation: Remediation) => ToolRefusal;
891
+ }
892
+ /**
893
+ * The narrow store {@link ToolReplay.wrap} reads and writes: `get` and `set`
894
+ * on string values, nothing else.
895
+ *
896
+ * @remarks
897
+ * Core's `KeyValueStore` already satisfies it structurally, so a
898
+ * `KeyValueStore` value can be passed as is. An application backed by its
899
+ * own table implements just these two methods, instead of stubbing the
900
+ * `remove`, `clear`, `size` and `keys` that `KeyValueStore.makeStringOnly` demands.
901
+ *
902
+ * `get` answers `undefined` for a missing key, the same contract as
903
+ * `KeyValueStore.get`. A failure from either method is never a tool error:
904
+ * a failed `get` is a miss and a failed `set` is a skipped write, both
905
+ * logged at debug level, so the error channel is `unknown`.
906
+ *
907
+ * @public
908
+ */
909
+ interface ToolReplayStore {
910
+ /** The stored value for `key`, or `undefined` when nothing is stored under it. */
911
+ readonly get: (key: string) => Effect.Effect<string | undefined, unknown>;
912
+ /** Stores `value` under `key`, replacing any earlier value. */
913
+ readonly set: (key: string, value: string) => Effect.Effect<void, unknown>;
914
+ }
915
+ /**
916
+ * What {@link ToolReplay.wrap} needs to know about one tool.
917
+ *
918
+ * @typeParam Store - The type of {@link ToolReplayOptions.store}; when it is
919
+ * omitted, `wrap` requires `KeyValueStore` in `R` instead.
920
+ *
921
+ * @public
922
+ */
923
+ interface ToolReplayOptions<P, S extends Schema.Top, Store extends ToolReplayStore | undefined = undefined> {
924
+ /** The served tool name. It must not contain `:`, the separator of the stored key ({@link ToolReplay.storeKey}). */
925
+ readonly tool: string;
926
+ /**
927
+ * The tool's success schema. Results are stored encoded through it and
928
+ * decoded through it on replay. It must declare {@link ToolReplay.marker} on
929
+ * each object member, or core's encoder drops the marker from the reply:
930
+ * build each member with {@link ToolReplay.success} so it cannot be left out.
931
+ */
932
+ readonly success: S;
933
+ /**
934
+ * The idempotency key for one call; `Option.none()` runs the handler with
935
+ * nothing stored or replayed.
936
+ *
937
+ * @remarks
938
+ * The key must identify the request completely. A replay compares nothing
939
+ * else: not the params, not the schema version. So a reused key with
940
+ * different params returns the FIRST call's result. When the params under
941
+ * one caller-supplied id can vary, fold a digest of them into the key.
942
+ */
943
+ readonly key: (params: P) => Option.Option<string>;
944
+ /**
945
+ * Where results are stored. Omitted, `wrap` reads core's `KeyValueStore`
946
+ * from `R`; given, it uses this store and `KeyValueStore` leaves `R`.
947
+ */
948
+ readonly store?: Store;
949
+ }
950
+ /**
951
+ * Idempotent replay for a write tool: a repeated call with the same key returns
952
+ * the first call's stored result, marked `_idempotentReplay: true`, instead of
953
+ * running the mutation again.
954
+ *
955
+ * @remarks
956
+ * The store is core's `KeyValueStore` (`effect/persistence`), required in `R`
957
+ * (`KeyValueStore.layerMemory`, `layerFileSystem` or `layerSql`), unless
958
+ * {@link ToolReplayOptions.store} supplies a {@link ToolReplayStore}: just
959
+ * `get` and `set`, for an application's own table. Then `R` carries no
960
+ * `KeyValueStore`.
961
+ *
962
+ * Inside an MCP server, pass the store. A service left in a handler's `R`
963
+ * has to be declared in the tool's `dependencies`, which makes it a
964
+ * request-level dependency that only MCP middleware can meet. Read
965
+ * `KeyValueStore` once when the handlers are built, with the effect form of
966
+ * `Toolkit.toLayer`, and hand it to `wrap` as `store`: a `KeyValueStore`
967
+ * satisfies {@link ToolReplayStore} as is.
968
+ *
969
+ * Semantics, per call:
970
+ *
971
+ * - A key of `Option.none()` runs the handler; nothing is read or stored.
972
+ * - A stored value that decodes through `success` is returned with the marker,
973
+ * and the handler does not run.
974
+ * - A miss runs the handler and stores its result, best-effort. A store that
975
+ * fails, a row that no longer decodes (written under an older schema, or
976
+ * corrupt), and a failed write are each a miss or a skipped write, never a
977
+ * tool error. Each is logged at debug level with the tool name.
978
+ * - A handler failure stores nothing, so a retry runs it again.
979
+ * - An `ok: false` result (a {@link ToolOutcome} envelope failure) is never
980
+ * stored: a "not found" must not replay after the thing exists.
981
+ *
982
+ * This is replay, not exactly-once: two concurrent calls with one key both
983
+ * miss and both run.
984
+ *
985
+ * The key is the whole identity of a request. A hit compares neither the
986
+ * params nor the schema version (a row written under an older schema replays
987
+ * whenever it still decodes), and rows never expire: the store keeps a result
988
+ * until something removes it. A key reused with different params therefore
989
+ * replays the first result, so {@link ToolReplayOptions.key} must fold in a
990
+ * digest of any params that can vary under one id.
991
+ *
992
+ * Wrap it inside {@link ToolOutcome.catchAsEnvelope}, so the envelope sees the
993
+ * handler's real errors and replay sees only real successes.
994
+ *
995
+ * @example
996
+ * ```ts
997
+ * import { ToolReplay } from "@effected/mcp-kit"
998
+ * import { Option, Schema } from "effect"
999
+ *
1000
+ * const Created = ToolReplay.success(Schema.Struct({ ok: Schema.Literal(true), id: Schema.Number }))
1001
+ * const create = ToolReplay.wrap({
1002
+ * tool: "goal_create",
1003
+ * success: Created,
1004
+ * key: (params: { readonly requestId?: string }) => Option.fromUndefinedOr(params.requestId),
1005
+ * })(createGoal)
1006
+ * ```
1007
+ *
1008
+ * @public
1009
+ */
1010
+ export declare class ToolReplay {
1011
+ private constructor();
1012
+ /**
1013
+ * The marker field, to spread into each object member of a replayable
1014
+ * success schema. {@link ToolReplay.success} adds it for you.
1015
+ */
1016
+ static readonly marker: {
1017
+ readonly _idempotentReplay: Schema.optionalKey<Schema.Literal<true>>;
1018
+ };
1019
+ /**
1020
+ * A struct with {@link ToolReplay.marker} added. Use it for each object
1021
+ * success member, so core's encoder keeps the marker on a replayed reply.
1022
+ *
1023
+ * @remarks
1024
+ * Built with core's `fieldsAssign`, which, like `mapFields`, does not carry
1025
+ * the struct's own `.check(...)` refinements over: apply checks to the
1026
+ * result, not to the struct passed in.
1027
+ */
1028
+ static readonly success: <const Fields extends Schema.Struct.Fields>(schema: Schema.Struct<Fields>) => Schema.Struct<Struct.Simplify<Struct.Assign<Fields, typeof ToolReplay.marker>>>;
1029
+ /**
1030
+ * The `KeyValueStore` key one call is stored under: `<tool>:<key>`. Split on
1031
+ * the first `:` to recover both parts; MCP tool names are `[A-Za-z0-9_.-]`.
1032
+ */
1033
+ static readonly storeKey: (tool: string, key: string) => string;
1034
+ /**
1035
+ * Wraps a handler with replay. Throws at wrap time when `tool` contains
1036
+ * `:`, a wiring mistake, so it surfaces when the server is built.
1037
+ *
1038
+ * @remarks
1039
+ * Call it where the handler is built, once: the effect form of
1040
+ * `Toolkit.toLayer`, reading the store's services there and passing them as
1041
+ * `store`. Calling it per request, inside the handler, rebuilds the JSON
1042
+ * codec on every call and turns the wiring check's throw into a defect at
1043
+ * the first call instead of a failure when the server is built.
1044
+ */
1045
+ static readonly wrap: <P, S extends Schema.Top, Store extends ToolReplayStore | undefined = undefined>(options: ToolReplayOptions<P, S, Store>) => (<E, R>(handler: (params: P) => Effect.Effect<S["Type"], E, R>) => (params: P) => Effect.Effect<S["Type"], E, R | ([Store] extends [ToolReplayStore] ? never : KeyValueStore.KeyValueStore) | S["DecodingServices"] | S["EncodingServices"]>);
1046
+ }
1047
+ export type { FormatUnknownKeysOptions, McpLaunchOptions, McpStdioOptions, McpStdioRegistrationServices, McpToolkitOptions, ToolOutcomeEnvelopeOptions, ToolOutcomeErrorInput, ToolOutcomeFailure, ToolOutcomeFailures, ToolReplayOptions, ToolReplayStore, UnionHandlerOptions, UnionTool, UnionToolOptions, UnknownKeysLevel };
1048
+ //# sourceMappingURL=index.d.ts.map