@effected/mcp-kit 0.0.0-stage → 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/LICENSE +21 -0
- package/McpGuard.js +81 -0
- package/McpHarness.js +357 -0
- package/McpProbe.js +94 -0
- package/McpProcess.js +196 -0
- package/McpStdio.js +175 -0
- package/McpTestFailure.js +49 -0
- package/McpToolAudit.js +109 -0
- package/McpToolkit.js +298 -0
- package/README.md +574 -2
- package/StdioCapture.js +146 -0
- package/ToolFailure.js +75 -0
- package/ToolInputSchema.js +180 -0
- package/ToolOutcome.js +119 -0
- package/ToolOutputSchema.js +65 -0
- package/ToolRefusal.js +57 -0
- package/ToolReplay.js +136 -0
- package/guard.d.ts +189 -0
- package/guard.js +3 -0
- package/index.d.ts +1048 -0
- package/index.js +10 -0
- package/internal/LaunchFailed.js +34 -0
- package/internal/StdinFrames.js +201 -0
- package/internal/guardLaunch.js +13 -0
- package/internal/wire.js +65 -0
- package/node.d.ts +126 -0
- package/node.js +3 -0
- package/package.json +60 -5
- package/testing.d.ts +519 -0
- package/testing.js +7 -0
- package/tsdoc-metadata.json +11 -0
package/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { McpStdio } from "./McpStdio.js";
|
|
2
|
+
import { McpToolkit } from "./McpToolkit.js";
|
|
3
|
+
import { ToolFailure } from "./ToolFailure.js";
|
|
4
|
+
import { ToolInputSchema } from "./ToolInputSchema.js";
|
|
5
|
+
import { ToolOutcome } from "./ToolOutcome.js";
|
|
6
|
+
import { ToolOutputSchema } from "./ToolOutputSchema.js";
|
|
7
|
+
import { ToolRefusal } from "./ToolRefusal.js";
|
|
8
|
+
import { ToolReplay } from "./ToolReplay.js";
|
|
9
|
+
|
|
10
|
+
export { McpStdio, McpToolkit, ToolFailure, ToolInputSchema, ToolOutcome, ToolOutputSchema, ToolRefusal, ToolReplay };
|
|
@@ -0,0 +1,34 @@
|
|
|
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 };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { Effect, Layer, Stdio, Stream } from "effect";
|
|
2
|
+
import { McpSchema } from "effect/ai";
|
|
3
|
+
|
|
4
|
+
// src/internal/StdinFrames.ts
|
|
5
|
+
/**
|
|
6
|
+
* The line-length cap core's stdio NDJSON decoder applies, in the same unit
|
|
7
|
+
* (UTF-16 code units of the decoded text) and with the same predicate
|
|
8
|
+
* (`length > cap`). The guard answers a longer line itself, so no line core
|
|
9
|
+
* would reject ever reaches core.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
const MAX_FRAME_LENGTH = 16777216;
|
|
14
|
+
/**
|
|
15
|
+
* The JSON-RPC 2.0 answer to a frame that is not JSON, newline-framed. The id
|
|
16
|
+
* is `null` because no id can be read from an unparseable frame.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
const PARSE_ERROR_FRAME = `${JSON.stringify({
|
|
21
|
+
jsonrpc: "2.0",
|
|
22
|
+
id: null,
|
|
23
|
+
error: {
|
|
24
|
+
code: McpSchema.PARSE_ERROR_CODE,
|
|
25
|
+
message: "Parse error"
|
|
26
|
+
}
|
|
27
|
+
})}\n`;
|
|
28
|
+
/**
|
|
29
|
+
* The JSON-RPC 2.0 answer to a frame that is JSON but no JSON-RPC message
|
|
30
|
+
* core can handle, newline-framed. The id is `null` because such a frame
|
|
31
|
+
* carries no usable id.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
const INVALID_REQUEST_FRAME = `${JSON.stringify({
|
|
36
|
+
jsonrpc: "2.0",
|
|
37
|
+
id: null,
|
|
38
|
+
error: {
|
|
39
|
+
code: McpSchema.INVALID_REQUEST_ERROR_CODE,
|
|
40
|
+
message: "Invalid Request"
|
|
41
|
+
}
|
|
42
|
+
})}\n`;
|
|
43
|
+
/** JSON's insignificant whitespace, minus the newline that ends the line. */
|
|
44
|
+
const BLANK = /^[ \t\r]*$/;
|
|
45
|
+
const isNullish = (value) => value === null || value === void 0;
|
|
46
|
+
/**
|
|
47
|
+
* The guard's answer to one non-blank line within the cap, or `undefined` to
|
|
48
|
+
* forward it to core.
|
|
49
|
+
*
|
|
50
|
+
* @remarks
|
|
51
|
+
* A frame that parses is forwarded unless core would skip it without a
|
|
52
|
+
* reply, where JSON-RPC 2.0 requires an Invalid Request:
|
|
53
|
+
*
|
|
54
|
+
* - a value that is neither an object nor an array;
|
|
55
|
+
* - an object with a `method` that is not a string and no usable `id` (with
|
|
56
|
+
* an `id`, core answers `-32601` itself);
|
|
57
|
+
* - an object with neither `method` nor `id`: every JSON-RPC request carries a
|
|
58
|
+
* `method` and every response an `id`, so it is neither, and core ignores it
|
|
59
|
+
* as a response to nothing.
|
|
60
|
+
*
|
|
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`.
|
|
64
|
+
*
|
|
65
|
+
* Arrays are forwarded: core answers a batch `-32600` itself. So is any object
|
|
66
|
+
* with an `id` and no `method`, which is a response; JSON-RPC never answers a
|
|
67
|
+
* response.
|
|
68
|
+
*/
|
|
69
|
+
const answerFor = (line) => {
|
|
70
|
+
let value;
|
|
71
|
+
try {
|
|
72
|
+
value = JSON.parse(line);
|
|
73
|
+
} catch {
|
|
74
|
+
return PARSE_ERROR_FRAME;
|
|
75
|
+
}
|
|
76
|
+
if (typeof value !== "object" || value === null) return INVALID_REQUEST_FRAME;
|
|
77
|
+
if (Array.isArray(value)) return void 0;
|
|
78
|
+
const message = value;
|
|
79
|
+
if (Object.hasOwn(message, "method")) return typeof message.method !== "string" && isNullish(message.id) ? INVALID_REQUEST_FRAME : void 0;
|
|
80
|
+
return Object.hasOwn(message, "id") ? void 0 : INVALID_REQUEST_FRAME;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* A stateful step over stdin chunks that forwards only complete lines core's
|
|
84
|
+
* decoder will parse and answer, each with its newline.
|
|
85
|
+
*
|
|
86
|
+
* @remarks
|
|
87
|
+
* Core's stdio decoder skips a line it cannot use and keeps serving
|
|
88
|
+
* (Effect-TS/effect PR #8541): a line that is not JSON, a JSON value that is
|
|
89
|
+
* not an object, or an object that is no JSON-RPC message is dropped without
|
|
90
|
+
* a reply, and a line past its buffer cap is logged on stderr and dropped.
|
|
91
|
+
* JSON-RPC 2.0 requires a reply to each of them, so the guard frames stdin
|
|
92
|
+
* exactly as core does and answers such a line itself, returning
|
|
93
|
+
* {@link PARSE_ERROR_FRAME} or {@link INVALID_REQUEST_FRAME} for the caller
|
|
94
|
+
* to write.
|
|
95
|
+
*
|
|
96
|
+
* - Decoding mirrors core: one streaming UTF-8 decoder, so a character split
|
|
97
|
+
* across chunks decodes whole, a byte-order mark is stripped only at the
|
|
98
|
+
* start of the stream, and a U+FEFF opening any later line stays in that
|
|
99
|
+
* line, where `JSON.parse` rejects it as core's would.
|
|
100
|
+
* - A partial line is held until its newline arrives.
|
|
101
|
+
* - A line of JSON whitespace only is not a frame: dropped, not answered.
|
|
102
|
+
* - A line that is JSON is answered `-32600` when it is not an object or an
|
|
103
|
+
* array, when its `method` is not a string and it has no usable `id`, or
|
|
104
|
+
* when it has neither `method` nor `id`.
|
|
105
|
+
* - Everything else goes to core.
|
|
106
|
+
* - A line longer than `maxFrameLength` code units is answered once, as soon
|
|
107
|
+
* as the held part exceeds the cap, and the rest of it is discarded up to
|
|
108
|
+
* its newline. Nothing is held beyond the cap.
|
|
109
|
+
*
|
|
110
|
+
* Create one guard per stdin: its state must outlive core re-subscribing to
|
|
111
|
+
* stdin after any failure in its read loop, or a held partial line is lost.
|
|
112
|
+
*
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
const makeFrameGuard = (maxFrameLength = MAX_FRAME_LENGTH) => {
|
|
116
|
+
const decoder = new TextDecoder();
|
|
117
|
+
const encoder = new TextEncoder();
|
|
118
|
+
let pending = "";
|
|
119
|
+
let discarding = false;
|
|
120
|
+
return (chunk) => {
|
|
121
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
122
|
+
let forward = "";
|
|
123
|
+
const replies = [];
|
|
124
|
+
let start = 0;
|
|
125
|
+
if (discarding) {
|
|
126
|
+
const newline = text.indexOf("\n");
|
|
127
|
+
if (newline === -1) return {
|
|
128
|
+
forward: void 0,
|
|
129
|
+
replies
|
|
130
|
+
};
|
|
131
|
+
discarding = false;
|
|
132
|
+
start = newline + 1;
|
|
133
|
+
}
|
|
134
|
+
let newline = text.indexOf("\n", start);
|
|
135
|
+
while (newline !== -1) {
|
|
136
|
+
const line = pending + text.slice(start, newline);
|
|
137
|
+
pending = "";
|
|
138
|
+
start = newline + 1;
|
|
139
|
+
newline = text.indexOf("\n", start);
|
|
140
|
+
if (line.length > maxFrameLength) {
|
|
141
|
+
replies.push(PARSE_ERROR_FRAME);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (BLANK.test(line)) continue;
|
|
145
|
+
const answer = answerFor(line);
|
|
146
|
+
if (answer === void 0) forward += `${line}\n`;
|
|
147
|
+
else replies.push(answer);
|
|
148
|
+
}
|
|
149
|
+
pending += text.slice(start);
|
|
150
|
+
if (pending.length > maxFrameLength) {
|
|
151
|
+
pending = "";
|
|
152
|
+
discarding = true;
|
|
153
|
+
replies.push(PARSE_ERROR_FRAME);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
forward: forward === "" ? void 0 : encoder.encode(forward),
|
|
157
|
+
replies
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* `Stdio` whose `stdin` carries only lines core's decoder will parse and
|
|
163
|
+
* answer, writing the guard's reply to every other line on `stdout`: one
|
|
164
|
+
* write per chunk, replies in stdin order. Everything else is the ambient
|
|
165
|
+
* `Stdio`.
|
|
166
|
+
*
|
|
167
|
+
* @remarks
|
|
168
|
+
* One guard per call, shared by every subscription to the returned `stdin`,
|
|
169
|
+
* because core re-subscribes after a failure and builds its own decoder once.
|
|
170
|
+
*
|
|
171
|
+
* @internal
|
|
172
|
+
*/
|
|
173
|
+
const guardStdin = (stdio) => {
|
|
174
|
+
const step = makeFrameGuard();
|
|
175
|
+
return Stdio.make({
|
|
176
|
+
args: stdio.args,
|
|
177
|
+
stdinIsTerminal: stdio.stdinIsTerminal,
|
|
178
|
+
stdoutIsTerminal: stdio.stdoutIsTerminal,
|
|
179
|
+
stdout: (options) => stdio.stdout(options),
|
|
180
|
+
stderr: (options) => stdio.stderr(options),
|
|
181
|
+
stdin: stdio.stdin.pipe(Stream.mapEffect((chunk) => {
|
|
182
|
+
const { forward, replies } = step(chunk);
|
|
183
|
+
return replies.length === 0 ? Effect.succeed(forward) : Stream.run(Stream.make(replies.join("")), stdio.stdout()).pipe(Effect.as(forward));
|
|
184
|
+
}), Stream.filter((chunk) => chunk !== void 0))
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* A layer replacing the ambient `Stdio` with {@link guardStdin}'s, for one
|
|
189
|
+
* server layer only.
|
|
190
|
+
*
|
|
191
|
+
* @remarks
|
|
192
|
+
* A function, never a module constant: layers memoize by reference, so one
|
|
193
|
+
* shared constant is built once per graph, and a second server in that graph
|
|
194
|
+
* would read the first server's guarded stdin. Each call mints a fresh layer;
|
|
195
|
+
* `McpStdio.layer` calls it once per server.
|
|
196
|
+
*
|
|
197
|
+
* @internal
|
|
198
|
+
*/
|
|
199
|
+
const makeGuardedStdio = () => Layer.effect(Stdio.Stdio, Effect.map(Stdio.Stdio, guardStdin));
|
|
200
|
+
|
|
201
|
+
export { INVALID_REQUEST_FRAME, MAX_FRAME_LENGTH, PARSE_ERROR_FRAME, guardStdin, makeFrameGuard, makeGuardedStdio };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { McpStdio } from "../McpStdio.js";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
// src/internal/guardLaunch.ts
|
|
5
|
+
/**
|
|
6
|
+
* The server half of `McpGuard.run`, reached only through a dynamic import so
|
|
7
|
+
* the guard module itself evaluates nothing but its own code.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
const launchGuarded = (server, onReady) => server.runMain(McpStdio.launch(server.layer, { onReady: Effect.sync(onReady) }), { teardown: McpStdio.teardown });
|
|
12
|
+
|
|
13
|
+
export { launchGuarded };
|
package/internal/wire.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Result } from "effect";
|
|
2
|
+
|
|
3
|
+
// src/internal/wire.ts
|
|
4
|
+
/** @internal */
|
|
5
|
+
const DEFAULT_CLIENT_INFO = {
|
|
6
|
+
name: "effected-mcp-testing",
|
|
7
|
+
version: "0.0.0"
|
|
8
|
+
};
|
|
9
|
+
const isRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u);
|
|
10
|
+
/**
|
|
11
|
+
* The pointer `McpProcess` appends to a `StreamEnded` message. `McpProbe`
|
|
12
|
+
* strips it: the probe folds stderr into its own message, so the pointer is
|
|
13
|
+
* stale there.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
const STDERR_HINT = "; read stderrFinal for why";
|
|
18
|
+
/** @internal */
|
|
19
|
+
const parseFrame = (line) => Result.getOrUndefined(Result.try(() => JSON.parse(line)));
|
|
20
|
+
/** @internal */
|
|
21
|
+
const isJsonRpcMessage = (u) => isRecord(u) && u.jsonrpc === "2.0";
|
|
22
|
+
/** @internal */
|
|
23
|
+
const isResponse = (message) => (typeof message.id === "string" || typeof message.id === "number") && message.method === void 0;
|
|
24
|
+
/** @internal */
|
|
25
|
+
const requestKey = (id) => `${typeof id}:${id}`;
|
|
26
|
+
/** @internal */
|
|
27
|
+
const isStateless = (protocol) => protocol.runtime._tag === "Stateless";
|
|
28
|
+
/**
|
|
29
|
+
* A request (with `id`) or notification frame. On the stateless revision every
|
|
30
|
+
* frame carries the protocol fields under `params._meta`, with any `_meta`
|
|
31
|
+
* the caller passed spread over them, so the caller wins. That matches core's
|
|
32
|
+
* own harness (`McpStdioHarness.ts`, `withRequestMetadata`) and lets a test
|
|
33
|
+
* send a deliberately wrong revision.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
const frame = (protocol, clientInfo, method, params, id) => {
|
|
38
|
+
const withMeta = () => {
|
|
39
|
+
const base = isRecord(params) ? params : {};
|
|
40
|
+
const caller = isRecord(base._meta) ? base._meta : {};
|
|
41
|
+
return {
|
|
42
|
+
...base,
|
|
43
|
+
_meta: {
|
|
44
|
+
"io.modelcontextprotocol/protocolVersion": protocol.protocolVersion,
|
|
45
|
+
"io.modelcontextprotocol/clientCapabilities": {},
|
|
46
|
+
"io.modelcontextprotocol/clientInfo": clientInfo,
|
|
47
|
+
...caller
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
jsonrpc: "2.0",
|
|
53
|
+
...id === void 0 ? {} : { id },
|
|
54
|
+
method,
|
|
55
|
+
...isStateless(protocol) ? { params: withMeta() } : params === void 0 ? {} : { params }
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/** @internal */
|
|
59
|
+
const initializeParams = (protocol, clientInfo) => ({
|
|
60
|
+
protocolVersion: protocol.protocolVersion,
|
|
61
|
+
capabilities: {},
|
|
62
|
+
clientInfo
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export { DEFAULT_CLIENT_INFO, STDERR_HINT, frame, initializeParams, isJsonRpcMessage, isResponse, isStateless, parseFrame, requestKey };
|
package/node.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Writable } from "node:stream";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
/**
|
|
4
|
+
* Anything with a replaceable `write`: a patched stream (Node's
|
|
5
|
+
* `process.stdout` and `process.stderr`) and a sink captured writes go to (a
|
|
6
|
+
* Node `Writable`, {@link StdioCapture.nullWritable}, {@link StdioCapture.discard},
|
|
7
|
+
* {@link StdioCapture.toStderr}).
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* A sink's `write` receives exactly the arguments the patched stream's
|
|
11
|
+
* `write` was called with, including any trailing callback, so a sink must
|
|
12
|
+
* call that callback itself, as a `Writable` does.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
interface StdioSink {
|
|
17
|
+
write(chunk: any, ...rest: Array<any>): boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The two streams {@link StdioCapture.install} patches. Node's `process`
|
|
21
|
+
* satisfies it; pass it in from `main.ts`.
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
interface StdioCaptureHost {
|
|
26
|
+
readonly stdout: StdioSink;
|
|
27
|
+
readonly stderr: StdioSink;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The sinks for one capture. `stderr` passes through to the real stream when
|
|
31
|
+
* omitted.
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
interface StdioCaptureOptions {
|
|
36
|
+
readonly stdout: StdioSink;
|
|
37
|
+
readonly stderr?: StdioSink | undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Diverts a host's stdout (and optionally stderr) writes to a sink, for code
|
|
41
|
+
* running inside one async context only, while every other context keeps
|
|
42
|
+
* writing to the real streams.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* For an MCP server over stdio, stdout is the JSON-RPC wire. A chatty library
|
|
46
|
+
* run in-process (a test runner, a bundler, a linter) must keep its output off
|
|
47
|
+
* it while other handlers, and the transport itself, keep writing normally.
|
|
48
|
+
*
|
|
49
|
+
* `install` patches the host's `write` methods once, keeping the unbound
|
|
50
|
+
* originals, and routes every write through an `AsyncLocalStorage`: inside a
|
|
51
|
+
* {@link StdioCapture.run} or {@link StdioCapture.capture} context a write goes
|
|
52
|
+
* to that context's sink, and outside it to the original stream.
|
|
53
|
+
*
|
|
54
|
+
* An Effect fiber resumes in the async context it suspended in, not the
|
|
55
|
+
* context of whatever woke it, so a server fiber woken by code inside a
|
|
56
|
+
* capture (its response written to a queue, say) keeps writing to the wire.
|
|
57
|
+
*
|
|
58
|
+
* Two kinds of write escape a capture and reach the original stream:
|
|
59
|
+
*
|
|
60
|
+
* - **Writes from a context the capture lost.** Callbacks registered outside the
|
|
61
|
+
* capture keep the context they were registered in: native addons, worker
|
|
62
|
+
* thread output relayed by the main thread, listeners on a child process's
|
|
63
|
+
* pipes attached earlier. On stdout, that is the wire.
|
|
64
|
+
* - **Fibers started inside the capture keep capturing after it ends.** A fiber
|
|
65
|
+
* forked inside it (`Effect.forkDetach`, a memoized layer first built inside
|
|
66
|
+
* it, a background fiber a library starts) keeps writing to the sink, even
|
|
67
|
+
* after the capture's own work is done.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { StdioCapture } from "@effected/mcp-kit/node"
|
|
72
|
+
*
|
|
73
|
+
* const capture = StdioCapture.install(process)
|
|
74
|
+
* // in a tool handler: keep the runner's own console output off the wire
|
|
75
|
+
* const report = yield* runTests(files).pipe(capture.capture({ stdout: capture.toStderr }))
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @public
|
|
79
|
+
*/
|
|
80
|
+
export declare class StdioCapture {
|
|
81
|
+
/** A sink that writes through the host's original, unpatched stderr: captured output stays visible off the wire. */
|
|
82
|
+
readonly toStderr: StdioSink;
|
|
83
|
+
private constructor();
|
|
84
|
+
/**
|
|
85
|
+
* Patches `host`'s two streams. Idempotent per stream, across every installed
|
|
86
|
+
* copy of this package: a stream already patched (through another host
|
|
87
|
+
* object, or another copy) keeps its one patch and its original `write`.
|
|
88
|
+
*/
|
|
89
|
+
static readonly install: (host: StdioCaptureHost) => StdioCapture;
|
|
90
|
+
/**
|
|
91
|
+
* A write-only sink that drops every write and calls the write's callback.
|
|
92
|
+
*
|
|
93
|
+
* @remarks
|
|
94
|
+
* It is a {@link StdioSink}, not a stream: it has no `on`, `pipe` target
|
|
95
|
+
* protocol or `setMaxListeners`, so it cannot stand in where a library
|
|
96
|
+
* expects a `Writable` (a test runner's logger stream that worker output
|
|
97
|
+
* is piped into, for one). Use {@link StdioCapture.nullWritable} there; it
|
|
98
|
+
* serves as a capture sink too.
|
|
99
|
+
*/
|
|
100
|
+
static readonly discard: StdioSink;
|
|
101
|
+
/**
|
|
102
|
+
* A new Node `Writable` that drops everything written to it: a real stream,
|
|
103
|
+
* so it works as a `pipe` target, takes `setMaxListeners`, emits `drain`
|
|
104
|
+
* and `finish`, and is also a {@link StdioSink} for a capture.
|
|
105
|
+
*
|
|
106
|
+
* @remarks
|
|
107
|
+
* A factory, not a shared value: a stream ends once, and a source piped in
|
|
108
|
+
* with the default `{ end: true }` ends its destination. Build one per
|
|
109
|
+
* consumer.
|
|
110
|
+
*/
|
|
111
|
+
static readonly nullWritable: () => Writable;
|
|
112
|
+
/**
|
|
113
|
+
* Runs `evaluate` with its writes captured, for code that is not Effect.
|
|
114
|
+
* A synchronous throw from `evaluate` rejects the returned promise.
|
|
115
|
+
*/
|
|
116
|
+
run<A>(options: StdioCaptureOptions, evaluate: () => PromiseLike<A>): Promise<A>;
|
|
117
|
+
/**
|
|
118
|
+
* Runs an effect with its writes captured: on its own fiber, started inside
|
|
119
|
+
* the capture's context, so the context survives every suspension of that
|
|
120
|
+
* fiber. Interrupting the returned effect interrupts that fiber and waits
|
|
121
|
+
* for it.
|
|
122
|
+
*/
|
|
123
|
+
capture(options: StdioCaptureOptions): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
124
|
+
}
|
|
125
|
+
export type { StdioCaptureHost, StdioCaptureOptions, StdioSink };
|
|
126
|
+
//# sourceMappingURL=node.d.ts.map
|
package/node.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@effected/mcp-kit",
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"private": false,
|
|
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
|
+
"keywords": [
|
|
7
|
+
"effect",
|
|
8
|
+
"mcp",
|
|
9
|
+
"model-context-protocol",
|
|
10
|
+
"stdio",
|
|
11
|
+
"json-rpc",
|
|
12
|
+
"effected"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/mcp-kit#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/spencerbeggs/effected/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/spencerbeggs/effected.git",
|
|
21
|
+
"directory": "packages/mcp-kit"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": {
|
|
25
|
+
"name": "C. Spencer Beggs",
|
|
26
|
+
"email": "spencer@beggs.codes",
|
|
27
|
+
"url": "https://spencerbeg.gs"
|
|
28
|
+
},
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"type": "module",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./index.d.ts",
|
|
34
|
+
"import": "./index.js",
|
|
35
|
+
"default": "./index.js"
|
|
36
|
+
},
|
|
37
|
+
"./guard": {
|
|
38
|
+
"types": "./guard.d.ts",
|
|
39
|
+
"import": "./guard.js",
|
|
40
|
+
"default": "./guard.js"
|
|
41
|
+
},
|
|
42
|
+
"./node": {
|
|
43
|
+
"types": "./node.d.ts",
|
|
44
|
+
"import": "./node.js",
|
|
45
|
+
"default": "./node.js"
|
|
46
|
+
},
|
|
47
|
+
"./testing": {
|
|
48
|
+
"types": "./testing.d.ts",
|
|
49
|
+
"import": "./testing.js",
|
|
50
|
+
"default": "./testing.js"
|
|
51
|
+
},
|
|
52
|
+
"./package.json": "./package.json"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@effected/kit": "^0.13.0",
|
|
56
|
+
"effect": "~4.1.0"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=24.11.0"
|
|
60
|
+
}
|
|
61
|
+
}
|