@effected/mcp 0.12.0 → 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/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 };
package/StdioCapture.js DELETED
@@ -1,146 +0,0 @@
1
- import { Effect, Fiber } from "effect";
2
- import { AsyncLocalStorage } from "node:async_hooks";
3
- import { Writable } from "node:stream";
4
-
5
- // src/StdioCapture.ts
6
- /**
7
- * One registry per process, shared by every installed copy of this package
8
- * through a `Symbol.for` key on `globalThis`, so two copies never stack
9
- * patches or split the context they route on.
10
- */
11
- const registry = (() => {
12
- const key = Symbol.for("@effected/mcp/StdioCapture");
13
- const global = globalThis;
14
- global[key] ??= {
15
- storage: new AsyncLocalStorage(),
16
- originals: /* @__PURE__ */ new WeakMap()
17
- };
18
- return global[key];
19
- })();
20
- /** Patches `stream` once and returns its original `write`; a stream already patched keeps its first patch. */
21
- const patch = (stream, pick) => {
22
- const known = registry.originals.get(stream);
23
- if (known !== void 0) return known;
24
- const original = stream.write;
25
- registry.originals.set(stream, original);
26
- const { storage } = registry;
27
- stream.write = function capturedWrite() {
28
- const options = storage.getStore();
29
- const sink = options === void 0 ? void 0 : pick(options);
30
- return sink === void 0 ? original.apply(this, arguments) : sink.write.apply(sink, arguments);
31
- };
32
- return original;
33
- };
34
- const callbackOf = (rest) => {
35
- const last = rest.at(-1);
36
- return typeof last === "function" ? last : void 0;
37
- };
38
- /**
39
- * Diverts a host's stdout (and optionally stderr) writes to a sink, for code
40
- * running inside one async context only, while every other context keeps
41
- * writing to the real streams.
42
- *
43
- * @remarks
44
- * For an MCP server over stdio, stdout is the JSON-RPC wire. A chatty library
45
- * run in-process (a test runner, a bundler, a linter) must keep its output off
46
- * it while other handlers, and the transport itself, keep writing normally.
47
- *
48
- * `install` patches the host's `write` methods once, keeping the unbound
49
- * originals, and routes every write through an `AsyncLocalStorage`: inside a
50
- * {@link StdioCapture.run} or {@link StdioCapture.capture} context a write goes
51
- * to that context's sink, and outside it to the original stream.
52
- *
53
- * An Effect fiber resumes in the async context it suspended in, not the
54
- * context of whatever woke it, so a server fiber woken by code inside a
55
- * capture (its response written to a queue, say) keeps writing to the wire.
56
- *
57
- * Two kinds of write escape a capture and reach the original stream:
58
- *
59
- * - **Writes from a context the capture lost.** Callbacks registered outside the
60
- * capture keep the context they were registered in: native addons, worker
61
- * thread output relayed by the main thread, listeners on a child process's
62
- * pipes attached earlier. On stdout, that is the wire.
63
- * - **Fibers started inside the capture keep capturing after it ends.** A fiber
64
- * forked inside it (`Effect.forkDetach`, a memoized layer first built inside
65
- * it, a background fiber a library starts) keeps writing to the sink, even
66
- * after the capture's own work is done.
67
- *
68
- * @example
69
- * ```ts
70
- * import { StdioCapture } from "@effected/mcp/node"
71
- *
72
- * const capture = StdioCapture.install(process)
73
- * // in a tool handler: keep the runner's own console output off the wire
74
- * const report = yield* runTests(files).pipe(capture.capture({ stdout: capture.toStderr }))
75
- * ```
76
- *
77
- * @public
78
- */
79
- var StdioCapture = class StdioCapture {
80
- /** A sink that writes through the host's original, unpatched stderr: captured output stays visible off the wire. */
81
- toStderr;
82
- constructor(toStderr) {
83
- this.toStderr = toStderr;
84
- }
85
- /**
86
- * Patches `host`'s two streams. Idempotent per stream, across every installed
87
- * copy of this package: a stream already patched (through another host
88
- * object, or another copy) keeps its one patch and its original `write`.
89
- */
90
- static install = (host) => {
91
- patch(host.stdout, (options) => options.stdout);
92
- const stderr = patch(host.stderr, (options) => options.stderr);
93
- return new StdioCapture({ write: (...args) => stderr.apply(host.stderr, args) });
94
- };
95
- /**
96
- * A write-only sink that drops every write and calls the write's callback.
97
- *
98
- * @remarks
99
- * It is a {@link StdioSink}, not a stream: it has no `on`, `pipe` target
100
- * protocol or `setMaxListeners`, so it cannot stand in where a library
101
- * expects a `Writable` (a test runner's logger stream that worker output
102
- * is piped into, for one). Use {@link StdioCapture.nullWritable} there; it
103
- * serves as a capture sink too.
104
- */
105
- static discard = { write: (_chunk, ...rest) => {
106
- const callback = callbackOf(rest);
107
- if (callback !== void 0) queueMicrotask(callback);
108
- return true;
109
- } };
110
- /**
111
- * A new Node `Writable` that drops everything written to it: a real stream,
112
- * so it works as a `pipe` target, takes `setMaxListeners`, emits `drain`
113
- * and `finish`, and is also a {@link StdioSink} for a capture.
114
- *
115
- * @remarks
116
- * A factory, not a shared value: a stream ends once, and a source piped in
117
- * with the default `{ end: true }` ends its destination. Build one per
118
- * consumer.
119
- */
120
- static nullWritable = () => new Writable({
121
- decodeStrings: false,
122
- write: (_chunk, _encoding, callback) => callback()
123
- });
124
- /**
125
- * Runs `evaluate` with its writes captured, for code that is not Effect.
126
- * A synchronous throw from `evaluate` rejects the returned promise.
127
- */
128
- run(options, evaluate) {
129
- return registry.storage.run(options, async () => evaluate());
130
- }
131
- /**
132
- * Runs an effect with its writes captured: on its own fiber, started inside
133
- * the capture's context, so the context survives every suspension of that
134
- * fiber. Interrupting the returned effect interrupts that fiber and waits
135
- * for it.
136
- */
137
- capture(options) {
138
- const { storage } = registry;
139
- return (self) => Effect.uninterruptibleMask((restore) => Effect.withFiber((parent) => {
140
- const fiber = storage.run(options, () => Effect.runForkWith(parent.context)(self));
141
- return Effect.ensuring(restore(Fiber.join(fiber)), Fiber.interrupt(fiber));
142
- }));
143
- }
144
- };
145
-
146
- export { StdioCapture };
package/ToolFailure.js DELETED
@@ -1,75 +0,0 @@
1
- import { Schema } from "effect";
2
- import { Remediation } from "@effected/engine";
3
-
4
- // src/ToolFailure.ts
5
- const isHighSurrogate = (code) => code >= 55296 && code <= 56319;
6
- /**
7
- * How a tool's declared failure should read on the wire.
8
- *
9
- * @remarks
10
- * Core sends a declared failure that is an `Error` instance — every
11
- * `Schema.TaggedError` is — as `isError: true` with `error.message` as the only
12
- * text and no `structuredContent` (effect `ai/McpServer.ts`,
13
- * `declaredFailureResult`). So the remediation must be folded into `message`
14
- * when the error is constructed: whatever is not in the message never reaches
15
- * the agent.
16
- *
17
- * Spread {@link ToolFailure.fields} into your tool's `Schema.TaggedError`, build
18
- * `message` with {@link ToolFailure.message}, and pass every caller-supplied
19
- * value through {@link ToolFailure.truncate} before echoing it. For a plain
20
- * refusal with no fields of its own, `ToolRefusal` is this shape ready-made.
21
- *
22
- * Only a declared failure keeps its message: core scrubs an undeclared
23
- * failure or a defect to "Tool execution failed due to an internal server
24
- * error.", so audit every throw path a tool can take and declare each one the
25
- * agent should read.
26
- *
27
- * @example
28
- * ```ts
29
- * import { ToolFailure } from "@effected/mcp";
30
- * import { Schema } from "effect";
31
- *
32
- * class NotFound extends Schema.TaggedError<NotFound>()("NotFound", { ...ToolFailure.fields, id: Schema.String }) {}
33
- * const remediation = { hint: "List the ids first.", suggestedTool: "list_things" }
34
- * new NotFound({ id, remediation, message: ToolFailure.message(`No thing "${ToolFailure.truncate(id)}".`, remediation) })
35
- * ```
36
- *
37
- * @public
38
- */
39
- var ToolFailure = class ToolFailure {
40
- constructor() {}
41
- /** The cap, in UTF-16 code units, on a caller-supplied value echoed into a message. */
42
- static ECHO_LIMIT = 200;
43
- /** The larger cap for a value the engine produced (a path, a diagnostic), not one a caller typed. */
44
- static ENGINE_ECHO_LIMIT = 2e3;
45
- /**
46
- * The `message` and `remediation` fields to spread into a consumer's
47
- * `Schema.TaggedError`, each carrying a default `description` that a served
48
- * JSON Schema (a tool's `outputSchema`) shows the agent.
49
- */
50
- static fields = {
51
- message: Schema.String.annotate({ description: "What went wrong, as one plain sentence." }),
52
- remediation: Remediation.annotate({ description: "What to do next: a hint, and optionally the tool to call next and the arguments to call it with." })
53
- };
54
- /**
55
- * `"<raw> <hint> Try <suggestedTool>."`, dropping any empty part, so a
56
- * missing tool or an empty hint never leaves a double space.
57
- */
58
- static message = (raw, remediation) => [
59
- raw,
60
- remediation.hint,
61
- remediation.suggestedTool === void 0 ? "" : `Try ${remediation.suggestedTool}.`
62
- ].filter((part) => part !== "").join(" ");
63
- /**
64
- * `value` cut to `limit` code units with a trailing ellipsis, backing off one
65
- * unit rather than splitting a surrogate pair. A hostile multi-megabyte
66
- * argument is otherwise echoed into the agent's context whole.
67
- */
68
- static truncate = (value, limit = ToolFailure.ECHO_LIMIT) => {
69
- if (value.length <= limit) return value;
70
- const end = limit > 0 && isHighSurrogate(value.charCodeAt(limit - 1)) ? limit - 1 : limit;
71
- return `${value.slice(0, end)}…`;
72
- };
73
- };
74
-
75
- export { ToolFailure };
@@ -1,180 +0,0 @@
1
- import { ToolFailure } from "./ToolFailure.js";
2
- import { Result } from "effect";
3
-
4
- // 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) {
81
- const acceptedPatterns = patterns.map(([pattern]) => pattern);
82
- out.push({
83
- path,
84
- unknown: extra,
85
- accepted,
86
- ...acceptedPatterns.length > 0 ? { acceptedPatterns } : {}
87
- });
88
- }
89
- for (const key of accepted) if (Object.hasOwn(value, key)) walk(value[key], properties[key], [...path, key], depth + 1);
90
- for (const [key, schema] of patterned) walk(value[key], schema, [...path, key], depth + 1);
91
- if (additional !== void 0) for (const key of extra) walk(value[key], additional, [...path, key], depth + 1);
92
- };
93
- walk(payload, root, [], 0);
94
- return out;
95
- };
96
- /**
97
- * Pure walkers over a tool's served JSON Schema: every unknown key at every
98
- * depth, a message naming them all, and an object-rooted form of a union.
99
- *
100
- * @remarks
101
- * For a `Tool.make` tool, core's strict decode already names every unknown
102
- * key, with every missing or invalid field, in one response. Core never
103
- * validates a `Tool.dynamic` tool's raw JSON Schema, so these walkers are
104
- * for that case: a handler runs them against the schema it registered.
105
- * They run over the schema as served, so what they accept matches what the
106
- * tool advertises:
107
- *
108
- * - Only `additionalProperties: false` closes a node, as in JSON Schema; core
109
- * emits it on every object node of a strict tool and `true` otherwise.
110
- * - `allOf` members' properties merge; `oneOf`/`anyOf` members are chosen by an
111
- * `action`, `kind`, `_tag` or `type` literal (tried in that order), or by
112
- * being the only object member. A payload whose discriminant matches no
113
- * member, or is missing, is skipped and left for decoding.
114
- * - `items`, `prefixItems`, `$ref` into `$defs` (JSON Pointer escapes
115
- * honoured), and `patternProperties` are followed; a key matched by a
116
- * pattern is accepted and walked with the first matching pattern's schema.
117
- * Effect serves a pattern-keyed `Schema.Record` with `patternProperties`
118
- * only when the key pattern's RegExp has the `u` flag; without it the
119
- * record is served open, and every key is accepted.
120
- * - The payload is untrusted: the walk stops descending at depth 256 rather
121
- * than overflowing the stack, so a deeper unknown key goes unreported and
122
- * is left for decoding. Every hop counts toward that cap: each property or
123
- * array position, and also each union member selection, which adds a hop
124
- * without adding a path segment.
125
- *
126
- * @public
127
- */
128
- var ToolInputSchema = class {
129
- constructor() {}
130
- /** Every level of `payload` that carries a key `schema` does not accept. */
131
- static unknownKeys = (payload, schema) => collect(payload, schema);
132
- /**
133
- * `Unrecognized parameter(s): a, b.c. Accepted params: x, y.` per level,
134
- * followed by `Accepted keys matching: ^x-.` when the level also accepts
135
- * keys by pattern. A level that accepts keys only by pattern names its
136
- * patterns alone, never `Accepted params: (none).` Each echoed key and
137
- * pattern is truncated, and at most 20 keys, 20 patterns and 20 levels are
138
- * named; a trailing `(and N more levels).` counts the levels left out.
139
- */
140
- static formatUnknownKeys = (levels, options = {}) => {
141
- const limit = options.echoLimit ?? ToolFailure.ECHO_LIMIT;
142
- const sentences = levels.slice(0, MAX_ECHOED).map((level) => {
143
- const shown = level.unknown.slice(0, MAX_ECHOED).map((key) => ToolFailure.truncate([...level.path, key].join("."), limit));
144
- const more = level.unknown.length > MAX_ECHOED ? ` (and ${level.unknown.length - MAX_ECHOED} more)` : "";
145
- const patterns = (level.acceptedPatterns ?? []).slice(0, MAX_ECHOED).map((pattern) => ToolFailure.truncate(pattern, limit));
146
- const params = level.accepted.length > 0 ? ` Accepted params: ${level.accepted.join(", ")}.` : patterns.length === 0 ? " Accepted params: (none)." : "";
147
- const matching = patterns.length > 0 ? ` Accepted keys matching: ${patterns.join(", ")}.` : "";
148
- return `Unrecognized parameter(s): ${shown.join(", ")}${more}.${params}${matching}`;
149
- });
150
- const hidden = levels.length > MAX_ECHOED ? ` (and ${levels.length - MAX_ECHOED} more levels).` : "";
151
- return `${sentences.join(" ")}${hidden}`;
152
- };
153
- /**
154
- * `schema` with a root `$ref` inlined and a top-level union of objects
155
- * sharing an `action`, `kind`, `_tag` or `type` literal rewritten to an
156
- * object root carrying `type: "object"`, `oneOf` and `"x-discriminator"` —
157
- * MCP requires an object root, and core dies at boot on a union root.
158
- * Returned as the same value when nothing changes.
159
- */
160
- static objectRooted = (schema) => {
161
- const root = resolveRef(schema, schema);
162
- const members = Array.isArray(root.anyOf) ? root.anyOf : Array.isArray(root.oneOf) ? root.oneOf : void 0;
163
- if (root.type === void 0 && members !== void 0) {
164
- const resolved = members.filter(isNode).map((member) => resolveRef(member, schema));
165
- const key = resolved.length === members.length ? discriminantOf(resolved) : void 0;
166
- if (key !== void 0) {
167
- const { anyOf: _anyOf, oneOf: _oneOf, ...rest } = root;
168
- return {
169
- type: "object",
170
- ...rest,
171
- oneOf: resolved,
172
- "x-discriminator": key
173
- };
174
- }
175
- }
176
- return root;
177
- };
178
- };
179
-
180
- export { ToolInputSchema };