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