@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/LICENSE +21 -0
- package/McpGuard.js +81 -0
- package/McpHarness.js +357 -0
- package/McpProbe.js +94 -0
- package/McpProcess.js +196 -0
- package/McpStdio.js +175 -0
- package/McpTestFailure.js +49 -0
- package/McpToolAudit.js +109 -0
- package/McpToolkit.js +298 -0
- package/README.md +574 -2
- package/StdioCapture.js +146 -0
- package/ToolFailure.js +75 -0
- package/ToolInputSchema.js +180 -0
- package/ToolOutcome.js +119 -0
- package/ToolOutputSchema.js +65 -0
- package/ToolRefusal.js +57 -0
- package/ToolReplay.js +136 -0
- package/guard.d.ts +189 -0
- package/guard.js +3 -0
- package/index.d.ts +1048 -0
- package/index.js +10 -0
- package/internal/LaunchFailed.js +34 -0
- package/internal/StdinFrames.js +201 -0
- package/internal/guardLaunch.js +13 -0
- package/internal/wire.js +65 -0
- package/node.d.ts +126 -0
- package/node.js +3 -0
- package/package.json +60 -5
- package/testing.d.ts +519 -0
- package/testing.js +7 -0
- package/tsdoc-metadata.json +11 -0
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
|
+
}
|