@vitest-agent/mcp 3.0.4 → 4.0.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/README.md +7 -6
- package/annotations.js +18 -0
- package/bin/vitest-agent-mcp.js +5 -140
- package/{middleware/idempotency.js → idempotency.js} +48 -49
- package/index.d.ts +5610 -1549
- package/index.js +35 -17
- package/main.d.ts +31 -0
- package/main.js +144 -0
- package/package.json +9 -6
- package/prompts/layer.js +108 -0
- package/register-toolkit.js +311 -0
- package/server.js +24 -894
- package/{context.js → session.js} +38 -22
- package/toolkit.js +86 -0
- package/tools/acceptance-metrics.js +26 -14
- package/tools/cache-health.js +28 -17
- package/tools/commit-changes.js +33 -10
- package/tools/configure.js +33 -10
- package/tools/coverage.js +34 -5
- package/tools/errors.js +36 -26
- package/tools/failure-signature-get.js +33 -10
- package/tools/file-coverage.js +37 -13
- package/tools/help.js +45 -4
- package/tools/history.js +39 -19
- package/tools/hypothesis.js +118 -102
- package/tools/inventory.js +149 -146
- package/tools/note.js +131 -113
- package/tools/overview.js +33 -10
- package/tools/ping.js +22 -10
- package/tools/register-agent.js +88 -62
- package/tools/run-tests.js +76 -23
- package/tools/settings-list.js +27 -10
- package/tools/status.js +35 -10
- package/tools/tdd-artifact.js +37 -22
- package/tools/tdd-behavior.js +120 -108
- package/tools/tdd-goal.js +98 -85
- package/tools/tdd-phase-transition-request.js +201 -166
- package/tools/tdd-progress-push.js +102 -0
- package/tools/tdd-task.js +140 -138
- package/tools/test.js +152 -138
- package/tools/trends.js +37 -18
- package/tools/triage-brief.js +34 -15
- package/tools/turn-search.js +39 -16
- package/tools/wrapup-prompt.js +37 -17
- package/utils/crash-guards.js +0 -22
- package/utils/replay-marker.js +12 -0
- package/utils/safe-format-fatal-error.js +0 -16
- package/utils/tool-error-envelope.js +3 -3
- package/version.js +13 -0
- package/layers/McpLive.js +0 -29
- package/prompts/index.js +0 -89
- package/router.js +0 -74
- package/session-env.js +0 -112
- package/utils/effect-to-zod.js +0 -158
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { RenderText } from "./annotations.js";
|
|
2
|
+
import { buildUnexpectedToolErrorEnvelope } from "./utils/tool-error-envelope.js";
|
|
3
|
+
import { Cause, Context, Effect, Layer, Option, Result, Schema, Sink, Stream } from "effect";
|
|
4
|
+
import { AiError, McpSchema, McpServer, Tool } from "effect/unstable/ai";
|
|
5
|
+
|
|
6
|
+
//#region src/register-toolkit.ts
|
|
7
|
+
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
/** Top-level discriminants a union of parameter shapes may dispatch on. */
|
|
9
|
+
const DISCRIMINANT_KEYS = ["action", "kind"];
|
|
10
|
+
/** Length cap for every echoed unknown key, so a hostile payload cannot balloon the error. */
|
|
11
|
+
const ECHO_LIMIT = 200;
|
|
12
|
+
const truncate = (value) => value.length > ECHO_LIMIT ? `${value.slice(0, ECHO_LIMIT)}…` : value;
|
|
13
|
+
const literalValues = (node) => {
|
|
14
|
+
if (!isPlainObject(node)) return void 0;
|
|
15
|
+
if (Array.isArray(node.enum)) return node.enum;
|
|
16
|
+
if ("const" in node) return [node.const];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The discriminant every member of a union shares as a literal-valued
|
|
20
|
+
* property, or `undefined` when the members do not form such a union.
|
|
21
|
+
*/
|
|
22
|
+
const findDiscriminant = (members) => {
|
|
23
|
+
if (members.length === 0) return void 0;
|
|
24
|
+
for (const key of DISCRIMINANT_KEYS) if (members.every((member) => {
|
|
25
|
+
if (!isPlainObject(member) || !isPlainObject(member.properties)) return false;
|
|
26
|
+
return literalValues(member.properties[key]) !== void 0;
|
|
27
|
+
})) return key;
|
|
28
|
+
};
|
|
29
|
+
const REF_PREFIX = "#/$defs/";
|
|
30
|
+
const lookupRef = (ref, root) => {
|
|
31
|
+
if (typeof ref !== "string" || !ref.startsWith(REF_PREFIX)) return void 0;
|
|
32
|
+
const defs = root.$defs;
|
|
33
|
+
if (!isPlainObject(defs)) return void 0;
|
|
34
|
+
const target = defs[ref.slice(8)];
|
|
35
|
+
return isPlainObject(target) ? target : void 0;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Replace a `$ref` node with its `$defs` target merged under the node's
|
|
39
|
+
* own keywords (the node's keys win). Follows chained refs; gives up on a
|
|
40
|
+
* cycle or a dangling ref and returns the node as-is.
|
|
41
|
+
*/
|
|
42
|
+
const inlineRef = (node, root, seen = /* @__PURE__ */ new Set()) => {
|
|
43
|
+
const target = lookupRef(node.$ref, root);
|
|
44
|
+
if (target === void 0 || seen.has(node.$ref)) return node;
|
|
45
|
+
const { $ref, ...rest } = node;
|
|
46
|
+
return inlineRef({
|
|
47
|
+
...target,
|
|
48
|
+
...rest
|
|
49
|
+
}, root, /* @__PURE__ */ new Set([...seen, $ref]));
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* A schema whose root (or whose top-level union members) is a `$ref` —
|
|
53
|
+
* what Effect emits for any schema carrying an `identifier` annotation —
|
|
54
|
+
* is inlined so the object / discriminant checks below see the real
|
|
55
|
+
* shape. `$defs` is kept for any nested references.
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
const inlineRootRefs = (schema) => {
|
|
60
|
+
const root = inlineRef(schema, schema);
|
|
61
|
+
const combinator = Array.isArray(root.anyOf) ? "anyOf" : Array.isArray(root.oneOf) ? "oneOf" : void 0;
|
|
62
|
+
if (combinator === void 0) return root;
|
|
63
|
+
const members = root[combinator].map((member) => isPlainObject(member) ? inlineRef(member, schema) : member);
|
|
64
|
+
return {
|
|
65
|
+
...root,
|
|
66
|
+
[combinator]: members
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Deep-copy `schema`, setting `additionalProperties: false` on every
|
|
71
|
+
* object node that declares `properties` (or is a bare object with no
|
|
72
|
+
* combinator), and rewriting a top-level union of discriminated object
|
|
73
|
+
* shapes to `{ type: "object", oneOf, "x-discriminator" }` so the served
|
|
74
|
+
* schema still satisfies MCP's object-root requirement.
|
|
75
|
+
*
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
const strictifyJsonSchema = (schema) => {
|
|
79
|
+
const visit = (node) => {
|
|
80
|
+
if (Array.isArray(node)) return node.map(visit);
|
|
81
|
+
if (!isPlainObject(node)) return node;
|
|
82
|
+
const out = {};
|
|
83
|
+
for (const [key, value] of Object.entries(node)) switch (key) {
|
|
84
|
+
case "properties":
|
|
85
|
+
case "$defs":
|
|
86
|
+
case "definitions":
|
|
87
|
+
out[key] = isPlainObject(value) ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, visit(v)])) : value;
|
|
88
|
+
break;
|
|
89
|
+
case "items":
|
|
90
|
+
case "prefixItems":
|
|
91
|
+
case "anyOf":
|
|
92
|
+
case "oneOf":
|
|
93
|
+
case "allOf":
|
|
94
|
+
case "not":
|
|
95
|
+
out[key] = visit(value);
|
|
96
|
+
break;
|
|
97
|
+
case "additionalProperties":
|
|
98
|
+
out[key] = isPlainObject(value) ? visit(value) : value;
|
|
99
|
+
break;
|
|
100
|
+
default: out[key] = value;
|
|
101
|
+
}
|
|
102
|
+
const hasCombinator = Array.isArray(out.anyOf) || Array.isArray(out.oneOf) || Array.isArray(out.allOf);
|
|
103
|
+
if ((out.type === "object" || isPlainObject(out.properties)) && !hasCombinator && !isPlainObject(out.additionalProperties)) out.additionalProperties = false;
|
|
104
|
+
return out;
|
|
105
|
+
};
|
|
106
|
+
const strict = visit(inlineRootRefs(schema));
|
|
107
|
+
const members = strict.anyOf ?? strict.oneOf;
|
|
108
|
+
if (Array.isArray(members) && strict.type === void 0) {
|
|
109
|
+
const discriminant = findDiscriminant(members);
|
|
110
|
+
if (discriminant !== void 0) {
|
|
111
|
+
const { anyOf: _anyOf, oneOf: _oneOf, ...rest } = strict;
|
|
112
|
+
return {
|
|
113
|
+
type: "object",
|
|
114
|
+
...rest,
|
|
115
|
+
oneOf: members,
|
|
116
|
+
"x-discriminator": discriminant
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return strict;
|
|
121
|
+
};
|
|
122
|
+
const resolveRef = (node, root) => lookupRef(node.$ref, root) ?? node;
|
|
123
|
+
/** Pick the union member a payload object matches, or `undefined` when none does unambiguously. */
|
|
124
|
+
const selectMember = (members, value, root) => {
|
|
125
|
+
const resolved = members.map((m) => isPlainObject(m) ? resolveRef(m, root) : m);
|
|
126
|
+
const discriminant = findDiscriminant(resolved);
|
|
127
|
+
if (discriminant !== void 0) {
|
|
128
|
+
const actual = value[discriminant];
|
|
129
|
+
return resolved.find((member) => {
|
|
130
|
+
if (!isPlainObject(member) || !isPlainObject(member.properties)) return false;
|
|
131
|
+
return literalValues(member.properties[discriminant])?.includes(actual) ?? false;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const objectMembers = resolved.filter((m) => isPlainObject(m) && isPlainObject(m.properties));
|
|
135
|
+
return objectMembers.length === 1 ? objectMembers[0] : void 0;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Walk `value` against `schema`, collecting — at every object level — the
|
|
139
|
+
* keys the payload carries that the schema does not declare.
|
|
140
|
+
*
|
|
141
|
+
* @internal
|
|
142
|
+
*/
|
|
143
|
+
const collectUnknownKeys = (value, schema) => {
|
|
144
|
+
const out = [];
|
|
145
|
+
const walk = (current, rawNode, path) => {
|
|
146
|
+
if (!isPlainObject(rawNode)) return;
|
|
147
|
+
const node = resolveRef(rawNode, schema);
|
|
148
|
+
if (Array.isArray(node.allOf)) {
|
|
149
|
+
const merged = {};
|
|
150
|
+
for (const member of node.allOf) {
|
|
151
|
+
const resolvedMember = isPlainObject(member) ? resolveRef(member, schema) : member;
|
|
152
|
+
if (isPlainObject(resolvedMember) && isPlainObject(resolvedMember.properties)) Object.assign(merged, resolvedMember.properties);
|
|
153
|
+
}
|
|
154
|
+
if (Object.keys(merged).length > 0) {
|
|
155
|
+
walk(current, {
|
|
156
|
+
...node,
|
|
157
|
+
allOf: void 0,
|
|
158
|
+
properties: {
|
|
159
|
+
...merged,
|
|
160
|
+
...isPlainObject(node.properties) ? node.properties : {}
|
|
161
|
+
}
|
|
162
|
+
}, path);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (isPlainObject(node.properties)) {
|
|
167
|
+
if (!isPlainObject(current)) return;
|
|
168
|
+
const accepted = Object.keys(node.properties);
|
|
169
|
+
const unknown = Object.keys(current).filter((key) => !accepted.includes(key));
|
|
170
|
+
if (unknown.length > 0 && node.additionalProperties !== true && !isPlainObject(node.additionalProperties)) out.push({
|
|
171
|
+
path,
|
|
172
|
+
unknown,
|
|
173
|
+
accepted
|
|
174
|
+
});
|
|
175
|
+
for (const key of accepted) if (key in current) walk(current[key], node.properties[key], [...path, key]);
|
|
176
|
+
if (isPlainObject(node.additionalProperties)) for (const key of unknown) walk(current[key], node.additionalProperties, [...path, key]);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const members = node.oneOf ?? node.anyOf;
|
|
180
|
+
if (Array.isArray(members)) {
|
|
181
|
+
if (!isPlainObject(current)) return;
|
|
182
|
+
const member = selectMember(members, current, schema);
|
|
183
|
+
if (member !== void 0) walk(current, member, path);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (Array.isArray(current)) {
|
|
187
|
+
if (Array.isArray(node.prefixItems)) for (const [index, item] of node.prefixItems.entries()) walk(current[index], item, [...path, String(index)]);
|
|
188
|
+
if (isPlainObject(node.items)) for (const [index, item] of current.entries()) walk(item, node.items, [...path, String(index)]);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (isPlainObject(current) && node.type === "object" && node.additionalProperties === false) {
|
|
192
|
+
const unknown = Object.keys(current);
|
|
193
|
+
if (unknown.length > 0) out.push({
|
|
194
|
+
path,
|
|
195
|
+
unknown,
|
|
196
|
+
accepted: []
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
walk(value, schema, []);
|
|
201
|
+
return out;
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* The exact wording the pre-Effect `strict()` helper produced, so agents
|
|
205
|
+
* that learned to self-correct on it keep working.
|
|
206
|
+
*/
|
|
207
|
+
const formatUnknownKeys = (levels) => levels.map((level) => {
|
|
208
|
+
const qualify = (key) => truncate([...level.path, key].join("."));
|
|
209
|
+
const accepted = level.accepted.length === 0 ? "(none)" : level.accepted.join(", ");
|
|
210
|
+
return `Unrecognized parameter(s): ${level.unknown.map(qualify).join(", ")}. Accepted params: ${accepted}`;
|
|
211
|
+
}).join(" ");
|
|
212
|
+
/**
|
|
213
|
+
* MCP models `structuredContent` as a JSON object, so a `null`, array or
|
|
214
|
+
* primitive encoded result is omitted rather than sent through as-is.
|
|
215
|
+
*/
|
|
216
|
+
const toStructuredContent = (value) => isPlainObject(value) ? value : void 0;
|
|
217
|
+
const declaredFailureResult = (message) => new McpSchema.CallToolResult({
|
|
218
|
+
isError: true,
|
|
219
|
+
content: [{
|
|
220
|
+
type: "text",
|
|
221
|
+
text: message
|
|
222
|
+
}]
|
|
223
|
+
});
|
|
224
|
+
const envelopeResult = (toolName, err) => {
|
|
225
|
+
const envelope = buildUnexpectedToolErrorEnvelope(toolName, err);
|
|
226
|
+
return new McpSchema.CallToolResult({
|
|
227
|
+
isError: true,
|
|
228
|
+
structuredContent: envelope,
|
|
229
|
+
content: [{
|
|
230
|
+
type: "text",
|
|
231
|
+
text: JSON.stringify(envelope)
|
|
232
|
+
}]
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Register every tool of `toolkit` with the ambient `McpServer` under the
|
|
237
|
+
* strict contract described in the module docs.
|
|
238
|
+
*
|
|
239
|
+
* @internal
|
|
240
|
+
*/
|
|
241
|
+
const registerStrictToolkitEffect = Effect.fnUntraced(function* (toolkit) {
|
|
242
|
+
const registry = yield* McpServer.McpServer;
|
|
243
|
+
const built = yield* toolkit;
|
|
244
|
+
const services = yield* Effect.context();
|
|
245
|
+
for (const tool of Object.values(built.tools)) {
|
|
246
|
+
const annotations = tool.annotations;
|
|
247
|
+
const render = Context.get(annotations, RenderText);
|
|
248
|
+
const isDeclaredFailure = Schema.is(tool.failureSchema);
|
|
249
|
+
const toolMeta = Context.getOrUndefined(annotations, Tool.Meta);
|
|
250
|
+
const outputJsonSchema = inlineRootRefs(Tool.getJsonSchemaFromSchema(tool.successSchema));
|
|
251
|
+
const outputSchema = outputJsonSchema.type === "object" ? yield* Schema.decodeUnknownEffect(McpSchema.ToolJsonSchema)(outputJsonSchema).pipe(Effect.orDie) : void 0;
|
|
252
|
+
const servedInput = strictifyJsonSchema(Tool.getJsonSchema(tool));
|
|
253
|
+
const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJsonSchema)(servedInput).pipe(Effect.orDie);
|
|
254
|
+
const mcpTool = new McpSchema.Tool({
|
|
255
|
+
name: tool.name,
|
|
256
|
+
description: Tool.getDescription(tool),
|
|
257
|
+
inputSchema,
|
|
258
|
+
...outputSchema === void 0 ? {} : { outputSchema },
|
|
259
|
+
annotations: {
|
|
260
|
+
...Context.getOption(annotations, Tool.Title).pipe(Option.map((title) => ({ title })), Option.getOrUndefined),
|
|
261
|
+
readOnlyHint: Context.get(annotations, Tool.Readonly),
|
|
262
|
+
destructiveHint: Context.get(annotations, Tool.Destructive),
|
|
263
|
+
idempotentHint: Context.get(annotations, Tool.Idempotent),
|
|
264
|
+
openWorldHint: Context.get(annotations, Tool.OpenWorld)
|
|
265
|
+
},
|
|
266
|
+
_meta: toolMeta
|
|
267
|
+
});
|
|
268
|
+
yield* registry.addTool({
|
|
269
|
+
tool: mcpTool,
|
|
270
|
+
annotations,
|
|
271
|
+
handle(payload) {
|
|
272
|
+
const raw = payload ?? {};
|
|
273
|
+
const unknownKeys = collectUnknownKeys(raw, servedInput);
|
|
274
|
+
if (unknownKeys.length > 0) return Effect.fail(new McpSchema.InvalidParams({ message: formatUnknownKeys(unknownKeys) }));
|
|
275
|
+
return built.handle(tool.name, raw).pipe(Stream.unwrap, Stream.run(Sink.last()), Effect.flatMap(Effect.fromOption), Effect.map((result) => {
|
|
276
|
+
const encoded = result.encodedResult;
|
|
277
|
+
const text = render?.(encoded) ?? JSON.stringify(encoded);
|
|
278
|
+
return new McpSchema.CallToolResult({
|
|
279
|
+
isError: false,
|
|
280
|
+
structuredContent: toStructuredContent(encoded),
|
|
281
|
+
content: encoded === void 0 ? [] : [{
|
|
282
|
+
type: "text",
|
|
283
|
+
text
|
|
284
|
+
}]
|
|
285
|
+
});
|
|
286
|
+
}), Effect.provideContext(services), Effect.catchCause((cause) => {
|
|
287
|
+
const failure = Cause.findError(cause);
|
|
288
|
+
if (Result.isFailure(failure) && !Cause.hasDies(cause)) return Effect.failCause(failure.failure);
|
|
289
|
+
const err = Result.isSuccess(failure) ? failure.success : Cause.squash(cause);
|
|
290
|
+
if (AiError.isAiError(err) && err.reason._tag === "ToolParameterValidationError") return Effect.fail(new McpSchema.InvalidParams({ message: err.reason.message }));
|
|
291
|
+
const logged = Effect.logError(`tool ${tool.name} failed`, cause);
|
|
292
|
+
if (isDeclaredFailure(err) && err instanceof Error) return Effect.as(logged, declaredFailureResult(err.message));
|
|
293
|
+
return Effect.as(logged, envelopeResult(tool.name, err));
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
/**
|
|
300
|
+
* A layer that registers `toolkit` with the strict contract. Same shape
|
|
301
|
+
* as `McpServer.toolkit`: it requires the toolkit's handlers (from
|
|
302
|
+
* `kit.toLayer(handlers)`) and every service the tools declare in
|
|
303
|
+
* `dependencies`, and shares the memoized `McpServer` with the transport
|
|
304
|
+
* layer it is provided into.
|
|
305
|
+
*
|
|
306
|
+
* @public
|
|
307
|
+
*/
|
|
308
|
+
const registerStrictToolkit = (toolkit) => Layer.effectDiscard(registerStrictToolkitEffect(toolkit)).pipe(Layer.provide(McpServer.McpServer.layer));
|
|
309
|
+
|
|
310
|
+
//#endregion
|
|
311
|
+
export { collectUnknownKeys, inlineRootRefs, registerStrictToolkit, registerStrictToolkitEffect, strictifyJsonSchema };
|