@effected/mcp 0.11.0 → 0.13.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/ToolOutcome.js DELETED
@@ -1,119 +0,0 @@
1
- import { ToolFailure } from "./ToolFailure.js";
2
- import { ToolOutputSchema } from "./ToolOutputSchema.js";
3
- import { Effect, Schema } from "effect";
4
-
5
- // src/ToolOutcome.ts
6
- /**
7
- * The `ok: false` structured-outcome envelope: an expected error an agent
8
- * should branch on, returned on a tool's success channel so it reaches the
9
- * client as `structuredContent` instead of message text.
10
- *
11
- * @remarks
12
- * Two channels, two jobs, one error shape:
13
- *
14
- * - {@link ToolRefusal} is a declared failure: the call itself was wrong, and
15
- * the agent reads one sentence (`isError: true`, message text only; core
16
- * never sends a failure's `structuredContent`).
17
- * - `ToolOutcome` is an expected domain outcome: an id that does not exist, a
18
- * transition that is not allowed. The agent branches on its `_tag` and
19
- * fields (`isError: false`, `structuredContent` carries the envelope).
20
- *
21
- * Both carry {@link ToolFailure.fields} (`message`, `remediation`), so an error
22
- * can move between channels without reshaping.
23
- *
24
- * Success members are not reshaped. Spread {@link ToolOutcome.ok} into each
25
- * success struct so a client can discriminate on `ok`; a member that
26
- * discriminates another way (`found: true | false`) stays legal.
27
- *
28
- * @example
29
- * ```ts
30
- * import { ToolOutcome } from "@effected/mcp"
31
- * import { Effect, Schema } from "effect"
32
- * import { Tool } from "effect/ai"
33
- *
34
- * const NotFound = ToolOutcome.error("GoalNotFound", { id: Schema.Number })
35
- * const Lookup = Tool.make("goal", {
36
- * parameters: Schema.Struct({ id: Schema.Number }),
37
- * success: ToolOutcome.envelope(Schema.Struct({ ...ToolOutcome.ok, goal: Goal }), [NotFound]),
38
- * })
39
- * // in the handler:
40
- * findGoal(id).pipe(
41
- * Effect.map((goal) => ({ ok: true as const, goal })),
42
- * ToolOutcome.catchAsEnvelope({
43
- * GoalNotFound: (e) => ({ id: e.id, message: `No goal ${e.id}.`, remediation: { hint: "List goals first.", suggestedTool: "goal" } }),
44
- * }),
45
- * )
46
- * ```
47
- *
48
- * @public
49
- */
50
- var ToolOutcome = class ToolOutcome {
51
- constructor() {}
52
- /** `{ ok: true }` as schema fields, to spread into each success member. */
53
- static ok = { ok: Schema.Literal(true).annotate({ description: "true: the call succeeded." }) };
54
- /**
55
- * The schema of one envelope error: `_tag`, {@link ToolFailure.fields} and
56
- * `fields`. A field named `_tag`, `message` or `remediation` is rejected by
57
- * the type, because it would replace the shared one.
58
- */
59
- static error = (tag, fields) => Schema.Struct({
60
- _tag: Schema.Literal(tag).annotate({ description: "The error's tag: the value to branch on." }),
61
- ...ToolFailure.fields,
62
- ...fields
63
- });
64
- /**
65
- * A tool's success schema: `success`, or `{ ok: false, error }` where `error`
66
- * is one of `errors`. Object-rooted with {@link ToolOutputSchema.objectRooted},
67
- * so core serves its `outputSchema` on every protocol revision.
68
- *
69
- * @remarks
70
- * The failure member, its `ok` and its `error` carry default descriptions.
71
- * `options` names the member (`identifier`, served once under `$defs`) or
72
- * replaces its description.
73
- */
74
- static envelope = (success, errors, options) => ToolOutputSchema.objectRooted(Schema.Union([success, Schema.Struct({
75
- ok: Schema.Literal(false).annotate({ description: "false: the call hit an expected error, described in error." }),
76
- error: Schema.Union(errors).annotate({ description: "The expected error. Branch on its _tag." })
77
- }).annotate({
78
- description: options?.description ?? "An expected error the caller should branch on, returned in place of a result.",
79
- ...options?.identifier === void 0 ? {} : { identifier: options.identifier }
80
- })]));
81
- /** One `ok: false` value, for a handler that decides on a failure without catching an error. */
82
- static failure = (tag, error) => ({
83
- ok: false,
84
- error: {
85
- ...error,
86
- _tag: tag
87
- }
88
- });
89
- /**
90
- * Turns each handled tagged error into its `ok: false` value on the success
91
- * channel; every other error stays in the error channel. Typed like core's
92
- * `Effect.catchTags`: a case for a tag the effect cannot fail with is a type
93
- * error, and the handled tags leave `E`.
94
- *
95
- * @remarks
96
- * Write it inline in the effect's `pipe`. `E` is inferred from the effect
97
- * being piped, so `effect.pipe(ToolOutcome.catchAsEnvelope({ … }))` checks
98
- * every case name against the effect's real tags. Bound to a `const` first,
99
- * the combinator never sees that effect: `E` infers as `unknown`, the
100
- * unknown-tag check switches off, and a misspelled tag compiles and never
101
- * matches. Core's `catchTags` looks a case up only among `cases`' own keys,
102
- * so an error tagged `"toString"` or `"constructor"` never hits an inherited
103
- * `Object.prototype` member and stays in the error channel.
104
- */
105
- static catchAsEnvelope = (cases) => (self) => {
106
- const handlers = {};
107
- for (const [tag, handle] of Object.entries(cases)) handlers[tag] = (error) => Effect.succeed(ToolOutcome.failure(tag, handle(error)));
108
- return Effect.catchTags(self, handlers);
109
- };
110
- /**
111
- * Whether `value` is an `ok: false` envelope member: an object whose `ok` is
112
- * exactly `false`. A success member that discriminates some other way, or a
113
- * non-object, is not a failure. Narrows to `{ ok: false }` only: it checks
114
- * the discriminant, not the `error` payload.
115
- */
116
- static isFailure = (value) => typeof value === "object" && value !== null && value.ok === false;
117
- };
118
-
119
- export { ToolOutcome };
@@ -1,65 +0,0 @@
1
- import { Schema, SchemaAST } from "effect";
2
-
3
- // src/ToolOutputSchema.ts
4
- const MARK = "~effected/mcp/objectRooted";
5
- /** Always passes; contributes `type: "object"` beside whatever the node already emits. */
6
- const objectRoot = Schema.makeFilter(() => true, {
7
- [MARK]: true,
8
- toJsonSchema: () => ({ type: "object" })
9
- });
10
- const isObjectRooted = (ast) => ast.checks?.some((check) => check.annotations?.[MARK] === true) === true;
11
- /**
12
- * Shaping for a tool's `success` schema, so the `outputSchema` core serves
13
- * is one every client accepts.
14
- *
15
- * @remarks
16
- * MCP requires a tool's `outputSchema` to be rooted in an object, and a
17
- * top-level `Schema.Union` emits a bare `anyOf`. Core drops such an
18
- * `outputSchema` from `tools/list` on the stateful revisions and serves it
19
- * verbatim on the stateless one, where strict clients reject it.
20
- * `McpToolAudit`'s `objectRootedOutput` check reports it either way.
21
- *
22
- * {@link ToolOutputSchema.objectRooted} is the fix when every member of the
23
- * union is an object shape; never apply it to a union with a primitive or
24
- * array member. It works on the Effect
25
- * schema, not on JSON Schema — unlike `ToolInputSchema.objectRooted`, which
26
- * rewrites the served input document of a `Tool.dynamic`.
27
- *
28
- * @public
29
- */
30
- var ToolOutputSchema = class {
31
- constructor() {}
32
- /**
33
- * `schema` with `type: "object"` added beside the `anyOf` at its JSON
34
- * Schema root, through a check that always passes. Decoding, encoding and
35
- * the schema's type are unchanged.
36
- *
37
- * @remarks
38
- * Order against `.annotate({ identifier })` does not matter. A check added
39
- * after an identifier starts a new node that would drop the definition
40
- * name, so the identifier the schema already resolves is carried onto the
41
- * new check, and the served documents are equal either way (key order
42
- * can differ when the schema carries a title or description). Applying it
43
- * twice is a no-op.
44
- *
45
- * Every union member must be an object shape: the added `type: "object"`
46
- * is a claim about the whole union, and nothing checks it. Do not use it
47
- * on a union with a primitive or array member — the served schema would
48
- * contradict the values the tool returns; give such a tool an object
49
- * envelope instead.
50
- *
51
- * @example
52
- * ```ts
53
- * const Result = ToolOutputSchema.objectRooted(Schema.Union([Found, Missing])).annotate({ identifier: "Result" })
54
- * Tool.make("lookup", { parameters, success: Result })
55
- * ```
56
- */
57
- static objectRooted = (schema) => {
58
- if (isObjectRooted(schema.ast)) return schema;
59
- const identifier = SchemaAST.resolveIdentifier(schema.ast);
60
- const checked = schema.check(objectRoot);
61
- return identifier === void 0 ? checked : checked.annotate({ identifier });
62
- };
63
- };
64
-
65
- export { ToolOutputSchema };
package/ToolRefusal.js DELETED
@@ -1,57 +0,0 @@
1
- import { ToolFailure } from "./ToolFailure.js";
2
- import { Schema } from "effect";
3
-
4
- // src/ToolRefusal.ts
5
- /**
6
- * A tool call refused for a reason the caller can fix: the kit's ready-made
7
- * declared failure, built on {@link ToolFailure.fields}.
8
- *
9
- * @remarks
10
- * Core scrubs every undeclared failure and every defect to one generic
11
- * internal-error sentence, so a refusal the agent must act on (an unknown id,
12
- * a missing argument, a precondition) reaches it only when the tool declares
13
- * it. Declare `ToolRefusal` in the tool's `failure` schema and fail with
14
- * {@link ToolRefusal.refuse}; its message already carries the remediation,
15
- * because a declared `Error` failure sends `message` and nothing else.
16
- *
17
- * Audit every throw path of a tool: any failure left undeclared loses its
18
- * message on the wire, however carefully it was worded.
19
- *
20
- * **Structured data on a refusal is deliberately unreachable: put it in the
21
- * message.** Core's `McpServer.registerToolkit` builds every failed call's
22
- * result with `structuredContent` unset and no `_meta`, on every protocol
23
- * revision. Under the default `failureMode: "error"` a declared `Error`
24
- * failure sends `message` as the only text; under `failureMode: "return"`
25
- * it sends the whole encoded failure JSON-stringified into the text, still
26
- * with no `structuredContent`, which buries the human message in JSON a
27
- * client must parse. So `refuse` takes no data argument: a list of valid
28
- * names, the root that was searched, the id that was not found all belong
29
- * in `reason`, truncated with {@link ToolFailure.truncate}. When an agent
30
- * must branch on the data itself, return it on the success channel as an
31
- * `ok: false` envelope, where `structuredContent` does reach the client.
32
- *
33
- * @example
34
- * ```ts
35
- * import { ToolFailure, ToolRefusal } from "@effected/mcp";
36
- * import { Tool } from "effect/ai";
37
- *
38
- * const Lookup = Tool.make("lookup", { parameters, success, failure: ToolRefusal })
39
- * // in the handler:
40
- * return yield* ToolRefusal.refuse(`No run "${ToolFailure.truncate(id)}".`, { hint: "List runs first.", suggestedTool: "list_runs" })
41
- * ```
42
- *
43
- * @public
44
- */
45
- var ToolRefusal = class ToolRefusal extends Schema.TaggedError()("ToolRefusal", ToolFailure.fields) {
46
- /**
47
- * A refusal for `reason`, with `remediation` folded into its message by
48
- * {@link ToolFailure.message}. `reason` must already truncate any
49
- * caller-supplied value it echoes, with {@link ToolFailure.truncate}.
50
- */
51
- static refuse = (reason, remediation) => new ToolRefusal({
52
- message: ToolFailure.message(reason, remediation),
53
- remediation
54
- });
55
- };
56
-
57
- export { ToolRefusal };
package/ToolReplay.js DELETED
@@ -1,122 +0,0 @@
1
- import { ToolOutcome } from "./ToolOutcome.js";
2
- import { Effect, Option, Schema } from "effect";
3
- import { KeyValueStore } from "effect/persistence";
4
-
5
- // src/ToolReplay.ts
6
- /**
7
- * Idempotent replay for a write tool: a repeated call with the same key returns
8
- * the first call's stored result, marked `_idempotentReplay: true`, instead of
9
- * running the mutation again.
10
- *
11
- * @remarks
12
- * The store is core's `KeyValueStore` (`effect/persistence`), required in `R`
13
- * (`KeyValueStore.layerMemory`, `layerFileSystem` or `layerSql`), unless
14
- * {@link ToolReplayOptions.store} supplies a {@link ToolReplayStore}: just
15
- * `get` and `set`, for an application's own table. Then `R` carries no
16
- * `KeyValueStore`.
17
- *
18
- * Semantics, per call:
19
- *
20
- * - A key of `Option.none()` runs the handler; nothing is read or stored.
21
- * - A stored value that decodes through `success` is returned with the marker,
22
- * and the handler does not run.
23
- * - A miss runs the handler and stores its result, best-effort. A store that
24
- * fails, a row that no longer decodes (written under an older schema, or
25
- * corrupt), and a failed write are each a miss or a skipped write, never a
26
- * tool error. Each is logged at debug level with the tool name.
27
- * - A handler failure stores nothing, so a retry runs it again.
28
- * - An `ok: false` result (a {@link ToolOutcome} envelope failure) is never
29
- * stored: a "not found" must not replay after the thing exists.
30
- *
31
- * This is replay, not exactly-once: two concurrent calls with one key both
32
- * miss and both run.
33
- *
34
- * The key is the whole identity of a request. A hit compares neither the
35
- * params nor the schema version (a row written under an older schema replays
36
- * whenever it still decodes), and rows never expire: the store keeps a result
37
- * until something removes it. A key reused with different params therefore
38
- * replays the first result, so {@link ToolReplayOptions.key} must fold in a
39
- * digest of any params that can vary under one id.
40
- *
41
- * Wrap it inside {@link ToolOutcome.catchAsEnvelope}, so the envelope sees the
42
- * handler's real errors and replay sees only real successes.
43
- *
44
- * @example
45
- * ```ts
46
- * import { ToolReplay } from "@effected/mcp"
47
- * import { Option, Schema } from "effect"
48
- *
49
- * const Created = ToolReplay.success(Schema.Struct({ ok: Schema.Literal(true), id: Schema.Number }))
50
- * const create = ToolReplay.wrap({
51
- * tool: "goal_create",
52
- * success: Created,
53
- * key: (params: { readonly requestId?: string }) => Option.fromUndefinedOr(params.requestId),
54
- * })(createGoal)
55
- * ```
56
- *
57
- * @public
58
- */
59
- var ToolReplay = class ToolReplay {
60
- constructor() {}
61
- /**
62
- * The marker field, to spread into each object member of a replayable
63
- * success schema. {@link ToolReplay.success} adds it for you.
64
- */
65
- static marker = { _idempotentReplay: Schema.optionalKey(Schema.Literal(true).annotate({ description: "Present and true when this result replays an earlier call with the same idempotency key: the stored result is returned and the mutation did not run again." })) };
66
- /**
67
- * A struct with {@link ToolReplay.marker} added. Use it for each object
68
- * success member, so core's encoder keeps the marker on a replayed reply.
69
- *
70
- * @remarks
71
- * Built with core's `fieldsAssign`, which, like `mapFields`, does not carry
72
- * the struct's own `.check(...)` refinements over: apply checks to the
73
- * result, not to the struct passed in.
74
- */
75
- static success = (schema) => schema.pipe(Schema.fieldsAssign(ToolReplay.marker));
76
- /**
77
- * The `KeyValueStore` key one call is stored under: `<tool>:<key>`. Split on
78
- * the first `:` to recover both parts; MCP tool names are `[A-Za-z0-9_.-]`.
79
- */
80
- static storeKey = (tool, key) => `${tool}:${key}`;
81
- /**
82
- * Wraps a handler with replay. Throws at wrap time when `tool` contains
83
- * `:`, a wiring mistake, so it surfaces when the server is built.
84
- */
85
- static wrap = (options) => {
86
- if (options.tool.includes(":")) throw new Error(`ToolReplay.wrap: tool name "${options.tool}" contains ":", the stored-key separator.`);
87
- const { tool, success } = options;
88
- const given = options.store;
89
- const json = Schema.fromJsonString(Schema.toCodecJson(success));
90
- const decode = Schema.decodeEffect(json);
91
- const encode = Schema.encodeEffect(json);
92
- return (handler) => (params) => {
93
- const found = options.key(params);
94
- if (Option.isNone(found)) return handler(params);
95
- const storeKey = ToolReplay.storeKey(tool, found.value);
96
- return Effect.gen(function* () {
97
- const store = given ?? (yield* KeyValueStore.KeyValueStore);
98
- const cached = yield* store.get(storeKey).pipe(Effect.flatMap((raw) => raw === void 0 ? Effect.succeedNone : Effect.asSome(decode(raw))), Effect.tapCause((cause) => Effect.logDebug("ToolReplay lookup failed; treating as a miss", {
99
- tool,
100
- cause
101
- })), Effect.orElseSucceed(() => Option.none()));
102
- if (Option.isSome(cached)) {
103
- yield* Effect.annotateCurrentSpan("mcp.tool.replayed", true);
104
- return withMarker(cached.value);
105
- }
106
- yield* Effect.annotateCurrentSpan("mcp.tool.replayed", false);
107
- const result = yield* handler(params);
108
- if (!ToolOutcome.isFailure(result)) yield* encode(result).pipe(Effect.flatMap((encoded) => store.set(storeKey, encoded)), Effect.tapCause((cause) => Effect.logDebug("ToolReplay store failed; result not stored", {
109
- tool,
110
- cause
111
- })), Effect.ignore);
112
- return result;
113
- }).pipe(Effect.withSpan("ToolReplay.wrap", { attributes: { "mcp.tool.name": tool } }));
114
- };
115
- };
116
- };
117
- const withMarker = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? {
118
- ...value,
119
- _idempotentReplay: true
120
- } : value;
121
-
122
- export { ToolReplay };
package/guard.d.ts DELETED
@@ -1,189 +0,0 @@
1
- import { Effect, Layer, Runtime } from "effect";
2
- /**
3
- * The slice of the host process the guard uses. Node's `process` satisfies it.
4
- *
5
- * @public
6
- */
7
- interface McpGuardHost {
8
- /** Register the `uncaughtException` listener. */
9
- on(event: "uncaughtException", listener: (error: Error, origin: string) => void): unknown;
10
- /** Register the `unhandledRejection` listener. */
11
- on(event: "unhandledRejection", listener: (reason: unknown) => void): unknown;
12
- /** Raise an `uncaughtException` to the registered listeners. Used only by {@link McpGuardRunOptions.injectCrash}. */
13
- emit(event: "uncaughtException", error: Error, origin: "uncaughtException" | "unhandledRejection"): unknown;
14
- /** Raise an `unhandledRejection` to the registered listeners. Used only by {@link McpGuardRunOptions.injectCrash}. */
15
- emit(event: "unhandledRejection", reason: unknown, promise: Promise<unknown>): unknown;
16
- /** Where every guard report is written. Never stdout: that is the JSON-RPC wire. */
17
- readonly stderr: {
18
- write(chunk: string): unknown;
19
- };
20
- /** End the process at once. */
21
- exit(code?: number): never;
22
- }
23
- /**
24
- * When a stray exception or rejection ends the process.
25
- *
26
- * @remarks
27
- * `"exit"` exits 1 whenever it happens. `"exitBeforeConnect"` exits 1 until
28
- * the server is serving, then logs and keeps serving: a server that dies
29
- * mid-session deregisters every tool from the client. `"log"` never exits.
30
- *
31
- * @public
32
- */
33
- interface McpGuardPolicy {
34
- /** For `uncaughtException`. */
35
- readonly onUncaught: "exit" | "exitBeforeConnect";
36
- /** For `unhandledRejection`. Defaults to `"exit"`. */
37
- readonly onRejection?: "exit" | "exitBeforeConnect" | "log" | undefined;
38
- }
39
- /**
40
- * What {@link McpGuardRunOptions.load} resolves to: the server and the
41
- * platform runner to launch it with.
42
- *
43
- * @public
44
- */
45
- interface McpGuardedServer<ROut, E> {
46
- /** The whole server layer, stdio included, with nothing left to provide. */
47
- readonly layer: Layer.Layer<ROut, E, never>;
48
- /** The platform's `runMain`, such as `NodeRuntime.runMain` from `@effect/platform-node`. */
49
- readonly runMain: (effect: Effect.Effect<never, Error>, options: {
50
- readonly teardown: Runtime.Teardown;
51
- }) => void;
52
- /** Formats a reported error from here on; the guard's own dependency-free formatter until then. */
53
- readonly format?: ((error: unknown) => string) | undefined;
54
- }
55
- /**
56
- * Options for {@link McpGuard.run}.
57
- *
58
- * @public
59
- */
60
- interface McpGuardRunOptions<ROut, E> {
61
- /** Prefixes every report the guard writes, as in `my-server: uncaughtException (…): …`. */
62
- readonly label: string;
63
- /** The process: pass `process`. */
64
- readonly host: McpGuardHost;
65
- /** Defaults to `{ onUncaught: "exit", onRejection: "exit" }`. */
66
- readonly policy?: McpGuardPolicy | undefined;
67
- /**
68
- * Import and assemble the server. Everything that can throw while loading
69
- * belongs here, behind a dynamic `import()`, so the guards are already
70
- * listening when it runs.
71
- */
72
- readonly load: () => Promise<McpGuardedServer<ROut, E>>;
73
- /**
74
- * For a test of the guards themselves: raise one stray `kind` at `at`, so
75
- * both halves of a policy can be driven end to end in a real process.
76
- *
77
- * @remarks
78
- * - `"load"` raises it once both listeners are installed and before
79
- * `load()` is called, then waits for the guard to handle it: `load()`
80
- * is not called until the listener has run. If a test double's `exit`
81
- * throws instead of ending the process, `run` rejects with what it
82
- * threw and never calls `load()`. The pre-connect half of
83
- * the policy applies, so `"exitBeforeConnect"` exits 1 here, and only
84
- * an `onRejection` of `"log"` lets the process go on to load and serve.
85
- * The report uses the guard's own formatter, since no `format` is
86
- * loaded yet.
87
- * - `"connected"` raises it on a timer right after the server is
88
- * serving, where `"exitBeforeConnect"` logs and keeps serving. A throw
89
- * from a test double's `exit` there is dropped.
90
- *
91
- * **The `"connected"` report is asynchronous.** It is raised on a later
92
- * tick, a `setTimeout(0)` after the server starts serving, so it can land
93
- * after the first responses a test reads: reading stderr once, right
94
- * after the `initialize` or `tools/list` response, can see an empty
95
- * buffer. Wait for it with `McpProcess.stderrUntil` from
96
- * `@effected/mcp/testing` rather than reading it once.
97
- *
98
- * Either is raised only through `host.emit`, on a `setTimeout(0)` tick,
99
- * never as a real throw or rejection, so the guard's listeners handle it
100
- * the same way under the real `process` and under a test double. Under
101
- * `process`, every listener registered for the event sees it, not only
102
- * the guard's. It carries an `[injected]` message; a rejection is emitted
103
- * with an already-handled rejected promise. `undefined`, or
104
- * an `at` or `kind` outside these values, does nothing at all. Wire it to
105
- * an environment variable only a test sets.
106
- */
107
- readonly injectCrash?: InjectCrash | undefined;
108
- }
109
- /**
110
- * One test-only injected crash, as {@link McpGuard.parseInjectCrash} returns
111
- * it and {@link McpGuardRunOptions.injectCrash} takes it: which point of the
112
- * launch to raise it at, and which process event to raise. The same shape as
113
- * `ProcessGuardInjection` from `@effected/engine/guard`.
114
- *
115
- * @public
116
- */
117
- interface InjectCrash {
118
- /** `"load"`: before the server module loads. `"connected"`: once the server is serving. */
119
- readonly at: "load" | "connected";
120
- /** The process event raised to the guard's listeners. */
121
- readonly kind: "uncaughtException" | "unhandledRejection";
122
- }
123
- /**
124
- * Crash guards for an MCP server process, installed before the server's
125
- * module graph is loaded. Imported from `@effected/mcp/guard`.
126
- *
127
- * @remarks
128
- * {@link McpGuard.run} is `ProcessGuard.run` from `@effected/engine/guard`
129
- * with an MCP launch in its `load`: it registers `uncaughtException` and
130
- * `unhandledRejection` listeners on `host`, then awaits `load()`, then
131
- * launches the server with `McpStdio.launch` and `McpStdio.teardown` under
132
- * the `runMain` it returned. A server on another transport (an LSP over
133
- * `vscode-languageserver`, say) uses `ProcessGuard.run` directly and calls
134
- * its `markConnected` itself. This entrypoint statically imports only
135
- * `@effected/engine/guard`, which imports nothing: it loads `effect` and the
136
- * stdio wiring only after the guards are listening, so a throw while any of
137
- * it evaluates is still reported on stderr.
138
- *
139
- * - "Connected" means the whole server layer has built, so stdin is being
140
- * read. That is before any client sends `initialize`.
141
- * - `injectCrash` drives either half of the policy from a test, before
142
- * `load()` or once connected; unset, it does nothing.
143
- * - A `load()` that rejects is reported as `startup failed` and exits 1
144
- * whatever the policy. Left to a log-only rejection listener, it would
145
- * let the event loop drain and exit 0 with no server.
146
- * - Installing an `unhandledRejection` listener switches off Node's default
147
- * of throwing on one, so `"log"` really does keep the process running.
148
- * - An exit from a guard skips Effect's finalizers and the teardown.
149
- * - Keeping on serving after a crash works because the platform `runMain`
150
- * holds the process open while the server fiber lives.
151
- * - A layer that fails to build is still reported by `McpStdio.launch`.
152
- *
153
- * @example
154
- * ```ts
155
- * import { McpGuard } from "@effected/mcp/guard";
156
- *
157
- * await McpGuard.run({
158
- * label: "my-server",
159
- * host: process,
160
- * policy: { onUncaught: "exitBeforeConnect", onRejection: "log" },
161
- * load: async () => {
162
- * const { NodeRuntime } = await import("@effect/platform-node");
163
- * const { Main } = await import("./server.js");
164
- * return { layer: Main, runMain: NodeRuntime.runMain };
165
- * },
166
- * });
167
- * ```
168
- *
169
- * @public
170
- */
171
- export declare class McpGuard {
172
- private constructor();
173
- /**
174
- * Parse a test-only crash-injection setting into {@link McpGuardRunOptions.injectCrash}: `<at>:<kind>`, where `at`
175
- * is `"load"` or `"connected"` and `kind` is `"uncaughtException"` or `"unhandledRejection"`. Anything else, or no
176
- * value, is `undefined`. The same grammar as `ProcessGuard.parseInjectCrash` from `@effected/engine/guard`, which
177
- * this is, so an MCP launcher need not import the engine guard for it.
178
- *
179
- * @example
180
- * ```ts
181
- * McpGuard.parseInjectCrash("load:uncaughtException"); // => { at: "load", kind: "uncaughtException" }
182
- * ```
183
- */
184
- static readonly parseInjectCrash: (value: string | undefined) => InjectCrash | undefined;
185
- /** Install the guards, load the server, and launch it. Resolves once the server is launched. */
186
- static readonly run: <ROut, E>(options: McpGuardRunOptions<ROut, E>) => Promise<void>;
187
- }
188
- export type { InjectCrash, McpGuardHost, McpGuardPolicy, McpGuardRunOptions, McpGuardedServer };
189
- //# sourceMappingURL=guard.d.ts.map
package/guard.js DELETED
@@ -1,3 +0,0 @@
1
- import { McpGuard } from "./McpGuard.js";
2
-
3
- export { McpGuard };
@@ -1,34 +0,0 @@
1
- import { Runtime } from "effect";
2
-
3
- // src/internal/LaunchFailed.ts
4
- /**
5
- * The failure `McpStdio.launch` re-raises after reporting a launch failure on
6
- * stderr itself. `errorReported: false` keeps `runMain` from reporting it a
7
- * second time — through a logger that writes to stdout — and the original exit
8
- * code rides along for the teardown.
9
- *
10
- * @remarks
11
- * Both runtime markers are prototype getters rather than own fields, so a JSON
12
- * or logger dump of the error does not carry them; the code lives in a private
13
- * field.
14
- *
15
- * @internal
16
- */
17
- var LaunchFailed = class extends Error {
18
- #code;
19
- constructor(code) {
20
- super(`the MCP server failed to launch (exit ${code})`);
21
- this.name = "LaunchFailed";
22
- this.#code = code;
23
- }
24
- /** Already reported on stderr: `runMain` must not report it again. */
25
- get [Runtime.errorReported]() {
26
- return false;
27
- }
28
- /** The exit code the launch failed with. */
29
- get [Runtime.errorExitCode]() {
30
- return this.#code;
31
- }
32
- };
33
-
34
- export { LaunchFailed };