@effected/mcp 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Array, Effect, JsonSchema, Layer, Runtime, Schema, Stdio } from "effect";
2
- import { McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/unstable/ai";
1
+ import { Array, Context, Effect, JsonSchema, Layer, Runtime, Schema, Stdio } from "effect";
2
+ import { McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
3
3
  import { Remediation } from "@effected/engine";
4
4
  //#region src/McpStdio.d.ts
5
5
  /**
@@ -19,6 +19,26 @@ interface McpStdioOptions {
19
19
  /** Defaults to {@link McpStdio.protocols}. Read its ordering rules before overriding. */
20
20
  readonly protocols?: Array.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter> | undefined;
21
21
  }
22
+ /**
23
+ * Options for {@link McpStdio.launch}.
24
+ *
25
+ * @public
26
+ */
27
+ interface McpLaunchOptions {
28
+ /**
29
+ * Runs once the whole layer has built, and before the launch waits
30
+ * forever. By then the stdio protocol is reading stdin: the server is
31
+ * serving, although no client may have sent `initialize` yet.
32
+ *
33
+ * @remarks
34
+ * `McpGuard.run` (`@effected/mcp/guard`) uses it as its "connected"
35
+ * signal. A layer built after the server layer, as in
36
+ * `Layer.effectDiscard(ready).pipe(Layer.provide(Main))`, gives the same
37
+ * signal, but only if every consumer re-derives that `Layer.provide`
38
+ * builds its dependency first.
39
+ */
40
+ readonly onReady?: Effect.Effect<void> | undefined;
41
+ }
22
42
  /**
23
43
  * Serve an MCP server over stdio without ever writing a log line or a failure
24
44
  * report onto stdout, which is the JSON-RPC wire.
@@ -79,26 +99,35 @@ export declare class McpStdio {
79
99
  *
80
100
  * The server reads stdin through a guard that frames it exactly as core's
81
101
  * decoder does: one streaming UTF-8 decoder, a byte-order mark stripped
82
- * only at the start of the stream, lines split on `\n`. A line that is
83
- * not JSON is answered on stdout with a JSON-RPC parse error, code
84
- * `-32700` and `id: null`, and never reaches core's decoder, which would
85
- * otherwise throw on that line again for every later chunk and stop
86
- * answering. So is a line longer than core's cap of 16 Mi UTF-16 code
87
- * units: it is answered once, as soon as it passes the cap, and the rest
88
- * of it is discarded up to its newline. A line of JSON whitespace (space,
89
- * tab, carriage return) is ignored.
102
+ * only at the start of the stream, lines split on `\n`. Core itself skips
103
+ * a line it cannot use and keeps serving, but sends no reply, where
104
+ * JSON-RPC 2.0 requires one; the guard supplies it. A line that is not
105
+ * JSON is answered on stdout with a parse error, code `-32700` and
106
+ * `id: null`, and never reaches core. So is a line longer than core's cap
107
+ * of 16 Mi UTF-16 code units: it is answered once, as soon as it passes
108
+ * the cap, and the rest of it is discarded up to its newline (core would
109
+ * log it on stderr and drop it). A line of JSON whitespace (space, tab,
110
+ * carriage return) is ignored.
90
111
  *
91
112
  * A line that is JSON but no JSON-RPC message core can handle is answered
92
113
  * with an Invalid Request, code `-32600` and `id: null`, and never reaches
93
- * core: a value that is neither an object nor an array (core throws on
94
- * `null`, dropping every other frame in its chunk, and ignores a number,
95
- * string or boolean without a reply), an object whose `method` is not a
96
- * string and whose `id` is absent or `null` (core throws on it too), and
97
- * an object with neither `method` nor `id`, which is neither a request nor
98
- * a response. Everything else goes to core: an array, which core answers
99
- * `-32600` itself because it serves no batches; an object with an `id`
100
- * and no `method`, which is a response and gets no reply; and a request
101
- * with an `id`, which core answers even when its `method` is malformed.
114
+ * core: a value that is neither an object nor an array, an object whose
115
+ * `method` is not a string and whose `id` is absent or `null`, and an
116
+ * object with neither `method` nor `id`, which is neither a request nor a
117
+ * response.
118
+ *
119
+ * A line whose `method` starts with `@effect/rpc/` never reaches core,
120
+ * whose JSON-RPC decoder would read one without an `id` as its own RPC
121
+ * control message: `@effect/rpc/Eof` alone silently stops the server
122
+ * (Effect-TS/effect#8499, open upstream). Such a request is answered
123
+ * `-32601` Method not found, echoing its `id`; such a notification is
124
+ * dropped. The guard stops doing this once core no longer treats a
125
+ * client's `@effect/rpc/` frame as a control message.
126
+ *
127
+ * Everything else goes to core: an array, which core answers `-32600`
128
+ * itself because it serves no batches; an object with an `id` and no
129
+ * `method`, which is a response and gets no reply; and a request with an
130
+ * `id`, which core answers even when its `method` is malformed.
102
131
  *
103
132
  * Give each server a fresh layer memo map. Core's stdio protocol layer
104
133
  * is a shared constant, so a second `McpStdio.layer` server whose build
@@ -107,8 +136,12 @@ export declare class McpStdio {
107
136
  * one graph does that, and so does building or providing the second
108
137
  * anywhere under the first one's `Effect.provide`: nested `Layer.build`
109
138
  * and `Effect.provide` fork the ambient memo map rather than starting a
110
- * new one. Isolate each server with its own `ManagedRuntime`,
111
- * `Effect.provide(layer, { local: true })` or its own process.
139
+ * new one. The tool registry is shared the same way. Isolate each server
140
+ * by wrapping its whole bundle (its toolkit layers together with this
141
+ * layer) in `Layer.fresh`, or with its own `ManagedRuntime`,
142
+ * `Effect.provide(layer, { local: true })` or its own process. Never put
143
+ * the `Layer.fresh` boundary between a toolkit and this layer: the server
144
+ * would serve an empty registry.
112
145
  *
113
146
  * Code after a completed `Effect.provide` of a stdio server never runs:
114
147
  * core's stdio protocol interrupts the fiber that built it when its stdin
@@ -129,8 +162,11 @@ export declare class McpStdio {
129
162
  * `Effect.provideService` reaches, and Effect's default logger writes
130
163
  * through `console.log` unless `LogToStderr` is set. For an MCP server
131
164
  * that is stdout, which is the wire.
165
+ *
166
+ * `options.onReady` runs after the layer has built and before the launch
167
+ * waits forever, as {@link McpLaunchOptions.onReady} describes.
132
168
  */
133
- static readonly launch: <ROut, E, R>(layer: Layer.Layer<ROut, E, R>) => Effect.Effect<never, Error, R>;
169
+ static readonly launch: <ROut, E, R>(layer: Layer.Layer<ROut, E, R>, options?: McpLaunchOptions) => Effect.Effect<never, Error, R>;
134
170
  /**
135
171
  * Success or an interrupt-only exit maps to 0. Anything else goes to
136
172
  * `Runtime.defaultTeardown`.
@@ -157,6 +193,11 @@ interface UnknownKeysLevel {
157
193
  readonly unknown: ReadonlyArray<string>;
158
194
  /** Keys this level declares, in schema order. */
159
195
  readonly accepted: ReadonlyArray<string>;
196
+ /**
197
+ * The `patternProperties` patterns this level also accepts keys by, in
198
+ * schema order. Present only when the level has at least one.
199
+ */
200
+ readonly acceptedPatterns?: ReadonlyArray<string>;
160
201
  }
