@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 +14 -0
- package/McpProbe.js +15 -2
- package/McpProcess.js +17 -0
- package/McpStdio.js +3 -4
- package/McpTestFailure.js +2 -0
- package/McpToolkit.js +5 -0
- package/README.md +4 -4
- package/ToolFailure.js +3 -0
- package/ToolRefusal.js +5 -2
- package/index.d.ts +16 -6
- package/internal/LaunchFailed.js +15 -3
- package/package.json +3 -3
- package/testing.d.ts +49 -3
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
|
|
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
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
[](https://nodejs.org/)
|
|
8
8
|
[](https://www.typescriptlang.org/)
|
|
9
9
|
|
|
10
|
-
> **Pre
|
|
11
|
-
>
|
|
12
|
-
> `1.0.0`
|
|
13
|
-
>
|
|
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
|
|
18
|
-
*
|
|
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
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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
|
|
598
|
-
*
|
|
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" })
|
package/internal/LaunchFailed.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
+
"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.
|
|
51
|
-
"effect": "4.0.0
|
|
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
|
|
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
|
|
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
|