@effected/mcp 0.2.0 → 0.4.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/McpHarness.js +24 -8
- package/McpProbe.js +16 -3
- package/McpProcess.js +19 -2
- package/McpStdio.js +30 -22
- package/McpTestFailure.js +4 -2
- package/McpToolAudit.js +5 -3
- package/McpToolkit.js +125 -76
- package/README.md +79 -35
- package/ToolFailure.js +4 -1
- package/ToolInputSchema.js +28 -14
- package/ToolRefusal.js +5 -2
- package/index.d.ts +125 -84
- package/internal/LaunchFailed.js +15 -3
- package/internal/StdinFrames.js +61 -23
- package/package.json +4 -4
- package/testing.d.ts +61 -13
package/testing.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Effect, JsonSchema, Layer, PlatformError, Schema, Scope, Stdio } from "effect";
|
|
2
|
-
import { McpProtocol } from "effect/
|
|
3
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
2
|
+
import { McpProtocol } from "effect/ai";
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
4
|
//#region src/McpTestFailure.d.ts
|
|
5
5
|
declare const McpTestFailure_base: Schema.Class<McpTestFailure, Schema.TaggedStruct<"McpTestFailure", {
|
|
6
|
+
/** Which failure occurred; see the list above for which client raises which. */
|
|
6
7
|
readonly reason: Schema.Literals<readonly ["StreamEnded", "ServerStopped", "NotJsonRpc", "NotInitialized", "ErrorResponse"]>;
|
|
8
|
+
/** A human-readable description of what went wrong. */
|
|
7
9
|
readonly message: Schema.String;
|
|
8
10
|
}>, import("effect/Cause").YieldableError>;
|
|
9
11
|
/**
|
|
@@ -23,8 +25,8 @@ declare const McpTestFailure_base: Schema.Class<McpTestFailure, Schema.TaggedStr
|
|
|
23
25
|
* not `catchTag`.
|
|
24
26
|
* - `ServerStopped` (`McpHarness`): the in-process server stopped, usually
|
|
25
27
|
* because stdin closed while the request was in flight.
|
|
26
|
-
* - `NotInitialized` (`McpHarness`): a request other than `initialize`
|
|
27
|
-
* made on a stateful revision before `initialize` was sent. The request
|
|
28
|
+
* - `NotInitialized` (`McpHarness`): a request other than `initialize` or
|
|
29
|
+
* `ping` was made on a stateful revision before `initialize` was sent. The request
|
|
28
30
|
* is never written; the server would only have refused it opaquely:
|
|
29
31
|
* `-32602 Invalid request metadata` when a stateless adapter is listed
|
|
30
32
|
* first, `-32603 Internal error` when only stateful revisions are served.
|
|
@@ -140,11 +142,27 @@ interface McpHarnessOptions {
|
|
|
140
142
|
* anything that went through `console.log`, which in a real server is
|
|
141
143
|
* the wire. Assert it empty.
|
|
142
144
|
* - On a stateful revision (the default `2025-11-25`), send `initialize`
|
|
143
|
-
* first.
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* `-
|
|
145
|
+
* first. `ping` is the exception: the server answers it `{}` before
|
|
146
|
+
* initialization, so it is written. Every other request before
|
|
147
|
+
* `initialize` fails with `NotInitialized` and is never written: the
|
|
148
|
+
* server would only answer an opaque refusal,
|
|
149
|
+
* `-32602 Invalid request metadata` when a stateless adapter is listed
|
|
150
|
+
* first (as in `McpStdio.protocols`), or `-32603 Internal error` when
|
|
151
|
+
* only stateful revisions are served.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* import { McpHarness } from "@effected/mcp/testing";
|
|
156
|
+
* import { Effect } from "effect";
|
|
157
|
+
*
|
|
158
|
+
* // `ServerLayer` is the server's layer, built without its own `Stdio`.
|
|
159
|
+
* const program = Effect.gen(function* () {
|
|
160
|
+
* const harness = yield* McpHarness.make(ServerLayer);
|
|
161
|
+
* yield* harness.initialize;
|
|
162
|
+
* const tools = yield* harness.listTools;
|
|
163
|
+
* return tools.map((tool) => tool.name);
|
|
164
|
+
* }).pipe(Effect.scoped);
|
|
165
|
+
* ```
|
|
148
166
|
*
|
|
149
167
|
* @public
|
|
150
168
|
*/
|
|
@@ -173,7 +191,7 @@ export declare class McpHarness {
|
|
|
173
191
|
readonly discover: Effect.Effect<JsonRpcMessage, McpTestFailure>;
|
|
174
192
|
/** Send a request and wait for its response. */
|
|
175
193
|
readonly request: (method: string, params?: unknown) => Effect.Effect<JsonRpcMessage, McpTestFailure>;
|
|
176
|
-
/** Send a request now; wait for its response later. Fails with `NotInitialized`, sending nothing, on a stateful revision before `initialize
|
|
194
|
+
/** Send a request now; wait for its response later. Fails with `NotInitialized`, sending nothing, on a stateful revision before `initialize` (`ping` excepted). */
|
|
177
195
|
readonly startRequest: (method: string, params?: unknown) => Effect.Effect<{
|
|
178
196
|
readonly id: number;
|
|
179
197
|
readonly response: Effect.Effect<JsonRpcMessage, McpTestFailure>;
|
|
@@ -236,8 +254,7 @@ interface McpProbeResult {
|
|
|
236
254
|
*
|
|
237
255
|
* @remarks
|
|
238
256
|
* Stdin stays open until the id-1 response has arrived. Closing it right
|
|
239
|
-
* after writing
|
|
240
|
-
* drop the in-flight response and exit 0, so a slow boot reads as a pass with
|
|
257
|
+
* after writing makes an Effect server drop the in-flight response and exit 0, so a slow boot reads as a pass with
|
|
241
258
|
* no response. The caller asserts `response.error === undefined`, `stderr` is
|
|
242
259
|
* empty and `exitCode` is 0; this is the MCP half of a packed-install proof.
|
|
243
260
|
* Checking stderr and the exit code alone passes a server that answers
|
|
@@ -251,6 +268,20 @@ interface McpProbeResult {
|
|
|
251
268
|
* - A child that ignores stdin EOF never exits, and the probe waits for it.
|
|
252
269
|
* Wrap the probe in `Effect.timeout`.
|
|
253
270
|
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* import { McpProbe } from "@effected/mcp/testing";
|
|
274
|
+
* import { Effect } from "effect";
|
|
275
|
+
* import { ChildProcess } from "effect/process";
|
|
276
|
+
*
|
|
277
|
+
* const program = Effect.gen(function* () {
|
|
278
|
+
* const probe = yield* McpProbe.initialize(ChildProcess.make(process.execPath, ["./bin/server.js"]));
|
|
279
|
+
* // A clean boot: no JSON-RPC error, empty stderr, exit code 0.
|
|
280
|
+
* return probe.response.error === undefined && probe.stderr === "" && probe.exitCode === 0;
|
|
281
|
+
* });
|
|
282
|
+
* // Requires a `ChildProcessSpawner` from the platform layer.
|
|
283
|
+
* ```
|
|
284
|
+
*
|
|
254
285
|
* @public
|
|
255
286
|
*/
|
|
256
287
|
export declare class McpProbe {
|
|
@@ -283,6 +314,23 @@ export declare class McpProbe {
|
|
|
283
314
|
* - A request in flight when stdin closes is dropped by an Effect server:
|
|
284
315
|
* read its response first.
|
|
285
316
|
*
|
|
317
|
+
* @example
|
|
318
|
+
* ```ts
|
|
319
|
+
* import { McpProcess } from "@effected/mcp/testing";
|
|
320
|
+
* import { Effect } from "effect";
|
|
321
|
+
* import { ChildProcess } from "effect/process";
|
|
322
|
+
*
|
|
323
|
+
* const program = Effect.gen(function* () {
|
|
324
|
+
* const child = yield* McpProcess.spawn(ChildProcess.make(process.execPath, ["./bin/server.js"]));
|
|
325
|
+
* yield* child.handshake();
|
|
326
|
+
* yield* child.send({ jsonrpc: "2.0", id: 2, method: "tools/list" });
|
|
327
|
+
* const { response } = yield* child.readUntilResponse(2);
|
|
328
|
+
* yield* child.closeStdin;
|
|
329
|
+
* return response;
|
|
330
|
+
* }).pipe(Effect.scoped);
|
|
331
|
+
* // Requires a `ChildProcessSpawner` from the platform layer.
|
|
332
|
+
* ```
|
|
333
|
+
*
|
|
286
334
|
* @public
|
|
287
335
|
*/
|
|
288
336
|
export declare class McpProcess {
|
|
@@ -347,7 +395,7 @@ interface McpToolAuditPolicy {
|
|
|
347
395
|
readonly requireOutputSchema?: boolean | undefined;
|
|
348
396
|
/**
|
|
349
397
|
* A served `outputSchema` must be rooted at `type: "object"`. Defaults to
|
|
350
|
-
* `true
|
|
398
|
+
* `true`. Only the stateless revision serves a non-object root, so
|
|
351
399
|
* only there does this fire; a union root (`anyOf` or `oneOf`) names
|
|
352
400
|
* `ToolOutputSchema.objectRooted` as the fix for a union whose members are
|
|
353
401
|
* all objects (a union with a primitive member needs an object envelope
|