@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/McpGuard.js +121 -0
- package/McpHarness.js +46 -10
- package/McpProbe.js +1 -1
- package/McpProcess.js +2 -2
- package/McpStdio.js +37 -21
- package/McpTestFailure.js +2 -2
- package/McpToolAudit.js +10 -5
- package/McpToolkit.js +199 -36
- package/README.md +195 -31
- package/ToolFailure.js +8 -2
- package/ToolInputSchema.js +28 -14
- package/ToolOutputSchema.js +66 -0
- package/ToolRefusal.js +42 -0
- package/guard.d.ts +157 -0
- package/guard.js +3 -0
- package/index.d.ts +331 -64
- package/index.js +3 -1
- package/internal/StdinFrames.js +61 -23
- package/internal/guardLaunch.js +14 -0
- package/package.json +9 -4
- package/testing.d.ts +44 -12
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/
|
|
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`.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* units: it is answered once, as soon as it passes
|
|
88
|
-
* of it is discarded up to its newline
|
|
89
|
-
*
|
|
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
|
|
94
|
-
* `
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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.
|
|
111
|
-
*
|
|
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
|
|
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
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
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
|
|
203
|
-
*
|
|
204
|
-
*
|
|
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).
|
|
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
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
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
|
-
*
|
|
244
|
-
* strict
|
|
245
|
-
*
|
|
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
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
|
|
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
|
|
256
|
-
*
|
|
257
|
-
*
|
|
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
|
-
* `
|
|
293
|
-
*
|
|
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 `
|
|
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
|
-
|
|
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 };
|