@effected/mcp 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/McpGuard.js ADDED
@@ -0,0 +1,121 @@
1
+ //#region src/McpGuard.ts
2
+ const isInjectedKind = (kind) => kind === "uncaughtException" || kind === "unhandledRejection";
3
+ /** Emit one injected crash through the host, to the listeners the guard installed there. */
4
+ const emitInjected = (host, kind) => {
5
+ const error = /* @__PURE__ */ new Error(`[injected] ${kind}`);
6
+ if (kind === "uncaughtException") {
7
+ host.emit("uncaughtException", error, "uncaughtException");
8
+ return;
9
+ }
10
+ const promise = Promise.reject(error);
11
+ promise.catch(() => void 0);
12
+ host.emit("unhandledRejection", error, promise);
13
+ };
14
+ const fallbackFormat = (error) => error instanceof Error ? error.stack ?? error.message : String(error);
15
+ /**
16
+ * Crash guards for an MCP server process, installed before the server's
17
+ * module graph is loaded. Imported from `@effected/mcp/guard`.
18
+ *
19
+ * @remarks
20
+ * {@link McpGuard.run} registers `uncaughtException` and `unhandledRejection`
21
+ * listeners on `host`, then awaits `load()`, then launches the server with
22
+ * `McpStdio.launch` and `McpStdio.teardown` under the `runMain` it returned.
23
+ * This entrypoint has no static runtime import: it loads `effect` and the
24
+ * stdio wiring only after the guards are listening, so a throw while any of
25
+ * it evaluates is still reported on stderr.
26
+ *
27
+ * - "Connected" means the whole server layer has built, so stdin is being
28
+ * read. That is before any client sends `initialize`.
29
+ * - `injectCrash` drives either half of the policy from a test, before
30
+ * `load()` or once connected; unset, it does nothing.
31
+ * - A `load()` that rejects is reported as `startup failed` and exits 1
32
+ * whatever the policy. Left to a log-only rejection listener, it would
33
+ * let the event loop drain and exit 0 with no server.
34
+ * - Installing an `unhandledRejection` listener switches off Node's default
35
+ * of throwing on one, so `"log"` really does keep the process running.
36
+ * - An exit from a guard skips Effect's finalizers and the teardown.
37
+ * - Keeping on serving after a crash works because the platform `runMain`
38
+ * holds the process open while the server fiber lives.
39
+ * - A layer that fails to build is still reported by `McpStdio.launch`.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { McpGuard } from "@effected/mcp/guard";
44
+ *
45
+ * await McpGuard.run({
46
+ * label: "my-server",
47
+ * host: process,
48
+ * policy: { onUncaught: "exitBeforeConnect", onRejection: "log" },
49
+ * load: async () => {
50
+ * const { NodeRuntime } = await import("@effect/platform-node");
51
+ * const { Main } = await import("./server.js");
52
+ * return { layer: Main, runMain: NodeRuntime.runMain };
53
+ * },
54
+ * });
55
+ * ```
56
+ *
57
+ * @public
58
+ */
59
+ var McpGuard = class {
60
+ constructor() {}
61
+ /** Install the guards, load the server, and launch it. Resolves once the server is launched. */
62
+ static run = async (options) => {
63
+ const { host, label } = options;
64
+ const onUncaught = options.policy?.onUncaught ?? "exit";
65
+ const onRejection = options.policy?.onRejection ?? "exit";
66
+ let connected = false;
67
+ let format = fallbackFormat;
68
+ const describe = (error) => {
69
+ try {
70
+ return format(error);
71
+ } catch {
72
+ try {
73
+ return fallbackFormat(error);
74
+ } catch {
75
+ return "<unformattable error value>";
76
+ }
77
+ }
78
+ };
79
+ const exits = (mode) => mode === "exit" || mode === "exitBeforeConnect" && !connected;
80
+ host.on("uncaughtException", (error, origin) => {
81
+ host.stderr.write(`${label}: uncaughtException (${origin}): ${describe(error)}\n`);
82
+ if (exits(onUncaught)) host.exit(1);
83
+ });
84
+ host.on("unhandledRejection", (reason) => {
85
+ host.stderr.write(`${label}: unhandledRejection: ${describe(reason)}\n`);
86
+ if (exits(onRejection)) host.exit(1);
87
+ });
88
+ const inject = options.injectCrash;
89
+ const injectKind = isInjectedKind(inject?.kind) ? inject.kind : void 0;
90
+ if (inject?.at === "load" && injectKind !== void 0) await new Promise((resolve, reject) => {
91
+ setTimeout(() => {
92
+ try {
93
+ emitInjected(host, injectKind);
94
+ resolve();
95
+ } catch (error) {
96
+ reject(error);
97
+ }
98
+ }, 0);
99
+ });
100
+ const onReady = () => {
101
+ connected = true;
102
+ if (inject?.at === "connected" && injectKind !== void 0) setTimeout(() => {
103
+ try {
104
+ emitInjected(host, injectKind);
105
+ } catch {}
106
+ }, 0);
107
+ };
108
+ try {
109
+ const server = await options.load();
110
+ if (server.format !== void 0) format = server.format;
111
+ const { launchGuarded } = await import("./internal/guardLaunch.js");
112
+ launchGuarded(server, onReady);
113
+ } catch (error) {
114
+ host.stderr.write(`${label}: startup failed: ${describe(error)}\n`);
115
+ host.exit(1);
116
+ }
117
+ };
118
+ };
119
+
120
+ //#endregion
121
+ export { McpGuard };
package/McpHarness.js CHANGED
@@ -2,7 +2,7 @@ import { ToolFailure } from "./ToolFailure.js";
2
2
  import { DEFAULT_CLIENT_INFO, frame, initializeParams, isJsonRpcMessage, isResponse, isStateless, parseFrame, requestKey } from "./internal/wire.js";
