@vitest-agent/mcp 1.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/LICENSE +21 -0
- package/README.md +44 -0
- package/bin/vitest-agent-mcp.js +93 -0
- package/context.js +72 -0
- package/index.d.ts +1577 -0
- package/index.js +19 -0
- package/layers/McpLive.js +30 -0
- package/middleware/idempotency.js +128 -0
- package/package.json +58 -0
- package/prompts/explain-failure.js +27 -0
- package/prompts/index.js +89 -0
- package/prompts/regression-since-pass.js +28 -0
- package/prompts/tdd-resume.js +28 -0
- package/prompts/triage.js +24 -0
- package/prompts/why-flaky.js +30 -0
- package/prompts/wrapup.js +19 -0
- package/resources/index.js +155 -0
- package/resources/indexes.js +77 -0
- package/resources/manifest-schema.js +46 -0
- package/resources/paths.js +20 -0
- package/resources/patterns.js +22 -0
- package/resources/upstream-docs.js +22 -0
- package/router.js +74 -0
- package/server.js +838 -0
- package/tools/_tdd-error-envelope.js +98 -0
- package/tools/acceptance-metrics.js +75 -0
- package/tools/cache-health.js +83 -0
- package/tools/commit-changes.js +64 -0
- package/tools/configure.js +107 -0
- package/tools/coverage.js +76 -0
- package/tools/errors.js +151 -0
- package/tools/failure-signature-get.js +73 -0
- package/tools/file-coverage.js +106 -0
- package/tools/help.js +146 -0
- package/tools/history.js +121 -0
- package/tools/hypothesis.js +127 -0
- package/tools/inventory.js +377 -0
- package/tools/note.js +208 -0
- package/tools/overview.js +92 -0
- package/tools/ping.js +22 -0
- package/tools/register-agent.js +135 -0
- package/tools/run-tests.js +359 -0
- package/tools/settings-list.js +48 -0
- package/tools/status.js +74 -0
- package/tools/tdd-artifact.js +101 -0
- package/tools/tdd-behavior.js +177 -0
- package/tools/tdd-goal.js +147 -0
- package/tools/tdd-phase-transition-request.js +212 -0
- package/tools/tdd-task.js +278 -0
- package/tools/test.js +281 -0
- package/tools/trends.js +112 -0
- package/tools/triage-brief.js +42 -0
- package/tools/turn-search.js +60 -0
- package/tools/wrapup-prompt.js +49 -0
- package/tsdoc-metadata.json +11 -0
- package/utils/effect-to-zod.js +81 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { formatTriageEffect } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/tools/triage-brief.ts
|
|
6
|
+
/**
|
|
7
|
+
* `triage_brief` MCP tool — Schema-driven implementation.
|
|
8
|
+
*
|
|
9
|
+
* The structured payload is a thin envelope around the markdown
|
|
10
|
+
* rendering since this is a narrative tool — there's no underlying
|
|
11
|
+
* record set the agent would parse separately. The `hasContent` flag
|
|
12
|
+
* lets callers branch on the cold-start case without grepping prose.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
const TriageBriefResult = Schema.Struct({
|
|
17
|
+
hasContent: Schema.Boolean.annotations({ description: "`false` when no orientation signal is available yet (run tests to populate)." }),
|
|
18
|
+
markdown: Schema.String.annotations({ description: "Pre-rendered markdown brief or the empty-state message." })
|
|
19
|
+
}).annotations({
|
|
20
|
+
identifier: "TriageBriefResult",
|
|
21
|
+
title: "triage_brief result",
|
|
22
|
+
description: "Orientation triage envelope. Branch on `hasContent` for cold-start; consume `markdown` for rendering."
|
|
23
|
+
});
|
|
24
|
+
const triageBrief = publicProcedure.input(Schema.standardSchemaV1(Schema.Struct({
|
|
25
|
+
project: Schema.optional(Schema.String),
|
|
26
|
+
maxLines: Schema.optional(Schema.Number)
|
|
27
|
+
}))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
|
|
28
|
+
const md = yield* formatTriageEffect({
|
|
29
|
+
...input.project !== void 0 && { project: input.project },
|
|
30
|
+
...input.maxLines !== void 0 && { maxLines: input.maxLines }
|
|
31
|
+
});
|
|
32
|
+
return md.length > 0 ? {
|
|
33
|
+
hasContent: true,
|
|
34
|
+
markdown: md
|
|
35
|
+
} : {
|
|
36
|
+
hasContent: false,
|
|
37
|
+
markdown: "No orientation signal yet — run tests to populate the database."
|
|
38
|
+
};
|
|
39
|
+
})));
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
export { TriageBriefResult, triageBrief };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { DataReader } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, ParseResult, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/tools/turn-search.ts
|
|
6
|
+
/**
|
|
7
|
+
* `turn_search` MCP tool — Schema-driven implementation.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
const TurnRow = Schema.Struct({
|
|
12
|
+
id: Schema.Number.annotations({ description: "Numeric primary key of this turn row." }),
|
|
13
|
+
sessionId: Schema.Number.annotations({ description: "Owning `sessions.id` (integer FK)." }),
|
|
14
|
+
turnNo: Schema.Number.annotations({ description: "Turn ordinal within the session (1-based)." }),
|
|
15
|
+
type: Schema.String.annotations({ description: "Turn category (`user_prompt`, `tool_call`, `tool_result`, `file_edit`, `hook_fire`, `note`, `hypothesis`)." }),
|
|
16
|
+
payload: Schema.String.annotations({ description: "Type-specific payload as a JSON-encoded string. Decode shape depends on `type`." }),
|
|
17
|
+
occurredAt: Schema.String.annotations({ description: "ISO-8601 timestamp the turn was recorded at." })
|
|
18
|
+
}).annotations({
|
|
19
|
+
identifier: "TurnRow",
|
|
20
|
+
description: "One row from the turns log."
|
|
21
|
+
});
|
|
22
|
+
const TurnSearchResult = Schema.Struct({
|
|
23
|
+
count: Schema.Number.annotations({ description: "Number of matching turn rows returned." }),
|
|
24
|
+
turns: Schema.Array(TurnRow).annotations({ description: "Matching turns ordered by `occurredAt` ascending." })
|
|
25
|
+
}).annotations({
|
|
26
|
+
identifier: "TurnSearchResult",
|
|
27
|
+
title: "turn_search result",
|
|
28
|
+
description: "Turn-log search results across all sessions, optionally filtered by session, time, type."
|
|
29
|
+
});
|
|
30
|
+
const formatTurnSearchMarkdown = (data) => {
|
|
31
|
+
if (data.turns.length === 0) return "No turns matched.";
|
|
32
|
+
const lines = ["# Turns", ""];
|
|
33
|
+
for (const t of data.turns) lines.push(`- session=${t.sessionId} turn=${t.turnNo} type=${t.type} at=${t.occurredAt}`);
|
|
34
|
+
return lines.join("\n");
|
|
35
|
+
};
|
|
36
|
+
const TurnSearchAsMarkdown = Schema.transformOrFail(TurnSearchResult, Schema.String, {
|
|
37
|
+
strict: true,
|
|
38
|
+
decode: (data) => ParseResult.succeed(formatTurnSearchMarkdown(data)),
|
|
39
|
+
encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "TurnSearchAsMarkdown is one-way: markdown cannot be parsed back to TurnSearchResult."))
|
|
40
|
+
});
|
|
41
|
+
const turnSearch = publicProcedure.input(Schema.standardSchemaV1(Schema.Struct({
|
|
42
|
+
sessionId: Schema.optional(Schema.Number),
|
|
43
|
+
since: Schema.optional(Schema.String),
|
|
44
|
+
type: Schema.optional(Schema.Literal("user_prompt", "tool_call", "tool_result", "file_edit", "hook_fire", "note", "hypothesis")),
|
|
45
|
+
limit: Schema.optional(Schema.Number)
|
|
46
|
+
}))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
|
|
47
|
+
const rows = yield* (yield* DataReader).searchTurns({
|
|
48
|
+
...input.sessionId !== void 0 && { sessionId: input.sessionId },
|
|
49
|
+
...input.since !== void 0 && { since: input.since },
|
|
50
|
+
...input.type !== void 0 && { type: input.type },
|
|
51
|
+
limit: input.limit ?? 100
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
count: rows.length,
|
|
55
|
+
turns: rows
|
|
56
|
+
};
|
|
57
|
+
})));
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
export { TurnSearchAsMarkdown, TurnSearchResult, turnSearch };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { formatWrapupEffect } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/tools/wrapup-prompt.ts
|
|
6
|
+
/**
|
|
7
|
+
* `wrapup_prompt` MCP tool — Schema-driven implementation.
|
|
8
|
+
*
|
|
9
|
+
* Same envelope shape as `triage_brief`: thin wrapper around the
|
|
10
|
+
* markdown rendering with a `hasContent` discriminant for the empty
|
|
11
|
+
* case.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
const WrapupPromptResult = Schema.Struct({
|
|
16
|
+
hasContent: Schema.Boolean.annotations({ description: "`false` when there is nothing to wrap up for the named session/kind." }),
|
|
17
|
+
kind: Schema.Literal("stop", "session_end", "pre_compact", "tdd_handoff", "user_prompt_nudge").annotations({ description: "Echo of the wrap-up kind that was rendered (defaulted to `session_end` when omitted)." }),
|
|
18
|
+
markdown: Schema.String.annotations({ description: "Pre-rendered wrap-up markdown or the empty-state message." })
|
|
19
|
+
}).annotations({
|
|
20
|
+
identifier: "WrapupPromptResult",
|
|
21
|
+
title: "wrapup_prompt result",
|
|
22
|
+
description: "Wrap-up envelope. Branch on `hasContent` for the empty case; consume `markdown` for rendering."
|
|
23
|
+
});
|
|
24
|
+
const wrapupPrompt = publicProcedure.input(Schema.standardSchemaV1(Schema.Struct({
|
|
25
|
+
sessionId: Schema.optional(Schema.Number),
|
|
26
|
+
chatId: Schema.optional(Schema.String),
|
|
27
|
+
kind: Schema.optional(Schema.Literal("stop", "session_end", "pre_compact", "tdd_handoff", "user_prompt_nudge")),
|
|
28
|
+
userPromptHint: Schema.optional(Schema.String)
|
|
29
|
+
}))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
|
|
30
|
+
const kind = input.kind ?? "session_end";
|
|
31
|
+
const md = yield* formatWrapupEffect({
|
|
32
|
+
...input.sessionId !== void 0 && { sessionId: input.sessionId },
|
|
33
|
+
...input.chatId !== void 0 && { chatId: input.chatId },
|
|
34
|
+
kind,
|
|
35
|
+
...input.userPromptHint !== void 0 && { userPromptHint: input.userPromptHint }
|
|
36
|
+
});
|
|
37
|
+
return md.length > 0 ? {
|
|
38
|
+
hasContent: true,
|
|
39
|
+
kind,
|
|
40
|
+
markdown: md
|
|
41
|
+
} : {
|
|
42
|
+
hasContent: false,
|
|
43
|
+
kind,
|
|
44
|
+
markdown: "Nothing to wrap up."
|
|
45
|
+
};
|
|
46
|
+
})));
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
export { WrapupPromptResult, wrapupPrompt };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.58.9"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { JSONSchema } from "effect";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/utils/effect-to-zod.ts
|
|
5
|
+
/**
|
|
6
|
+
* Convert an Effect `Schema.Schema<A, I, never>` to a zod schema by
|
|
7
|
+
* serializing it to JSON Schema (`JSONSchema.make`) and ingesting the
|
|
8
|
+
* result via zod 4's `z.fromJSONSchema`.
|
|
9
|
+
*
|
|
10
|
+
* Trade-offs:
|
|
11
|
+
* - Effect-Schema-only refinements (custom predicates, Brand types)
|
|
12
|
+
* erase to plain JSON Schema primitives during the round-trip,
|
|
13
|
+
* so the resulting zod schema does not enforce them. Tools that
|
|
14
|
+
* need refinement enforcement at the MCP boundary should declare
|
|
15
|
+
* zod directly.
|
|
16
|
+
* - `Schema.NullOr(...)` round-trips correctly via JSON Schema's
|
|
17
|
+
* `oneOf` / nullable representation that zod understands.
|
|
18
|
+
* - `z.fromJSONSchema` is marked experimental in zod 4. The bridge
|
|
19
|
+
* contains a smoke-test in the corresponding test file so an
|
|
20
|
+
* incompatible upgrade surfaces immediately instead of in
|
|
21
|
+
* production tool registrations.
|
|
22
|
+
*
|
|
23
|
+
* Implementation note: zod 4's `z.fromJSONSchema` does not resolve
|
|
24
|
+
* `$ref` lookups into `$defs` — every `{ $ref: "#/$defs/X" }` it
|
|
25
|
+
* encounters throws "Reference not found". Effect's `JSONSchema.make`
|
|
26
|
+
* emits a `$ref`-and-`$defs` representation whenever a Schema carries
|
|
27
|
+
* an `identifier` annotation. The bridge therefore inlines every
|
|
28
|
+
* `$ref` in the document before handing it to zod (recursive
|
|
29
|
+
* substitution, then drop `$defs`). The schemas don't use
|
|
30
|
+
* `Schema.suspend`, so the substitution is acyclic.
|
|
31
|
+
*
|
|
32
|
+
* MCP SDK constraint: `outputSchema` must normalise to a Zod object
|
|
33
|
+
* schema (`normalizeObjectSchema` returns `undefined` for unions, then
|
|
34
|
+
* `safeParseAsync(undefined, ...)` crashes with "Cannot read properties
|
|
35
|
+
* of undefined (reading '_zod')"). When the resulting zod schema is not
|
|
36
|
+
* object-typed (e.g. came from `Schema.Union` of discriminated
|
|
37
|
+
* variants), the bridge wraps it in a permissive `z.object({}).catchall(z.unknown())`
|
|
38
|
+
* so the SDK accepts it. The structured content the tool emits still
|
|
39
|
+
* conforms to the original Effect Schema; consumers just don't get a
|
|
40
|
+
* rich JSON Schema for the union in the tool listing. Restructure the
|
|
41
|
+
* source schema as a single `Schema.Struct` with a discriminator field
|
|
42
|
+
* if the rich listing matters.
|
|
43
|
+
*/
|
|
44
|
+
const effectToZodSchema = (schema) => {
|
|
45
|
+
const inlined = inlineAllRefs(JSONSchema.make(schema));
|
|
46
|
+
const zodSchema = z.fromJSONSchema(inlined);
|
|
47
|
+
if (isObjectLike(zodSchema)) return zodSchema;
|
|
48
|
+
return z.object({}).catchall(z.unknown());
|
|
49
|
+
};
|
|
50
|
+
const isObjectLike = (schema) => {
|
|
51
|
+
const def = schema._zod?.def;
|
|
52
|
+
return def?.type === "object" || def?.shape !== void 0;
|
|
53
|
+
};
|
|
54
|
+
const REF_PREFIX = "#/$defs/";
|
|
55
|
+
/**
|
|
56
|
+
* Walk a JSON Schema tree and replace every `$ref: "#/$defs/X"` node
|
|
57
|
+
* with the contents of `$defs.X`, recursively. The `$defs` table is
|
|
58
|
+
* dropped from the returned root.
|
|
59
|
+
*/
|
|
60
|
+
const inlineAllRefs = (root) => {
|
|
61
|
+
const defs = root.$defs ?? {};
|
|
62
|
+
const visit = (value) => {
|
|
63
|
+
if (Array.isArray(value)) return value.map(visit);
|
|
64
|
+
if (value === null || typeof value !== "object") return value;
|
|
65
|
+
const obj = value;
|
|
66
|
+
if (typeof obj.$ref === "string" && obj.$ref.startsWith(REF_PREFIX)) {
|
|
67
|
+
const target = defs[obj.$ref.slice(8)];
|
|
68
|
+
if (target !== void 0) return visit(target);
|
|
69
|
+
}
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
72
|
+
if (k === "$defs") continue;
|
|
73
|
+
out[k] = visit(v);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
};
|
|
77
|
+
return visit(root);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
export { effectToZodSchema };
|