@effected/mcp 0.10.0 → 0.12.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
@@ -3,8 +3,38 @@ import { DEFAULT_CLIENT_INFO, frame, initializeParams, isJsonRpcMessage, isRespo
3
3
  import { McpTestFailure } from "./McpTestFailure.js";
4
4
  import { Console, Deferred, Effect, Exit, Fiber, Layer, Queue, Sink, Stdio, Stream } from "effect";
5
5
  import { McpProtocol } from "effect/ai";
6
+ import { ErrorText } from "@effected/engine";
6
7
 
7
8
  // src/McpHarness.ts
9
+ /** The generic tag `Object.prototype.toString` answers with: a value with no string form of its own. */
10
+ const GENERIC_STRING = /^\[object [^\]]*\]$/;
11
+ /**
12
+ * One captured console argument as text, as close to what the console would show as a runtime-free rendering gets,
13
+ * and never throwing.
14
+ *
15
+ * @remarks
16
+ * A harness records what the server would have printed, so the record must not lose what the console shows. Hence
17
+ * the order: a value's own string form first (`String(error)` is `Error: boom`, where `ErrorText.describe` would keep
18
+ * only `boom`, and it is the same for any object carrying a `message`), then JSON for an array or a plain object,
19
+ * whose string form is `1,2` or `[object Object]`. `ErrorText.describe` is the last resort for a value whose string
20
+ * form throws and which has no JSON: an object with no prototype has no `toString`, and `String` throws on it inside
21
+ * the server's own console call.
22
+ */
23
+ const showArg = (arg) => {
24
+ if (typeof arg === "string") return arg;
25
+ if (typeof arg !== "object" || arg === null) return ErrorText.describe(arg);
26
+ try {
27
+ if (!Array.isArray(arg)) {
28
+ const text = String(arg);
29
+ if (!GENERIC_STRING.test(text)) return text;
30
+ }
31
+ } catch {}
32
+ try {
33
+ const json = JSON.stringify(arg);
34
+ if (json !== void 0) return json;
35
+ } catch {}
36
+ return ErrorText.describe(arg);
37
+ };
8
38
  /**
9
39
  * An in-process MCP client for a real server layer, over queue-backed stdio.
10
40
  *
@@ -109,7 +139,15 @@ var McpHarness = class McpHarness {
109
139
  sentSoFar;
110
140
  /** Everything written to stderr, plus every captured log line. */
111
141
  stderrSoFar;
112
- /** Every captured `console.log`, `info` or `debug` call. */
142
+ /**
143
+ * Every captured `console.log`, `info` or `debug` call, its arguments joined with a space, each rendered close to
144
+ * what the console would show: a string as itself, a value with its own string form as that form (an `Error` as
145
+ * `Error: boom`, a number, a date), an array or a plain object as JSON (`{"message":"x"}`, not just `x`).
146
+ *
147
+ * @remarks
148
+ * Rendering never throws inside the server's own call. A value whose string form throws (an object with no
149
+ * prototype, a throwing `toString`) and has no JSON falls back to `ErrorText.describe` from `@effected/engine`.
150
+ */
113
151
  consoleLogSoFar;
