@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 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 provided = options.captureLogs === false ? server.pipe(Layer.provide(stdio)) : server.pipe(Layer.provide(stdio), Layer.provide(Layer.succeed(Console.Console, captured)));
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.13.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.12.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}.