@vitest-agent/mcp 3.0.4 → 4.0.1

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.
Files changed (54) hide show
  1. package/README.md +7 -6
  2. package/annotations.js +18 -0
  3. package/bin/vitest-agent-mcp.js +5 -140
  4. package/{middleware/idempotency.js → idempotency.js} +48 -49
  5. package/index.d.ts +5610 -1549
  6. package/index.js +35 -17
  7. package/main.d.ts +31 -0
  8. package/main.js +144 -0
  9. package/package.json +9 -6
  10. package/prompts/layer.js +108 -0
  11. package/register-toolkit.js +311 -0
  12. package/server.js +24 -894
  13. package/{context.js → session.js} +38 -22
  14. package/toolkit.js +86 -0
  15. package/tools/acceptance-metrics.js +26 -14
  16. package/tools/cache-health.js +28 -17
  17. package/tools/commit-changes.js +33 -10
  18. package/tools/configure.js +33 -10
  19. package/tools/coverage.js +34 -5
  20. package/tools/errors.js +36 -26
  21. package/tools/failure-signature-get.js +33 -10
  22. package/tools/file-coverage.js +37 -13
  23. package/tools/help.js +45 -4
  24. package/tools/history.js +39 -19
  25. package/tools/hypothesis.js +118 -102
  26. package/tools/inventory.js +149 -146
  27. package/tools/note.js +131 -113
  28. package/tools/overview.js +33 -10
  29. package/tools/ping.js +22 -10
  30. package/tools/register-agent.js +88 -62
  31. package/tools/run-tests.js +76 -23
  32. package/tools/settings-list.js +27 -10
  33. package/tools/status.js +35 -10
  34. package/tools/tdd-artifact.js +37 -22
  35. package/tools/tdd-behavior.js +120 -108
  36. package/tools/tdd-goal.js +98 -85
  37. package/tools/tdd-phase-transition-request.js +201 -166
  38. package/tools/tdd-progress-push.js +102 -0
  39. package/tools/tdd-task.js +140 -138
  40. package/tools/test.js +152 -138
  41. package/tools/trends.js +37 -18
  42. package/tools/triage-brief.js +34 -15
  43. package/tools/turn-search.js +39 -16
  44. package/tools/wrapup-prompt.js +37 -17
  45. package/utils/crash-guards.js +0 -22
  46. package/utils/replay-marker.js +12 -0
  47. package/utils/safe-format-fatal-error.js +0 -16
  48. package/utils/tool-error-envelope.js +3 -3
  49. package/version.js +13 -0
  50. package/layers/McpLive.js +0 -29
  51. package/prompts/index.js +0 -89
  52. package/router.js +0 -74
  53. package/session-env.js +0 -112
  54. 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 };