114
152
  /**
115
153
  * End stdin, as a client disconnecting does. The server drains first:
@@ -171,7 +209,7 @@ var McpHarness = class McpHarness {
171
209
  }))
172
210
  });
173
211
  const ambient = yield* Console.Console;
174
- const show = (args) => args.map((arg) => typeof arg === "string" ? arg : String(arg)).join(" ");
212
+ const show = (args) => args.map(showArg).join(" ");
175
213
  const toStdout = (...args) => {
176
214
  consoleLog.push(show(args));
177
215
  };
package/McpStdio.js CHANGED
@@ -16,7 +16,8 @@ import { McpProtocol, McpServer } from "effect/ai";
16
16
  * ```
17
17
  *
18
18
  * - {@link McpStdio.layer} is `McpServer.layerStdio` with `LogToStderr`
19
- * provided and merged into its output, and `Layer.orDie`. It answers a
19
+ * provided and merged into its output, and a bad `protocols` list
20
+ * turned into a defect. It answers a
20
21
  * stdin line that is not JSON with a JSON-RPC `-32700` parse error, and
21
22
  * JSON that is not a JSON-RPC message with `-32600`, and keeps serving.
22
23
  * - {@link McpStdio.launch} reports a launch failure itself, on stderr, and
@@ -57,14 +58,24 @@ var McpStdio = class McpStdio {
57
58
  /**
58
59
  * `McpServer.layerStdio` with `LogToStderr` provided and merged into the
59
60
  * output, so every layer it provides logs to stderr too. A bad
60
- * `protocols` list dies: it is the implementer's defect.
61
+ * `protocols` list dies: it is the implementer's defect. So does a
62
+ * registration that fails with `Cause.IllegalArgumentError`, which core
63
+ * reports for the same class of mistake; any other registration error
64
+ * stays in the error channel.
61
65
  *
62
66
  * @remarks
63
- * Only a layer this one provides inherits `LogToStderr`: compose with
64
- * `Layer.provideMerge(McpStdio.layer(...))`. A sibling merged beside it
65
- * with `Layer.mergeAll` is not provided by it, so its build logs still
67
+ * Pass the server's registration layers as `options.registrations`: they
68
+ * are built before the server reads its first request, under
69
+ * `LogToStderr`. Providing this layer to them, as
70
+ * `Registrations.pipe(Layer.provideMerge(McpStdio.layer(...)))`, also
71
+ * serves them, and they inherit `LogToStderr` the same way. A sibling
72
+ * merged beside it with `Layer.mergeAll` is neither: its build logs still
66
73
  * go through `console.log`, onto the wire.
67
-
74
+ *
75
+ * The output is `McpServer` and the `McpServer.McpRequest<"Requires", Scope>`
76
+ * marker the transport discharges. `McpServerClient` is not in it: the
77
+ * stateless `v2026_07_28` revision has no client session, so a handler
78
+ * reads the client's details from `McpSchema.McpRequestContext` instead.
68
79
  *
69
80
  * `LogToStderr` only reaches a logger that reads it: Effect's default
70
81
  * logger and `Logger.consolePretty`. `Logger.consoleJson`,
@@ -94,14 +105,6 @@ var McpStdio = class McpStdio {
94
105
  * object with neither `method` nor `id`, which is neither a request nor a
95
106
  * response.
96
107
  *
97
- * A line whose `method` starts with `@effect/rpc/` never reaches core,
98
- * whose JSON-RPC decoder would read one without an `id` as its own RPC
99
- * control message: `@effect/rpc/Eof` alone silently stops the server
100
- * (Effect-TS/effect#8499, open upstream). Such a request is answered
101
- * `-32601` Method not found, echoing its `id`; such a notification is
102
- * dropped. The guard stops doing this once core no longer treats a
103
- * client's `@effect/rpc/` frame as a control message.
104
- *
105
108
  * Everything else goes to core: an array, which core answers `-32600`
106
109
  * itself because it serves no batches; an object with an `id` and no
107
110
  * `method`, which is a response and gets no reply; and a request with an
@@ -115,8 +118,8 @@ var McpStdio = class McpStdio {
115
118
  * anywhere under the first one's `Effect.provide`: nested `Layer.build`
116
119
  * and `Effect.provide` fork the ambient memo map rather than starting a
117
120
  * new one. The tool registry is shared the same way. Isolate each server
118
- * by wrapping its whole bundle (its toolkit layers together with this
119
- * layer) in `Layer.fresh`, or with its own `ManagedRuntime`,
121
+ * by wrapping its whole bundle (this layer with its `registrations`, or its
122
+ * toolkit layers together with this layer) in `Layer.fresh`, or with its own `ManagedRuntime`,
120
123
  * `Effect.provide(layer, { local: true })` or its own process. Never put
121
124
  * the `Layer.fresh` boundary between a toolkit and this layer: the server
122
125
  * would serve an empty registry.
@@ -134,8 +137,9 @@ var McpStdio = class McpStdio {
134
137
  version: options.version,
135
138
  instructions: options.instructions,
136
139
  description: options.description,
137
- protocols: options.protocols ?? McpStdio.protocols
138
- }).pipe(Layer.provide(makeGuardedStdio()), Layer.provideMerge(Layer.succeed(References.LogToStderr, true)), Layer.orDie);
140
+ protocols: options.protocols ?? McpStdio.protocols,
141
+ registrations: options.registrations
142
+ }).pipe(Layer.provide(makeGuardedStdio()), Layer.provideMerge(Layer.succeed(References.LogToStderr, true)), Layer.catchCause((cause) => Layer.effectContext(Effect.failCause(cause).pipe(Effect.catchIf(Cause.isIllegalArgumentError, (error) => Effect.die(error))))));
139
143
  /**
140
144
  * `Layer.launch`, with any failure other than an interrupt logged on
141
145
  * stderr here and re-raised marked as already reported, keeping its exit
package/McpToolkit.js CHANGED
@@ -232,6 +232,17 @@ var McpToolkit = class {
232
232
  /**
233
233
  * `McpServer.toolkit`'s registration layer, strict by default.
234
234
  * `options.unknownKeyMessage` is deprecated and ignored.
235
+ *
236
+ * @remarks
237
+ * Its requirements are core's `McpServer.ToolkitRequirements` less
238
+ * `McpServer`, which the layer provides itself: the handlers, any service
239
+ * a parameter or success schema needs, and each service a tool declares
240
+ * with `dependencies` or `addDependency` as an
241
+ * `McpServer.McpRequest<"Requires", S>` marker. A marker is met per
242
+ * request by MCP middleware (`McpServer.middleware`, its `layer` provided
243
+ * to this one), never by providing `S`. A service the handlers only need
244
+ * when they are built is captured by the effect form of
245
+ * `Toolkit.toLayer` instead, and declared nowhere.
235
246
  */