161
202
  /**
162
203
  * Options for {@link ToolInputSchema.formatUnknownKeys}.
@@ -172,10 +213,12 @@ interface FormatUnknownKeysOptions {
172
213
  * depth, a message naming them all, and an object-rooted form of a union.
173
214
  *
174
215
  * @remarks
175
- * Core's `Tool.Strict` rejects unknown keys but reports only the first
176
- * (effect `unstable/ai/McpServer.ts` decodes without `errors: "all"`), so an
177
- * agent fixes one typo per round trip. These walkers run over the schema core
178
- * actually serves, so what they accept matches what the tool advertises:
216
+ * For a `Tool.make` tool, core's strict decode already names every unknown
217
+ * key, with every missing or invalid field, in one response. Core never
218
+ * validates a `Tool.dynamic` tool's raw JSON Schema, so these walkers are
219
+ * for that case: a handler runs them against the schema it registered.
220
+ * They run over the schema as served, so what they accept matches what the
221
+ * tool advertises:
179
222
  *
180
223
  * - Only `additionalProperties: false` closes a node, as in JSON Schema; core
181
224
  * emits it on every object node of a strict tool and `true` otherwise.
@@ -186,6 +229,9 @@ interface FormatUnknownKeysOptions {
186
229
  * - `items`, `prefixItems`, `$ref` into `$defs` (JSON Pointer escapes
187
230
  * honoured), and `patternProperties` are followed; a key matched by a
188
231
  * pattern is accepted and walked with the first matching pattern's schema.
232
+ * Effect serves a pattern-keyed `Schema.Record` with `patternProperties`
233
+ * only when the key pattern's RegExp has the `u` flag; without it the
234
+ * record is served open, and every key is accepted.
189
235
  * - The payload is untrusted: the walk stops descending at depth 256 rather
190
236
  * than overflowing the stack, so a deeper unknown key goes unreported and
191
237
  * is left for decoding. Every hop counts toward that cap: each property or
@@ -199,9 +245,12 @@ export declare class ToolInputSchema {
199
245
  /** Every level of `payload` that carries a key `schema` does not accept. */