3
3
  import { McpTestFailure } from "./McpTestFailure.js";
4
4
  import { Console, Deferred, Effect, Exit, Layer, Queue, Sink, Stdio, Stream } from "effect";
5
- import { McpProtocol } from "effect/unstable/ai";
5
+ import { McpProtocol } from "effect/ai";
6
6
 
7
7
  //#region src/McpHarness.ts
8
8
  /**
@@ -38,11 +38,13 @@ import { McpProtocol } from "effect/unstable/ai";
38
38
  * anything that went through `console.log`, which in a real server is
39
39
  * the wire. Assert it empty.
40
40
  * - On a stateful revision (the default `2025-11-25`), send `initialize`
41
- * first. Every other request before it fails with `NotInitialized` and
42
- * is never written, `ping` included: the server would only answer an
43
- * opaque refusal, `-32602 Invalid request metadata` when a stateless
44
- * adapter is listed first (as in `McpStdio.protocols`), or
45
- * `-32603 Internal error` when only stateful revisions are served.
41
+ * first. `ping` is the exception: the server answers it `{}` before
42
+ * initialization, so it is written. Every other request before
43
+ * `initialize` fails with `NotInitialized` and is never written: the
44
+ * server would only answer an opaque refusal,
45
+ * `-32602 Invalid request metadata` when a stateless adapter is listed
46
+ * first (as in `McpStdio.protocols`), or `-32603 Internal error` when
47
+ * only stateful revisions are served.
46
48
  *
47
49
  * @public
48
50
  */