236
247
  static layer = (toolkit, options = {}) => Layer.effectDiscard(Effect.gen(function* () {
237
248
  const registry = yield* McpServer.McpServer;
package/README.md CHANGED
@@ -8,10 +8,10 @@ The boundary layer of an `effect/ai` MCP server: stdio wiring that keeps stdout
8
8
  [![TypeScript 7.0](https://img.shields.io/badge/TypeScript-7.0-3178c6.svg)](https://www.typescriptlang.org/)
9
9
 
10
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
11
+ > Effect v4 (`effect` `~4.1.0`) and still in `0.x` development. Stable Effect
12
12
  > makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
13
13
  > `@effect/*` versions on the line the kit is built and tested against, install
14
- > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
14
+ > [`@effected/pnpm-plugin-effected`](https://www.npmjs.com/package/@effected/pnpm-plugin-effected).
15
15
  >
16
16
  > **Stability: unstable.** This package's API surface is not yet considered
17
17
  > complete and may change across `0.x` releases. Pin an exact version — even a
@@ -486,7 +486,7 @@ with no child process and no sockets. Pass `server` **without** a `Stdio`
486
486
  of its own — a `Stdio` the server provides internally would talk to the
487
487
  real terminal instead of the test's queues, including one that arrives
488
488
  bundled: `@effect/platform-node`'s `NodeServices.layer` brings its own
489
- `Stdio` along with `FileSystem`, `Path`, `ChildProcessSpawner`, `Crypto`
489
+ `Stdio` along with `FileSystem`, `Path`, `ChildProcessSpawner`
490
490
  and `Terminal`, so composing the server with that bundle loses the harness's
491
491
  `Stdio` the same way. Compose the individual platform layers the server
492
492
  needs instead and leave `Stdio` out; the harness supplies it. The harness builds the server
package/ToolReplay.js CHANGED
@@ -15,6 +15,13 @@ import { KeyValueStore } from "effect/persistence";
15
15
  * `get` and `set`, for an application's own table. Then `R` carries no
16
16
  * `KeyValueStore`.
17
17
  *
18
+ * Inside an MCP server, pass the store. A service left in a handler's `R`
19
+ * has to be declared in the tool's `dependencies`, which makes it a
20
+ * request-level dependency that only MCP middleware can meet. Read
21
+ * `KeyValueStore` once when the handlers are built, with the effect form of
22
+ * `Toolkit.toLayer`, and hand it to `wrap` as `store`: a `KeyValueStore`
23
+ * satisfies {@link ToolReplayStore} as is.
24
+ *
18
25
  * Semantics, per call:
19
26
  *
20
27
  * - A key of `Option.none()` runs the handler; nothing is read or stored.
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Array, Context, Effect, JsonSchema, Layer, Option, Runtime, Schema, Stdio, Struct } from "effect";
1
+ import { Array, Cause, Context, Effect, JsonSchema, Layer, Option, Runtime, Schema, Scope, Stdio, Struct } from "effect";
2
2
  import { McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
3
3
  import { Remediation } from "@effected/engine";
4
4
  import { KeyValueStore } from "effect/persistence";
@@ -7,7 +7,7 @@ import { KeyValueStore } from "effect/persistence";
7
7
  *
8
8
  * @public
9
9
  */
10
- interface McpStdioOptions {
10
+ interface McpStdioOptions<RA = never, RE = never, RR = never> {
11
11
  /** The server name reported to the client. */
12
12
  readonly name: string;
13
13
  /** The server version reported to the client. */
@@ -18,7 +18,28 @@ interface McpStdioOptions {
18
18
  readonly description?: string | undefined;
19
19
  /** Defaults to {@link McpStdio.protocols}. Read its ordering rules before overriding. */
20
20
  readonly protocols?: Array.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter> | undefined;
21
+ /**
22
+ * The server's registration layers (toolkits, resources, prompts), built
23
+ * before the server reads its first request and under `LogToStderr`.
24
+ *
25
+ * @remarks
26
+ * Core's `registrations` option of `McpServer.layerStdio`. The transport
27
+ * discharges each `McpServer.McpRequest<"Requires", Scope>` marker; any
28
+ * other request-level marker stays in the layer's requirements, to be met
29
+ * by MCP middleware. A registration that needs `Stdio` when it is built
30
+ * gets the guarded one this layer serves over.
31
+ */
32
+ readonly registrations?: Layer.Layer<RA, RE, RR> | undefined;
21
33
  }
34
+ /**
35
+ * The services a registration layer needs that {@link McpStdio.layer} does
36
+ * not supply: everything but `McpServer` and `Stdio` (which the layer
37
+ * requires itself), with each request-level marker but
38
+ * `McpServer.McpRequest<"Requires", Scope>`, which the transport discharges.
39
+ *
40
+ * @public
41
+ */
42
+ type McpStdioRegistrationServices<RR> = Exclude<McpServer.McpRequest.Without<RR>, McpServer.McpServer> | McpServer.McpRequest.From<"Requires", Exclude<McpServer.McpRequest.Only<"Requires", RR>, Scope.Scope>>;
22
43
  /**
23
44
  * Options for {@link McpStdio.launch}.
24
45
  *
@@ -51,7 +72,8 @@ interface McpLaunchOptions {
51
72
  * ```
52
73
  *
53
74
  * - {@link McpStdio.layer} is `McpServer.layerStdio` with `LogToStderr`
54
- * provided and merged into its output, and `Layer.orDie`. It answers a
75
+ * provided and merged into its output, and a bad `protocols` list
76
+ * turned into a defect. It answers a
55
77
  * stdin line that is not JSON with a JSON-RPC `-32700` parse error, and
56
78
  * JSON that is not a JSON-RPC message with `-32600`, and keeps serving.
57
79
  * - {@link McpStdio.launch} reports a launch failure itself, on stderr, and
@@ -88,14 +110,24 @@ export declare class McpStdio {
88
110
  /**
89
111
  * `McpServer.layerStdio` with `LogToStderr` provided and merged into the
90
112
  * output, so every layer it provides logs to stderr too. A bad
91
- * `protocols` list dies: it is the implementer's defect.
113
+ * `protocols` list dies: it is the implementer's defect. So does a
114
+ * registration that fails with `Cause.IllegalArgumentError`, which core
115
+ * reports for the same class of mistake; any other registration error
116
+ * stays in the error channel.
92
117
  *
93
118
  * @remarks
94
- * Only a layer this one provides inherits `LogToStderr`: compose with
95
- * `Layer.provideMerge(McpStdio.layer(...))`. A sibling merged beside it
96
- * with `Layer.mergeAll` is not provided by it, so its build logs still
119
+ * Pass the server's registration layers as `options.registrations`: they
120
+ * are built before the server reads its first request, under
121
+ * `LogToStderr`. Providing this layer to them, as
122
+ * `Registrations.pipe(Layer.provideMerge(McpStdio.layer(...)))`, also
123
+ * serves them, and they inherit `LogToStderr` the same way. A sibling
124
+ * merged beside it with `Layer.mergeAll` is neither: its build logs still
97
125
  * go through `console.log`, onto the wire.
98
-
126
+ *
127
+ * The output is `McpServer` and the `McpServer.McpRequest<"Requires", Scope>`
128
+ * marker the transport discharges. `McpServerClient` is not in it: the
129
+ * stateless `v2026_07_28` revision has no client session, so a handler
130
+ * reads the client's details from `McpSchema.McpRequestContext` instead.
99
131
  *
100
132
  * `LogToStderr` only reaches a logger that reads it: Effect's default
101
133
  * logger and `Logger.consolePretty`. `Logger.consoleJson`,
@@ -125,14 +157,6 @@ export declare class McpStdio {
125
157
  * object with neither `method` nor `id`, which is neither a request nor a
126
158
  * response.
127
159
  *
128
- * A line whose `method` starts with `@effect/rpc/` never reaches core,
129
- * whose JSON-RPC decoder would read one without an `id` as its own RPC
130
- * control message: `@effect/rpc/Eof` alone silently stops the server
131
- * (Effect-TS/effect#8499, open upstream). Such a request is answered
132
- * `-32601` Method not found, echoing its `id`; such a notification is
133
- * dropped. The guard stops doing this once core no longer treats a
134
- * client's `@effect/rpc/` frame as a control message.
135
- *
136
160
  * Everything else goes to core: an array, which core answers `-32600`
137
161
  * itself because it serves no batches; an object with an `id` and no
138
162
  * `method`, which is a response and gets no reply; and a request with an
@@ -146,8 +170,8 @@ export declare class McpStdio {
146
170
  * anywhere under the first one's `Effect.provide`: nested `Layer.build`
147
171
  * and `Effect.provide` fork the ambient memo map rather than starting a
148
172
  * new one. The tool registry is shared the same way. Isolate each server
149
- * by wrapping its whole bundle (its toolkit layers together with this
150
- * layer) in `Layer.fresh`, or with its own `ManagedRuntime`,
173
+ * by wrapping its whole bundle (this layer with its `registrations`, or its
174
+ * toolkit layers together with this layer) in `Layer.fresh`, or with its own `ManagedRuntime`,
151
175
  * `Effect.provide(layer, { local: true })` or its own process. Never put
152
176
  * the `Layer.fresh` boundary between a toolkit and this layer: the server
153
177
  * would serve an empty registry.
@@ -160,7 +184,7 @@ export declare class McpStdio {
160
184
  * Each call mints a fresh layer; bind the result to a `const` or the
161
185
  * server builds twice.
162
186
  */
163
- static readonly layer: (options: McpStdioOptions) => Layer.Layer<McpServer.McpServer | McpSchema.McpServerClient, never, Stdio.Stdio>;
187
+ static readonly layer: <RA = never, RE = never, RR = never>(options: McpStdioOptions<RA, RE, RR>) => Layer.Layer<McpServer.McpServer | McpServer.McpRequest<"Requires", Scope.Scope>, Exclude<RE, Cause.IllegalArgumentError>, Stdio.Stdio | McpStdioRegistrationServices<RR>>;
164
188
  /**
165
189
  * `Layer.launch`, with any failure other than an interrupt logged on
166
190
  * stderr here and re-raised marked as already reported, keeping its exit
@@ -470,8 +494,19 @@ export declare class McpToolkit {
470
494
  /**
471
495
  * `McpServer.toolkit`'s registration layer, strict by default.
472
496
  * `options.unknownKeyMessage` is deprecated and ignored.
473
- */
474
- static readonly layer: <Tools extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<Tools>, options?: McpToolkitOptions) => Layer.Layer<never, never, Tool.HandlersFor<Tools> | Exclude<Tool.HandlerServices<Tools>, McpSchema.McpRequestContext>>;
497
+ *
498
+ * @remarks
499
+ * Its requirements are core's `McpServer.ToolkitRequirements` less
500
+ * `McpServer`, which the layer provides itself: the handlers, any service
501
+ * a parameter or success schema needs, and each service a tool declares
502
+ * with `dependencies` or `addDependency` as an
503
+ * `McpServer.McpRequest<"Requires", S>` marker. A marker is met per
504
+ * request by MCP middleware (`McpServer.middleware`, its `layer` provided
505
+ * to this one), never by providing `S`. A service the handlers only need
506
+ * when they are built is captured by the effect form of
507
+ * `Toolkit.toLayer` instead, and declared nowhere.
508
+ */
509
+ static readonly layer: <Tools extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<Tools>, options?: McpToolkitOptions) => Layer.Layer<never, never, Exclude<McpServer.ToolkitRequirements<Tools>, McpServer.McpServer>>;
475
510
  }
476
511
  /**
477
512
  * How a tool's declared failure should read on the wire.
@@ -810,7 +845,7 @@ export declare class ToolRefusal extends ToolRefusal_base {
810
845
  * Core's `KeyValueStore` already satisfies it structurally, so a
811
846
  * `KeyValueStore` value can be passed as is. An application backed by its
812
847
  * own table implements just these two methods, instead of stubbing the
813
- * `remove`, `clear` and `size` that `KeyValueStore.makeStringOnly` demands.
848
+ * `remove`, `clear`, `size` and `keys` that `KeyValueStore.makeStringOnly` demands.
814
849
  *
815
850
  * `get` answers `undefined` for a missing key, the same contract as
816
851
  * `KeyValueStore.get`. A failure from either method is never a tool error:
@@ -872,6 +907,13 @@ interface ToolReplayOptions<P, S extends Schema.Top, Store extends ToolReplaySto
872
907
  * `get` and `set`, for an application's own table. Then `R` carries no
873
908
  * `KeyValueStore`.
874
909
  *
910
+ * Inside an MCP server, pass the store. A service left in a handler's `R`
911
+ * has to be declared in the tool's `dependencies`, which makes it a
912
+ * request-level dependency that only MCP middleware can meet. Read
913
+ * `KeyValueStore` once when the handlers are built, with the effect form of
914
+ * `Toolkit.toLayer`, and hand it to `wrap` as `store`: a `KeyValueStore`
915
+ * satisfies {@link ToolReplayStore} as is.
916
+ *
875
917
  * Semantics, per call:
876
918
  *
877
919
  * - A key of `Option.none()` runs the handler; nothing is read or stored.
@@ -943,5 +985,5 @@ export declare class ToolReplay {
943
985
  */
944
986
  static readonly wrap: <P, S extends Schema.Top, Store extends ToolReplayStore | undefined = undefined>(options: ToolReplayOptions<P, S, Store>) => (<E, R>(handler: (params: P) => Effect.Effect<S["Type"], E, R>) => (params: P) => Effect.Effect<S["Type"], E, R | ([Store] extends [ToolReplayStore] ? never : KeyValueStore.KeyValueStore) | S["DecodingServices"] | S["EncodingServices"]>);
945
987
  }
946
- export type { FormatUnknownKeysOptions, McpLaunchOptions, McpStdioOptions, McpToolkitOptions, ToolOutcomeEnvelopeOptions, ToolOutcomeErrorInput, ToolOutcomeFailure, ToolOutcomeFailures, ToolReplayOptions, ToolReplayStore, UnionHandlerOptions, UnionTool, UnionToolOptions, UnknownKeysLevel };
988
+ export type { FormatUnknownKeysOptions, McpLaunchOptions, McpStdioOptions, McpStdioRegistrationServices, McpToolkitOptions, ToolOutcomeEnvelopeOptions, ToolOutcomeErrorInput, ToolOutcomeFailure, ToolOutcomeFailures, ToolReplayOptions, ToolReplayStore, UnionHandlerOptions, UnionTool, UnionToolOptions, UnknownKeysLevel };
947
989
  //# sourceMappingURL=index.d.ts.map
@@ -40,36 +40,12 @@ const INVALID_REQUEST_FRAME = `${JSON.stringify({
40
40
  message: "Invalid Request"
41
41
  }
42
42
  })}\n`;
43
- /**
44
- * The JSON-RPC 2.0 answer to a request whose method is one of Effect's
45
- * internal `@effect/rpc/` control messages, newline-framed, echoing the
46
- * request's `id`.
47
- *
48
- * @internal
49
- */
50
- const methodNotFoundFrame = (id) => `${JSON.stringify({
51
- jsonrpc: "2.0",
52
- id,
53
- error: {
54
- code: McpSchema.METHOD_NOT_FOUND_ERROR_CODE,
55
- message: "Method not found"
56
- }
57
- })}\n`;
58
- /**
59
- * The method prefix core's JSON-RPC decoder reserves for its own RPC control
60
- * messages (`Eof`, `Interrupt`, `Ack`, …).
61
- *
62
- * @internal
63
- */
64
- const RPC_CONTROL_PREFIX = "@effect/rpc/";
65
43
  /** JSON's insignificant whitespace, minus the newline that ends the line. */
66
44
  const BLANK = /^[ \t\r]*$/;
67
45
  const isNullish = (value) => value === null || value === void 0;
68
- /** {@link answerFor}'s verdict for a line that is neither forwarded nor answered. */
69
- const DROP = "";
70
46
  /**
71
- * The guard's answer to one non-blank line within the cap, `DROP` to swallow
72
- * it silently, or `undefined` to forward it to core.
47
+ * The guard's answer to one non-blank line within the cap, or `undefined` to
48
+ * forward it to core.
73
49
  *
74
50
  * @remarks
75
51
  * A frame that parses is forwarded unless core would skip it without a
@@ -82,16 +58,9 @@ const DROP = "";
82
58
  * `method` and every response an `id`, so it is neither, and core ignores it
83
59
  * as a response to nothing.
84
60
  *
85
- * A frame whose `method` starts with `@effect/rpc/` never reaches core. Core's
86
- * JSON-RPC decoder reads such a frame with no `id` as one of its own RPC
87
- * control messages, so `{"jsonrpc":"2.0","method":"@effect/rpc/Eof"}` from
88
- * any client silently stops the server (Effect-TS/effect#8499). A
89
- * notification of that shape is dropped; a request, which carries an `id`,
90
- * is answered `-32601` Method not found, as no MCP method has that prefix.
91
- * Remove this branch once Effect-TS/effect#8499 is fixed in the installed
92
- * `effect` (the open PR #8509 proposes the fix) and a probe shows an
93
- * unguarded server still answering a ping sent after an `@effect/rpc/Eof`
94
- * notification.
61
+ * A frame whose `method` starts with `@effect/rpc/` is an ordinary request or
62
+ * notification to core, which serves MCP with Effect's RPC control messages
63
+ * turned off: a notification is ignored and a request answered `-32601`.
95
64
  *
96
65
  * Arrays are forwarded: core answers a batch `-32600` itself. So is any object
97
66
  * with an `id` and no `method`, which is a response; JSON-RPC never answers a
@@ -107,10 +76,7 @@ const answerFor = (line) => {
107
76
  if (typeof value !== "object" || value === null) return INVALID_REQUEST_FRAME;
108
77
  if (Array.isArray(value)) return void 0;
109
78
  const message = value;
110
- if (Object.hasOwn(message, "method")) {
111
- if (typeof message.method === "string" && message.method.startsWith("@effect/rpc/")) return isNullish(message.id) ? DROP : methodNotFoundFrame(message.id);
112
- return typeof message.method !== "string" && isNullish(message.id) ? INVALID_REQUEST_FRAME : void 0;
113
- }
79
+ if (Object.hasOwn(message, "method")) return typeof message.method !== "string" && isNullish(message.id) ? INVALID_REQUEST_FRAME : void 0;
114
80
  return Object.hasOwn(message, "id") ? void 0 : INVALID_REQUEST_FRAME;
115
81
  };
116
82
  /**
@@ -125,8 +91,7 @@ const answerFor = (line) => {
125
91
  * JSON-RPC 2.0 requires a reply to each of them, so the guard frames stdin
126
92
  * exactly as core does and answers such a line itself, returning
127
93
  * {@link PARSE_ERROR_FRAME} or {@link INVALID_REQUEST_FRAME} for the caller
128
- * to write. It also keeps `@effect/rpc/` control methods away from core (see
129
- * `answerFor`), which would otherwise let one client line stop the server.
94
+ * to write.
130
95
  *
131
96
  * - Decoding mirrors core: one streaming UTF-8 decoder, so a character split
132
97
  * across chunks decodes whole, a byte-order mark is stripped only at the
@@ -137,8 +102,6 @@ const answerFor = (line) => {
137
102
  * - A line that is JSON is answered `-32600` when it is not an object or an
138
103
  * array, when its `method` is not a string and it has no usable `id`, or
139
104
  * when it has neither `method` nor `id`.
140
- * - A line whose `method` starts with `@effect/rpc/` is answered `-32601`
141
- * when it carries an `id` and dropped when it does not.
142
105
  * - Everything else goes to core.
143
106
  * - A line longer than `maxFrameLength` code units is answered once, as soon
144
107
  * as the held part exceeds the cap, and the rest of it is discarded up to
@@ -181,7 +144,7 @@ const makeFrameGuard = (maxFrameLength = MAX_FRAME_LENGTH) => {
181
144
  if (BLANK.test(line)) continue;
182
145
  const answer = answerFor(line);
183
146
  if (answer === void 0) forward += `${line}\n`;
184
- else if (answer !== DROP) replies.push(answer);
147
+ else replies.push(answer);
185
148
  }
186
149
  pending += text.slice(start);
187
150
  if (pending.length > maxFrameLength) {
@@ -235,4 +198,4 @@ const guardStdin = (stdio) => {
235
198
  */
236
199
  const makeGuardedStdio = () => Layer.effect(Stdio.Stdio, Effect.map(Stdio.Stdio, guardStdin));
237
200
 
238
- export { INVALID_REQUEST_FRAME, MAX_FRAME_LENGTH, PARSE_ERROR_FRAME, RPC_CONTROL_PREFIX, guardStdin, makeFrameGuard, makeGuardedStdio, methodNotFoundFrame };
201
+ export { INVALID_REQUEST_FRAME, MAX_FRAME_LENGTH, PARSE_ERROR_FRAME, guardStdin, makeFrameGuard, makeGuardedStdio };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/mcp",
3
- "version": "0.10.0",
3
+ "version": "0.12.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": [
@@ -52,8 +52,8 @@
52
52
  "./package.json": "./package.json"
53
53
  },
54
54
  "peerDependencies": {
55
- "@effected/engine": "^0.9.0",
56
- "effect": "^4.0.0"
55
+ "@effected/engine": "^0.11.0",
56
+ "effect": "~4.1.0"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=24.11.0"
package/testing.d.ts CHANGED
@@ -214,7 +214,15 @@ export declare class McpHarness {
214
214
  readonly sentSoFar: Effect.Effect<ReadonlyArray<unknown>>;
215
215
  /** Everything written to stderr, plus every captured log line. */
216
216
  readonly stderrSoFar: Effect.Effect<string>;
217
- /** Every captured `console.log`, `info` or `debug` call. */
217
+ /**
218
+ * Every captured `console.log`, `info` or `debug` call, its arguments joined with a space, each rendered close to
219
+ * what the console would show: a string as itself, a value with its own string form as that form (an `Error` as
220
+ * `Error: boom`, a number, a date), an array or a plain object as JSON (`{"message":"x"}`, not just `x`).
221
+ *
222
+ * @remarks
223
+ * Rendering never throws inside the server's own call. A value whose string form throws (an object with no
224
+ * prototype, a throwing `toString`) and has no JSON falls back to `ErrorText.describe` from `@effected/engine`.
225
+ */
218
226
  readonly consoleLogSoFar: Effect.Effect<ReadonlyArray<string>>;
219
227
  /**
220
228
  * End stdin, as a client disconnecting does. The server drains first:
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.59.4"
8
+ "packageVersion": "7.60.0"
9
9
  }
10
10
  ]
11
11
  }