200
246
  static readonly unknownKeys: (payload: unknown, schema: JsonSchema.JsonSchema) => ReadonlyArray<UnknownKeysLevel>;
201
247
  /**
202
- * `Unrecognized parameter(s): a, b.c. Accepted params: x, y.` per level. Each
203
- * echoed key is truncated, and at most 20 keys and 20 levels are named; a
204
- * trailing `(and N more levels).` counts the levels left out.
248
+ * `Unrecognized parameter(s): a, b.c. Accepted params: x, y.` per level,
249
+ * followed by `Accepted keys matching: ^x-.` when the level also accepts
250
+ * keys by pattern. A level that accepts keys only by pattern names its
251
+ * patterns alone, never `Accepted params: (none).` Each echoed key and
252
+ * pattern is truncated, and at most 20 keys, 20 patterns and 20 levels are
253
+ * named; a trailing `(and N more levels).` counts the levels left out.
205
254
  */
206
255
  static readonly formatUnknownKeys: (levels: ReadonlyArray<UnknownKeysLevel>, options?: FormatUnknownKeysOptions) => string;
207
256
  /**
@@ -226,51 +275,114 @@ interface McpToolkitOptions {
226
275
  * annotation is served and decoded strict. `"annotated"`: only tools
227
276
  * annotated `Tool.Strict` true are. In both modes an explicit annotation,
228
277
  * true or false, always wins, and a `Tool.dynamic` tool is never
229
- * re-annotated (core dies at registration on a strict dynamic tool). Only
230
- * a tool that ends up strict gets the unknown-key pre-check; a lenient
231
- * one is passed through untouched even if its raw schema is closed.
278
+ * re-annotated (core dies at registration on a strict dynamic tool).
232
279
  */
233
280
  readonly strict?: "all" | "annotated" | undefined;
