@effected/mcp-kit 0.0.0-stage → 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/testing.d.ts ADDED
@@ -0,0 +1,519 @@
1
+ import { Context, Duration, Effect, JsonSchema, Layer, PlatformError, Schema, Scope, Stdio } from "effect";
2
+ import { McpProtocol } from "effect/ai";
3
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
4
+ declare const McpTestFailure_base: Schema.Class<McpTestFailure, Schema.TaggedStruct<"McpTestFailure", {
5
+ /** Which failure occurred; see the list above for which client raises which. */
6
+ readonly reason: Schema.Literals<readonly ["StreamEnded", "ServerStopped", "NotJsonRpc", "NotInitialized", "ErrorResponse", "TimedOut"]>;
7
+ /** A human-readable description of what went wrong. */
8
+ readonly message: Schema.String;
9
+ }>, import("effect/Cause").YieldableError>;
10
+ /**
11
+ * Why an MCP test client could not produce what was asked of it.
12
+ *
13
+ * @remarks
14
+ * Each reason is raised by a specific client, and not every client raises
15
+ * every reason:
16
+ *
17
+ * - `StreamEnded` (`McpProcess`, `McpProbe`): the spawned child's stdout
18
+ * ended first, because the child exited. `McpProbe` folds the exit code
19
+ * and stderr into the message. `McpProcess.stderrUntil` raises it when
20
+ * stderr ends before its predicate holds.
21
+ * - `TimedOut` (`McpProcess.stderrUntil`): the predicate did not hold
22
+ * within the given timeout. The message carries the stderr seen so far.
23
+ * - `NotJsonRpc` (`McpProcess.readUntilResponse`, `McpProbe`): a spawned
24
+ * child's stdout line was not a JSON-RPC frame. `McpHarness` never raises
25
+ * it: under `strictStdout` a non-JSON-RPC line DIES the pending wait as a
26
+ * defect instead, so assert it with `Effect.exit` and `Cause.hasDies`,
27
+ * not `catchTag`.
28
+ * - `ServerStopped` (`McpHarness`): the in-process server stopped, usually
29
+ * because stdin closed while the request was in flight.
30
+ * - `NotInitialized` (`McpHarness`): a request other than `initialize` or
31
+ * `ping` was made on a stateful revision before `initialize` was sent. The request
32
+ * is never written; the server would only have refused it opaquely:
33
+ * `-32602 Invalid request metadata` when a stateless adapter is listed
34
+ * first, `-32603 Internal error` when only stateful revisions are served.
35
+ * - `ErrorResponse` (`McpHarness.listTools`): the server answered with a
36
+ * JSON-RPC error where a result was required. Every other request method
37
+ * returns the whole response, error included, as data.
38
+ *
39
+ * @public
40
+ */
41
+ export declare class McpTestFailure extends McpTestFailure_base {}
42
+ /**
43
+ * One JSON-RPC 2.0 frame as an MCP test client sees it.
44
+ *
45
+ * @public
46
+ */
47
+ interface JsonRpcMessage {
48
+ readonly jsonrpc: "2.0";
49
+ readonly id?: string | number | null | undefined;
50
+ readonly method?: string | undefined;
51
+ readonly params?: unknown;
52
+ readonly result?: unknown;
53
+ readonly error?: unknown;
54
+ }
55
+ /**
56
+ * One entry of a `tools/list` result.
57
+ *
58
+ * @public
59
+ */
60
+ interface ServedTool {
61
+ readonly name: string;
62
+ readonly title?: string | undefined;
63
+ readonly description?: string | undefined;
64
+ readonly inputSchema: JsonSchema.JsonSchema;
65
+ readonly outputSchema?: JsonSchema.JsonSchema | undefined;
66
+ readonly annotations?: {
67
+ readonly [key: string]: unknown;
68
+ } | undefined;
69
+ readonly _meta?: {
70
+ readonly [key: string]: unknown;
71
+ } | undefined;
72
+ }
73
+ /**
74
+ * One entry of a `resources/list` result.
75
+ *
76
+ * @public
77
+ */
78
+ interface ServedResource {
79
+ readonly uri: string;
80
+ readonly name: string;
81
+ readonly title?: string | undefined;
82
+ readonly description?: string | undefined;
83
+ readonly mimeType?: string | undefined;
84
+ readonly annotations?: {
85
+ readonly [key: string]: unknown;
86
+ } | undefined;
87
+ readonly size?: number | undefined;
88
+ readonly _meta?: {
89
+ readonly [key: string]: unknown;
90
+ } | undefined;
91
+ }
92
+ /**
93
+ * Options for {@link McpHarness.make}.
94
+ *
95
+ * @public
96
+ */
97
+ interface McpHarnessOptions<S = never> {
98
+ /** The revision this client speaks. Defaults to `McpProtocol.v2025_11_25`; a stateless adapter switches to `server/discover` and per-request `_meta`. */
99
+ readonly protocol?: McpProtocol.ProtocolAdapter | undefined;
100
+ /** The `clientInfo` sent in `initialize` and in stateless `_meta`. */
101
+ readonly clientInfo?: {
102
+ readonly name: string;
103
+ readonly version: string;
104
+ } | undefined;
105
+ /** Capture the server's console so logs are asserted, not printed. Defaults to `true`. */
106
+ readonly captureLogs?: boolean | undefined;
107
+ /** Treat any stdout line that is not JSON-RPC as a defect. Defaults to `true`. */
108
+ readonly strictStdout?: boolean | undefined;
109
+ /**
110
+ * A pre-built context of the services the server needs, provided to it as built.
111
+ *
112
+ * @remarks
113
+ * Build the services once, seed them from the test, and pass them here: the tools then read the instance the test
114
+ * wrote to, where providing the services' layer instead would build a second one. The harness's queue-backed
115
+ * `Stdio` always serves the server, because the services are provided outside it: a `Stdio` in the context,
116
+ * such as a platform layer's real process one, never reaches the server. The type removes `S` from the
117
+ * server's requirements.
118
+ */
119
+ readonly services?: Context.Context<S> | undefined;
120
+ }
121
+ /**
122
+ * An in-process MCP client for a real server layer, over queue-backed stdio.
123
+ *
124
+ * @remarks
125
+ * Builds your server with a queue-backed `Stdio.layerTest` that satisfies the
126
+ * server's `Stdio` requirement, so tests see the exact served schemas and wire
127
+ * results a real client would, with no child and no sockets. Pass the server
128
+ * WITHOUT a `Stdio` of its own: a `Stdio` the server provides internally wins,
129
+ * and talks to the real terminal. A bundled platform layer such as
130
+ * `NodeServices.layer` (`@effect/platform-node`) brings its own `Stdio` along
131
+ * with `FileSystem`, `Path`, `ChildProcessSpawner`, `Crypto` and `Terminal` —
132
+ * providing it to the server loses `Stdio` the same way, and every wait
133
+ * hangs to timeout with no error naming why. Compose the individual layers
134
+ * the server needs instead and leave `Stdio` out; this harness supplies it.
135
+ *
136
+ * The server is built with a fresh layer memo map, never the ambient one, so
137
+ * a harness made under another stdio server's `Effect.provide` still serves
138
+ * its own. Never pass a server layer that provides a layer the test also
139
+ * provides and then reads — `McpHarness.make(server.pipe(Layer.provide(AppLayer)))`
140
+ * under `Effect.provide(AppLayer)` builds `AppLayer` twice, so the tools write
141
+ * to one instance and the test reads the other. Leave the service in the
142
+ * server layer's requirements and provide it once from the test, or build it
143
+ * once and pass `Layer.succeed(Tag, value)`.
144
+ *
145
+ * - Responses are matched by id, so notifications may interleave freely.
146
+ * - Every response wait and `awaitOutboundMethod` fails with `ServerStopped`
147
+ * when the server stops, and dies when `strictStdout` sees a line that is
148
+ * not JSON-RPC. `stop` stops the server at once; `close` ends stdin and lets
149
+ * in-flight requests answer first, so only `stop` releases a wait on a
150
+ * request that never completes.
151
+ * - With `captureLogs`, the server's console is captured: `stderrSoFar`
152
+ * holds stderr writes and every log line, and `consoleLogSoFar` holds
153
+ * anything that went through `console.log`, which in a real server is
154
+ * the wire. Assert it empty.
155
+ * - On a stateful revision (the default `2025-11-25`), send `initialize`
156
+ * first. `ping` is the exception: the server answers it `{}` before
157
+ * initialization, so it is written. Every other request before
158
+ * `initialize` fails with `NotInitialized` and is never written: the
159
+ * server would only answer an opaque refusal,
160
+ * `-32602 Invalid request metadata` when a stateless adapter is listed
161
+ * first (as in `McpStdio.protocols`), or `-32603 Internal error` when
162
+ * only stateful revisions are served.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * import { McpHarness } from "@effected/mcp-kit/testing";
167
+ * import { Effect } from "effect";
168
+ *
169
+ * // `ServerLayer` is the server's layer, built without its own `Stdio`.
170
+ * const program = Effect.gen(function* () {
171
+ * const harness = yield* McpHarness.make(ServerLayer);
172
+ * yield* harness.initialize;
173
+ * const tools = yield* harness.listTools;
174
+ * return tools.map((tool) => tool.name);
175
+ * }).pipe(Effect.scoped);
176
+ * ```
177
+ *
178
+ * Services the test also seeds go in `options.services`, built once. End the
179
+ * session with `harness.close` (the server answers what is in flight, then
180
+ * stops) or, when a request may never complete, `harness.stop` (the server is
181
+ * interrupted at once):
182
+ *
183
+ * ```ts
184
+ * import { McpHarness } from "@effected/mcp-kit/testing";
185
+ * import { Context, Effect } from "effect";
186
+ *
187
+ * // `Database` is a service the server's tools need, and the test seeds it.
188
+ * const program = Effect.gen(function* () {
189
+ * const database = yield* makeDatabase;
190
+ * const harness = yield* McpHarness.make(ServerLayer, { services: Context.make(Database, database) });
191
+ * yield* harness.initialize;
192
+ * yield* harness.stop;
193
+ * }).pipe(Effect.scoped);
194
+ * ```
195
+ *
196
+ * @public
197
+ */
198
+ export declare class McpHarness {
199
+ /** The revision this client speaks. */
200
+ readonly protocol: McpProtocol.ProtocolAdapter;
201
+ /** `initialize` then `notifications/initialized`; `server/discover` on a stateless revision. Call it first on a stateful revision. */
202
+ readonly initialize: Effect.Effect<JsonRpcMessage, McpTestFailure>;
203
+ /**
204
+ * `initialize` asking for `protocolVersion` instead of the harness's own
205
+ * revision, then `notifications/initialized` only when the response
206
+ * carries no error. Returns the whole response: assert the negotiated
207
+ * `result.protocolVersion`, or the JSON-RPC `error`.
208
+ *
209
+ * @remarks
210
+ * It moves between stateful revisions only. A stateful server never
211
+ * refuses a version: it counter-offers its latest stateful revision, so
212
+ * asking for an unknown one, or for the stateless `2026-07-28`, comes back
213
+ * as `2025-11-25`. On a harness made with a stateless revision the server
214
+ * does not serve `initialize` at all, and the response is a JSON-RPC
215
+ * `-32601` "Method not found" error, with no `notifications/initialized`
216
+ * sent after it.
217
+ */
218
+ readonly initializeWith: (protocolVersion: string) => Effect.Effect<JsonRpcMessage, McpTestFailure>;
219
+ /** `server/discover`. */
220
+ readonly discover: Effect.Effect<JsonRpcMessage, McpTestFailure>;
221
+ /** Send a request and wait for its response. */
222
+ readonly request: (method: string, params?: unknown) => Effect.Effect<JsonRpcMessage, McpTestFailure>;
223
+ /** Send a request now; wait for its response later. Fails with `NotInitialized`, sending nothing, on a stateful revision before `initialize` (`ping` excepted). */
224
+ readonly startRequest: (method: string, params?: unknown) => Effect.Effect<{
225
+ readonly id: number;
226
+ readonly response: Effect.Effect<JsonRpcMessage, McpTestFailure>;
227
+ }, McpTestFailure>;
228
+ /** Send a notification. */
229
+ readonly notify: (method: string, params?: unknown) => Effect.Effect<void>;
230
+ /** `tools/call`, returning the whole response: a JSON-RPC error is data here. */
231
+ readonly callTool: (name: string, args?: unknown) => Effect.Effect<JsonRpcMessage, McpTestFailure>;
232
+ /** `tools/list`'s tools; a JSON-RPC error fails with `ErrorResponse`. */
233
+ readonly listTools: Effect.Effect<ReadonlyArray<ServedTool>, McpTestFailure>;
234
+ /** `resources/list`'s resources; a JSON-RPC error fails with `ErrorResponse`. */
235
+ readonly listResources: Effect.Effect<ReadonlyArray<ServedResource>, McpTestFailure>;
236
+ /** `resources/read`, returning the whole response. */
237
+ readonly readResource: (uri: string) => Effect.Effect<JsonRpcMessage, McpTestFailure>;
238
+ /** Write any value as one newline-framed line. Never gated; a raw `initialize` counts as sent. */
239
+ readonly sendRaw: (message: unknown) => Effect.Effect<void>;
240
+ /** The next server-initiated frame with this method, skipping and keeping others; fails with `ServerStopped` once the server stops. */
241
+ readonly awaitOutboundMethod: (method: string) => Effect.Effect<JsonRpcMessage, McpTestFailure>;
242
+ /** 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. */
243
+ readonly sentSoFar: Effect.Effect<ReadonlyArray<unknown>>;
244
+ /** Everything written to stderr, plus every captured log line. */
245
+ readonly stderrSoFar: Effect.Effect<string>;
246
+ /**
247
+ * Every captured `console.log`, `info` or `debug` call, its arguments joined with a space, each rendered close to
248
+ * what the console would show: a string as itself, a value with its own string form as that form (an `Error` as
249
+ * `Error: boom`, a number, a date), an array or a plain object as JSON (`{"message":"x"}`, not just `x`).
250
+ *
251
+ * @remarks
252
+ * Rendering never throws inside the server's own call. A value whose string form throws (an object with no
253
+ * prototype, a throwing `toString`) and has no JSON falls back to `ErrorText.describe` from `@effected/kit`.
254
+ */
255
+ readonly consoleLogSoFar: Effect.Effect<ReadonlyArray<string>>;
256
+ /**
257
+ * End stdin, as a client disconnecting does. The server drains first:
258
+ * requests already in flight still answer, and the server stops once they
259
+ * have, so a wait on a request that never completes keeps waiting. Use
260
+ * {@link McpHarness.stop} to end the server regardless.
261
+ */
262
+ readonly close: Effect.Effect<void>;
263
+ /**
264
+ * Interrupt the server without draining, as a client killing the process
265
+ * does: every pending response wait and `awaitOutboundMethod` fails with
266
+ * `ServerStopped`.
267
+ */
268
+ readonly stop: Effect.Effect<void>;
269
+ private constructor();
270
+ /** Build `server` over queue-backed stdio and return a client for it; fails with the layer's own error if it cannot build. */
271
+ static readonly make: <ROut, E, R, S = never>(server: Layer.Layer<ROut, E, R>, options?: McpHarnessOptions<S>) => Effect.Effect<McpHarness, E, Scope.Scope | Exclude<R, Stdio.Stdio | S>>;
272
+ }
273
+ /**
274
+ * Options for {@link McpProbe.initialize}.
275
+ *
276
+ * @public
277
+ */
278
+ interface McpProbeOptions {
279
+ /** Defaults to `McpProtocol.v2025_11_25`; a stateless adapter probes with `server/discover`. */
280
+ readonly protocol?: McpProtocol.ProtocolAdapter | undefined;
281
+ }
282
+ /**
283
+ * What {@link McpProbe.initialize} observed.
284
+ *
285
+ * @public
286
+ */
287
+ interface McpProbeResult {
288
+ /** The id-1 response. A refused handshake is still a response: assert `response.error === undefined`. */
289
+ readonly response: JsonRpcMessage;
290
+ /** Every raw stdout line, in order. */
291
+ readonly stdout: ReadonlyArray<string>;
292
+ /** Everything the child wrote to stderr; a clean boot leaves it empty. */
293
+ readonly stderr: string;
294
+ /** The child's exit code after stdin closed; a clean boot exits 0. */
295
+ readonly exitCode: number;
296
+ }
297
+ /**
298
+ * The smallest proof that an installed MCP bin boots: one initialize, a clean
299
+ * close, and exit 0.
300
+ *
301
+ * @remarks
302
+ * Stdin stays open until the id-1 response has arrived. Closing it right
303
+ * after writing proves nothing against a server that stops at stdin EOF
304
+ * without answering what is in flight: it replies nothing and exits 0, so a
305
+ * slow boot reads as a pass with no response. The caller asserts `response.error === undefined`, `stderr` is
306
+ * empty and `exitCode` is 0; this is the MCP half of a packed-install proof.
307
+ * Checking stderr and the exit code alone passes a server that answers
308
+ * the handshake with a JSON-RPC error, exits 0 and writes nothing.
309
+ *
310
+ * - Any stdout line that is not JSON-RPC fails with `NotJsonRpc`, naming the
311
+ * line: a server that logs to stdout corrupts the wire, so the probe fails
312
+ * rather than skipping the line.
313
+ * - A child that exits before responding fails with `StreamEnded`, and the
314
+ * message carries its exit code and stderr.
315
+ * - A child that ignores stdin EOF never exits, and the probe waits for it.
316
+ * Wrap the probe in `Effect.timeout`.
317
+ *
318
+ * @example
319
+ * ```ts
320
+ * import { McpProbe } from "@effected/mcp-kit/testing";
321
+ * import { Effect } from "effect";
322
+ * import { ChildProcess } from "effect/process";
323
+ *
324
+ * const program = Effect.gen(function* () {
325
+ * const probe = yield* McpProbe.initialize(ChildProcess.make(process.execPath, ["./bin/server.js"]));
326
+ * // A clean boot: no JSON-RPC error, empty stderr, exit code 0.
327
+ * return probe.response.error === undefined && probe.stderr === "" && probe.exitCode === 0;
328
+ * });
329
+ * // Requires a `ChildProcessSpawner` from the platform layer.
330
+ * ```
331
+ *
332
+ * @public
333
+ */
334
+ export declare class McpProbe {
335
+ private constructor();
336
+ /**
337
+ * Spawn `command`, send one `initialize` (or `server/discover` on a
338
+ * stateless revision) as id 1, wait for its response, close stdin, and
339
+ * collect stdout, stderr and the exit code.
340
+ */
341
+ static readonly initialize: (command: ChildProcess.Command, options?: McpProbeOptions) => Effect.Effect<McpProbeResult, McpTestFailure | PlatformError.PlatformError, ChildProcessSpawner.ChildProcessSpawner>;
342
+ }
343
+ /**
344
+ * Options for {@link McpProcess.stderrUntil}.
345
+ *
346
+ * @public
347
+ */
348
+ interface McpProcessStderrUntilOptions {
349
+ /**
350
+ * How long to wait for the predicate to hold before failing with
351
+ * `TimedOut`. Real time: under `it.effect`'s `TestClock` it never fires,
352
+ * so a test that waits on stderr runs under `it.live`. Keep it below the
353
+ * test runner's own timeout, or the runner kills the test first.
354
+ */
355
+ readonly timeout: Duration.Input;
356
+ }
357
+ /**
358
+ * A spawned MCP server bin a test writes to while it runs.
359
+ *
360
+ * @remarks
361
+ * Build the command in the test file with `execPath` and an explicit `env`:
362
+ * `ChildProcess.make(process.execPath, [bin], { env })`.
363
+ *
364
+ * - `nextLine` fails with `StreamEnded` when stdout ends, so a child that
365
+ * exits early fails the test instead of hanging it.
366
+ * - `send` writes one JSON-encoded line; `sendRaw` writes a string or bytes
367
+ * exactly as given, for a malformed frame or one frame split across
368
+ * writes, even mid-character.
369
+ * - `readUntilResponse` reads past interleaved notifications, such as
370
+ * `list_changed`, to the matching id and returns what it saw.
371
+ * - `closeStdin` is `Queue.end`, never `Queue.shutdown`, so every frame
372
+ * already sent is delivered before stdin closes.
373
+ * - A request in flight when stdin closes still answers from an Effect
374
+ * server, which drains before it exits; a server that does not drain
375
+ * drops it, so a portable client reads the response first.
376
+ *
377
+ * @example
378
+ * ```ts
379
+ * import { McpProcess } from "@effected/mcp-kit/testing";
380
+ * import { Effect } from "effect";
381
+ * import { ChildProcess } from "effect/process";
382
+ *
383
+ * const program = Effect.gen(function* () {
384
+ * const child = yield* McpProcess.spawn(ChildProcess.make(process.execPath, ["./bin/server.js"]));
385
+ * yield* child.handshake();
386
+ * yield* child.send({ jsonrpc: "2.0", id: 2, method: "tools/list" });
387
+ * const { response } = yield* child.readUntilResponse(2);
388
+ * yield* child.closeStdin;
389
+ * return response;
390
+ * }).pipe(Effect.scoped);
391
+ * // Requires a `ChildProcessSpawner` from the platform layer.
392
+ * ```
393
+ *
394
+ * @public
395
+ */
396
+ export declare class McpProcess {
397
+ /**
398
+ * Write one JSON-encoded, newline-framed message to the child's stdin.
399
+ *
400
+ * @remarks
401
+ * Never fails. A frame sent after `closeStdin` is dropped, and a write the
402
+ * child can no longer receive fails the stdin pump instead; the next
403
+ * `StreamEnded` message names that failure.
404
+ */
405
+ readonly send: (message: unknown) => Effect.Effect<void>;
406
+ /**
407
+ * Write `text` to the child's stdin exactly as given: a string as UTF-8,
408
+ * bytes unchanged, with no JSON encoding and no newline added.
409
+ *
410
+ * @remarks
411
+ * For frames `send` cannot produce: a line that is not JSON, a blank line,
412
+ * or one frame split across several writes, including mid-character by
413
+ * passing bytes. Include the `\n` yourself.
414
+ * Never fails, on the same terms as `send`.
415
+ */
416
+ readonly sendRaw: (text: string | Uint8Array) => Effect.Effect<void>;
417
+ /** The next non-empty stdout line; fails with `StreamEnded` once stdout ends. */
418
+ readonly nextLine: Effect.Effect<string, McpTestFailure>;
419
+ /** Read JSON-RPC lines up to and including the response with this id. */
420
+ readonly readUntilResponse: (id: string | number) => Effect.Effect<{
421
+ readonly response: JsonRpcMessage;
422
+ readonly seen: ReadonlyArray<JsonRpcMessage>;
423
+ }, McpTestFailure>;
424
+ /** `initialize` (id 1) then `notifications/initialized`, or `server/discover` (id 1) on a stateless revision. Use ids of 2 or more for your own requests. */
425
+ readonly handshake: (protocol?: McpProtocol.ProtocolAdapter) => Effect.Effect<JsonRpcMessage, McpTestFailure>;
426
+ /** End stdin after every frame already sent. */
427
+ readonly closeStdin: Effect.Effect<void>;
428
+ /** Wait for the child to exit. */
429
+ readonly exitCode: Effect.Effect<number, PlatformError.PlatformError>;
430
+ /** Everything written to stderr so far. */
431
+ readonly stderrSoFar: Effect.Effect<string>;
432
+ /**
433
+ * Wait until everything written to stderr so far satisfies `predicate`,
434
+ * and return that text.
435
+ *
436
+ * @remarks
437
+ * Event-driven: the predicate is checked against the current text at once,
438
+ * then again on every chunk the child writes, never on a polling timer.
439
+ * Fails with `StreamEnded` when stderr ends (the child exited) without the
440
+ * predicate holding, and with `TimedOut` once `options.timeout` passes;
441
+ * both messages carry the stderr seen so far. Reach for it whenever a
442
+ * report lands on a later tick than the responses a test reads, such as
443
+ * `McpGuard`'s `injectCrash: { at: "connected" }` report, instead of
444
+ * reading `stderrSoFar` once. `options.timeout` is real time: run the test
445
+ * under `it.live`.
446
+ */
447
+ readonly stderrUntil: (predicate: (stderr: string) => boolean, options: McpProcessStderrUntilOptions) => Effect.Effect<string, McpTestFailure>;
448
+ /** Everything written to stderr, once stderr has ended. Waits for the child to exit. */
449
+ readonly stderrFinal: Effect.Effect<string>;
450
+ private constructor();
451
+ /** Spawn `command` for the life of the current scope. */
452
+ static readonly spawn: (command: ChildProcess.Command) => Effect.Effect<McpProcess, PlatformError.PlatformError, ChildProcessSpawner.ChildProcessSpawner | Scope.Scope>;
453
+ }
454
+ /**
455
+ * The policy {@link McpToolAudit.check} enforces.
456
+ *
457
+ * @public
458
+ */
459
+ interface McpToolAuditPolicy {
460
+ /** `"closed"`: every object node rejects unknown keys. `"open"`: none with properties does. `"any"`: not checked. */
461
+ readonly input: "open" | "closed" | "any";
462
+ /** Every tool must carry a non-empty `title`, top-level or in `annotations.title`. */
463
+ readonly requireTitle?: boolean | undefined;
464
+ /**
465
+ * Every tool must serve an `outputSchema`. The stateful revisions drop a
466
+ * non-object one, so check the revision you serve; the violation names
467
+ * the likely cause, a union success schema, and `ToolOutputSchema.objectRooted`.
468
+ */
469
+ readonly requireOutputSchema?: boolean | undefined;
470
+ /**
471
+ * A served `outputSchema` must be rooted at `type: "object"`. Defaults to
472
+ * `true`. Only the stateless revision serves a non-object root, so
473
+ * only there does this fire; a union root (`anyOf` or `oneOf`) names
474
+ * `ToolOutputSchema.objectRooted` as the fix for a union whose members are
475
+ * all objects (a union with a primitive member needs an object envelope
476
+ * instead). On a stateful revision the
477
+ * same schema is dropped, and `requireOutputSchema` reports it instead.
478
+ */
479
+ readonly objectRootedOutput?: boolean | undefined;
480
+ /** The longest `description` allowed, in UTF-16 code units; a missing description counts as 0. */
481
+ readonly maxDescription?: number | undefined;
482
+ /** All four MCP annotation hints must be present as booleans. */
483
+ readonly requireHints?: boolean | undefined;
484
+ }
485
+ /**
486
+ * A pure sweep over a served `tools/list` that returns every policy
487
+ * violation, as `"<tool>: <what>"` strings.
488
+ *
489
+ * @remarks
490
+ * Run it against what a real server serves (`McpHarness.listTools`), not
491
+ * against your own schemas: the served document is what a client sees, and
492
+ * it differs by protocol revision. The stateless `2026-07-28` adapter passes a
493
+ * non-object output through, while the stateful adapters drop it — which is
494
+ * why `objectRootedOutput` defaults to `true`.
495
+ *
496
+ * `"closed"` is only as sound as the walk. It follows `properties`,
497
+ * `items`, a schema-valued `additionalProperties`, `prefixItems`, `anyOf`,
498
+ * `oneOf` and `$defs`, and merges an `allOf` member's `properties` and
499
+ * `additionalProperties: false` onto its node. It does not visit
500
+ * `patternProperties` values, an `allOf` member's `items` or
501
+ * `additionalProperties` schema, or an `allOf` nested inside another; and
502
+ * a key declared in two `allOf` members is last-write-wins. A hand-authored
503
+ * or `Tool.dynamic` schema can therefore pass `"closed"` with an open node
504
+ * the walk never reached. Core-emitted strict schemas close every node, so
505
+ * a strict `Tool.make` tool is reported faithfully.
506
+ *
507
+ * @public
508
+ */
509
+ export declare class McpToolAudit {
510
+ private constructor();
511
+ /**
512
+ * Every violation of `policy` across `tools`, as `"<tool>: <what>"`, in
513
+ * tool order. Empty means the sweep passed. A duplicate tool name is
514
+ * reported under every policy, `input: "any"` included.
515
+ */
516
+ static readonly check: (tools: ReadonlyArray<ServedTool>, policy: McpToolAuditPolicy) => ReadonlyArray<string>;
517
+ }
518
+ export type { JsonRpcMessage, McpHarnessOptions, McpProbeOptions, McpProbeResult, McpProcessStderrUntilOptions, McpToolAuditPolicy, ServedResource, ServedTool };
519
+ //# sourceMappingURL=testing.d.ts.map
package/testing.js ADDED
@@ -0,0 +1,7 @@
1
+ import { McpHarness } from "./McpHarness.js";
2
+ import { McpProbe } from "./McpProbe.js";
3
+ import { McpProcess } from "./McpProcess.js";
4
+ import { McpTestFailure } from "./McpTestFailure.js";
5
+ import { McpToolAudit } from "./McpToolAudit.js";
6
+
7
+ export { McpHarness, McpProbe, McpProcess, McpTestFailure, McpToolAudit };
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.60.1"
9
+ }
10
+ ]
11
+ }