@@ -51,11 +53,27 @@ var McpHarness = class McpHarness {
51
53
  protocol;
52
54
  /** `initialize` then `notifications/initialized`; `server/discover` on a stateless revision. Call it first on a stateful revision. */
53
55
  initialize;
56
+ /**
57
+ * `initialize` asking for `protocolVersion` instead of the harness's own
58
+ * revision, then `notifications/initialized` only when the response
59
+ * carries no error. Returns the whole response: assert the negotiated
60
+ * `result.protocolVersion`, or the JSON-RPC `error`.
61
+ *
62
+ * @remarks
63
+ * It moves between stateful revisions only. A stateful server never
64
+ * refuses a version: it counter-offers its latest stateful revision, so
65
+ * asking for an unknown one, or for the stateless `2026-07-28`, comes back
66
+ * as `2025-11-25`. On a harness made with a stateless revision the server
67
+ * does not serve `initialize` at all, and the response is a JSON-RPC
68
+ * `-32601` "Method not found" error, with no `notifications/initialized`
69
+ * sent after it.
70
+ */
71
+ initializeWith;
54
72
  /** `server/discover`. */
55
73
  discover;
56
74
  /** Send a request and wait for its response. */
57
75
  request;
58
- /** Send a request now; wait for its response later. Fails with `NotInitialized`, sending nothing, on a stateful revision before `initialize`. */
76
+ /** Send a request now; wait for its response later. Fails with `NotInitialized`, sending nothing, on a stateful revision before `initialize` (`ping` excepted). */
59
77
  startRequest;
60
78
  /** Send a notification. */
61
79
  notify;
@@ -71,6 +89,8 @@ var McpHarness = class McpHarness {
71
89
  sendRaw;
72
90
  /** The next server-initiated frame with this method, skipping and keeping others; fails with `ServerStopped` once the server stops. */
73
91
  awaitOutboundMethod;
92
+ /** Every frame written to the server's stdin so far, in order, as the JSON-parsed bytes written (a snapshot the caller's later mutations cannot reach): requests, notifications and `sendRaw` values. */
93
+ sentSoFar;
74
94
  /** Everything written to stderr, plus every captured log line. */
75
95
  stderrSoFar;
76
96
  /** Every captured `console.log`, `info` or `debug` call. */
@@ -80,6 +100,7 @@ var McpHarness = class McpHarness {
80
100
  constructor(parts) {
81
101
  this.protocol = parts.protocol;
82
102
  this.initialize = parts.initialize;
103
+ this.initializeWith = parts.initializeWith;
83
104
  this.discover = parts.discover;
84
105
  this.request = parts.request;
85
106
  this.startRequest = parts.startRequest;
@@ -90,6 +111,7 @@ var McpHarness = class McpHarness {
90
111
  this.readResource = parts.readResource;
91
112
  this.sendRaw = parts.sendRaw;
92
113
  this.awaitOutboundMethod = parts.awaitOutboundMethod;
114
+ this.sentSoFar = parts.sentSoFar;
93
115
  this.stderrSoFar = parts.stderrSoFar;
94
116
  this.consoleLogSoFar = parts.consoleLogSoFar;
95
117
  this.close = parts.close;
@@ -164,12 +186,16 @@ var McpHarness = class McpHarness {
164
186
  const awaitResponse = (waiter) => Effect.suspend(() => Deferred.isDoneUnsafe(waiter) ? Deferred.await(waiter) : stopAware(Deferred.await(waiter)));
165
187
  const stateful = !isStateless(protocol);
166
188
  let initializeSent = false;
189
+ const sent = [];
167
190
  const sendRaw = (message) => Effect.suspend(() => {
168
- if (isJsonRpcMessage(message) && message.method === "initialize") initializeSent = true;
169
- return Effect.asVoid(Queue.offer(stdin, encoder.encode(`${JSON.stringify(message)}\n`)));
191
+ const line = JSON.stringify(message);
192
+ const written = line === void 0 ? void 0 : JSON.parse(line);
193
+ sent.push(written);
194
+ if (isJsonRpcMessage(written) && written.method === "initialize") initializeSent = true;
195
+ return Effect.asVoid(Queue.offer(stdin, encoder.encode(`${line}\n`)));
170
196
  });
171
197
  const startRequest = (method, params) => Effect.gen(function* () {
172
- if (stateful && !initializeSent && method !== "initialize") return yield* new McpTestFailure({
198
+ if (stateful && !initializeSent && method !== "initialize" && method !== "ping") return yield* new McpTestFailure({
173
199
  reason: "NotInitialized",
174
200
  message: `${method} was not sent: call initialize first on stateful protocol ${protocol.protocolVersion}`
175
201
  });
@@ -190,6 +216,14 @@ var McpHarness = class McpHarness {
190
216
  yield* notify("notifications/initialized");
191
217
  return response;
192
218
  });
219
+ const initializeWith = (protocolVersion) => Effect.gen(function* () {
220
+ const response = yield* request("initialize", {
221
+ ...initializeParams(protocol, clientInfo),
222
+ protocolVersion
223
+ });
224
+ if (response.error === void 0) yield* notify("notifications/initialized");
225
+ return response;
226
+ });
193
227
  const listTools = Effect.flatMap(request("tools/list"), (response) => response.error === void 0 ? Effect.succeed(response.result.tools) : Effect.fail(new McpTestFailure({
194
228
  reason: "ErrorResponse",
195
229
  message: `tools/list failed: ${ToolFailure.truncate(JSON.stringify(response.error))}`
@@ -212,6 +246,7 @@ var McpHarness = class McpHarness {
212
246
  return new McpHarness({
213
247
  protocol,
214
248
  initialize,
249
+ initializeWith,
215
250
  discover,
216
251
  request,
217
252
  startRequest,
@@ -225,6 +260,7 @@ var McpHarness = class McpHarness {
225
260
  readResource: (uri) => request("resources/read", { uri }),
226
261
  sendRaw,
227
262
  awaitOutboundMethod,
263
+ sentSoFar: Effect.sync(() => [...sent]),
228
264
  stderrSoFar: Effect.sync(() => stderr.join("")),
229
265
  consoleLogSoFar: Effect.sync(() => [...consoleLog]),
230
266
  close: Effect.asVoid(Queue.end(stdin))
package/McpProbe.js CHANGED
@@ -3,7 +3,7 @@ import { DEFAULT_CLIENT_INFO, STDERR_HINT, frame, initializeParams, isJsonRpcMes
3
3
  import { McpTestFailure } from "./McpTestFailure.js";
4
4
  import { McpProcess } from "./McpProcess.js";
5
5
  import { Effect } from "effect";
6
- import { McpProtocol } from "effect/unstable/ai";
6
+ import { McpProtocol } from "effect/ai";
7
7
 
8
8
  //#region src/McpProbe.ts
9
9
  /**
package/McpProcess.js CHANGED
@@ -2,8 +2,8 @@ import { ToolFailure } from "./ToolFailure.js";
2
2
  import { DEFAULT_CLIENT_INFO, STDERR_HINT, frame, initializeParams, isJsonRpcMessage, isResponse, isStateless, parseFrame } from "./internal/wire.js";
3
3
  import { McpTestFailure } from "./McpTestFailure.js";
4
4
  import { Deferred, Effect, Queue, Ref, Stream } from "effect";
5
- import { McpProtocol } from "effect/unstable/ai";
6
- import { ChildProcessSpawner } from "effect/unstable/process";
5
+ import { McpProtocol } from "effect/ai";
6
+ import { ChildProcessSpawner } from "effect/process";
7
7
 
8
8
  //#region src/McpProcess.ts
9
9
  /**
package/McpStdio.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { LaunchFailed } from "./internal/LaunchFailed.js";
2
2
  import { makeGuardedStdio } from "./internal/StdinFrames.js";
3
3
  import { Cause, Effect, Exit, Layer, References, Runtime } from "effect";
4
- import { McpProtocol, McpServer } from "effect/unstable/ai";
4
+ import { McpProtocol, McpServer } from "effect/ai";
5
5
 
6
6
  //#region src/McpStdio.ts
7
7
  /**
@@ -68,26 +68,35 @@ var McpStdio = class McpStdio {
68
68
  *
69
69
  * The server reads stdin through a guard that frames it exactly as core's
70
70
  * decoder does: one streaming UTF-8 decoder, a byte-order mark stripped
71
- * only at the start of the stream, lines split on `\n`. A line that is
72
- * not JSON is answered on stdout with a JSON-RPC parse error, code
73
- * `-32700` and `id: null`, and never reaches core's decoder, which would
74
- * otherwise throw on that line again for every later chunk and stop
75
- * answering. So is a line longer than core's cap of 16 Mi UTF-16 code
76
- * units: it is answered once, as soon as it passes the cap, and the rest
77
- * of it is discarded up to its newline. A line of JSON whitespace (space,
78
- * tab, carriage return) is ignored.
71
+ * only at the start of the stream, lines split on `\n`. Core itself skips
72
+ * a line it cannot use and keeps serving, but sends no reply, where
73
+ * JSON-RPC 2.0 requires one; the guard supplies it. A line that is not
74
+ * JSON is answered on stdout with a parse error, code `-32700` and
75
+ * `id: null`, and never reaches core. So is a line longer than core's cap
76
+ * of 16 Mi UTF-16 code units: it is answered once, as soon as it passes
77
+ * the cap, and the rest of it is discarded up to its newline (core would
78
+ * log it on stderr and drop it). A line of JSON whitespace (space, tab,
79
+ * carriage return) is ignored.
79
80
  *
80
81
  * A line that is JSON but no JSON-RPC message core can handle is answered
81
82
  * with an Invalid Request, code `-32600` and `id: null`, and never reaches
82
- * core: a value that is neither an object nor an array (core throws on
83
- * `null`, dropping every other frame in its chunk, and ignores a number,
84
- * string or boolean without a reply), an object whose `method` is not a
85
- * string and whose `id` is absent or `null` (core throws on it too), and
86
- * an object with neither `method` nor `id`, which is neither a request nor
87
- * a response. Everything else goes to core: an array, which core answers
88
- * `-32600` itself because it serves no batches; an object with an `id`
89
- * and no `method`, which is a response and gets no reply; and a request
90
- * with an `id`, which core answers even when its `method` is malformed.
83
+ * core: a value that is neither an object nor an array, an object whose
84
+ * `method` is not a string and whose `id` is absent or `null`, and an
85
+ * object with neither `method` nor `id`, which is neither a request nor a
86
+ * response.
87
+ *
88
+ * A line whose `method` starts with `@effect/rpc/` never reaches core,
89
+ * whose JSON-RPC decoder would read one without an `id` as its own RPC
90
+ * control message: `@effect/rpc/Eof` alone silently stops the server
91
+ * (Effect-TS/effect#8499, open upstream). Such a request is answered
92
+ * `-32601` Method not found, echoing its `id`; such a notification is
93
+ * dropped. The guard stops doing this once core no longer treats a
94
+ * client's `@effect/rpc/` frame as a control message.
95
+ *
96
+ * Everything else goes to core: an array, which core answers `-32600`
97
+ * itself because it serves no batches; an object with an `id` and no
98
+ * `method`, which is a response and gets no reply; and a request with an
99
+ * `id`, which core answers even when its `method` is malformed.
91
100
  *
92
101
  * Give each server a fresh layer memo map. Core's stdio protocol layer
93
102
  * is a shared constant, so a second `McpStdio.layer` server whose build
@@ -96,8 +105,12 @@ var McpStdio = class McpStdio {
96
105
  * one graph does that, and so does building or providing the second
97
106
  * anywhere under the first one's `Effect.provide`: nested `Layer.build`
98
107
  * and `Effect.provide` fork the ambient memo map rather than starting a
99
- * new one. Isolate each server with its own `ManagedRuntime`,
100
- * `Effect.provide(layer, { local: true })` or its own process.
108
+ * new one. The tool registry is shared the same way. Isolate each server
109
+ * by wrapping its whole bundle (its toolkit layers together with this
110
+ * layer) in `Layer.fresh`, or with its own `ManagedRuntime`,
111
+ * `Effect.provide(layer, { local: true })` or its own process. Never put
112
+ * the `Layer.fresh` boundary between a toolkit and this layer: the server
113
+ * would serve an empty registry.
101
114
  *
102
115
  * Code after a completed `Effect.provide` of a stdio server never runs:
103
116
  * core's stdio protocol interrupts the fiber that built it when its stdin
@@ -124,8 +137,11 @@ var McpStdio = class McpStdio {
124
137
  * `Effect.provideService` reaches, and Effect's default logger writes
125
138
  * through `console.log` unless `LogToStderr` is set. For an MCP server
126
139
  * that is stdout, which is the wire.
140
+ *
141
+ * `options.onReady` runs after the layer has built and before the launch
142
+ * waits forever, as {@link McpLaunchOptions.onReady} describes.
127
143
  */
128
- static launch = (layer) => Layer.launch(layer).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));
144
+ 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));
129
145
  /**
130
146
  * Success or an interrupt-only exit maps to 0. Anything else goes to
131
147
  * `Runtime.defaultTeardown`.
package/McpTestFailure.js CHANGED
@@ -18,8 +18,8 @@ import { Schema } from "effect";
18
18
  * not `catchTag`.
19
19
  * - `ServerStopped` (`McpHarness`): the in-process server stopped, usually
20
20
  * because stdin closed while the request was in flight.
21
- * - `NotInitialized` (`McpHarness`): a request other than `initialize` was
22
- * made on a stateful revision before `initialize` was sent. The request
21
+ * - `NotInitialized` (`McpHarness`): a request other than `initialize` or
22
+ * `ping` was made on a stateful revision before `initialize` was sent. The request
23
23
  * is never written; the server would only have refused it opaquely:
24
24
  * `-32602 Invalid request metadata` when a stateless adapter is listed
25
25
  * first, `-32603 Internal error` when only stateful revisions are served.
package/McpToolAudit.js CHANGED
@@ -24,10 +24,12 @@ const objectNodes = (schema) => {
24
24
  return acc;
25
25
  }, Object.create(null));
26
26
  const closed = merged.some((part) => part.additionalProperties === false);
27
+ const droppedKeyCheck = merged.some((part) => isNode(part.additionalProperties) && isNode(part.propertyNames));
27
28
  if (isNode(properties) || merged.some((part) => part.type === "object")) out.push({
28
29
  path,
29
30
  properties,
30
- closed
31
+ closed,
32
+ droppedKeyCheck
31
33
  });
32
34
  if (isNode(properties)) for (const [key, child] of Object.entries(properties)) visit(child, join(path, key));
33
35
  visit(node.items, join(path, "items"));
@@ -81,15 +83,18 @@ var McpToolAudit = class {
81
83
  };
82
84
  if (seen.has(tool.name)) report("duplicate tool name");
83
85
  seen.add(tool.name);
84
- if (policy.input !== "any") for (const { path, properties, closed } of objectNodes(tool.inputSchema)) {
86
+ if (policy.input !== "any") for (const { path, properties, closed, droppedKeyCheck } of objectNodes(tool.inputSchema)) {
85
87
  const where = path === "" ? "the root" : path;
86
- if (policy.input === "closed" && !closed) report(`input schema is open at ${where}`);
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))" : ""}`);
87
89
  if (policy.input === "open" && isNode(properties) && closed) report(`input schema is closed at ${where}`);
88
90
  }
89
91
  const title = tool.title ?? tool.annotations?.title;
90
92
  if (policy.requireTitle === true && (typeof title !== "string" || title === "")) report("no title");
91
- if (policy.requireOutputSchema === true && tool.outputSchema === void 0) report("no outputSchema");
92
- if ((policy.objectRootedOutput ?? true) && tool.outputSchema !== void 0 && tool.outputSchema.type !== "object") report(`outputSchema is not object-rooted (root type: ${String(tool.outputSchema.type ?? "none")})`);
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
+ }
93
98
  const length = (tool.description ?? "").length;
94
99
  if (policy.maxDescription !== void 0 && length > policy.maxDescription) report(`description is ${length} characters, over the ${policy.maxDescription} limit`);
95
100
  if (policy.requireHints === true) {