@effected/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,167 @@
1
+ import { ToolFailure } from "./ToolFailure.js";
2
+ import { Result } from "effect";
3
+
4
+ //#region src/ToolInputSchema.ts
5
+ const MAX_DEPTH = 256;
6
+ const MAX_ECHOED = 20;
7
+ const DISCRIMINANTS = [
8
+ "action",
9
+ "kind",
10
+ "_tag",
11
+ "type"
12
+ ];
13
+ const REF_PREFIX = "#/$defs/";
14
+ const isNode = (u) => typeof u === "object" && u !== null && !Array.isArray(u);
15
+ const resolveRef = (node, root, seen = /* @__PURE__ */ new Set()) => {
16
+ const ref = node.$ref;
17
+ if (typeof ref !== "string" || !ref.startsWith(REF_PREFIX) || seen.has(ref)) return node;
18
+ const defs = root.$defs;
19
+ const name = ref.slice(8).replaceAll("~1", "/").replaceAll("~0", "~");
20
+ const target = isNode(defs) && Object.hasOwn(defs, name) ? defs[name] : void 0;
21
+ if (!isNode(target)) return node;
22
+ const { $ref: _ref, ...siblings } = node;
23
+ return resolveRef({
24
+ ...target,
25
+ ...siblings
26
+ }, root, /* @__PURE__ */ new Set([...seen, ref]));
27
+ };
28
+ const literalValues = (node) => {
29
+ if (!isNode(node)) return void 0;
30
+ if (Array.isArray(node.enum)) return node.enum;
31
+ return "const" in node ? [node.const] : void 0;
32
+ };
33
+ const discriminantOf = (members) => members.length === 0 ? void 0 : DISCRIMINANTS.find((key) => members.every((member) => isNode(member.properties) && literalValues(member.properties[key]) !== void 0));
34
+ const selectMember = (members, value, root) => {
35
+ const resolved = members.filter(isNode).map((member) => resolveRef(member, root));
36
+ const key = discriminantOf(resolved);
37
+ if (key !== void 0) return resolved.find((member) => literalValues(member.properties[key])?.includes(value[key]) === true);
38
+ const objects = resolved.filter((member) => isNode(member.properties) || member.type === "object");
39
+ return objects.length === 1 ? objects[0] : void 0;
40
+ };
41
+ const matchesPattern = (pattern, key) => Result.getOrUndefined(Result.try(() => new RegExp(pattern, "u").test(key))) === true;
42
+ const collect = (payload, root) => {
43
+ const out = [];
44
+ const walk = (value, raw, path, depth) => {
45
+ if (depth > MAX_DEPTH || !isNode(raw)) return;
46
+ const node = resolveRef(raw, root);
47
+ const parts = [node, ...Array.isArray(node.allOf) ? node.allOf.filter(isNode).map((m) => resolveRef(m, root)) : []];
48
+ const members = Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : void 0;
49
+ const additional = parts.map((part) => part.additionalProperties).find(isNode);
50
+ const isClosed = parts.some((part) => part.additionalProperties === false);
51
+ if (Array.isArray(value)) {
52
+ const prefix = Array.isArray(node.prefixItems) ? node.prefixItems : [];
53
+ if (prefix.length === 0 && !isNode(node.items) && members !== void 0) {
54
+ const arrays = members.filter(isNode).map((member) => resolveRef(member, root)).filter((member) => member.type === "array");
55
+ if (arrays.length === 1) walk(value, arrays[0], path, depth + 1);
56
+ return;
57
+ }
58
+ for (const [index, item] of value.entries()) walk(item, index < prefix.length ? prefix[index] : node.items, [...path, String(index)], depth + 1);
59
+ return;
60
+ }
61
+ if (!isNode(value)) return;
62
+ const declared = parts.filter((part) => isNode(part.properties));
63
+ if (declared.length === 0 && members !== void 0) {
64
+ const member = selectMember(members, value, root);
65
+ if (member !== void 0) walk(value, member, path, depth + 1);
66
+ return;
67
+ }
68
+ const properties = Object.create(null);
69
+ for (const part of declared) for (const [key, schema] of Object.entries(part.properties)) properties[key] = schema;
70
+ const patterns = parts.flatMap((part) => isNode(part.patternProperties) ? Object.entries(part.patternProperties) : []);
71
+ const accepted = Object.keys(properties);
72
+ const extra = [];
73
+ const patterned = [];
74
+ for (const key of Object.keys(value)) {
75
+ if (Object.hasOwn(properties, key)) continue;
76
+ const match = patterns.find(([pattern]) => matchesPattern(pattern, key));
77
+ if (match === void 0) extra.push(key);
78
+ else patterned.push([key, match[1]]);
79
+ }
80
+ if (extra.length > 0 && isClosed) out.push({
81
+ path,
82
+ unknown: extra,
83
+ accepted
84
+ });
85
+ for (const key of accepted) if (Object.hasOwn(value, key)) walk(value[key], properties[key], [...path, key], depth + 1);
86
+ for (const [key, schema] of patterned) walk(value[key], schema, [...path, key], depth + 1);
87
+ if (additional !== void 0) for (const key of extra) walk(value[key], additional, [...path, key], depth + 1);
88
+ };
89
+ walk(payload, root, [], 0);
90
+ return out;
91
+ };
92
+ /**
93
+ * Pure walkers over a tool's served JSON Schema: every unknown key at every
94
+ * depth, a message naming them all, and an object-rooted form of a union.
95
+ *
96
+ * @remarks
97
+ * Core's `Tool.Strict` rejects unknown keys but reports only the first
98
+ * (effect `unstable/ai/McpServer.ts` decodes without `errors: "all"`), so an
99
+ * agent fixes one typo per round trip. These walkers run over the schema core
100
+ * actually serves, so what they accept matches what the tool advertises:
101
+ *
102
+ * - Only `additionalProperties: false` closes a node, as in JSON Schema; core
103
+ * emits it on every object node of a strict tool and `true` otherwise.
104
+ * - `allOf` members' properties merge; `oneOf`/`anyOf` members are chosen by an
105
+ * `action`, `kind`, `_tag` or `type` literal (tried in that order), or by
106
+ * being the only object member. A payload whose discriminant matches no
107
+ * member, or is missing, is skipped and left for decoding.
108
+ * - `items`, `prefixItems`, `$ref` into `$defs` (JSON Pointer escapes
109
+ * honoured), and `patternProperties` are followed; a key matched by a
110
+ * pattern is accepted and walked with the first matching pattern's schema.
111
+ * - The payload is untrusted: the walk stops descending at depth 256 rather
112
+ * than overflowing the stack, so a deeper unknown key goes unreported and
113
+ * is left for decoding. Every hop counts toward that cap: each property or
114
+ * array position, and also each union member selection, which adds a hop
115
+ * without adding a path segment.
116
+ *
117
+ * @public
118
+ */
119
+ var ToolInputSchema = class {
120
+ constructor() {}
121
+ /** Every level of `payload` that carries a key `schema` does not accept. */
122
+ static unknownKeys = (payload, schema) => collect(payload, schema);
123
+ /**
124
+ * `Unrecognized parameter(s): a, b.c. Accepted params: x, y.` per level. Each
125
+ * echoed key is truncated, and at most 20 keys and 20 levels are named; a
126
+ * trailing `(and N more levels).` counts the levels left out.
127
+ */
128
+ static formatUnknownKeys = (levels, options = {}) => {
129
+ const limit = options.echoLimit ?? ToolFailure.ECHO_LIMIT;
130
+ const sentences = levels.slice(0, MAX_ECHOED).map((level) => {
131
+ const shown = level.unknown.slice(0, MAX_ECHOED).map((key) => ToolFailure.truncate([...level.path, key].join("."), limit));
132
+ const more = level.unknown.length > MAX_ECHOED ? ` (and ${level.unknown.length - MAX_ECHOED} more)` : "";
133
+ const accepted = level.accepted.length === 0 ? "(none)" : level.accepted.join(", ");
134
+ return `Unrecognized parameter(s): ${shown.join(", ")}${more}. Accepted params: ${accepted}.`;
135
+ });
136
+ const hidden = levels.length > MAX_ECHOED ? ` (and ${levels.length - MAX_ECHOED} more levels).` : "";
137
+ return `${sentences.join(" ")}${hidden}`;
138
+ };
139
+ /**
140
+ * `schema` with a root `$ref` inlined and a top-level union of objects
141
+ * sharing an `action`, `kind`, `_tag` or `type` literal rewritten to an
142
+ * object root carrying `type: "object"`, `oneOf` and `"x-discriminator"` —
143
+ * MCP requires an object root, and core dies at boot on a union root.
144
+ * Returned as the same value when nothing changes.
145
+ */
146
+ static objectRooted = (schema) => {
147
+ const root = resolveRef(schema, schema);
148
+ const members = Array.isArray(root.anyOf) ? root.anyOf : Array.isArray(root.oneOf) ? root.oneOf : void 0;
149
+ if (root.type === void 0 && members !== void 0) {
150
+ const resolved = members.filter(isNode).map((member) => resolveRef(member, schema));
151
+ const key = resolved.length === members.length ? discriminantOf(resolved) : void 0;
152
+ if (key !== void 0) {
153
+ const { anyOf: _anyOf, oneOf: _oneOf, ...rest } = root;
154
+ return {
155
+ type: "object",
156
+ ...rest,
157
+ oneOf: resolved,
158
+ "x-discriminator": key
159
+ };
160
+ }
161
+ }
162
+ return root;
163
+ };
164
+ };
165
+
166
+ //#endregion
167
+ export { ToolInputSchema };
package/index.d.ts ADDED
@@ -0,0 +1,352 @@
1
+ import { Array, Effect, JsonSchema, Layer, Runtime, Schema, Stdio } from "effect";
2
+ import { McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/unstable/ai";
3
+ import { Remediation } from "@effected/engine";
4
+ //#region src/McpStdio.d.ts
5
+ /**
6
+ * Options for {@link McpStdio.layer}.
7
+ *
8
+ * @public
9
+ */
10
+ interface McpStdioOptions {
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
+ /**
23
+ * Serve an MCP server over stdio without ever writing a log line or a failure
24
+ * report onto stdout, which is the JSON-RPC wire.
25
+ *
26
+ * @remarks
27
+ * The one-line `main.ts`:
28
+ *
29
+ * ```ts
30
+ * NodeRuntime.runMain(McpStdio.launch(Main), { teardown: McpStdio.teardown })
31
+ * ```
32
+ *
33
+ * - {@link McpStdio.layer} is `McpServer.layerStdio` with `LogToStderr`
34
+ * provided and merged into its output, and `Layer.orDie`. It answers a
35
+ * stdin line that is not JSON with a JSON-RPC `-32700` parse error, and
36
+ * JSON that is not a JSON-RPC message with `-32600`, and keeps serving.
37
+ * - {@link McpStdio.launch} reports a launch failure itself, on stderr, and
38
+ * hides it from `runMain`, whose own report is written outside anything
39
+ * the program can provide.
40
+ * - {@link McpStdio.teardown} maps stdin EOF, the normal end of every
41
+ * session, to exit 0 instead of 130.
42
+ *
43
+ * Never `NodeRuntime.runMain(Layer.launch(Main).pipe(Effect.provideService(References.LogToStderr, true)))`:
44
+ * it typechecks and serves, but a launch failure is still reported by
45
+ * `runMain` through `console.log`, straight onto the wire, where the client
46
+ * reads it as a malformed frame instead of an error.
47
+ *
48
+ * @public
49
+ */
50
+ export declare class McpStdio {
51
+ private constructor();
52
+ /**
53
+ * `[v2026_07_28, v2025_11_25, v2025_06_18]` — the stateless revision
54
+ * first, then the two newest stateful ones.
55
+ *
56
+ * @remarks
57
+ * Real clients open with either handshake: Claude Code 2.1.281 was
58
+ * measured opening a stdio server with the stateless `server/discover`,
59
+ * while most other shipping clients still send `initialize`. The order
60
+ * is load-bearing:
61
+ *
62
+ * - Stateless first: a request with no session and no `_meta` falls to
63
+ * `protocols[0]`.
64
+ * - Never a single entry: `initialize` only matches stateful adapters, so
65
+ * a stateless-only list refuses every client that opens with it.
66
+ * - At most one stateless adapter; a second fails the layer.
67
+ */
68
+ static readonly protocols: Array.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>;
69
+ /**
70
+ * `McpServer.layerStdio` with `LogToStderr` provided and merged into the
71
+ * output, so every layer it provides logs to stderr too. A bad
72
+ * `protocols` list dies: it is the implementer's defect.
73
+ *
74
+ * @remarks
75
+ * Only a layer this one provides inherits `LogToStderr`: compose with
76
+ * `Layer.provideMerge(McpStdio.layer(...))`. A sibling merged beside it
77
+ * with `Layer.mergeAll` is not provided by it, so its build logs still
78
+ * go through `console.log`, onto the wire.
79
+ *
80
+ * The server reads stdin through a guard that frames it exactly as core's
81
+ * 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.
90
+ *
91
+ * A line that is JSON but no JSON-RPC message core can handle is answered
92
+ * 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.
102
+ *
103
+ * Give each server a fresh layer memo map. Core's stdio protocol layer
104
+ * is a shared constant, so a second `McpStdio.layer` server whose build
105
+ * sees the first one's memo map shares its protocol: only the first
106
+ * server reads stdin, and the second never answers. Merging both into
107
+ * one graph does that, and so does building or providing the second
108
+ * anywhere under the first one's `Effect.provide`: nested `Layer.build`
109
+ * 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.
112
+ *
113
+ * Code after a completed `Effect.provide` of a stdio server never runs:
114
+ * core's stdio protocol interrupts the fiber that built it when its stdin
115
+ * loop ends, which closing the provide's scope does. In a test, serve the
116
+ * layer through `McpHarness` from `@effected/mcp/testing` instead.
117
+ *
118
+ * Each call mints a fresh layer; bind the result to a `const` or the
119
+ * server builds twice.
120
+ */
121
+ static readonly layer: (options: McpStdioOptions) => Layer.Layer<McpServer.McpServer | McpSchema.McpServerClient, never, Stdio.Stdio>;
122
+ /**
123
+ * `Layer.launch`, with any failure other than an interrupt logged on
124
+ * stderr here and re-raised marked as already reported, keeping its exit
125
+ * code.
126
+ *
127
+ * @remarks
128
+ * `runMain` logs an unhandled failure from outside the program, where no
129
+ * `Effect.provideService` reaches, and Effect's default logger writes
130
+ * through `console.log` unless `LogToStderr` is set. For an MCP server
131
+ * that is stdout, which is the wire.
132
+ */
133
+ static readonly launch: <ROut, E, R>(layer: Layer.Layer<ROut, E, R>) => Effect.Effect<never, Error, R>;
134
+ /**
135
+ * Success or an interrupt-only exit maps to 0. Anything else goes to
136
+ * `Runtime.defaultTeardown`.
137
+ *
138
+ * @remarks
139
+ * Stdin EOF, the normal end of every session, ends in an interrupt-only
140
+ * exit, and so do SIGINT and SIGTERM: `runMain` interrupts the program on
141
+ * either signal. All three exit 0 here, not the 130 a default teardown
142
+ * reports for an interrupt.
143
+ */
144
+ static readonly teardown: Runtime.Teardown;
145
+ }
146
+ //#endregion
147
+ //#region src/ToolInputSchema.d.ts
148
+ /**
149
+ * The unknown keys found at one object level of a tool-call payload.
150
+ *
151
+ * @public
152
+ */
153
+ interface UnknownKeysLevel {
154
+ /** Keys from the payload root to this object; empty at the root. Array positions are decimal strings. */
155
+ readonly path: ReadonlyArray<string>;
156
+ /** Keys the payload carries that this level does not accept, in payload order. */
157
+ readonly unknown: ReadonlyArray<string>;
158
+ /** Keys this level declares, in schema order. */
159
+ readonly accepted: ReadonlyArray<string>;
160
+ }
161
+ /**
162
+ * Options for {@link ToolInputSchema.formatUnknownKeys}.
163
+ *
164
+ * @public
165
+ */
166
+ interface FormatUnknownKeysOptions {
167
+ /** The cap on each echoed key path, in UTF-16 code units. Defaults to `ToolFailure.ECHO_LIMIT`. */
168
+ readonly echoLimit?: number | undefined;
169
+ }
170
+ /**
171
+ * Pure walkers over a tool's served JSON Schema: every unknown key at every
172
+ * depth, a message naming them all, and an object-rooted form of a union.
173
+ *
174
+ * @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:
179
+ *
180
+ * - Only `additionalProperties: false` closes a node, as in JSON Schema; core
181
+ * emits it on every object node of a strict tool and `true` otherwise.
182
+ * - `allOf` members' properties merge; `oneOf`/`anyOf` members are chosen by an
183
+ * `action`, `kind`, `_tag` or `type` literal (tried in that order), or by
184
+ * being the only object member. A payload whose discriminant matches no
185
+ * member, or is missing, is skipped and left for decoding.
186
+ * - `items`, `prefixItems`, `$ref` into `$defs` (JSON Pointer escapes
187
+ * honoured), and `patternProperties` are followed; a key matched by a
188
+ * pattern is accepted and walked with the first matching pattern's schema.
189
+ * - The payload is untrusted: the walk stops descending at depth 256 rather
190
+ * than overflowing the stack, so a deeper unknown key goes unreported and
191
+ * is left for decoding. Every hop counts toward that cap: each property or
192
+ * array position, and also each union member selection, which adds a hop
193
+ * without adding a path segment.
194
+ *
195
+ * @public
196
+ */
197
+ export declare class ToolInputSchema {
198
+ private constructor();
199
+ /** Every level of `payload` that carries a key `schema` does not accept. */
200
+ static readonly unknownKeys: (payload: unknown, schema: JsonSchema.JsonSchema) => ReadonlyArray<UnknownKeysLevel>;
201
+ /**
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.
205
+ */
206
+ static readonly formatUnknownKeys: (levels: ReadonlyArray<UnknownKeysLevel>, options?: FormatUnknownKeysOptions) => string;
207
+ /**
208
+ * `schema` with a root `$ref` inlined and a top-level union of objects
209
+ * sharing an `action`, `kind`, `_tag` or `type` literal rewritten to an
210
+ * object root carrying `type: "object"`, `oneOf` and `"x-discriminator"` —
211
+ * MCP requires an object root, and core dies at boot on a union root.
212
+ * Returned as the same value when nothing changes.
213
+ */
214
+ static readonly objectRooted: (schema: JsonSchema.JsonSchema) => JsonSchema.JsonSchema;
215
+ }
216
+ //#endregion
217
+ //#region src/McpToolkit.d.ts
218
+ /**
219
+ * Options for {@link McpToolkit.layer}.
220
+ *
221
+ * @public
222
+ */
223
+ interface McpToolkitOptions {
224
+ /**
225
+ * `"all"` (the default): every tool without its own `Tool.Strict`
226
+ * annotation is served and decoded strict. `"annotated"`: only tools
227
+ * annotated `Tool.Strict` true are. In both modes an explicit annotation,
228
+ * 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.
232
+ */
233
+ readonly strict?: "all" | "annotated" | undefined;
234
+ /**
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.
239
+ */
240
+ readonly unknownKeyMessage?: ((levels: ReadonlyArray<UnknownKeysLevel>) => string) | undefined;
241
+ }
242
+ /**
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.
246
+ *
247
+ * @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
254
+ * `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.
269
+ *
270
+ * Registration goes through core's module-level `McpServer.McpServer.layer`,
271
+ * shared by reference with `McpStdio.layer`'s own copy — provide both into
272
+ * the same graph, as with `McpServer.toolkit`. Never wrap that layer in
273
+ * `Layer.fresh`: tools would register into a second registry nobody serves.
274
+ *
275
+ * Each call mints a fresh layer; bind the result to a `const` or the
276
+ * registration runs twice.
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * import { Layer } from "effect";
281
+ * import { McpStdio, McpToolkit } from "@effected/mcp";
282
+ *
283
+ * const ToolsLayer = McpToolkit.layer(MyToolkit).pipe(Layer.provide(MyHandlers));
284
+ * const ServerLayer = ToolsLayer.pipe(Layer.provideMerge(McpStdio.layer({ name: "my-server", version: "1.0.0" })));
285
+ * ```
286
+ *
287
+ * @public
288
+ */
289
+ export declare class McpToolkit {
290
+ private constructor();
291
+ /**
292
+ * `McpServer.toolkit`'s registration layer, strict by default and naming
293
+ * every unknown argument in one `InvalidParams`.
294
+ */
295
+ 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
+ }
297
+ //#endregion
298
+ //#region src/ToolFailure.d.ts
299
+ /**
300
+ * How a tool's declared failure should read on the wire.
301
+ *
302
+ * @remarks
303
+ * Core sends a declared failure that is an `Error` instance — every
304
+ * `Schema.TaggedError` is — as `isError: true` with `error.message` as the only
305
+ * text and no `structuredContent` (effect `unstable/ai/McpServer.ts`,
306
+ * `declaredFailureResult`). So the remediation must be folded into `message`
307
+ * when the error is constructed: whatever is not in the message never reaches
308
+ * the agent.
309
+ *
310
+ * Spread {@link ToolFailure.fields} into your tool's `Schema.TaggedError`, build
311
+ * `message` with {@link ToolFailure.message}, and pass every caller-supplied
312
+ * value through {@link ToolFailure.truncate} before echoing it.
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * class NotFound extends Schema.TaggedError<NotFound>()("NotFound", { ...ToolFailure.fields, id: Schema.String }) {}
317
+ * const remediation = { hint: "List the ids first.", suggestedTool: "list_things" }
318
+ * new NotFound({ id, remediation, message: ToolFailure.message(`No thing "${ToolFailure.truncate(id)}".`, remediation) })
319
+ * ```
320
+ *
321
+ * @public
322
+ */
323
+ export declare class ToolFailure {
324
+ private constructor();
325
+ /** The cap, in UTF-16 code units, on a caller-supplied value echoed into a message. */
326
+ static readonly ECHO_LIMIT: number;
327
+ /** The larger cap for a value the engine produced (a path, a diagnostic), not one a caller typed. */
328
+ static readonly ENGINE_ECHO_LIMIT: number;
329
+ /** The `message` and `remediation` fields to spread into a consumer's `Schema.TaggedError`. */
330
+ static readonly fields: {
331
+ readonly message: Schema.String;
332
+ readonly remediation: Schema.Struct<{
333
+ readonly hint: Schema.String;
334
+ readonly suggestedTool: Schema.optionalKey<Schema.String>;
335
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
336
+ }>;
337
+ };
338
+ /**
339
+ * `"<raw> <hint> Try <suggestedTool>."`, dropping any empty part, so a
340
+ * missing tool or an empty hint never leaves a double space.
341
+ */
342
+ static readonly message: (raw: string, remediation: Remediation) => string;
343
+ /**
344
+ * `value` cut to `limit` code units with a trailing ellipsis, backing off one
345
+ * unit rather than splitting a surrogate pair. A hostile multi-megabyte
346
+ * argument is otherwise echoed into the agent's context whole.
347
+ */
348
+ static readonly truncate: (value: string, limit?: number) => string;
349
+ }
350
+ //#endregion
351
+ export type { FormatUnknownKeysOptions, McpStdioOptions, McpToolkitOptions, UnknownKeysLevel };
352
+ //# sourceMappingURL=index.d.ts.map
package/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { McpStdio } from "./McpStdio.js";
2
+ import { ToolFailure } from "./ToolFailure.js";
3
+ import { ToolInputSchema } from "./ToolInputSchema.js";
4
+ import { McpToolkit } from "./McpToolkit.js";
5
+
6
+ export { McpStdio, McpToolkit, ToolFailure, ToolInputSchema };
@@ -0,0 +1,23 @@
1
+ import { Runtime } from "effect";
2
+
3
+ //#region src/internal/LaunchFailed.ts
4
+ /**
5
+ * The failure `McpStdio.launch` re-raises after reporting a launch failure on
6
+ * stderr itself. `errorReported: false` keeps `runMain` from reporting it a
7
+ * second time — through a logger that writes to stdout — and the original exit
8
+ * code rides along for the teardown.
9
+ *
10
+ * @internal
11
+ */
12
+ var LaunchFailed = class extends Error {
13
+ [Runtime.errorReported] = false;
14
+ [Runtime.errorExitCode];
15
+ constructor(code) {
16
+ super(`the MCP server failed to launch (exit ${code})`);
17
+ this.name = "LaunchFailed";
18
+ this[Runtime.errorExitCode] = code;
19
+ }
20
+ };
21
+
22
+ //#endregion
23
+ export { LaunchFailed };