@effected/mcp 0.3.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 CHANGED
@@ -46,6 +46,20 @@ import { McpProtocol } from "effect/ai";
46
46
  * first (as in `McpStdio.protocols`), or `-32603 Internal error` when
47
47
  * only stateful revisions are served.
48
48
  *
49
+ * @example
50
+ * ```ts
51
+ * import { McpHarness } from "@effected/mcp/testing";
52
+ * import { Effect } from "effect";
53
+ *
54
+ * // `ServerLayer` is the server's layer, built without its own `Stdio`.
55
+ * const program = Effect.gen(function* () {
56
+ * const harness = yield* McpHarness.make(ServerLayer);
57
+ * yield* harness.initialize;
58
+ * const tools = yield* harness.listTools;
59
+ * return tools.map((tool) => tool.name);
60
+ * }).pipe(Effect.scoped);
61
+ * ```
62
+ *
49
63
  * @public
50
64
  */
51
65
  var McpHarness = class McpHarness {
package/McpProbe.js CHANGED
@@ -12,8 +12,7 @@ import { McpProtocol } from "effect/ai";
12
12
  *
13
13
  * @remarks
14
14
  * Stdin stays open until the id-1 response has arrived. Closing it right
15
- * after writing, as every hand-rolled smoke test did, makes an Effect server
16
- * drop the in-flight response and exit 0, so a slow boot reads as a pass with
15
+ * after writing makes an Effect server drop the in-flight response and exit 0, so a slow boot reads as a pass with
17
16
  * no response. The caller asserts `response.error === undefined`, `stderr` is
18
17
  * empty and `exitCode` is 0; this is the MCP half of a packed-install proof.
19
18
  * Checking stderr and the exit code alone passes a server that answers
@@ -27,6 +26,20 @@ import { McpProtocol } from "effect/ai";
27
26
  * - A child that ignores stdin EOF never exits, and the probe waits for it.
28
27
  * Wrap the probe in `Effect.timeout`.
29
28
  *
29
+ * @example
30
+ * ```ts
31
+ * import { McpProbe } from "@effected/mcp/testing";
32
+ * import { Effect } from "effect";
33
+ * import { ChildProcess } from "effect/process";
34
+ *
35
+ * const program = Effect.gen(function* () {
36
+ * const probe = yield* McpProbe.initialize(ChildProcess.make(process.execPath, ["./bin/server.js"]));
37
+ * // A clean boot: no JSON-RPC error, empty stderr, exit code 0.
38
+ * return probe.response.error === undefined && probe.stderr === "" && probe.exitCode === 0;
39
+ * });
40
+ * // Requires a `ChildProcessSpawner` from the platform layer.
41
+ * ```
42
+ *
30
43
  * @public
31
44
  */
32
45
  var McpProbe = class {
package/McpProcess.js CHANGED
@@ -25,6 +25,23 @@ import { ChildProcessSpawner } from "effect/process";
25
25
  * - A request in flight when stdin closes is dropped by an Effect server:
26
26
  * read its response first.
27
27
  *
28
+ * @example
29
+ * ```ts
30
+ * import { McpProcess } from "@effected/mcp/testing";
31
+ * import { Effect } from "effect";
32
+ * import { ChildProcess } from "effect/process";
33
+ *
34
+ * const program = Effect.gen(function* () {
35
+ * const child = yield* McpProcess.spawn(ChildProcess.make(process.execPath, ["./bin/server.js"]));
36
+ * yield* child.handshake();
37
+ * yield* child.send({ jsonrpc: "2.0", id: 2, method: "tools/list" });
38
+ * const { response } = yield* child.readUntilResponse(2);
39
+ * yield* child.closeStdin;
40
+ * return response;
41
+ * }).pipe(Effect.scoped);
42
+ * // Requires a `ChildProcessSpawner` from the platform layer.
43
+ * ```
44
+ *
28
45
  * @public
29
46
  */
30
47
  var McpProcess = class McpProcess {
package/McpStdio.js CHANGED
@@ -39,10 +39,9 @@ var McpStdio = class McpStdio {
39
39
  * first, then the two newest stateful ones.
40
40
  *
41
41
  * @remarks
42
- * Real clients open with either handshake: Claude Code 2.1.281 was
43
- * measured opening a stdio server with the stateless `server/discover`,
44
- * while most other shipping clients still send `initialize`. The order
45
- * is load-bearing:
42
+ * Real clients open with either handshake: some (Claude Code, for one)
43
+ * open a stdio server with the stateless `server/discover`, while others
44
+ * still send `initialize`. The order is load-bearing:
46
45
  *
47
46
  * - Stateless first: a request with no session and no `_meta` falls to
48
47
  * `protocols[0]`.
package/McpTestFailure.js CHANGED
@@ -30,6 +30,7 @@ import { Schema } from "effect";
30
30
  * @public
31
31
  */
32
32
  var McpTestFailure = class extends Schema.TaggedError()("McpTestFailure", {
33
+ /** Which failure occurred; see the list above for which client raises which. */
33
34
  reason: Schema.Literals([
34
35
  "StreamEnded",
35
36
  "ServerStopped",
@@ -37,6 +38,7 @@ var McpTestFailure = class extends Schema.TaggedError()("McpTestFailure", {
37
38
  "NotInitialized",
38
39
  "ErrorResponse"
39
40
  ]),
41
+ /** A human-readable description of what went wrong. */
40
42
  message: Schema.String
41
43
  }) {};
42
44
 
package/McpToolkit.js CHANGED
@@ -185,6 +185,11 @@ var McpToolkit = class {
185
185
  *
186
186
  * @example
187
187
  * ```ts
188
+ * import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
189
+ * import { Schema } from "effect";
190
+ * import { Tool } from "effect/ai";
191
+ *
192
+ * // `AddNote`, `ListNotes`, `Added`, `Listed` are object schemas; `Kit` is a Toolkit that includes `Note`.
188
193
  * const Note = McpToolkit.unionTool("note", {
189
194
  * parameters: Schema.Union([AddNote, ListNotes]),
190
195
  * success: ToolOutputSchema.objectRooted(Schema.Union([Added, Listed])),
package/README.md CHANGED
@@ -7,10 +7,10 @@ The boundary layer of an `effect/ai` MCP server: stdio wiring that keeps stdout
7
7
  [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)
8
8
  [![TypeScript 7.0](https://img.shields.io/badge/TypeScript-7.0-3178c6.svg)](https://www.typescriptlang.org/)
9
9
 
10
- > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
- > development against a single pinned Effect v4 prerelease. Packages graduate to
12
- > `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
13
- > exactly the ones the kit is built and tested against, install
10
+ > **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
11
+ > Effect v4 (`effect` `^4.0.0`) and still in `0.x` development. Stable Effect
12
+ > makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
13
+ > `@effect/*` versions on the line the kit is built and tested against, install
14
14
  > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
15
15
  >
16
16
  > **Stability: unstable.** This package's API surface is not yet considered
package/ToolFailure.js CHANGED
@@ -26,6 +26,9 @@ const isHighSurrogate = (code) => code >= 55296 && code <= 56319;
26
26
  *
27
27
  * @example
28
28
  * ```ts
29
+ * import { ToolFailure } from "@effected/mcp";
30
+ * import { Schema } from "effect";
31
+ *
29
32
  * class NotFound extends Schema.TaggedError<NotFound>()("NotFound", { ...ToolFailure.fields, id: Schema.String }) {}
30
33
  * const remediation = { hint: "List the ids first.", suggestedTool: "list_things" }
31
34
  * new NotFound({ id, remediation, message: ToolFailure.message(`No thing "${ToolFailure.truncate(id)}".`, remediation) })
package/ToolRefusal.js CHANGED
@@ -14,11 +14,14 @@ import { Schema } from "effect";
14
14
  * {@link ToolRefusal.refuse}; its message already carries the remediation,
15
15
  * because a declared `Error` failure sends `message` and nothing else.
16
16
  *
17
- * Audit every throw path when porting a tool: any failure left undeclared
18
- * loses its message on the wire, however carefully it was worded.
17
+ * Audit every throw path of a tool: any failure left undeclared loses its
18
+ * message on the wire, however carefully it was worded.
19
19
  *
20
20
  * @example
21
21
  * ```ts
22
+ * import { ToolFailure, ToolRefusal } from "@effected/mcp";
23
+ * import { Tool } from "effect/ai";
24
+ *
22
25
  * const Lookup = Tool.make("lookup", { parameters, success, failure: ToolRefusal })
23
26
  * // in the handler:
24
27
  * return yield* ToolRefusal.refuse(`No run "${ToolFailure.truncate(id)}".`, { hint: "List runs first.", suggestedTool: "list_runs" })
package/index.d.ts CHANGED
@@ -74,10 +74,9 @@ export declare class McpStdio {
74
74
  * first, then the two newest stateful ones.
75
75
  *
76
76
  * @remarks
77
- * Real clients open with either handshake: Claude Code 2.1.281 was
78
- * measured opening a stdio server with the stateless `server/discover`,
79
- * while most other shipping clients still send `initialize`. The order
80
- * is load-bearing:
77
+ * Real clients open with either handshake: some (Claude Code, for one)
78
+ * open a stdio server with the stateless `server/discover`, while others
79
+ * still send `initialize`. The order is load-bearing:
81
80
  *
82
81
  * - Stateless first: a request with no session and no `_meta` falls to
83
82
  * `protocols[0]`.
@@ -429,6 +428,11 @@ export declare class McpToolkit {
429
428
  *
430
429
  * @example
431
430
  * ```ts
431
+ * import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
432
+ * import { Schema } from "effect";
433
+ * import { Tool } from "effect/ai";
434
+ *
435
+ * // `AddNote`, `ListNotes`, `Added`, `Listed` are object schemas; `Kit` is a Toolkit that includes `Note`.
432
436
  * const Note = McpToolkit.unionTool("note", {
433
437
  * parameters: Schema.Union([AddNote, ListNotes]),
434
438
  * success: ToolOutputSchema.objectRooted(Schema.Union([Added, Listed])),
@@ -488,6 +492,9 @@ export declare class McpToolkit {
488
492
  *
489
493
  * @example
490
494
  * ```ts
495
+ * import { ToolFailure } from "@effected/mcp";
496
+ * import { Schema } from "effect";
497
+ *
491
498
  * class NotFound extends Schema.TaggedError<NotFound>()("NotFound", { ...ToolFailure.fields, id: Schema.String }) {}
492
499
  * const remediation = { hint: "List the ids first.", suggestedTool: "list_things" }
493
500
  * new NotFound({ id, remediation, message: ToolFailure.message(`No thing "${ToolFailure.truncate(id)}".`, remediation) })
@@ -594,11 +601,14 @@ declare const ToolRefusal_base: Schema.Class<ToolRefusal, Schema.TaggedStruct<"T
594
601
  * {@link ToolRefusal.refuse}; its message already carries the remediation,
595
602
  * because a declared `Error` failure sends `message` and nothing else.
596
603
  *
597
- * Audit every throw path when porting a tool: any failure left undeclared
598
- * loses its message on the wire, however carefully it was worded.
604
+ * Audit every throw path of a tool: any failure left undeclared loses its
605
+ * message on the wire, however carefully it was worded.
599
606
  *
600
607
  * @example
601
608
  * ```ts
609
+ * import { ToolFailure, ToolRefusal } from "@effected/mcp";
610
+ * import { Tool } from "effect/ai";
611
+ *
602
612
  * const Lookup = Tool.make("lookup", { parameters, success, failure: ToolRefusal })
603
613
  * // in the handler:
604
614
  * return yield* ToolRefusal.refuse(`No run "${ToolFailure.truncate(id)}".`, { hint: "List runs first.", suggestedTool: "list_runs" })
@@ -7,15 +7,27 @@ import { Runtime } from "effect";
7
7
  * second time — through a logger that writes to stdout — and the original exit
8
8
  * code rides along for the teardown.
9
9
  *
10
+ * @remarks
11
+ * Both runtime markers are prototype getters rather than own fields, so a JSON
12
+ * or logger dump of the error does not carry them; the code lives in a private
13
+ * field.
14
+ *
10
15
  * @internal
11
16
  */
12
17
  var LaunchFailed = class extends Error {
13
- [Runtime.errorReported] = false;
14
- [Runtime.errorExitCode];
18
+ #code;
15
19
  constructor(code) {
16
20
  super(`the MCP server failed to launch (exit ${code})`);
17
21
  this.name = "LaunchFailed";
18
- this[Runtime.errorExitCode] = code;
22
+ this.#code = code;
23
+ }
24
+ /** Already reported on stderr: `runMain` must not report it again. */
25
+ get [Runtime.errorReported]() {
26
+ return false;
27
+ }
28
+ /** The exit code the launch failed with. */
29
+ get [Runtime.errorExitCode]() {
30
+ return this.#code;
19
31
  }
20
32
  };
21
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "The boundary layer of an effect/ai MCP server: stdio wiring that keeps stdout the wire, tool-failure shaping, strict-input walkers, and test clients",
6
6
  "keywords": [
@@ -47,8 +47,8 @@
47
47
  "./package.json": "./package.json"
48
48
  },
49
49
  "peerDependencies": {
50
- "@effected/engine": "^0.2.0",
51
- "effect": "4.0.0-rc.118"
50
+ "@effected/engine": "^0.3.0",
51
+ "effect": "^4.0.0"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=24.11.0"
package/testing.d.ts CHANGED
@@ -3,7 +3,9 @@ import { McpProtocol } from "effect/ai";
3
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
  /**
@@ -148,6 +150,20 @@ interface McpHarnessOptions {
148
150
  * first (as in `McpStdio.protocols`), or `-32603 Internal error` when
149
151
  * only stateful revisions are served.
150
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
+ * ```
166
+ *
151
167
  * @public
152
168
  */
153
169
  export declare class McpHarness {
@@ -238,8 +254,7 @@ interface McpProbeResult {
238
254
  *
239
255
  * @remarks
240
256
  * Stdin stays open until the id-1 response has arrived. Closing it right
241
- * after writing, as every hand-rolled smoke test did, makes an Effect server
242
- * 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
243
258
  * no response. The caller asserts `response.error === undefined`, `stderr` is
244
259
  * empty and `exitCode` is 0; this is the MCP half of a packed-install proof.
245
260
  * Checking stderr and the exit code alone passes a server that answers
@@ -253,6 +268,20 @@ interface McpProbeResult {
253
268
  * - A child that ignores stdin EOF never exits, and the probe waits for it.
254
269
  * Wrap the probe in `Effect.timeout`.
255
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
+ *
256
285
  * @public
257
286
  */
258
287
  export declare class McpProbe {
@@ -285,6 +314,23 @@ export declare class McpProbe {
285
314
  * - A request in flight when stdin closes is dropped by an Effect server:
286
315
  * read its response first.
287
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
+ *
288
334
  * @public
289
335
  */
290
336
  export declare class McpProcess {
@@ -349,7 +395,7 @@ interface McpToolAuditPolicy {
349
395
  readonly requireOutputSchema?: boolean | undefined;
350
396
  /**
351
397
  * A served `outputSchema` must be rooted at `type: "object"`. Defaults to
352
- * `true` (D10). Only the stateless revision serves a non-object root, so
398
+ * `true`. Only the stateless revision serves a non-object root, so
353
399
  * only there does this fire; a union root (`anyOf` or `oneOf`) names
354
400
  * `ToolOutputSchema.objectRooted` as the fix for a union whose members are
355
401
  * all objects (a union with a primitive member needs an object envelope