@effected/mcp-kit 0.13.0 → 0.14.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 +20 -1
- package/McpStdio.js +4 -0
- package/McpToolkit.js +41 -0
- package/README.md +5 -0
- package/ToolReplay.js +7 -0
- package/index.d.ts +59 -0
- package/package.json +2 -2
- package/testing.d.ts +32 -3
package/McpHarness.js
CHANGED
|
@@ -92,6 +92,24 @@ const showArg = (arg) => {
|
|
|
92
92
|
* }).pipe(Effect.scoped);
|
|
93
93
|
* ```
|
|
94
94
|
*
|
|
95
|
+
* Services the test also seeds go in `options.services`, built once. End the
|
|
96
|
+
* session with `harness.close` (the server answers what is in flight, then
|
|
97
|
+
* stops) or, when a request may never complete, `harness.stop` (the server is
|
|
98
|
+
* interrupted at once):
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* import { McpHarness } from "@effected/mcp-kit/testing";
|
|
102
|
+
* import { Context, Effect } from "effect";
|
|
103
|
+
*
|
|
104
|
+
* // `Database` is a service the server's tools need, and the test seeds it.
|
|
105
|
+
* const program = Effect.gen(function* () {
|
|
106
|
+
* const database = yield* makeDatabase;
|
|
107
|
+
* const harness = yield* McpHarness.make(ServerLayer, { services: Context.make(Database, database) });
|
|
108
|
+
* yield* harness.initialize;
|
|
109
|
+
* yield* harness.stop;
|
|
110
|
+
* }).pipe(Effect.scoped);
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
95
113
|
* @public
|
|
96
114
|
*/
|
|
97
115
|
var McpHarness = class McpHarness {
|
|
@@ -224,7 +242,8 @@ var McpHarness = class McpHarness {
|
|
|
224
242
|
error: toStderr,
|
|
225
243
|
warn: toStderr
|
|
226
244
|
};
|
|
227
|
-
const
|
|
245
|
+
const withLogs = options.captureLogs === false ? server.pipe(Layer.provide(stdio)) : server.pipe(Layer.provide(stdio), Layer.provide(Layer.succeed(Console.Console, captured)));
|
|
246
|
+
const provided = options.services === void 0 ? withLogs : withLogs.pipe(Layer.provide(Layer.succeedContext(options.services)));
|
|
228
247
|
const route = (line) => {
|
|
229
248
|
const parsed = parseFrame(line);
|
|
230
249
|
if (!isJsonRpcMessage(parsed)) return strictStdout ? Effect.asVoid(Deferred.die(corrupt, /* @__PURE__ */ new Error(`stdout carried a line that is not JSON-RPC: ${ToolFailure.truncate(line)}`))) : Effect.void;
|
package/McpStdio.js
CHANGED
|
@@ -137,6 +137,10 @@ var McpStdio = class McpStdio {
|
|
|
137
137
|
version: options.version,
|
|
138
138
|
instructions: options.instructions,
|
|
139
139
|
description: options.description,
|
|
140
|
+
websiteUrl: options.websiteUrl,
|
|
141
|
+
icons: options.icons,
|
|
142
|
+
extensions: options.extensions,
|
|
143
|
+
cacheHints: options.cacheHints,
|
|
140
144
|
protocols: options.protocols ?? McpStdio.protocols,
|
|
141
145
|
registrations: options.registrations
|
|
142
146
|
}).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))))));
|
package/McpToolkit.js
CHANGED
|
@@ -252,6 +252,47 @@ var McpToolkit = class {
|
|
|
252
252
|
});
|
|
253
253
|
yield* McpServer.registerToolkit(strictened(toolkit, options.strict ?? DEFAULT_STRICT)).pipe(Effect.provideService(McpServer.McpServer, decorated));
|
|
254
254
|
})).pipe(Layer.provide(McpServer.McpServer.layer));
|
|
255
|
+
/**
|
|
256
|
+
* Meet the request-level dependencies a tool declares with `dependencies`
|
|
257
|
+
* or `addDependency` from a layer built once for the server's lifetime.
|
|
258
|
+
* Provide the result to {@link McpToolkit.layer}.
|
|
259
|
+
*
|
|
260
|
+
* @remarks
|
|
261
|
+
* The layer builds with the server's graph, so a failure fails the server
|
|
262
|
+
* build rather than a request, and its finalizers run at server shutdown.
|
|
263
|
+
* It builds in that graph's memo map: a layer the graph also builds is
|
|
264
|
+
* shared, and two servers each wrapped whole in `Layer.fresh` build their
|
|
265
|
+
* own.
|
|
266
|
+
*
|
|
267
|
+
* It captures what the layer outputs and nothing else, so a key the build
|
|
268
|
+
* merely ran under (a span, a request-level reference) never shadows the
|
|
269
|
+
* request's own, which capturing `Effect.context` in hand-written
|
|
270
|
+
* middleware does. The outputs are taken as given: a layer that outputs
|
|
271
|
+
* `Effect.context()` wholesale, through `Layer.effectContext`, still leaks
|
|
272
|
+
* everything it read.
|
|
273
|
+
*
|
|
274
|
+
* A service that varies per request still needs its own
|
|
275
|
+
* `McpServer.middleware`. A service only the handlers' construction needs
|
|
276
|
+
* is captured by the effect form of `Toolkit.toLayer` and declared nowhere.
|
|
277
|
+
*
|
|
278
|
+
* Each call mints a fresh layer; bind the result to a `const` or the
|
|
279
|
+
* middleware builds again.
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```ts
|
|
283
|
+
* import { McpToolkit } from "@effected/mcp-kit";
|
|
284
|
+
* import { Layer } from "effect";
|
|
285
|
+
*
|
|
286
|
+
* // `Kit` declares `dependencies: [Inspector]`; `InspectorLive` builds it once.
|
|
287
|
+
* const Lifetime = McpToolkit.provideLifetime(InspectorLive);
|
|
288
|
+
* const ToolsLayer = McpToolkit.layer(Kit).pipe(Layer.provide(Lifetime), Layer.provide(KitHandlers));
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
static provideLifetime = (layer) => {
|
|
292
|
+
const Lifetime = Context.Service("@effected/mcp-kit/LifetimeContext");
|
|
293
|
+
const capture = Layer.fromBuild((memoMap, scope) => Effect.map(Layer.buildWithMemoMap(layer, memoMap, scope), (context) => Context.make(Lifetime, Context.omit(Layer.CurrentMemoMap)(context))));
|
|
294
|
+
return McpServer.middleware()(Effect.map(Effect.service(Lifetime), (context) => (effect) => Effect.updateContext(effect, (current) => Context.merge(current, context)))).layer.pipe(Layer.provide(capture));
|
|
295
|
+
};
|
|
255
296
|
};
|
|
256
297
|
|
|
257
298
|
export { McpToolkit, guardRegistration };
|
package/README.md
CHANGED
|
@@ -291,6 +291,11 @@ scopes:
|
|
|
291
291
|
`options.unknownKeyMessage` is deprecated and ignored. Pass `{ strict: "annotated" }` to leave
|
|
292
292
|
unannotated tools lenient, or annotate an individual tool
|
|
293
293
|
`Tool.Strict` false to opt it out under the default.
|
|
294
|
+
- **`McpToolkit.provideLifetime(layer)`** — meets the services tools declare
|
|
295
|
+
in `dependencies` from a layer built once with the server's graph. Provide
|
|
296
|
+
the result to `McpToolkit.layer`. It captures what the layer outputs and
|
|
297
|
+
nothing else, so nothing the build ran under shadows a request's own span
|
|
298
|
+
or references; a layer that outputs `Effect.context()` wholesale still leaks.
|
|
294
299
|
|
|
295
300
|
```ts
|
|
296
301
|
import { McpToolkit } from "@effected/mcp-kit";
|
package/ToolReplay.js
CHANGED
|
@@ -88,6 +88,13 @@ var ToolReplay = class ToolReplay {
|
|
|
88
88
|
/**
|
|
89
89
|
* Wraps a handler with replay. Throws at wrap time when `tool` contains
|
|
90
90
|
* `:`, a wiring mistake, so it surfaces when the server is built.
|
|
91
|
+
*
|
|
92
|
+
* @remarks
|
|
93
|
+
* Call it where the handler is built, once: the effect form of
|
|
94
|
+
* `Toolkit.toLayer`, reading the store's services there and passing them as
|
|
95
|
+
* `store`. Calling it per request, inside the handler, rebuilds the JSON
|
|
96
|
+
* codec on every call and turns the wiring check's throw into a defect at
|
|
97
|
+
* the first call instead of a failure when the server is built.
|
|
91
98
|
*/
|
|
92
99
|
static wrap = (options) => {
|
|
93
100
|
if (options.tool.includes(":")) throw new Error(`ToolReplay.wrap: tool name "${options.tool}" contains ":", the stored-key separator.`);
|
package/index.d.ts
CHANGED
|
@@ -16,6 +16,21 @@ interface McpStdioOptions<RA = never, RE = never, RR = never> {
|
|
|
16
16
|
readonly instructions?: string | undefined;
|
|
17
17
|
/** A one-line description of the server, reported to the client. */
|
|
18
18
|
readonly description?: string | undefined;
|
|
19
|
+
/** The server's website, reported in `server/discover` and `initialize`. */
|
|
20
|
+
readonly websiteUrl?: string | undefined;
|
|
21
|
+
/** Icons for the server, reported to the client. */
|
|
22
|
+
readonly icons?: ReadonlyArray<McpSchema.Icon> | undefined;
|
|
23
|
+
/** Capability extensions the server advertises, keyed `vendor/name`. */
|
|
24
|
+
readonly extensions?: (typeof McpSchema.ServerCapabilities)["Type"]["extensions"] | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* How long a client may treat each list result as fresh, per result kind.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* Core's `cacheHints`, forwarded unchanged. Only the stateless `2026-07-28` revision carries them; earlier
|
|
30
|
+
* revisions ignore the option. Omitted kinds keep a zero `ttl`. A positive `ttl` lets a client keep showing a list
|
|
31
|
+
* after registrations change, until it expires: raise it only for a list that is fixed for the server's lifetime.
|
|
32
|
+
*/
|
|
33
|
+
readonly cacheHints?: McpServer.CacheHints | undefined;
|
|
19
34
|
/** Defaults to {@link McpStdio.protocols}. Read its ordering rules before overriding. */
|
|
20
35
|
readonly protocols?: Array.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter> | undefined;
|
|
21
36
|
/**
|
|
@@ -507,6 +522,43 @@ export declare class McpToolkit {
|
|
|
507
522
|
* `Toolkit.toLayer` instead, and declared nowhere.
|
|
508
523
|
*/
|
|
509
524
|
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>>;
|
|
525
|
+
/**
|
|
526
|
+
* Meet the request-level dependencies a tool declares with `dependencies`
|
|
527
|
+
* or `addDependency` from a layer built once for the server's lifetime.
|
|
528
|
+
* Provide the result to {@link McpToolkit.layer}.
|
|
529
|
+
*
|
|
530
|
+
* @remarks
|
|
531
|
+
* The layer builds with the server's graph, so a failure fails the server
|
|
532
|
+
* build rather than a request, and its finalizers run at server shutdown.
|
|
533
|
+
* It builds in that graph's memo map: a layer the graph also builds is
|
|
534
|
+
* shared, and two servers each wrapped whole in `Layer.fresh` build their
|
|
535
|
+
* own.
|
|
536
|
+
*
|
|
537
|
+
* It captures what the layer outputs and nothing else, so a key the build
|
|
538
|
+
* merely ran under (a span, a request-level reference) never shadows the
|
|
539
|
+
* request's own, which capturing `Effect.context` in hand-written
|
|
540
|
+
* middleware does. The outputs are taken as given: a layer that outputs
|
|
541
|
+
* `Effect.context()` wholesale, through `Layer.effectContext`, still leaks
|
|
542
|
+
* everything it read.
|
|
543
|
+
*
|
|
544
|
+
* A service that varies per request still needs its own
|
|
545
|
+
* `McpServer.middleware`. A service only the handlers' construction needs
|
|
546
|
+
* is captured by the effect form of `Toolkit.toLayer` and declared nowhere.
|
|
547
|
+
*
|
|
548
|
+
* Each call mints a fresh layer; bind the result to a `const` or the
|
|
549
|
+
* middleware builds again.
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* ```ts
|
|
553
|
+
* import { McpToolkit } from "@effected/mcp-kit";
|
|
554
|
+
* import { Layer } from "effect";
|
|
555
|
+
*
|
|
556
|
+
* // `Kit` declares `dependencies: [Inspector]`; `InspectorLive` builds it once.
|
|
557
|
+
* const Lifetime = McpToolkit.provideLifetime(InspectorLive);
|
|
558
|
+
* const ToolsLayer = McpToolkit.layer(Kit).pipe(Layer.provide(Lifetime), Layer.provide(KitHandlers));
|
|
559
|
+
* ```
|
|
560
|
+
*/
|
|
561
|
+
static readonly provideLifetime: <S, E, R>(layer: Layer.Layer<S, E, R>) => Layer.Layer<McpServer.McpRequest.From<"Requires", S>, E, R>;
|
|
510
562
|
}
|
|
511
563
|
/**
|
|
512
564
|
* How a tool's declared failure should read on the wire.
|
|
@@ -982,6 +1034,13 @@ export declare class ToolReplay {
|
|
|
982
1034
|
/**
|
|
983
1035
|
* Wraps a handler with replay. Throws at wrap time when `tool` contains
|
|
984
1036
|
* `:`, a wiring mistake, so it surfaces when the server is built.
|
|
1037
|
+
*
|
|
1038
|
+
* @remarks
|
|
1039
|
+
* Call it where the handler is built, once: the effect form of
|
|
1040
|
+
* `Toolkit.toLayer`, reading the store's services there and passing them as
|
|
1041
|
+
* `store`. Calling it per request, inside the handler, rebuilds the JSON
|
|
1042
|
+
* codec on every call and turns the wiring check's throw into a defect at
|
|
1043
|
+
* the first call instead of a failure when the server is built.
|
|
985
1044
|
*/
|
|
986
1045
|
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"]>);
|
|
987
1046
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/mcp-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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,7 +52,7 @@
|
|
|
52
52
|
"./package.json": "./package.json"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@effected/kit": "^0.
|
|
55
|
+
"@effected/kit": "^0.13.0",
|
|
56
56
|
"effect": "~4.1.0"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
package/testing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Duration, Effect, JsonSchema, Layer, PlatformError, Schema, Scope, Stdio } from "effect";
|
|
1
|
+
import { Context, Duration, Effect, JsonSchema, Layer, PlatformError, Schema, Scope, Stdio } from "effect";
|
|
2
2
|
import { McpProtocol } from "effect/ai";
|
|
3
3
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
4
|
declare const McpTestFailure_base: Schema.Class<McpTestFailure, Schema.TaggedStruct<"McpTestFailure", {
|
|
@@ -94,7 +94,7 @@ interface ServedResource {
|
|
|
94
94
|
*
|
|
95
95
|
* @public
|
|
96
96
|
*/
|
|
97
|
-
interface McpHarnessOptions {
|
|
97
|
+
interface McpHarnessOptions<S = never> {
|
|
98
98
|
/** The revision this client speaks. Defaults to `McpProtocol.v2025_11_25`; a stateless adapter switches to `server/discover` and per-request `_meta`. */
|
|
99
99
|
readonly protocol?: McpProtocol.ProtocolAdapter | undefined;
|
|
100
100
|
/** The `clientInfo` sent in `initialize` and in stateless `_meta`. */
|
|
@@ -106,6 +106,17 @@ interface McpHarnessOptions {
|
|
|
106
106
|
readonly captureLogs?: boolean | undefined;
|
|
107
107
|
/** Treat any stdout line that is not JSON-RPC as a defect. Defaults to `true`. */
|
|
108
108
|
readonly strictStdout?: boolean | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* A pre-built context of the services the server needs, provided to it as built.
|
|
111
|
+
*
|
|
112
|
+
* @remarks
|
|
113
|
+
* Build the services once, seed them from the test, and pass them here: the tools then read the instance the test
|
|
114
|
+
* wrote to, where providing the services' layer instead would build a second one. The harness's queue-backed
|
|
115
|
+
* `Stdio` always serves the server, because the services are provided outside it: a `Stdio` in the context,
|
|
116
|
+
* such as a platform layer's real process one, never reaches the server. The type removes `S` from the
|
|
117
|
+
* server's requirements.
|
|
118
|
+
*/
|
|
119
|
+
readonly services?: Context.Context<S> | undefined;
|
|
109
120
|
}
|
|
110
121
|
/**
|
|
111
122
|
* An in-process MCP client for a real server layer, over queue-backed stdio.
|
|
@@ -164,6 +175,24 @@ interface McpHarnessOptions {
|
|
|
164
175
|
* }).pipe(Effect.scoped);
|
|
165
176
|
* ```
|
|
166
177
|
*
|
|
178
|
+
* Services the test also seeds go in `options.services`, built once. End the
|
|
179
|
+
* session with `harness.close` (the server answers what is in flight, then
|
|
180
|
+
* stops) or, when a request may never complete, `harness.stop` (the server is
|
|
181
|
+
* interrupted at once):
|
|
182
|
+
*
|
|
183
|
+
* ```ts
|
|
184
|
+
* import { McpHarness } from "@effected/mcp-kit/testing";
|
|
185
|
+
* import { Context, Effect } from "effect";
|
|
186
|
+
*
|
|
187
|
+
* // `Database` is a service the server's tools need, and the test seeds it.
|
|
188
|
+
* const program = Effect.gen(function* () {
|
|
189
|
+
* const database = yield* makeDatabase;
|
|
190
|
+
* const harness = yield* McpHarness.make(ServerLayer, { services: Context.make(Database, database) });
|
|
191
|
+
* yield* harness.initialize;
|
|
192
|
+
* yield* harness.stop;
|
|
193
|
+
* }).pipe(Effect.scoped);
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
167
196
|
* @public
|
|
168
197
|
*/
|
|
169
198
|
export declare class McpHarness {
|
|
@@ -239,7 +268,7 @@ export declare class McpHarness {
|
|
|
239
268
|
readonly stop: Effect.Effect<void>;
|
|
240
269
|
private constructor();
|
|
241
270
|
/** Build `server` over queue-backed stdio and return a client for it; fails with the layer's own error if it cannot build. */
|
|
242
|
-
static readonly make: <ROut, E, R>(server: Layer.Layer<ROut, E, R>, options?: McpHarnessOptions) => Effect.Effect<McpHarness, E, Scope.Scope | Exclude<R, Stdio.Stdio>>;
|
|
271
|
+
static readonly make: <ROut, E, R, S = never>(server: Layer.Layer<ROut, E, R>, options?: McpHarnessOptions<S>) => Effect.Effect<McpHarness, E, Scope.Scope | Exclude<R, Stdio.Stdio | S>>;
|
|
243
272
|
}
|
|
244
273
|
/**
|
|
245
274
|
* Options for {@link McpProbe.initialize}.
|