@effected/mcp-kit 0.0.0-stage → 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/LICENSE +21 -0
- package/McpGuard.js +81 -0
- package/McpHarness.js +338 -0
- package/McpProbe.js +94 -0
- package/McpProcess.js +196 -0
- package/McpStdio.js +171 -0
- package/McpTestFailure.js +49 -0
- package/McpToolAudit.js +109 -0
- package/McpToolkit.js +257 -0
- package/README.md +569 -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 +129 -0
- package/guard.d.ts +189 -0
- package/guard.js +3 -0
- package/index.d.ts +989 -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 +490 -0
- package/testing.js +7 -0
- package/tsdoc-metadata.json +11 -0
package/McpToolkit.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { ToolFailure } from "./ToolFailure.js";
|
|
2
|
+
import { ToolInputSchema } from "./ToolInputSchema.js";
|
|
3
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
4
|
+
import { McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
|
|
5
|
+
|
|
6
|
+
// src/McpToolkit.ts
|
|
7
|
+
/** The union a {@link McpToolkit.unionTool} decodes with; read by the `McpToolkit.layer` decorator. */
|
|
8
|
+
const UnionParameters = Context.Reference("@effected/mcp-kit/UnionParameters", { defaultValue: () => void 0 });
|
|
9
|
+
/**
|
|
10
|
+
* Effect's strict JSON Schema document for `parameters`, definitions attached
|
|
11
|
+
* as `$defs`, then object-rooted: the served input schema of a union tool.
|
|
12
|
+
*/
|
|
13
|
+
const unionInputJsonSchema = (parameters) => {
|
|
14
|
+
const document = Schema.toJsonSchemaDocument(parameters, { onExcessProperty: "error" });
|
|
15
|
+
return ToolInputSchema.objectRooted(Object.keys(document.definitions).length === 0 ? document.schema : {
|
|
16
|
+
...document.schema,
|
|
17
|
+
$defs: document.definitions
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
const invalidParameters = (name, message) => new McpSchema.InvalidParams({ message: `Invalid parameters for tool '${name}': ${message}` });
|
|
21
|
+
const MAX_ACCEPTED_LINES = 20;
|
|
22
|
+
/**
|
|
23
|
+
* `path` as core renders an issue path, `["a"][0]["b"]`: a segment indexing
|
|
24
|
+
* an array in `payload` is bare, any other is a JSON string. Truncated, since
|
|
25
|
+
* the segments are the caller's keys.
|
|
26
|
+
*/
|
|
27
|
+
const issuePath = (payload, path) => {
|
|
28
|
+
let node = payload;
|
|
29
|
+
let out = "";
|
|
30
|
+
for (const segment of path) {
|
|
31
|
+
out += Array.isArray(node) ? `[${segment}]` : `[${JSON.stringify(segment)}]`;
|
|
32
|
+
node = typeof node === "object" && node !== null ? node[segment] : void 0;
|
|
33
|
+
}
|
|
34
|
+
return ToolFailure.truncate(out);
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* One line per level of `payload` that carries a key the served
|
|
38
|
+
* `inputSchema` does not accept: `Accepted params at <path>: a, b.`, with
|
|
39
|
+
* `keys matching <pattern>` for a level that also accepts keys by
|
|
40
|
+
* `patternProperties`, and `This tool accepts no params.` for a
|
|
41
|
+
* zero-parameter root. At most 20 lines; empty when no level has an
|
|
42
|
+
* unknown key.
|
|
43
|
+
*/
|
|
44
|
+
const acceptedParamsLines = (payload, inputSchema) => ToolInputSchema.unknownKeys(payload, inputSchema).slice(0, MAX_ACCEPTED_LINES).map((level) => {
|
|
45
|
+
const patterns = level.acceptedPatterns ?? [];
|
|
46
|
+
if (level.path.length === 0 && level.accepted.length === 0 && patterns.length === 0) return "This tool accepts no params.";
|
|
47
|
+
const parts = [...level.accepted.length > 0 ? [level.accepted.join(", ")] : [], ...patterns.length > 0 ? [`keys matching ${patterns.join(", ")}`] : []];
|
|
48
|
+
return `Accepted params at ${level.path.length === 0 ? "the root" : issuePath(payload, level.path)}: ${parts.length > 0 ? parts.join("; ") : "(none)"}.`;
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* `error` with {@link acceptedParamsLines} appended to its message, one per
|
|
52
|
+
* line after core's own report; `error` itself when there is nothing to add.
|
|
53
|
+
* Core stays the decoder: this only annotates the report it produced.
|
|
54
|
+
*/
|
|
55
|
+
const withAcceptedParams = (error, payload, inputSchema) => {
|
|
56
|
+
const lines = acceptedParamsLines(payload, inputSchema);
|
|
57
|
+
return lines.length === 0 ? error : new McpSchema.InvalidParams({
|
|
58
|
+
message: [error.message, ...lines].join("\n"),
|
|
59
|
+
...error.data !== void 0 ? { data: error.data } : {}
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The one decode a union tool's payload gets, shared by the `McpToolkit.layer`
|
|
64
|
+
* decorator and {@link McpToolkit.unionHandler}: the union's strict decode
|
|
65
|
+
* with the options core decodes a strict `Tool.make` tool with
|
|
66
|
+
* (`onExcessProperty: "error"`, `errors: "all"`), so every excess, missing and
|
|
67
|
+
* invalid field of the matched member is reported together, worded as core
|
|
68
|
+
* words a `Tool.make` failure, with the accepted params appended as the layer
|
|
69
|
+
* appends them to core's own report.
|
|
70
|
+
*/
|
|
71
|
+
const decodeUnionPayload = (name, union, inputSchema) => {
|
|
72
|
+
const decode = Schema.decodeUnknownEffect(union);
|
|
73
|
+
return (payload) => {
|
|
74
|
+
const raw = payload ?? {};
|
|
75
|
+
return decode(raw, {
|
|
76
|
+
onExcessProperty: "error",
|
|
77
|
+
errors: "all"
|
|
78
|
+
}).pipe(Effect.mapError((error) => withAcceptedParams(invalidParameters(name, error.message), raw, inputSchema)));
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The registration `McpToolkit.layer` hands core in place of `registration`:
|
|
83
|
+
* a union tool gets its payload decoded first, with the wrapped handler built
|
|
84
|
+
* only after the decode passes, so a rejected payload never reaches it.
|
|
85
|
+
* Anything else is decoded by core as before, and core's parameter failure,
|
|
86
|
+
* the only `InvalidParams` its `handle` fails with, gets the accepted params
|
|
87
|
+
* appended.
|
|
88
|
+
*
|
|
89
|
+
* @internal
|
|
90
|
+
*/
|
|
91
|
+
const guardRegistration = (registration) => {
|
|
92
|
+
const union = Context.get(registration.annotations, UnionParameters);
|
|
93
|
+
if (union === void 0) return {
|
|
94
|
+
...registration,
|
|
95
|
+
handle: (payload) => registration.handle(payload).pipe(Effect.catchTag("InvalidParams", (error) => Effect.fail(withAcceptedParams(error, payload ?? {}, registration.tool.inputSchema))))
|
|
96
|
+
};
|
|
97
|
+
const check = decodeUnionPayload(registration.tool.name, union, registration.tool.inputSchema);
|
|
98
|
+
return {
|
|
99
|
+
...registration,
|
|
100
|
+
handle: (payload) => check(payload).pipe(Effect.flatMap(() => registration.handle(payload)))
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const DEFAULT_STRICT = "all";
|
|
104
|
+
const strictened = (toolkit, mode) => mode === "annotated" ? toolkit : Toolkit.make(...Object.values(toolkit.tools).map((tool) => Tool.isDynamic(tool) || Tool.getStrictMode(tool) !== void 0 ? tool : tool.annotate(Tool.Strict, true)));
|
|
105
|
+
/**
|
|
106
|
+
* Register a toolkit exactly as core's `McpServer.toolkit` does, except that
|
|
107
|
+
* every tool is strict by default and a {@link McpToolkit.unionTool} gets its
|
|
108
|
+
* union decoded.
|
|
109
|
+
*
|
|
110
|
+
* @remarks
|
|
111
|
+
* Rejection and reporting are core's: a tool annotated `Tool.Strict` is
|
|
112
|
+
* served with `additionalProperties: false` on every object node and decoded
|
|
113
|
+
* with `onExcessProperty: "error"` and `errors: "all"`, so one
|
|
114
|
+
* `InvalidParams` names every excess key at every depth together with every
|
|
115
|
+
* missing or invalid field. The layer appends to that report, never in place
|
|
116
|
+
* of it: for every object level of the payload that carries a key the served
|
|
117
|
+
* input schema does not accept, one line naming what that level does accept,
|
|
118
|
+
* `Accepted params at the root: query, filter.` or
|
|
119
|
+
* `Accepted params at ["filter"]: kind, tag.` (the path as core writes it),
|
|
120
|
+
* with `keys matching <pattern>` for keys a `patternProperties` level
|
|
121
|
+
* accepts, and `This tool accepts no params.` for a zero-parameter tool. An
|
|
122
|
+
* agent can fix the call from the reply alone. A failure with no unknown key
|
|
123
|
+
* (a missing or mistyped field only) is core's report unchanged. The lines
|
|
124
|
+
* are appended to core's `InvalidParams` as it leaves the tool's registered
|
|
125
|
+
* handler, the one failure that handler has for bad parameters.
|
|
126
|
+
*
|
|
127
|
+
* On top of that is the policy: under
|
|
128
|
+
* `strict: "all"` (the default) it annotates every tool that carries no
|
|
129
|
+
* `Tool.Strict` annotation of its own as strict, skipping `Tool.dynamic`
|
|
130
|
+
* tools (core dies at registration on a strict dynamic tool). It runs core's
|
|
131
|
+
* `registerToolkit` unchanged under a registration-scoped `McpServer` whose
|
|
132
|
+
* `addTool` puts the union decode in front of a union tool's handler and
|
|
133
|
+
* passes every other registration through untouched. It patches no core
|
|
134
|
+
* internals: it uses only `McpServer.registerToolkit` and `addTool`.
|
|
135
|
+
*
|
|
136
|
+
* Registration goes through core's module-level `McpServer.McpServer.layer`,
|
|
137
|
+
* shared by reference with `McpStdio.layer`'s own copy — provide both into
|
|
138
|
+
* the same graph, as with `McpServer.toolkit`. Never wrap that layer in
|
|
139
|
+
* `Layer.fresh`: tools would register into a second registry nobody serves.
|
|
140
|
+
* To give two servers separate registries, wrap each whole bundle (this
|
|
141
|
+
* layer together with `McpStdio.layer`) in `Layer.fresh` instead.
|
|
142
|
+
*
|
|
143
|
+
* Each call mints a fresh layer; bind the result to a `const` or the
|
|
144
|
+
* registration runs twice.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* import { Layer } from "effect";
|
|
149
|
+
* import { McpStdio, McpToolkit } from "@effected/mcp-kit";
|
|
150
|
+
*
|
|
151
|
+
* const ToolsLayer = McpToolkit.layer(MyToolkit).pipe(Layer.provide(MyHandlers));
|
|
152
|
+
* const ServerLayer = ToolsLayer.pipe(Layer.provideMerge(McpStdio.layer({ name: "my-server", version: "1.0.0" })));
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* @public
|
|
156
|
+
*/
|
|
157
|
+
var McpToolkit = class {
|
|
158
|
+
constructor() {}
|
|
159
|
+
/**
|
|
160
|
+
* A tool whose parameters are a `Schema.Union` of objects, which
|
|
161
|
+
* `Tool.make` cannot take: core dies at registration on a non-object
|
|
162
|
+
* `parameters` root.
|
|
163
|
+
*
|
|
164
|
+
* @remarks
|
|
165
|
+
* The tool is a `Tool.dynamic` served with Effect's strict JSON Schema
|
|
166
|
+
* document for `parameters` (`additionalProperties: false` on every
|
|
167
|
+
* object node, definitions as `$defs`), rewritten by
|
|
168
|
+
* `ToolInputSchema.objectRooted` to an object root carrying `type`,
|
|
169
|
+
* `oneOf` and `x-discriminator`. Write its handler with
|
|
170
|
+
* {@link McpToolkit.unionHandler}, which receives the decoded member.
|
|
171
|
+
*
|
|
172
|
+
* Registered through {@link McpToolkit.layer}, a union tool gets the same
|
|
173
|
+
* treatment as a strict `Tool.make` tool: a strict decode with
|
|
174
|
+
* `errors: "all"` before the handler runs, reporting every excess, missing
|
|
175
|
+
* and invalid field of the matched member in one `InvalidParams`, with the
|
|
176
|
+
* same `Accepted params at <path>: …` lines appended, so bad
|
|
177
|
+
* arguments answer JSON-RPC `-32602` on `2025-06-18` and an `isError`
|
|
178
|
+
* result on the later revisions, exactly as a `Tool.make` decode failure
|
|
179
|
+
* does. Registered through core's `McpServer.toolkit` it still decodes
|
|
180
|
+
* strictly, in the handler, but the `InvalidParams` is then a declared
|
|
181
|
+
* failure: an `isError` result on every revision.
|
|
182
|
+
*
|
|
183
|
+
* Never annotate it `Tool.Strict` true: core dies at registration on a
|
|
184
|
+
* strict tool with a raw JSON Schema.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp-kit";
|
|
189
|
+
* import { Schema } from "effect";
|
|
190
|
+
* import { Tool } from "effect/ai";
|
|
191
|
+
*
|
|
192
|
+
* // `AddNote`, `ListNotes`, `Added`, `Listed` are object schemas; `Kit` is a Toolkit that includes `Note`.
|
|
193
|
+
* const Note = McpToolkit.unionTool("note", {
|
|
194
|
+
* parameters: Schema.Union([AddNote, ListNotes]),
|
|
195
|
+
* success: ToolOutputSchema.objectRooted(Schema.Union([Added, Listed])),
|
|
196
|
+
* failure: ToolRefusal,
|
|
197
|
+
* }).annotate(Tool.Title, "Note")
|
|
198
|
+
* const handlers = Kit.toLayer({ note: McpToolkit.unionHandler(Note, (params) => handleNote(params)) })
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
static unionTool = (name, options) => {
|
|
202
|
+
const tool = Tool.dynamic(name, {
|
|
203
|
+
description: options.description,
|
|
204
|
+
parameters: unionInputJsonSchema(options.parameters),
|
|
205
|
+
success: options.success ?? Schema.Void,
|
|
206
|
+
failure: Schema.Union([options.failure ?? Schema.Never, McpSchema.InvalidParams])
|
|
207
|
+
}).annotate(UnionParameters, options.parameters);
|
|
208
|
+
return Object.assign(tool, { unionParameters: options.parameters });
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* The handler for a {@link McpToolkit.unionTool}: decodes the raw payload
|
|
212
|
+
* exactly as {@link McpToolkit.layer} does, and passes the decoded member
|
|
213
|
+
* to `handler`.
|
|
214
|
+
*
|
|
215
|
+
* @remarks
|
|
216
|
+
* A payload that does not decode strictly fails with one
|
|
217
|
+
* `McpSchema.InvalidParams`, worded as core words a `Tool.make` decode
|
|
218
|
+
* failure, naming every excess key at every depth together with every
|
|
219
|
+
* missing or invalid field of the matched member, then one
|
|
220
|
+
* `Accepted params at <path>: …` line per level with an unknown key. It
|
|
221
|
+
* is the same
|
|
222
|
+
* implementation the layer runs, so a handler called directly (a test
|
|
223
|
+
* helper, or a toolkit registered through core's `McpServer.toolkit`)
|
|
224
|
+
* reports what a client of the layer sees. Under {@link McpToolkit.layer}
|
|
225
|
+
* the layer has already rejected a bad call before the handler runs.
|
|
226
|
+
* `options.unknownKeyMessage` is deprecated and ignored.
|
|
227
|
+
*/
|
|
228
|
+
static unionHandler = (tool, handler, _options = {}) => {
|
|
229
|
+
const check = decodeUnionPayload(tool.name, tool.unionParameters, tool.jsonSchema);
|
|
230
|
+
return (payload) => check(payload).pipe(Effect.flatMap(handler));
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* `McpServer.toolkit`'s registration layer, strict by default.
|
|
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.
|
|
246
|
+
*/
|
|
247
|
+
static layer = (toolkit, options = {}) => Layer.effectDiscard(Effect.gen(function* () {
|
|
248
|
+
const registry = yield* McpServer.McpServer;
|
|
249
|
+
const decorated = McpServer.McpServer.of({
|
|
250
|
+
...registry,
|
|
251
|
+
addTool: (registration) => registry.addTool(guardRegistration(registration))
|
|
252
|
+
});
|
|
253
|
+
yield* McpServer.registerToolkit(strictened(toolkit, options.strict ?? DEFAULT_STRICT)).pipe(Effect.provideService(McpServer.McpServer, decorated));
|
|
254
|
+
})).pipe(Layer.provide(McpServer.McpServer.layer));
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export { McpToolkit, guardRegistration };
|