234
281
  /**
235
- * Replaces the default `ToolInputSchema.formatUnknownKeys` rendering. It
236
- * runs per rejected call inside the handler's effect, so a throw becomes
237
- * a defect of that call alone — the client receives a JSON-RPC internal
238
- * error (-32603) — never a crash of the server.
282
+ * Ignored. Core's strict decode reports every excess key at every depth,
283
+ * together with every missing or invalid field, in one `InvalidParams`,
284
+ * and the layer appends a fixed `Accepted params at <path>: …` line per
285
+ * level that carries an unknown key; this option changes neither.
286
+ *
287
+ * @deprecated A no-op, scheduled for removal in a later minor.
239
288
  */
240
289
  readonly unknownKeyMessage?: ((levels: ReadonlyArray<UnknownKeysLevel>) => string) | undefined;
241
290
  }
242
291
  /**
243
- * Register a toolkit exactly as core's `McpServer.toolkit` does, except that a
244
- * strict tool's unknown arguments are all named, at every depth, in one
245
- * response.
292
+ * A `Tool.dynamic` made by {@link McpToolkit.unionTool}: served with the
293
+ * strict, object-rooted JSON Schema of a `Schema.Union` of objects, and
294
+ * carrying that union so {@link McpToolkit.unionHandler} can decode it.
246
295
  *
247
296
  * @remarks
248
- * Rejection itself is core's: a tool annotated `Tool.Strict` is served with
249
- * `additionalProperties: false` on every object node and decoded with
250
- * `onExcessProperty: "error"`. But core reports only the FIRST excess key
251
- * (`Expected no excess property at ["extra"]`), so an agent fixes one typo
252
- * per round trip and never learns about a nested one until the next call.
253
- * This layer is the better report, not the rejecter: it runs core's
297
+ * `annotate` and `addDependency` keep the type, so a chained
298
+ * `.annotate(Tool.Title, …)` still hands `unionHandler` the union. The
299
+ * declared failure is `failure` together with `McpSchema.InvalidParams`.
300
+ *
301
+ * @public
302
+ */
303
+ interface UnionTool<Name extends string, P extends Schema.Decoder<unknown>, S extends Schema.Constraint, F extends Schema.Constraint, R = never> extends Tool.Dynamic<Name, {
304
+ readonly parameters: JsonSchema.JsonSchema;
305
+ readonly success: S;
306
+ readonly failure: Schema.Union<readonly [F, typeof McpSchema.InvalidParams]>;
307
+ readonly failureMode: "error";
308
+ }, R> {
309
+ /** The union the tool decodes its arguments with. */
310
+ readonly unionParameters: P;
311
+ /** Add an annotation, keeping the union. */
312
+ annotate<I, V>(tag: Context.Key<I, V>, value: V): UnionTool<Name, P, S, F, R>;
313
+ /** Add a request-level dependency, keeping the union. */
314
+ addDependency<Identifier, Service>(tag: Context.Key<Identifier, Service>): UnionTool<Name, P, S, F, Identifier | R>;
315
+ }
316
+ /**
317
+ * Options for {@link McpToolkit.unionTool}.
318
+ *
319
+ * @public
320
+ */
321
+ interface UnionToolOptions<P extends Schema.Decoder<unknown>, S extends Schema.Constraint, F extends Schema.Constraint, Dependencies extends ReadonlyArray<Context.Key<unknown, unknown>>> {
322
+ /** What the tool does, for the agent. */
323
+ readonly description?: string | undefined;
324
+ /** A `Schema.Union` of object schemas, discriminated by an `action`, `kind`, `_tag` or `type` literal. */
325
+ readonly parameters: P;
326
+ /** The success schema. Defaults to `Schema.Void`; object-root a union whose members are all objects with `ToolOutputSchema.objectRooted`. */
327
+ readonly success?: S | undefined;
328
+ /** Declared failures besides `InvalidParams`, such as `ToolRefusal`. Defaults to `Schema.Never`. */
329
+ readonly failure?: F | undefined;
330
+ /** Services the handler needs, as with `Tool.make`. */
331
+ readonly dependencies?: Dependencies | undefined;
332
+ }
333
+ /**
334
+ * Options for {@link McpToolkit.unionHandler}.
335
+ *
336
+ * @public
337
+ */
338
+ interface UnionHandlerOptions {
339
+ /**
340
+ * Ignored. The union decode reports every excess, missing and invalid
341
+ * field in one `InvalidParams`, followed by the same fixed
342
+ * `Accepted params at <path>: …` lines {@link McpToolkit.layer} appends;
343
+ * this option changes neither.
344
+ *
345
+ * @deprecated A no-op, scheduled for removal in a later minor.
346
+ */
347
+ readonly unknownKeyMessage?: ((levels: ReadonlyArray<UnknownKeysLevel>) => string) | undefined;
348
+ }
349
+ /**
350
+ * Register a toolkit exactly as core's `McpServer.toolkit` does, except that
351
+ * every tool is strict by default and a {@link McpToolkit.unionTool} gets its
352
+ * union decoded.
353
+ *
354
+ * @remarks
355
+ * Rejection and reporting are core's: a tool annotated `Tool.Strict` is
356
+ * served with `additionalProperties: false` on every object node and decoded
357
+ * with `onExcessProperty: "error"` and `errors: "all"`, so one
358
+ * `InvalidParams` names every excess key at every depth together with every
359
+ * missing or invalid field. The layer appends to that report, never in place
360
+ * of it: for every object level of the payload that carries a key the served
361
+ * input schema does not accept, one line naming what that level does accept,
362
+ * `Accepted params at the root: query, filter.` or
363
+ * `Accepted params at ["filter"]: kind, tag.` (the path as core writes it),
364
+ * with `keys matching <pattern>` for keys a `patternProperties` level
365
+ * accepts, and `This tool accepts no params.` for a zero-parameter tool. An
366
+ * agent can fix the call from the reply alone. A failure with no unknown key
367
+ * (a missing or mistyped field only) is core's report unchanged. The lines
368
+ * are appended to core's `InvalidParams` as it leaves the tool's registered
369
+ * handler, the one failure that handler has for bad parameters.
370
+ *
371
+ * On top of that is the policy: under
372
+ * `strict: "all"` (the default) it annotates every tool that carries no
373
+ * `Tool.Strict` annotation of its own as strict, skipping `Tool.dynamic`
374
+ * tools (core dies at registration on a strict dynamic tool). It runs core's
254
375
  * `registerToolkit` unchanged under a registration-scoped `McpServer` whose
255
- * `addTool` puts a `ToolInputSchema.unknownKeys` pre-check in front of each
256
- * handler. The pre-check walks the input schema the tool actually serves
257
- * (after core's own top-level `$ref` inlining), follows `$ref`, `allOf` and
258
- * discriminated `oneOf`/`anyOf` members, and fails with one
259
- * `McpSchema.InvalidParams` naming every unknown key path plus the accepted
260
- * params, before core decodes. Core's strict decode stays behind it as the
261
- * backstop. The pre-check is gated on the same predicate core uses to choose
262
- * strict decoding — the registered tool's `Tool.Strict` annotation is
263
- * `true` — so a lenient tool (explicit `Tool.Strict` false, a
264
- * `Tool.dynamic` tool, or any unannotated tool under `"annotated"`) is
265
- * never rejected here, even when its raw JSON Schema carries
266
- * `additionalProperties: false`. The pre-check runs under `Effect.suspend`,
267
- * so a throwing `unknownKeyMessage` dies inside the call's effect. No core
268
- * internals are patched, so this survives an rc bump without re-diffing.
376
+ * `addTool` puts the union decode in front of a union tool's handler and
377
+ * passes every other registration through untouched. It patches no core
378
+ * internals: it uses only `McpServer.registerToolkit` and `addTool`.
269
379
  *
270
380
  * Registration goes through core's module-level `McpServer.McpServer.layer`,
271
381
  * shared by reference with `McpStdio.layer`'s own copy — provide both into
272
382
  * the same graph, as with `McpServer.toolkit`. Never wrap that layer in
273
383
  * `Layer.fresh`: tools would register into a second registry nobody serves.
384
+ * To give two servers separate registries, wrap each whole bundle (this
385
+ * layer together with `McpStdio.layer`) in `Layer.fresh` instead.
274
386
  *
275
387
  * Each call mints a fresh layer; bind the result to a `const` or the
276
388
  * registration runs twice.
@@ -289,8 +401,65 @@ interface McpToolkitOptions {
289
401
  export declare class McpToolkit {
290
402
  private constructor();
291
403
  /**
292
- * `McpServer.toolkit`'s registration layer, strict by default and naming
293
- * every unknown argument in one `InvalidParams`.
404
+ * A tool whose parameters are a `Schema.Union` of objects, which
405
+ * `Tool.make` cannot take: core dies at registration on a non-object
406
+ * `parameters` root.
407
+ *
408
+ * @remarks
409
+ * The tool is a `Tool.dynamic` served with Effect's strict JSON Schema
410
+ * document for `parameters` (`additionalProperties: false` on every
411
+ * object node, definitions as `$defs`), rewritten by
412
+ * `ToolInputSchema.objectRooted` to an object root carrying `type`,
413
+ * `oneOf` and `x-discriminator`. Write its handler with
414
+ * {@link McpToolkit.unionHandler}, which receives the decoded member.
415
+ *
416
+ * Registered through {@link McpToolkit.layer}, a union tool gets the same
417
+ * treatment as a strict `Tool.make` tool: a strict decode with
418
+ * `errors: "all"` before the handler runs, reporting every excess, missing
419
+ * and invalid field of the matched member in one `InvalidParams`, with the
420
+ * same `Accepted params at <path>: …` lines appended, so bad
421
+ * arguments answer JSON-RPC `-32602` on `2025-06-18` and an `isError`
422
+ * result on the later revisions, exactly as a `Tool.make` decode failure
423
+ * does. Registered through core's `McpServer.toolkit` it still decodes
424
+ * strictly, in the handler, but the `InvalidParams` is then a declared
425
+ * failure: an `isError` result on every revision.
426
+ *
427
+ * Never annotate it `Tool.Strict` true: core dies at registration on a
428
+ * strict tool with a raw JSON Schema.
429
+ *
430
+ * @example
431
+ * ```ts
432
+ * const Note = McpToolkit.unionTool("note", {
433
+ * parameters: Schema.Union([AddNote, ListNotes]),
434
+ * success: ToolOutputSchema.objectRooted(Schema.Union([Added, Listed])),
435
+ * failure: ToolRefusal,
436
+ * }).annotate(Tool.Title, "Note")
437
+ * const handlers = Kit.toLayer({ note: McpToolkit.unionHandler(Note, (params) => handleNote(params)) })
438
+ * ```
439
+ */
440
+ 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]>>;
441
+ /**
442
+ * The handler for a {@link McpToolkit.unionTool}: decodes the raw payload
443
+ * exactly as {@link McpToolkit.layer} does, and passes the decoded member
444
+ * to `handler`.
445
+ *
446
+ * @remarks
447
+ * A payload that does not decode strictly fails with one
448
+ * `McpSchema.InvalidParams`, worded as core words a `Tool.make` decode
449
+ * failure, naming every excess key at every depth together with every
450
+ * missing or invalid field of the matched member, then one
451
+ * `Accepted params at <path>: …` line per level with an unknown key. It
452
+ * is the same
453
+ * implementation the layer runs, so a handler called directly (a test
454
+ * helper, or a toolkit registered through core's `McpServer.toolkit`)
455
+ * reports what a client of the layer sees. Under {@link McpToolkit.layer}
456
+ * the layer has already rejected a bad call before the handler runs.
457
+ * `options.unknownKeyMessage` is deprecated and ignored.
458
+ */
459
+ 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>);
460
+ /**
461
+ * `McpServer.toolkit`'s registration layer, strict by default.
462
+ * `options.unknownKeyMessage` is deprecated and ignored.
294
463
  */
