@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 +40 -2
- package/McpStdio.js +22 -18
- package/McpToolkit.js +11 -0
- package/README.md +3 -3
- package/ToolReplay.js +7 -0
- package/index.d.ts +65 -23
- package/internal/StdinFrames.js +9 -46
- package/package.json +3 -3
- package/testing.d.ts +9 -1
- package/tsdoc-metadata.json +1 -1
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
|
-
/**
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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 (
|
|
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
|
-
|
|
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
|
[](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`
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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 (
|
|
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 |
|
|
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
|
-
|
|
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 `
|
|
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
|
package/internal/StdinFrames.js
CHANGED
|
@@ -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, `
|
|
72
|
-
*
|
|
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/`
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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.
|
|
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
|
|
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,
|
|
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.
|
|
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.
|
|
56
|
-
"effect": "
|
|
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
|
-
/**
|
|
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:
|