@effected/mcp 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/McpStdio.js DELETED
@@ -1,171 +0,0 @@
1
- import { LaunchFailed } from "./internal/LaunchFailed.js";
2
- import { makeGuardedStdio } from "./internal/StdinFrames.js";
3
- import { Cause, Effect, Exit, Layer, References, Runtime } from "effect";
4
- import { McpProtocol, McpServer } from "effect/ai";
5
-
6
- // src/McpStdio.ts
7
- /**
8
- * Serve an MCP server over stdio without ever writing a log line or a failure
9
- * report onto stdout, which is the JSON-RPC wire.
10
- *
11
- * @remarks
12
- * The one-line `main.ts`:
13
- *
14
- * ```ts
15
- * NodeRuntime.runMain(McpStdio.launch(Main), { teardown: McpStdio.teardown })
16
- * ```
17
- *
18
- * - {@link McpStdio.layer} is `McpServer.layerStdio` with `LogToStderr`
19
- * provided and merged into its output, and a bad `protocols` list
20
- * turned into a defect. It answers a
21
- * stdin line that is not JSON with a JSON-RPC `-32700` parse error, and
22
- * JSON that is not a JSON-RPC message with `-32600`, and keeps serving.
23
- * - {@link McpStdio.launch} reports a launch failure itself, on stderr, and
24
- * hides it from `runMain`, whose own report is written outside anything
25
- * the program can provide.
26
- * - {@link McpStdio.teardown} maps stdin EOF, the normal end of every
27
- * session, to exit 0 instead of 130.
28
- *
29
- * Never `NodeRuntime.runMain(Layer.launch(Main).pipe(Effect.provideService(References.LogToStderr, true)))`:
30
- * it typechecks and serves, but a launch failure is still reported by
31
- * `runMain` through `console.log`, straight onto the wire, where the client
32
- * reads it as a malformed frame instead of an error.
33
- *
34
- * @public
35
- */
36
- var McpStdio = class McpStdio {
37
- constructor() {}
38
- /**
39
- * `[v2026_07_28, v2025_11_25, v2025_06_18]` — the stateless revision
40
- * first, then the two newest stateful ones.
41
- *
42
- * @remarks
43
- * Real clients open with either handshake: some (Claude Code, for one)
44
- * open a stdio server with the stateless `server/discover`, while others
45
- * still send `initialize`. The order is load-bearing:
46
- *
47
- * - Stateless first: a request with no session and no `_meta` falls to
48
- * `protocols[0]`.
49
- * - Never a single entry: `initialize` only matches stateful adapters, so
50
- * a stateless-only list refuses every client that opens with it.
51
- * - At most one stateless adapter; a second fails the layer.
52
- */
53
- static protocols = [
54
- McpProtocol.v2026_07_28,
55
- McpProtocol.v2025_11_25,
56
- McpProtocol.v2025_06_18
57
- ];
58
- /**
59
- * `McpServer.layerStdio` with `LogToStderr` provided and merged into the
60
- * output, so every layer it provides logs to stderr too. A bad
61
- * `protocols` list dies: it is the implementer's defect. So does a
62
- * registration that fails with `Cause.IllegalArgumentError`, which core
63
- * reports for the same class of mistake; any other registration error
64
- * stays in the error channel.
65
- *
66
- * @remarks
67
- * Pass the server's registration layers as `options.registrations`: they
68
- * are built before the server reads its first request, under
69
- * `LogToStderr`. Providing this layer to them, as
70
- * `Registrations.pipe(Layer.provideMerge(McpStdio.layer(...)))`, also
71
- * serves them, and they inherit `LogToStderr` the same way. A sibling
72
- * merged beside it with `Layer.mergeAll` is neither: its build logs still
73
- * go through `console.log`, onto the wire.
74
- *
75
- * The output is `McpServer` and the `McpServer.McpRequest<"Requires", Scope>`
76
- * marker the transport discharges. `McpServerClient` is not in it: the
77
- * stateless `v2026_07_28` revision has no client session, so a handler
78
- * reads the client's details from `McpSchema.McpRequestContext` instead.
79
- *
80
- * `LogToStderr` only reaches a logger that reads it: Effect's default
81
- * logger and `Logger.consolePretty`. `Logger.consoleJson`,
82
- * `Logger.consoleLogFmt` and `Logger.consoleStructured` are built with
83
- * `Logger.withConsoleLog`, which writes through `console.log` whatever
84
- * `LogToStderr` says, so a server that installs one puts every log line on
85
- * the wire. Never install them under this layer; wrap the formatter in
86
- * `Logger.withConsoleError` instead, as in
87
- * `Logger.layer([Logger.withConsoleError(Logger.formatJson)])`.
88
- *
89
- * The server reads stdin through a guard that frames it exactly as core's
90
- * decoder does: one streaming UTF-8 decoder, a byte-order mark stripped
91
- * only at the start of the stream, lines split on `\n`. Core itself skips
92
- * a line it cannot use and keeps serving, but sends no reply, where
93
- * JSON-RPC 2.0 requires one; the guard supplies it. A line that is not
94
- * JSON is answered on stdout with a parse error, code `-32700` and
95
- * `id: null`, and never reaches core. So is a line longer than core's cap
96
- * of 16 Mi UTF-16 code units: it is answered once, as soon as it passes
97
- * the cap, and the rest of it is discarded up to its newline (core would
98
- * log it on stderr and drop it). A line of JSON whitespace (space, tab,
99
- * carriage return) is ignored.
100
- *
101
- * A line that is JSON but no JSON-RPC message core can handle is answered
102
- * with an Invalid Request, code `-32600` and `id: null`, and never reaches
103
- * core: a value that is neither an object nor an array, an object whose
104
- * `method` is not a string and whose `id` is absent or `null`, and an
105
- * object with neither `method` nor `id`, which is neither a request nor a
106
- * response.
107
- *
108
- * Everything else goes to core: an array, which core answers `-32600`
109
- * itself because it serves no batches; an object with an `id` and no
110
- * `method`, which is a response and gets no reply; and a request with an
111
- * `id`, which core answers even when its `method` is malformed.
112
- *
113
- * Give each server a fresh layer memo map. Core's stdio protocol layer
114
- * is a shared constant, so a second `McpStdio.layer` server whose build
115
- * sees the first one's memo map shares its protocol: only the first
116
- * server reads stdin, and the second never answers. Merging both into
117
- * one graph does that, and so does building or providing the second
118
- * anywhere under the first one's `Effect.provide`: nested `Layer.build`
119
- * and `Effect.provide` fork the ambient memo map rather than starting a
120
- * new one. The tool registry is shared the same way. Isolate each server
121
- * by wrapping its whole bundle (this layer with its `registrations`, or its
122
- * toolkit layers together with this layer) in `Layer.fresh`, or with its own `ManagedRuntime`,
123
- * `Effect.provide(layer, { local: true })` or its own process. Never put
124
- * the `Layer.fresh` boundary between a toolkit and this layer: the server
125
- * would serve an empty registry.
126
- *
127
- * Code after a completed `Effect.provide` of a stdio server never runs:
128
- * core's stdio protocol interrupts the fiber that built it when its stdin
129
- * loop ends, which closing the provide's scope does. In a test, serve the
130
- * layer through `McpHarness` from `@effected/mcp/testing` instead.
131
- *
132
- * Each call mints a fresh layer; bind the result to a `const` or the
133
- * server builds twice.
134
- */
135
- static layer = (options) => McpServer.layerStdio({
136
- name: options.name,
137
- version: options.version,
138
- instructions: options.instructions,
139
- description: options.description,
140
- protocols: options.protocols ?? McpStdio.protocols,
141
- registrations: options.registrations
142
- }).pipe(Layer.provide(makeGuardedStdio()), Layer.provideMerge(Layer.succeed(References.LogToStderr, true)), Layer.catchCause((cause) => Layer.effectContext(Effect.failCause(cause).pipe(Effect.catchIf(Cause.isIllegalArgumentError, (error) => Effect.die(error))))));
143
- /**
144
- * `Layer.launch`, with any failure other than an interrupt logged on
145
- * stderr here and re-raised marked as already reported, keeping its exit
146
- * code.
147
- *
148
- * @remarks
149
- * `runMain` logs an unhandled failure from outside the program, where no
150
- * `Effect.provideService` reaches, and Effect's default logger writes
151
- * through `console.log` unless `LogToStderr` is set. For an MCP server
152
- * that is stdout, which is the wire.
153
- *
154
- * `options.onReady` runs after the layer has built and before the launch
155
- * waits forever, as {@link McpLaunchOptions.onReady} describes.
156
- */
157
- static launch = (layer, options = {}) => (options.onReady === void 0 ? Layer.launch(layer) : Effect.scoped(Layer.build(layer).pipe(Effect.andThen(options.onReady), Effect.andThen(Effect.never)))).pipe(Effect.catchCause((cause) => Cause.hasInterruptsOnly(cause) ? Effect.failCause(cause) : Effect.logError(cause).pipe(Effect.andThen(Effect.fail(new LaunchFailed(Runtime.getErrorExitCode(Cause.squash(cause))))))), Effect.provideService(References.LogToStderr, true));
158
- /**
159
- * Success or an interrupt-only exit maps to 0. Anything else goes to
160
- * `Runtime.defaultTeardown`.
161
- *
162
- * @remarks
163
- * Stdin EOF, the normal end of every session, ends in an interrupt-only
164
- * exit, and so do SIGINT and SIGTERM: `runMain` interrupts the program on
165
- * either signal. All three exit 0 here, not the 130 a default teardown
166
- * reports for an interrupt.
167
- */
168
- static teardown = (exit, onExit) => Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause) ? onExit(0) : Runtime.defaultTeardown(exit, onExit);
169
- };
170
-
171
- export { McpStdio };
package/McpTestFailure.js DELETED
@@ -1,49 +0,0 @@
1
- import { Schema } from "effect";
2
-
3
- // src/McpTestFailure.ts
4
- /**
5
- * Why an MCP test client could not produce what was asked of it.
6
- *
7
- * @remarks
8
- * Each reason is raised by a specific client, and not every client raises
9
- * every reason:
10
- *
11
- * - `StreamEnded` (`McpProcess`, `McpProbe`): the spawned child's stdout
12
- * ended first, because the child exited. `McpProbe` folds the exit code
13
- * and stderr into the message. `McpProcess.stderrUntil` raises it when
14
- * stderr ends before its predicate holds.
15
- * - `TimedOut` (`McpProcess.stderrUntil`): the predicate did not hold
16
- * within the given timeout. The message carries the stderr seen so far.
17
- * - `NotJsonRpc` (`McpProcess.readUntilResponse`, `McpProbe`): a spawned
18
- * child's stdout line was not a JSON-RPC frame. `McpHarness` never raises
19
- * it: under `strictStdout` a non-JSON-RPC line DIES the pending wait as a
20
- * defect instead, so assert it with `Effect.exit` and `Cause.hasDies`,
21
- * not `catchTag`.
22
- * - `ServerStopped` (`McpHarness`): the in-process server stopped, usually
23
- * because stdin closed while the request was in flight.
24
- * - `NotInitialized` (`McpHarness`): a request other than `initialize` or
25
- * `ping` was made on a stateful revision before `initialize` was sent. The request
26
- * is never written; the server would only have refused it opaquely:
27
- * `-32602 Invalid request metadata` when a stateless adapter is listed
28
- * first, `-32603 Internal error` when only stateful revisions are served.
29
- * - `ErrorResponse` (`McpHarness.listTools`): the server answered with a
30
- * JSON-RPC error where a result was required. Every other request method
31
- * returns the whole response, error included, as data.
32
- *
33
- * @public
34
- */
35
- var McpTestFailure = class extends Schema.TaggedError()("McpTestFailure", {
36
- /** Which failure occurred; see the list above for which client raises which. */
37
- reason: Schema.Literals([
38
- "StreamEnded",
39
- "ServerStopped",
40
- "NotJsonRpc",
41
- "NotInitialized",
42
- "ErrorResponse",
43
- "TimedOut"
44
- ]),
45
- /** A human-readable description of what went wrong. */
46
- message: Schema.String
47
- }) {};
48
-
49
- export { McpTestFailure };
package/McpToolAudit.js DELETED
@@ -1,109 +0,0 @@
1
- // src/McpToolAudit.ts
2
- const isNode = (u) => typeof u === "object" && u !== null && !Array.isArray(u);
3
- const HINTS = [
4
- "readOnlyHint",
5
- "destructiveHint",
6
- "idempotentHint",
7
- "openWorldHint"
8
- ];
9
- /** Real branches whose members are separate object shapes, each worth its own report. `allOf` is not one of these: core emits it to merge keywords (declared `properties` plus a Record's `additionalProperties`) onto the SAME node, never to introduce a sibling shape. */
10
- const BRANCHES = [
11
- "prefixItems",
12
- "anyOf",
13
- "oneOf"
14
- ];
15
- const objectNodes = (schema) => {
16
- const out = [];
17
- const join = (path, key) => path === "" ? key : `${path}.${key}`;
18
- const visit = (node, path) => {
19
- if (!isNode(node)) return;
20
- const merged = Array.isArray(node.allOf) ? [node, ...node.allOf.filter(isNode)] : [node];
21
- const declaredParts = merged.filter((part) => isNode(part.properties));
22
- const properties = declaredParts.length === 0 ? void 0 : declaredParts.reduce((acc, part) => {
23
- for (const [key, child] of Object.entries(part.properties)) acc[key] = child;
24
- return acc;
25
- }, Object.create(null));
26
- const closed = merged.some((part) => part.additionalProperties === false);
27
- const droppedKeyCheck = merged.some((part) => isNode(part.additionalProperties) && isNode(part.propertyNames));
28
- if (isNode(properties) || merged.some((part) => part.type === "object")) out.push({
29
- path,
30
- properties,
31
- closed,
32
- droppedKeyCheck
33
- });
34
- if (isNode(properties)) for (const [key, child] of Object.entries(properties)) visit(child, join(path, key));
35
- visit(node.items, join(path, "items"));
36
- visit(node.additionalProperties, join(path, "additionalProperties"));
37
- for (const keyword of BRANCHES) {
38
- const list = node[keyword];
39
- if (Array.isArray(list)) for (const [index, child] of list.entries()) visit(child, join(path, `${keyword}[${index}]`));
40
- }
41
- if (isNode(node.$defs)) for (const [key, child] of Object.entries(node.$defs)) visit(child, `$defs.${key}`);
42
- };
43
- visit(schema, "");
44
- return out;
45
- };
46
- /**
47
- * A pure sweep over a served `tools/list` that returns every policy
48
- * violation, as `"<tool>: <what>"` strings.
49
- *
50
- * @remarks
51
- * Run it against what a real server serves (`McpHarness.listTools`), not
52
- * against your own schemas: the served document is what a client sees, and
53
- * it differs by protocol revision. The stateless `2026-07-28` adapter passes a
54
- * non-object output through, while the stateful adapters drop it — which is
55
- * why `objectRootedOutput` defaults to `true`.
56
- *
57
- * `"closed"` is only as sound as the walk. It follows `properties`,
58
- * `items`, a schema-valued `additionalProperties`, `prefixItems`, `anyOf`,
59
- * `oneOf` and `$defs`, and merges an `allOf` member's `properties` and
60
- * `additionalProperties: false` onto its node. It does not visit
61
- * `patternProperties` values, an `allOf` member's `items` or
62
- * `additionalProperties` schema, or an `allOf` nested inside another; and
63
- * a key declared in two `allOf` members is last-write-wins. A hand-authored
64
- * or `Tool.dynamic` schema can therefore pass `"closed"` with an open node
65
- * the walk never reached. Core-emitted strict schemas close every node, so
66
- * a strict `Tool.make` tool is reported faithfully.
67
- *
68
- * @public
69
- */
70
- var McpToolAudit = class {
71
- constructor() {}
72
- /**
73
- * Every violation of `policy` across `tools`, as `"<tool>: <what>"`, in
74
- * tool order. Empty means the sweep passed. A duplicate tool name is
75
- * reported under every policy, `input: "any"` included.
76
- */
77
- static check = (tools, policy) => {
78
- const violations = [];
79
- const seen = /* @__PURE__ */ new Set();
80
- for (const tool of tools) {
81
- const report = (what) => {
82
- violations.push(`${tool.name}: ${what}`);
83
- };
84
- if (seen.has(tool.name)) report("duplicate tool name");
85
- seen.add(tool.name);
86
- if (policy.input !== "any") for (const { path, properties, closed, droppedKeyCheck } of objectNodes(tool.inputSchema)) {
87
- const where = path === "" ? "the root" : path;
88
- if (policy.input === "closed" && !closed) report(`input schema is open at ${where}${droppedKeyCheck ? " (a Record whose key check was not served; a key pattern is served closed, as patternProperties, only when its RegExp has the u flag, e.g. Schema.isPattern(/^x-/u))" : ""}`);
89
- if (policy.input === "open" && isNode(properties) && closed) report(`input schema is closed at ${where}`);
90
- }
91
- const title = tool.title ?? tool.annotations?.title;
92
- if (policy.requireTitle === true && (typeof title !== "string" || title === "")) report("no title");
93
- if (policy.requireOutputSchema === true && tool.outputSchema === void 0) report("no outputSchema (a union success schema is dropped on stateful revisions; see ToolOutputSchema.objectRooted)");
94
- if ((policy.objectRootedOutput ?? true) && tool.outputSchema !== void 0 && tool.outputSchema.type !== "object") {
95
- const union = Array.isArray(tool.outputSchema.anyOf) || Array.isArray(tool.outputSchema.oneOf);
96
- report(`outputSchema is not object-rooted (root type: ${String(tool.outputSchema.type ?? "none")})${union ? "; if every union member is an object, wrap the union success schema in ToolOutputSchema.objectRooted" : ""}`);
97
- }
98
- const length = (tool.description ?? "").length;
99
- if (policy.maxDescription !== void 0 && length > policy.maxDescription) report(`description is ${length} characters, over the ${policy.maxDescription} limit`);
100
- if (policy.requireHints === true) {
101
- const missing = HINTS.filter((hint) => typeof tool.annotations?.[hint] !== "boolean");
102
- if (missing.length > 0) report(`missing annotation hints: ${missing.join(", ")}`);
103
- }
104
- }
105
- return violations;
106
- };
107
- };
108
-
109
- export { McpToolAudit };
package/McpToolkit.js DELETED
@@ -1,257 +0,0 @@
1
- import { ToolFailure } from "./ToolFailure.js";
2
- import { ToolInputSchema } from "./ToolInputSchema.js";
3
- import { Context, Effect, Layer, Schema } from "effect";
4
- import { McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
5
-
6
- // src/McpToolkit.ts
7
- /** The union a {@link McpToolkit.unionTool} decodes with; read by the `McpToolkit.layer` decorator. */
8
- const UnionParameters = Context.Reference("@effected/mcp/UnionParameters", { defaultValue: () => void 0 });
9
- /**
10
- * Effect's strict JSON Schema document for `parameters`, definitions attached
11
- * as `$defs`, then object-rooted: the served input schema of a union tool.
12
- */
13
- const unionInputJsonSchema = (parameters) => {
14
- const document = Schema.toJsonSchemaDocument(parameters, { onExcessProperty: "error" });
15
- return ToolInputSchema.objectRooted(Object.keys(document.definitions).length === 0 ? document.schema : {
16
- ...document.schema,
17
- $defs: document.definitions
18
- });
19
- };
20
- const invalidParameters = (name, message) => new McpSchema.InvalidParams({ message: `Invalid parameters for tool '${name}': ${message}` });
21
- const MAX_ACCEPTED_LINES = 20;
22
- /**
23
- * `path` as core renders an issue path, `["a"][0]["b"]`: a segment indexing
24
- * an array in `payload` is bare, any other is a JSON string. Truncated, since
25
- * the segments are the caller's keys.
26
- */
27
- const issuePath = (payload, path) => {
28
- let node = payload;
29
- let out = "";
30
- for (const segment of path) {
31
- out += Array.isArray(node) ? `[${segment}]` : `[${JSON.stringify(segment)}]`;
32
- node = typeof node === "object" && node !== null ? node[segment] : void 0;
33
- }
34
- return ToolFailure.truncate(out);
35
- };
36
- /**
37
- * One line per level of `payload` that carries a key the served
38
- * `inputSchema` does not accept: `Accepted params at <path>: a, b.`, with
39
- * `keys matching <pattern>` for a level that also accepts keys by
40
- * `patternProperties`, and `This tool accepts no params.` for a
41
- * zero-parameter root. At most 20 lines; empty when no level has an
42
- * unknown key.
43
- */
44
- const acceptedParamsLines = (payload, inputSchema) => ToolInputSchema.unknownKeys(payload, inputSchema).slice(0, MAX_ACCEPTED_LINES).map((level) => {
45
- const patterns = level.acceptedPatterns ?? [];
46
- if (level.path.length === 0 && level.accepted.length === 0 && patterns.length === 0) return "This tool accepts no params.";
47
- const parts = [...level.accepted.length > 0 ? [level.accepted.join(", ")] : [], ...patterns.length > 0 ? [`keys matching ${patterns.join(", ")}`] : []];
48
- return `Accepted params at ${level.path.length === 0 ? "the root" : issuePath(payload, level.path)}: ${parts.length > 0 ? parts.join("; ") : "(none)"}.`;
49
- });
50
- /**
51
- * `error` with {@link acceptedParamsLines} appended to its message, one per
52
- * line after core's own report; `error` itself when there is nothing to add.
53
- * Core stays the decoder: this only annotates the report it produced.
54
- */
55
- const withAcceptedParams = (error, payload, inputSchema) => {
56
- const lines = acceptedParamsLines(payload, inputSchema);
57
- return lines.length === 0 ? error : new McpSchema.InvalidParams({
58
- message: [error.message, ...lines].join("\n"),
59
- ...error.data !== void 0 ? { data: error.data } : {}
60
- });
61
- };
62
- /**
63
- * The one decode a union tool's payload gets, shared by the `McpToolkit.layer`
64
- * decorator and {@link McpToolkit.unionHandler}: the union's strict decode
65
- * with the options core decodes a strict `Tool.make` tool with
66
- * (`onExcessProperty: "error"`, `errors: "all"`), so every excess, missing and
67
- * invalid field of the matched member is reported together, worded as core
68
- * words a `Tool.make` failure, with the accepted params appended as the layer
69
- * appends them to core's own report.
70
- */
71
- const decodeUnionPayload = (name, union, inputSchema) => {
72
- const decode = Schema.decodeUnknownEffect(union);
73
- return (payload) => {
74
- const raw = payload ?? {};
75
- return decode(raw, {
76
- onExcessProperty: "error",
77
- errors: "all"
78
- }).pipe(Effect.mapError((error) => withAcceptedParams(invalidParameters(name, error.message), raw, inputSchema)));
79
- };
80
- };
81
- /**
82
- * The registration `McpToolkit.layer` hands core in place of `registration`:
83
- * a union tool gets its payload decoded first, with the wrapped handler built
84
- * only after the decode passes, so a rejected payload never reaches it.
85
- * Anything else is decoded by core as before, and core's parameter failure,
86
- * the only `InvalidParams` its `handle` fails with, gets the accepted params
87
- * appended.
88
- *
89
- * @internal
90
- */
91
- const guardRegistration = (registration) => {
92
- const union = Context.get(registration.annotations, UnionParameters);
93
- if (union === void 0) return {
94
- ...registration,
95
- handle: (payload) => registration.handle(payload).pipe(Effect.catchTag("InvalidParams", (error) => Effect.fail(withAcceptedParams(error, payload ?? {}, registration.tool.inputSchema))))
96
- };
97
- const check = decodeUnionPayload(registration.tool.name, union, registration.tool.inputSchema);
98
- return {
99
- ...registration,
100
- handle: (payload) => check(payload).pipe(Effect.flatMap(() => registration.handle(payload)))
101
- };
102
- };
103
- const DEFAULT_STRICT = "all";
104
- const strictened = (toolkit, mode) => mode === "annotated" ? toolkit : Toolkit.make(...Object.values(toolkit.tools).map((tool) => Tool.isDynamic(tool) || Tool.getStrictMode(tool) !== void 0 ? tool : tool.annotate(Tool.Strict, true)));
105
- /**
106
- * Register a toolkit exactly as core's `McpServer.toolkit` does, except that
107
- * every tool is strict by default and a {@link McpToolkit.unionTool} gets its
108
- * union decoded.
109
- *
110
- * @remarks
111
- * Rejection and reporting are core's: a tool annotated `Tool.Strict` is
112
- * served with `additionalProperties: false` on every object node and decoded
113
- * with `onExcessProperty: "error"` and `errors: "all"`, so one
114
- * `InvalidParams` names every excess key at every depth together with every
115
- * missing or invalid field. The layer appends to that report, never in place
116
- * of it: for every object level of the payload that carries a key the served
117
- * input schema does not accept, one line naming what that level does accept,
118
- * `Accepted params at the root: query, filter.` or
119
- * `Accepted params at ["filter"]: kind, tag.` (the path as core writes it),
120
- * with `keys matching <pattern>` for keys a `patternProperties` level
121
- * accepts, and `This tool accepts no params.` for a zero-parameter tool. An
122
- * agent can fix the call from the reply alone. A failure with no unknown key
123
- * (a missing or mistyped field only) is core's report unchanged. The lines
124
- * are appended to core's `InvalidParams` as it leaves the tool's registered
125
- * handler, the one failure that handler has for bad parameters.
126
- *
127
- * On top of that is the policy: under
128
- * `strict: "all"` (the default) it annotates every tool that carries no
129
- * `Tool.Strict` annotation of its own as strict, skipping `Tool.dynamic`
130
- * tools (core dies at registration on a strict dynamic tool). It runs core's
131
- * `registerToolkit` unchanged under a registration-scoped `McpServer` whose
132
- * `addTool` puts the union decode in front of a union tool's handler and
133
- * passes every other registration through untouched. It patches no core
134
- * internals: it uses only `McpServer.registerToolkit` and `addTool`.
135
- *
136
- * Registration goes through core's module-level `McpServer.McpServer.layer`,
137
- * shared by reference with `McpStdio.layer`'s own copy — provide both into
138
- * the same graph, as with `McpServer.toolkit`. Never wrap that layer in
139
- * `Layer.fresh`: tools would register into a second registry nobody serves.
140
- * To give two servers separate registries, wrap each whole bundle (this
141
- * layer together with `McpStdio.layer`) in `Layer.fresh` instead.
142
- *
143
- * Each call mints a fresh layer; bind the result to a `const` or the
144
- * registration runs twice.
145
- *
146
- * @example
147
- * ```ts
148
- * import { Layer } from "effect";
149
- * import { McpStdio, McpToolkit } from "@effected/mcp";
150
- *
151
- * const ToolsLayer = McpToolkit.layer(MyToolkit).pipe(Layer.provide(MyHandlers));
152
- * const ServerLayer = ToolsLayer.pipe(Layer.provideMerge(McpStdio.layer({ name: "my-server", version: "1.0.0" })));
153
- * ```
154
- *
155
- * @public
156
- */
157
- var McpToolkit = class {
158
- constructor() {}
159
- /**
160
- * A tool whose parameters are a `Schema.Union` of objects, which
161
- * `Tool.make` cannot take: core dies at registration on a non-object
162
- * `parameters` root.
163
- *
164
- * @remarks
165
- * The tool is a `Tool.dynamic` served with Effect's strict JSON Schema
166
- * document for `parameters` (`additionalProperties: false` on every
167
- * object node, definitions as `$defs`), rewritten by
168
- * `ToolInputSchema.objectRooted` to an object root carrying `type`,
169
- * `oneOf` and `x-discriminator`. Write its handler with
170
- * {@link McpToolkit.unionHandler}, which receives the decoded member.
171
- *
172
- * Registered through {@link McpToolkit.layer}, a union tool gets the same
173
- * treatment as a strict `Tool.make` tool: a strict decode with
174
- * `errors: "all"` before the handler runs, reporting every excess, missing
175
- * and invalid field of the matched member in one `InvalidParams`, with the
176
- * same `Accepted params at <path>: …` lines appended, so bad
177
- * arguments answer JSON-RPC `-32602` on `2025-06-18` and an `isError`
178
- * result on the later revisions, exactly as a `Tool.make` decode failure
179
- * does. Registered through core's `McpServer.toolkit` it still decodes
180
- * strictly, in the handler, but the `InvalidParams` is then a declared
181
- * failure: an `isError` result on every revision.
182
- *
183
- * Never annotate it `Tool.Strict` true: core dies at registration on a
184
- * strict tool with a raw JSON Schema.
185
- *
186
- * @example
187
- * ```ts
188
- * import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
189
- * import { Schema } from "effect";
190
- * import { Tool } from "effect/ai";
191
- *
192
- * // `AddNote`, `ListNotes`, `Added`, `Listed` are object schemas; `Kit` is a Toolkit that includes `Note`.
193
- * const Note = McpToolkit.unionTool("note", {
194
- * parameters: Schema.Union([AddNote, ListNotes]),
195
- * success: ToolOutputSchema.objectRooted(Schema.Union([Added, Listed])),
196
- * failure: ToolRefusal,
197
- * }).annotate(Tool.Title, "Note")
198
- * const handlers = Kit.toLayer({ note: McpToolkit.unionHandler(Note, (params) => handleNote(params)) })
199
- * ```
200
- */
201
- static unionTool = (name, options) => {
202
- const tool = Tool.dynamic(name, {
203
- description: options.description,
204
- parameters: unionInputJsonSchema(options.parameters),
205
- success: options.success ?? Schema.Void,
206
- failure: Schema.Union([options.failure ?? Schema.Never, McpSchema.InvalidParams])
207
- }).annotate(UnionParameters, options.parameters);
208
- return Object.assign(tool, { unionParameters: options.parameters });
209
- };
210
- /**
211
- * The handler for a {@link McpToolkit.unionTool}: decodes the raw payload
212
- * exactly as {@link McpToolkit.layer} does, and passes the decoded member
213
- * to `handler`.
214
- *
215
- * @remarks
216
- * A payload that does not decode strictly fails with one
217
- * `McpSchema.InvalidParams`, worded as core words a `Tool.make` decode
218
- * failure, naming every excess key at every depth together with every
219
- * missing or invalid field of the matched member, then one
220
- * `Accepted params at <path>: …` line per level with an unknown key. It
221
- * is the same
222
- * implementation the layer runs, so a handler called directly (a test
223
- * helper, or a toolkit registered through core's `McpServer.toolkit`)
224
- * reports what a client of the layer sees. Under {@link McpToolkit.layer}
225
- * the layer has already rejected a bad call before the handler runs.
226
- * `options.unknownKeyMessage` is deprecated and ignored.
227
- */
228
- static unionHandler = (tool, handler, _options = {}) => {
229
- const check = decodeUnionPayload(tool.name, tool.unionParameters, tool.jsonSchema);
230
- return (payload) => check(payload).pipe(Effect.flatMap(handler));
231
- };
232
- /**
233
- * `McpServer.toolkit`'s registration layer, strict by default.
234
- * `options.unknownKeyMessage` is deprecated and ignored.
235
- *
236
- * @remarks
237
- * Its requirements are core's `McpServer.ToolkitRequirements` less
238
- * `McpServer`, which the layer provides itself: the handlers, any service
239
- * a parameter or success schema needs, and each service a tool declares
240
- * with `dependencies` or `addDependency` as an
241
- * `McpServer.McpRequest<"Requires", S>` marker. A marker is met per
242
- * request by MCP middleware (`McpServer.middleware`, its `layer` provided
243
- * to this one), never by providing `S`. A service the handlers only need
244
- * when they are built is captured by the effect form of
245
- * `Toolkit.toLayer` instead, and declared nowhere.
246
- */
247
- static layer = (toolkit, options = {}) => Layer.effectDiscard(Effect.gen(function* () {
248
- const registry = yield* McpServer.McpServer;
249
- const decorated = McpServer.McpServer.of({
250
- ...registry,
251
- addTool: (registration) => registry.addTool(guardRegistration(registration))
252
- });
253
- yield* McpServer.registerToolkit(strictened(toolkit, options.strict ?? DEFAULT_STRICT)).pipe(Effect.provideService(McpServer.McpServer, decorated));
254
- })).pipe(Layer.provide(McpServer.McpServer.layer));
255
- };
256
-
257
- export { McpToolkit, guardRegistration };