295
464
  static readonly layer: <Tools extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<Tools>, options?: McpToolkitOptions) => Layer.Layer<never, never, Tool.HandlersFor<Tools> | Exclude<Tool.HandlerServices<Tools>, McpSchema.McpRequestContext>>;
296
465
  }
@@ -302,14 +471,20 @@ export declare class McpToolkit {
302
471
  * @remarks
303
472
  * Core sends a declared failure that is an `Error` instance — every
304
473
  * `Schema.TaggedError` is — as `isError: true` with `error.message` as the only
305
- * text and no `structuredContent` (effect `unstable/ai/McpServer.ts`,
474
+ * text and no `structuredContent` (effect `ai/McpServer.ts`,
306
475
  * `declaredFailureResult`). So the remediation must be folded into `message`
307
476
  * when the error is constructed: whatever is not in the message never reaches
308
477
  * the agent.
309
478
  *
310
479
  * Spread {@link ToolFailure.fields} into your tool's `Schema.TaggedError`, build
311
480
  * `message` with {@link ToolFailure.message}, and pass every caller-supplied
312
- * value through {@link ToolFailure.truncate} before echoing it.
481
+ * value through {@link ToolFailure.truncate} before echoing it. For a plain
482
+ * refusal with no fields of its own, `ToolRefusal` is this shape ready-made.
483
+ *
484
+ * Only a declared failure keeps its message: core scrubs an undeclared
485
+ * failure or a defect to "Tool execution failed due to an internal server
486
+ * error.", so audit every throw path a tool can take and declare each one the
487
+ * agent should read.
313
488
  *
314
489
  * @example
315
490
  * ```ts
@@ -348,5 +523,97 @@ export declare class ToolFailure {
348
523
  static readonly truncate: (value: string, limit?: number) => string;
349
524
  }
350
525
  //#endregion
351
- export type { FormatUnknownKeysOptions, McpStdioOptions, McpToolkitOptions, UnknownKeysLevel };
526
+ //#region src/ToolOutputSchema.d.ts
527
+ /**
528
+ * Shaping for a tool's `success` schema, so the `outputSchema` core serves
529
+ * is one every client accepts.
530
+ *
531
+ * @remarks
532
+ * MCP requires a tool's `outputSchema` to be rooted in an object, and a
533
+ * top-level `Schema.Union` emits a bare `anyOf`. Core drops such an
534
+ * `outputSchema` from `tools/list` on the stateful revisions and serves it
535
+ * verbatim on the stateless one, where strict clients reject it.
536
+ * `McpToolAudit`'s `objectRootedOutput` check reports it either way.
537
+ *
538
+ * {@link ToolOutputSchema.objectRooted} is the fix when every member of the
539
+ * union is an object shape; never apply it to a union with a primitive or
540
+ * array member. It works on the Effect
541
+ * schema, not on JSON Schema — unlike `ToolInputSchema.objectRooted`, which
542
+ * rewrites the served input document of a `Tool.dynamic`.
543
+ *
544
+ * @public
545
+ */
546
+ export declare class ToolOutputSchema {
547
+ private constructor();
548
+ /**
549
+ * `schema` with `type: "object"` added beside the `anyOf` at its JSON
550
+ * Schema root, through a check that always passes. Decoding, encoding and
551
+ * the schema's type are unchanged.
552
+ *
553
+ * @remarks
554
+ * Order against `.annotate({ identifier })` does not matter. A check added
555
+ * after an identifier starts a new node that would drop the definition
556
+ * name, so the identifier the schema already resolves is carried onto the
557
+ * new check, and the served documents are equal either way (key order
558
+ * can differ when the schema carries a title or description). Applying it
559
+ * twice is a no-op.
560
+ *
561
+ * Every union member must be an object shape: the added `type: "object"`
562
+ * is a claim about the whole union, and nothing checks it. Do not use it
563
+ * on a union with a primitive or array member — the served schema would
564
+ * contradict the values the tool returns; give such a tool an object
565
+ * envelope instead.
566
+ *
567
+ * @example
568
+ * ```ts
569
+ * const Result = ToolOutputSchema.objectRooted(Schema.Union([Found, Missing])).annotate({ identifier: "Result" })
570
+ * Tool.make("lookup", { parameters, success: Result })
571
+ * ```
572
+ */
573
+ static readonly objectRooted: <S extends Schema.Top>(schema: S) => S;
574
+ }
575
+ //#endregion
576
+ //#region src/ToolRefusal.d.ts
577
+ declare const ToolRefusal_base: Schema.Class<ToolRefusal, Schema.TaggedStruct<"ToolRefusal", {
578
+ readonly message: Schema.String;
579
+ readonly remediation: Schema.Struct<{
580
+ readonly hint: Schema.String;
581
+ readonly suggestedTool: Schema.optionalKey<Schema.String>;
582
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
583
+ }>;
584
+ }>, import("effect/Cause").YieldableError>;
585
+ /**
586
+ * A tool call refused for a reason the caller can fix: the kit's ready-made
587
+ * declared failure, built on {@link ToolFailure.fields}.
588
+ *
589
+ * @remarks
590
+ * Core scrubs every undeclared failure and every defect to one generic
591
+ * internal-error sentence, so a refusal the agent must act on (an unknown id,
592
+ * a missing argument, a precondition) reaches it only when the tool declares
593
+ * it. Declare `ToolRefusal` in the tool's `failure` schema and fail with
594
+ * {@link ToolRefusal.refuse}; its message already carries the remediation,
595
+ * because a declared `Error` failure sends `message` and nothing else.
596
+ *
597
+ * Audit every throw path when porting a tool: any failure left undeclared
598
+ * loses its message on the wire, however carefully it was worded.
599
+ *
600
+ * @example
601
+ * ```ts
602
+ * const Lookup = Tool.make("lookup", { parameters, success, failure: ToolRefusal })
603
+ * // in the handler:
604
+ * return yield* ToolRefusal.refuse(`No run "${ToolFailure.truncate(id)}".`, { hint: "List runs first.", suggestedTool: "list_runs" })
605
+ * ```
606
+ *
607
+ * @public
608
+ */
609
+ export declare class ToolRefusal extends ToolRefusal_base {
610
+ /**
611
+ * A refusal for `reason`, with `remediation` folded into its message by
612
+ * {@link ToolFailure.message}. `reason` must already truncate any
613
+ * caller-supplied value it echoes, with {@link ToolFailure.truncate}.
614
+ */
615
+ static readonly refuse: (reason: string, remediation: Remediation) => ToolRefusal;
616
+ }
617
+ //#endregion
618
+ export type { FormatUnknownKeysOptions, McpLaunchOptions, McpStdioOptions, McpToolkitOptions, UnionHandlerOptions, UnionTool, UnionToolOptions, UnknownKeysLevel };
352
619
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -2,5 +2,7 @@ import { McpStdio } from "./McpStdio.js";
2
2
  import { ToolFailure } from "./ToolFailure.js";
3
3
  import { ToolInputSchema } from "./ToolInputSchema.js";
4
4
  import { McpToolkit } from "./McpToolkit.js";
5
+ import { ToolOutputSchema } from "./ToolOutputSchema.js";
6
+ import { ToolRefusal } from "./ToolRefusal.js";
5
7
 
6
- export { McpStdio, McpToolkit, ToolFailure, ToolInputSchema };
8
+ export { McpStdio, McpToolkit, ToolFailure, ToolInputSchema, ToolOutputSchema, ToolRefusal };