@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
package/server.js
CHANGED
|
@@ -1,904 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { TestCoverageAsMarkdown, TestCoverageResult } from "./tools/coverage.js";
|
|
7
|
-
import { TestErrorsAsMarkdown, TestErrorsResult } from "./tools/errors.js";
|
|
8
|
-
import { FailureSignatureGetAsMarkdown, FailureSignatureGetResult } from "./tools/failure-signature-get.js";
|
|
9
|
-
import { FileCoverageAsMarkdown, FileCoverageResult } from "./tools/file-coverage.js";
|
|
10
|
-
import { HelpResult } from "./tools/help.js";
|
|
11
|
-
import { TestHistoryAsMarkdown, TestHistoryResult } from "./tools/history.js";
|
|
12
|
-
import { HYPOTHESIS_ACTIONS, HypothesisResult, formatHypothesisListMarkdown } from "./tools/hypothesis.js";
|
|
13
|
-
import { INVENTORY_KINDS, InventoryAsMarkdown, InventoryResult } from "./tools/inventory.js";
|
|
14
|
-
import { NOTE_ACTIONS, NoteResult, formatNoteListMarkdown } from "./tools/note.js";
|
|
15
|
-
import { TestOverviewAsMarkdown, TestOverviewResult } from "./tools/overview.js";
|
|
16
|
-
import { PingResult } from "./tools/ping.js";
|
|
17
|
-
import { RegisterAgentResult } from "./tools/register-agent.js";
|
|
18
|
-
import { RunTestsAsMarkdown, RunTestsResult } from "./tools/run-tests.js";
|
|
19
|
-
import { SettingsListAsMarkdown, SettingsListResult } from "./tools/settings-list.js";
|
|
20
|
-
import { TestStatusAsMarkdown, TestStatusResult } from "./tools/status.js";
|
|
21
|
-
import { TddArtifactListAsMarkdown, TddArtifactListResult } from "./tools/tdd-artifact.js";
|
|
22
|
-
import { TDD_BEHAVIOR_ACTIONS, TddBehaviorResult } from "./tools/tdd-behavior.js";
|
|
23
|
-
import { TDD_GOAL_ACTIONS, TddGoalResult } from "./tools/tdd-goal.js";
|
|
24
|
-
import { PhaseTransitionResult } from "./tools/tdd-phase-transition-request.js";
|
|
25
|
-
import { TDD_TASK_ACTIONS, TddTaskAsMarkdown, TddTaskResult } from "./tools/tdd-task.js";
|
|
26
|
-
import { TEST_ACTIONS, TestAsMarkdown, TestResult } from "./tools/test.js";
|
|
27
|
-
import { TestTrendsAsMarkdown, TestTrendsResult } from "./tools/trends.js";
|
|
28
|
-
import { TriageBriefResult } from "./tools/triage-brief.js";
|
|
29
|
-
import { TurnSearchAsMarkdown, TurnSearchResult } from "./tools/turn-search.js";
|
|
30
|
-
import { WrapupPromptResult } from "./tools/wrapup-prompt.js";
|
|
31
|
-
import { appRouter } from "./router.js";
|
|
32
|
-
import { registerAllPrompts } from "./prompts/index.js";
|
|
33
|
-
import { effectToZodSchema } from "./utils/effect-to-zod.js";
|
|
34
|
-
import { buildUnexpectedToolErrorEnvelope } from "./utils/tool-error-envelope.js";
|
|
35
|
-
import { Effect, Exit, Option, Schema } from "effect";
|
|
36
|
-
import { ChannelEvent, DataReader } from "@vitest-agent/sdk";
|
|
37
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
38
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
39
|
-
import { z } from "zod";
|
|
1
|
+
import { PromptsLayer } from "./prompts/layer.js";
|
|
2
|
+
import { registerStrictToolkit } from "./register-toolkit.js";
|
|
3
|
+
import { Kit, ToolsLayer } from "./toolkit.js";
|
|
4
|
+
import { Layer, Logger } from "effect";
|
|
5
|
+
import { McpProtocol, McpServer } from "effect/unstable/ai";
|
|
40
6
|
|
|
41
7
|
//#region src/server.ts
|
|
42
8
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* an agent to re-read the tool description to self-correct. Every
|
|
47
|
-
* `registerTool` input in this file goes through this helper so the whole
|
|
48
|
-
* served surface rejects unknown keys consistently (issue #200).
|
|
49
|
-
*
|
|
50
|
-
* The rule applies at EVERY object level, not just the top one: a nested
|
|
51
|
-
* plain `z.object` strips unknown keys, so a misspelled nested param
|
|
52
|
-
* decoded to an empty sub-object and the tool ran unfiltered (issue
|
|
53
|
-
* #243). Nested shapes go through this helper too.
|
|
54
|
-
*
|
|
55
|
-
* @internal
|
|
56
|
-
*/
|
|
57
|
-
function strict(shape) {
|
|
58
|
-
const acceptedKeys = Object.keys(shape);
|
|
59
|
-
return z.strictObject(shape, { error: (issue) => issue.code === "unrecognized_keys" ? `Unrecognized parameter(s): ${issue.keys.join(", ")}. Accepted params: ${acceptedKeys.join(", ")}` : void 0 });
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* For behavior-scoped events, resolve goalId/sessionId server-side from
|
|
63
|
-
* behaviorId so a stale orchestrator context cannot push the wrong tree
|
|
64
|
-
* coordinates. Goal-scoped events get sessionId resolved from goalId.
|
|
65
|
-
* Returns the enriched event object or the original on resolution failure.
|
|
66
|
-
*/
|
|
67
|
-
async function resolveChannelEvent(ctx, raw) {
|
|
68
|
-
const decoded = Schema.decodeUnknownExit(ChannelEvent)(raw);
|
|
69
|
-
if (Exit.isFailure(decoded)) return raw;
|
|
70
|
-
const event = decoded.value;
|
|
71
|
-
return ctx.runtime.runPromise(Effect.gen(function* () {
|
|
72
|
-
const reader = yield* DataReader;
|
|
73
|
-
switch (event.type) {
|
|
74
|
-
case "behavior_started":
|
|
75
|
-
case "phase_transition":
|
|
76
|
-
case "behavior_completed":
|
|
77
|
-
case "behavior_abandoned":
|
|
78
|
-
case "blocked": {
|
|
79
|
-
const goalIdOpt = yield* reader.resolveGoalIdForBehavior(event.behaviorId);
|
|
80
|
-
if (Option.isNone(goalIdOpt)) return event;
|
|
81
|
-
const goalDetailOpt = yield* reader.getGoalById(goalIdOpt.value);
|
|
82
|
-
if (Option.isNone(goalDetailOpt)) return event;
|
|
83
|
-
return {
|
|
84
|
-
...event,
|
|
85
|
-
goalId: goalIdOpt.value,
|
|
86
|
-
sessionId: goalDetailOpt.value.sessionId
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
case "goal_started":
|
|
90
|
-
case "goal_completed":
|
|
91
|
-
case "goal_abandoned": {
|
|
92
|
-
const goalDetailOpt = yield* reader.getGoalById(event.goalId);
|
|
93
|
-
if (Option.isNone(goalDetailOpt)) return event;
|
|
94
|
-
return {
|
|
95
|
-
...event,
|
|
96
|
-
sessionId: goalDetailOpt.value.sessionId
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
default: return event;
|
|
100
|
-
}
|
|
101
|
-
}));
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Emit both a human-readable text block (`content[]`) and a typed
|
|
105
|
-
* structured payload (`structuredContent`) per the MCP 2025-06-18
|
|
106
|
-
* tool-result contract.
|
|
107
|
-
*
|
|
108
|
-
* Per the spec, "for backwards compatibility, a tool that returns
|
|
109
|
-
* structured content SHOULD also return the serialized JSON in a
|
|
110
|
-
* TextContent block." The helper keeps the existing markdown/JSON
|
|
111
|
-
* text exactly as today (so the human-facing transcript is unchanged)
|
|
112
|
-
* and adds `structuredContent` on top so the LLM can parse the
|
|
113
|
-
* tool's data without inferring it from the rendered text.
|
|
114
|
-
*
|
|
115
|
-
* `structuredContent` MUST be a JSON object — not an array, not a
|
|
116
|
-
* primitive. Tools that conceptually return a list wrap it as
|
|
117
|
-
* `{ items: [...] }` (or a more specific key like `artifacts: [...]`).
|
|
118
|
-
*
|
|
119
|
-
* @internal
|
|
9
|
+
* One-line pointer agents see on `serverInfo.description`. `instructions`
|
|
10
|
+
* cannot be set through `McpServer.layerStdio` at rc.115, so this is the
|
|
11
|
+
* at-initialize orientation hook.
|
|
120
12
|
*/
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
content: [{
|
|
124
|
-
type: "text",
|
|
125
|
-
text
|
|
126
|
-
}],
|
|
127
|
-
structuredContent: structured
|
|
128
|
-
};
|
|
129
|
-
}
|
|
13
|
+
const DESCRIPTION = "vitest-agent MCP server: test results, coverage, history and TDD lifecycle for LLM coding agents. Call the `help` tool first for the full tool reference.";
|
|
130
14
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* tools that previously rendered their result as JSON.stringify; the
|
|
135
|
-
* structured payload is identical, so the agent gets the typed object
|
|
136
|
-
* via MCP's structuredContent channel without paying the markdown
|
|
137
|
-
* formatter ceremony of `Schema.decodeTo`.
|
|
138
|
-
*
|
|
139
|
-
* @internal
|
|
140
|
-
*/
|
|
141
|
-
function structuredJsonResult(value) {
|
|
142
|
-
return structuredResult(JSON.stringify(value, null, 2), value);
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Builds the fully-registered MCP server without connecting a transport.
|
|
146
|
-
*
|
|
147
|
-
* Constructs the MCP server instance, registers all tRPC-backed tools
|
|
148
|
-
* (wired through `ctx.runtime`), and calls `registerAllPrompts`. Split
|
|
149
|
-
* from {@link startMcpServer} so tests can connect the identical server
|
|
150
|
-
* to an in-memory transport and assert the *served* tool schemas — the
|
|
151
|
-
* MCP-SDK-side registrations here are hand-synced with the tRPC inputs
|
|
152
|
-
* in `tools/`, and a missed sync is invisible to router-level tests.
|
|
153
|
-
*
|
|
154
|
-
* @param ctx - the MCP context carrying the shared ManagedRuntime and session refs
|
|
155
|
-
* @public
|
|
156
|
-
*/
|
|
157
|
-
function buildMcpServer(ctx) {
|
|
158
|
-
const server = new McpServer({
|
|
159
|
-
name: "vitest-agent",
|
|
160
|
-
version: "0.1.0"
|
|
161
|
-
}, { capabilities: { experimental: { "claude/channel": {} } } });
|
|
162
|
-
const caller = createCallerFactory(appRouter)(ctx);
|
|
163
|
-
const originalRegisterTool = server.registerTool.bind(server);
|
|
164
|
-
server.registerTool = (...registerArgs) => {
|
|
165
|
-
const [name, config, cb] = registerArgs;
|
|
166
|
-
const wrapped = async (...handlerArgs) => {
|
|
167
|
-
try {
|
|
168
|
-
return await cb(...handlerArgs);
|
|
169
|
-
} catch (err) {
|
|
170
|
-
const envelope = buildUnexpectedToolErrorEnvelope(name, err);
|
|
171
|
-
console.error(`[vitest-agent-mcp] tool "${name}" resolver threw: ${envelope.error.message}`);
|
|
172
|
-
return {
|
|
173
|
-
content: [{
|
|
174
|
-
type: "text",
|
|
175
|
-
text: JSON.stringify(envelope, null, 2)
|
|
176
|
-
}],
|
|
177
|
-
isError: true,
|
|
178
|
-
structuredContent: envelope
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
return originalRegisterTool(name, config, wrapped);
|
|
183
|
-
};
|
|
184
|
-
server.registerTool("help", {
|
|
185
|
-
description: "Use when you need the catalog of available MCP tools and their parameters. Markdown in content[]; same string available as structuredContent.helpText.",
|
|
186
|
-
outputSchema: effectToZodSchema(HelpResult)
|
|
187
|
-
}, async () => {
|
|
188
|
-
const data = await caller.help();
|
|
189
|
-
return structuredResult(data.helpText, data);
|
|
190
|
-
});
|
|
191
|
-
server.registerTool("test_status", {
|
|
192
|
-
description: "Use when you need each project's current pass/fail state from the most recent run. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, manifestUpdatedAt, projectFilter?, entries[] } or absent variant).",
|
|
193
|
-
inputSchema: strict({ project: z.optional(z.string()).describe("Filter to a specific project") }),
|
|
194
|
-
outputSchema: effectToZodSchema(TestStatusResult)
|
|
195
|
-
}, async (args) => {
|
|
196
|
-
const data = await caller.test_status({ project: args.project });
|
|
197
|
-
return structuredResult(Schema.decodeSync(TestStatusAsMarkdown)(data), data);
|
|
198
|
-
});
|
|
199
|
-
server.registerTool("test_overview", {
|
|
200
|
-
description: "Use when you want a summary of the test landscape with per-project run metrics. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, projectFilter?, runs[] } or absent variant).",
|
|
201
|
-
inputSchema: strict({ project: z.optional(z.string()).describe("Filter to a specific project") }),
|
|
202
|
-
outputSchema: effectToZodSchema(TestOverviewResult)
|
|
203
|
-
}, async (args) => {
|
|
204
|
-
const data = await caller.test_overview({ project: args.project });
|
|
205
|
-
return structuredResult(Schema.decodeSync(TestOverviewAsMarkdown)(data), data);
|
|
206
|
-
});
|
|
207
|
-
server.registerTool("test_coverage", {
|
|
208
|
-
description: "Use when coverage drops and you need per-metric gap analysis against thresholds and targets. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, project, coverage } or absent variant).",
|
|
209
|
-
inputSchema: strict({ project: z.optional(z.string()).describe("Project name") }),
|
|
210
|
-
outputSchema: effectToZodSchema(TestCoverageResult)
|
|
211
|
-
}, async (args) => {
|
|
212
|
-
const data = await caller.test_coverage({ project: args.project });
|
|
213
|
-
return structuredResult(Schema.decodeSync(TestCoverageAsMarkdown)(data), data);
|
|
214
|
-
});
|
|
215
|
-
server.registerTool("test_history", {
|
|
216
|
-
description: "Use when failures recur and you need flaky, persistent, and recovered test classifications. Returns markdown in content[] and a typed JSON object in structuredContent (project, hasData, history, flaky[], persistent[], recovered[]). Optional testName/modulePath narrow to a single test; limit caps runs kept per test (default 20) — omit all three only when you actually need the whole project's history.",
|
|
217
|
-
inputSchema: strict({
|
|
218
|
-
project: z.string().describe("Project name (required)"),
|
|
219
|
-
testName: z.optional(z.string()).describe("Exact full_name match — narrows to a single test's history"),
|
|
220
|
-
modulePath: z.optional(z.string()).describe("Exact module_path match — narrows to tests in one file"),
|
|
221
|
-
limit: z.optional(z.coerce.number().int().positive()).describe("Max runs kept per test, most-recent-first; positive integer (default 20)")
|
|
222
|
-
}),
|
|
223
|
-
outputSchema: effectToZodSchema(TestHistoryResult)
|
|
224
|
-
}, async (args) => {
|
|
225
|
-
const data = await caller.test_history({
|
|
226
|
-
project: args.project,
|
|
227
|
-
testName: args.testName,
|
|
228
|
-
modulePath: args.modulePath,
|
|
229
|
-
limit: args.limit
|
|
230
|
-
});
|
|
231
|
-
return structuredResult(Schema.decodeSync(TestHistoryAsMarkdown)(data), data);
|
|
232
|
-
});
|
|
233
|
-
server.registerTool("test_trends", {
|
|
234
|
-
description: "Use when you want to see whether a project's coverage is trending up or down over time. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, project, trends? }).",
|
|
235
|
-
inputSchema: strict({
|
|
236
|
-
project: z.string().describe("Project name (required)"),
|
|
237
|
-
limit: z.optional(z.coerce.number()).describe("Max number of trend entries to return")
|
|
238
|
-
}),
|
|
239
|
-
outputSchema: effectToZodSchema(TestTrendsResult)
|
|
240
|
-
}, async (args) => {
|
|
241
|
-
const data = await caller.test_trends({
|
|
242
|
-
project: args.project,
|
|
243
|
-
limit: args.limit
|
|
244
|
-
});
|
|
245
|
-
return structuredResult(Schema.decodeSync(TestTrendsAsMarkdown)(data), data);
|
|
246
|
-
});
|
|
247
|
-
server.registerTool("test_errors", {
|
|
248
|
-
description: "Use when a test fails and you need error detail, diffs, and the cite-able test_errors.id / stack_frames.id values needed by hypothesis (action: record). Returns both a markdown rendering (in content[].text) and a typed JSON object (in structuredContent) — agents should prefer structuredContent.errors[].",
|
|
249
|
-
inputSchema: strict({
|
|
250
|
-
project: z.string().describe("Project name (required)"),
|
|
251
|
-
errorName: z.optional(z.string()).describe("Filter to a specific error name")
|
|
252
|
-
}),
|
|
253
|
-
outputSchema: effectToZodSchema(TestErrorsResult)
|
|
254
|
-
}, async (args) => {
|
|
255
|
-
const data = await caller.test_errors({
|
|
256
|
-
project: args.project,
|
|
257
|
-
...args.errorName !== void 0 && { errorName: args.errorName }
|
|
258
|
-
});
|
|
259
|
-
return structuredResult(Schema.decodeSync(TestErrorsAsMarkdown)(data), data);
|
|
260
|
-
});
|
|
261
|
-
server.registerTool("test", {
|
|
262
|
-
description: "Use to inspect tests, with an action discriminator: action='list' (project?, state?, module?, limit?) returns matching tests; action='get' (fullName, project?, modulePath?) returns details + errors + run history — a fullName that exists in more than one module returns found=false with ambiguous=true and candidateModules[], so pass modulePath to disambiguate; action='for_file' (filePath) returns test modules covering a source file; action='for_tag' (tag, project?) returns tests carrying a tag, grouped by project; action='annotations' (fullName, project?, modulePath?) returns the test annotations the author recorded via context.annotate; action='artifacts' (fullName, project?, modulePath?) returns the test artifacts recorded for the test — both return attachment descriptors (contentType, path, byteSize) and omit inline bodies unless maxBytes (a non-negative integer byte budget for the whole response, default 0) is passed, and neither has anything to do with TDD artifacts (see tdd_artifact_list). structuredContent carries the typed payload (discriminate on `action`, then on `found` for get).",
|
|
263
|
-
inputSchema: strict({
|
|
264
|
-
action: z.enum(TEST_ACTIONS).describe("Inspection discriminator"),
|
|
265
|
-
project: z.optional(z.string()),
|
|
266
|
-
state: z.optional(z.string()).describe("list: filter by state"),
|
|
267
|
-
module: z.optional(z.string()).describe("list: filter by module path"),
|
|
268
|
-
limit: z.optional(z.coerce.number()).describe("list: max rows to return"),
|
|
269
|
-
fullName: z.optional(z.string()).describe("get / annotations / artifacts: full test name"),
|
|
270
|
-
modulePath: z.optional(z.string()).describe("get / annotations / artifacts: exact module path, disambiguating a fullName present in several files"),
|
|
271
|
-
filePath: z.optional(z.string()).describe("for_file: source file path"),
|
|
272
|
-
tag: z.optional(z.string()).describe("for_tag: tag name"),
|
|
273
|
-
maxBytes: z.optional(z.coerce.number().int().nonnegative()).describe("annotations / artifacts: total byte budget for inline attachment bodies across the response; default 0 returns descriptors only")
|
|
274
|
-
}),
|
|
275
|
-
outputSchema: effectToZodSchema(TestResult)
|
|
276
|
-
}, async (args) => {
|
|
277
|
-
let data;
|
|
278
|
-
if (args.action === "list") data = await caller.test({
|
|
279
|
-
action: "list",
|
|
280
|
-
...args.project !== void 0 && { project: args.project },
|
|
281
|
-
...args.state !== void 0 && { state: args.state },
|
|
282
|
-
...args.module !== void 0 && { module: args.module },
|
|
283
|
-
...args.limit !== void 0 && { limit: args.limit }
|
|
284
|
-
});
|
|
285
|
-
else if (args.action === "get") data = await caller.test({
|
|
286
|
-
action: "get",
|
|
287
|
-
fullName: args.fullName,
|
|
288
|
-
...args.project !== void 0 && { project: args.project },
|
|
289
|
-
...args.modulePath !== void 0 && { modulePath: args.modulePath }
|
|
290
|
-
});
|
|
291
|
-
else if (args.action === "for_file") data = await caller.test({
|
|
292
|
-
action: "for_file",
|
|
293
|
-
filePath: args.filePath
|
|
294
|
-
});
|
|
295
|
-
else if (args.action === "annotations") data = await caller.test({
|
|
296
|
-
action: "annotations",
|
|
297
|
-
fullName: args.fullName,
|
|
298
|
-
...args.project !== void 0 && { project: args.project },
|
|
299
|
-
...args.modulePath !== void 0 && { modulePath: args.modulePath },
|
|
300
|
-
...args.maxBytes !== void 0 && { maxBytes: args.maxBytes }
|
|
301
|
-
});
|
|
302
|
-
else if (args.action === "artifacts") data = await caller.test({
|
|
303
|
-
action: "artifacts",
|
|
304
|
-
fullName: args.fullName,
|
|
305
|
-
...args.project !== void 0 && { project: args.project },
|
|
306
|
-
...args.modulePath !== void 0 && { modulePath: args.modulePath },
|
|
307
|
-
...args.maxBytes !== void 0 && { maxBytes: args.maxBytes }
|
|
308
|
-
});
|
|
309
|
-
else data = await caller.test({
|
|
310
|
-
action: "for_tag",
|
|
311
|
-
tag: args.tag,
|
|
312
|
-
...args.project !== void 0 && { project: args.project }
|
|
313
|
-
});
|
|
314
|
-
return structuredResult(Schema.decodeSync(TestAsMarkdown)(data), data);
|
|
315
|
-
});
|
|
316
|
-
server.registerTool("file_coverage", {
|
|
317
|
-
description: "Use when you need coverage for one source file: per-metric values, uncovered lines, and related tests. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, matched?, filePath, report?, totals?, relatedTestFiles[] }).",
|
|
318
|
-
inputSchema: strict({
|
|
319
|
-
filePath: z.string().describe("Source file path to check coverage for"),
|
|
320
|
-
project: z.optional(z.string()).describe("Project name")
|
|
321
|
-
}),
|
|
322
|
-
outputSchema: effectToZodSchema(FileCoverageResult)
|
|
323
|
-
}, async (args) => {
|
|
324
|
-
const data = await caller.file_coverage({
|
|
325
|
-
filePath: args.filePath,
|
|
326
|
-
project: args.project
|
|
327
|
-
});
|
|
328
|
-
return structuredResult(Schema.decodeSync(FileCoverageAsMarkdown)(data), data);
|
|
329
|
-
});
|
|
330
|
-
server.registerTool("configure", {
|
|
331
|
-
description: "Use when you need the captured Vitest settings for a test run. Returns markdown in content[] and a typed JSON object in structuredContent ({ found, source, settings?, requestedHash? }).",
|
|
332
|
-
inputSchema: strict({ settingsHash: z.optional(z.string()).describe("Settings hash from a manifest entry or test run") }),
|
|
333
|
-
outputSchema: effectToZodSchema(ConfigureResult)
|
|
334
|
-
}, async (args) => {
|
|
335
|
-
const data = await caller.configure({ settingsHash: args.settingsHash });
|
|
336
|
-
return structuredResult(Schema.decodeSync(ConfigureAsMarkdown)(data), data);
|
|
337
|
-
});
|
|
338
|
-
server.registerTool("cache_health", {
|
|
339
|
-
description: "Use when you suspect stale data and need manifest presence, project states, and staleness. Returns markdown in content[] and a typed JSON object in structuredContent ({ manifestPresent, manifest?, ageMs?, stale? }).",
|
|
340
|
-
outputSchema: effectToZodSchema(CacheHealthResult)
|
|
341
|
-
}, async () => {
|
|
342
|
-
const data = await caller.cache_health();
|
|
343
|
-
return structuredResult(Schema.decodeSync(CacheHealthAsMarkdown)(data), data);
|
|
344
|
-
});
|
|
345
|
-
server.registerTool("inventory", {
|
|
346
|
-
description: "Use to discover what exists in the workspace, with a kind discriminator: project / module / suite / session / tag. structuredContent discriminates on `inventoryKind` (project, module, suite, session_detail, session_list, tag_scoped, tag_unscoped) so callers can branch on the response shape without parsing markdown.",
|
|
347
|
-
inputSchema: strict({
|
|
348
|
-
kind: z.enum(INVENTORY_KINDS).describe("Inventory entity"),
|
|
349
|
-
id: z.optional(z.coerce.number()).describe("session: single-row lookup by id"),
|
|
350
|
-
project: z.optional(z.string()),
|
|
351
|
-
module: z.optional(z.string()).describe("suite: filter by module path"),
|
|
352
|
-
agentKind: z.optional(z.enum(["main", "subagent"])).describe("session: filter by agent kind"),
|
|
353
|
-
limit: z.optional(z.coerce.number()).describe("session: max rows")
|
|
354
|
-
}),
|
|
355
|
-
outputSchema: effectToZodSchema(InventoryResult)
|
|
356
|
-
}, async (args) => {
|
|
357
|
-
let data;
|
|
358
|
-
if (args.kind === "project") data = await caller.inventory({ kind: "project" });
|
|
359
|
-
else if (args.kind === "module") data = await caller.inventory({
|
|
360
|
-
kind: "module",
|
|
361
|
-
...args.project !== void 0 && { project: args.project }
|
|
362
|
-
});
|
|
363
|
-
else if (args.kind === "suite") data = await caller.inventory({
|
|
364
|
-
kind: "suite",
|
|
365
|
-
...args.project !== void 0 && { project: args.project },
|
|
366
|
-
...args.module !== void 0 && { module: args.module }
|
|
367
|
-
});
|
|
368
|
-
else if (args.kind === "session") data = await caller.inventory({
|
|
369
|
-
kind: "session",
|
|
370
|
-
...args.id !== void 0 && { id: args.id },
|
|
371
|
-
...args.project !== void 0 && { project: args.project },
|
|
372
|
-
...args.agentKind !== void 0 && { agentKind: args.agentKind },
|
|
373
|
-
...args.limit !== void 0 && { limit: args.limit }
|
|
374
|
-
});
|
|
375
|
-
else data = await caller.inventory({
|
|
376
|
-
kind: "tag",
|
|
377
|
-
...args.project !== void 0 && { project: args.project }
|
|
378
|
-
});
|
|
379
|
-
return structuredResult(Schema.decodeSync(InventoryAsMarkdown)(data), data);
|
|
380
|
-
});
|
|
381
|
-
server.registerTool("settings_list", {
|
|
382
|
-
description: "Use when you need every captured settings snapshot and its hash. Returns markdown in content[] and a typed JSON object in structuredContent ({ count, settings[] }).",
|
|
383
|
-
outputSchema: effectToZodSchema(SettingsListResult)
|
|
384
|
-
}, async () => {
|
|
385
|
-
const data = await caller.settings_list({});
|
|
386
|
-
return structuredResult(Schema.decodeSync(SettingsListAsMarkdown)(data), data);
|
|
387
|
-
});
|
|
388
|
-
server.registerTool("register_agent", {
|
|
389
|
-
description: "Use when an LLM-agent invocation starts and must be recorded in the per-project store. Idempotent on (chatId, agentType, parentAgentId, clientNonce). Returns ok:true with agentId on insert, or ok:false with error.code='AGENT_ALREADY_REGISTERED'/'PARENT_AGENT_NOT_FOUND'/'SESSION_NOT_FOUND'/'INVALID_AGENT_TYPE_PREFIX' on the four documented failure modes. agentType must begin with the host-kind prefix (e.g., 'claude-code-main').",
|
|
390
|
-
inputSchema: strict({
|
|
391
|
-
chatId: z.string().describe("Host's chat UUID (session_id from CC hook payload, etc.)"),
|
|
392
|
-
conversationId: z.optional(z.string()).describe("Canonical conversation UUID (from session-map mapConversation)"),
|
|
393
|
-
hostKind: z.optional(z.string()).describe("Host vendor identifier; defaults to 'claude-code'"),
|
|
394
|
-
agentType: z.string().describe("Agent type; must begin with the host-kind prefix"),
|
|
395
|
-
parentAgentId: z.optional(z.string()).describe("Parent agent UUID for subagent registrations"),
|
|
396
|
-
clientNonce: z.optional(z.string()).describe("Disambiguator for sibling-subagent registrations under the same parent; the server derives a deterministic default when omitted, which collapses parallel siblings into one row"),
|
|
397
|
-
startGitBranch: z.optional(z.string()),
|
|
398
|
-
startGitCommitSha: z.optional(z.string()),
|
|
399
|
-
startWorktreeDir: z.optional(z.string())
|
|
400
|
-
}),
|
|
401
|
-
outputSchema: effectToZodSchema(RegisterAgentResult)
|
|
402
|
-
}, async (args) => {
|
|
403
|
-
const result = await caller.register_agent({
|
|
404
|
-
chatId: args.chatId,
|
|
405
|
-
agentType: args.agentType,
|
|
406
|
-
...args.conversationId !== void 0 && { conversationId: args.conversationId },
|
|
407
|
-
...args.hostKind !== void 0 && { hostKind: args.hostKind },
|
|
408
|
-
...args.parentAgentId !== void 0 && { parentAgentId: args.parentAgentId },
|
|
409
|
-
...args.clientNonce !== void 0 && { clientNonce: args.clientNonce },
|
|
410
|
-
...args.startGitBranch !== void 0 && { startGitBranch: args.startGitBranch },
|
|
411
|
-
...args.startGitCommitSha !== void 0 && { startGitCommitSha: args.startGitCommitSha },
|
|
412
|
-
...args.startWorktreeDir !== void 0 && { startWorktreeDir: args.startWorktreeDir }
|
|
413
|
-
});
|
|
414
|
-
return {
|
|
415
|
-
content: [{
|
|
416
|
-
type: "text",
|
|
417
|
-
text: JSON.stringify(result)
|
|
418
|
-
}],
|
|
419
|
-
isError: result.ok === false,
|
|
420
|
-
structuredContent: result
|
|
421
|
-
};
|
|
422
|
-
});
|
|
423
|
-
server.registerTool("run_tests", {
|
|
424
|
-
description: "Use to run Vitest tests, with optional file, project, and tag filters. structuredContent carries the typed AgentReport plus per-test classifications (discriminate on `kind`: ok, timeout, error, no-match). Unknown parameters are rejected — accepted keys are files, project, tags, passWithNoTests, timeout, projectRoot. When projectRoot is omitted, the server anchors the Vitest root at the directory of the vitest (or vite) config Vitest would load anyway, walking up from its boot dir and stopping at the git root — so a server booted inside a package subtree still resolves the root config's relative globalSetup/setupFiles correctly. projectRoot overrides that for this call and is used verbatim, but only after validation: it must be an existing directory belonging to the same git repository as ctx.cwd (checked via `git rev-parse --git-common-dir`, which is identical across a repo and all its worktrees, including a sibling worktree checked out from the same repo). A path in a different repository, or a non-existent path, is rejected with `{ kind: \"error\" }` naming both paths — never a silent fallback to ctx.cwd. The resolved root actually used is always echoed back on success. The legacy format=json arg is dropped — structuredContent supersedes it.",
|
|
425
|
-
inputSchema: strict({
|
|
426
|
-
files: z.optional(z.array(z.string())).describe("Test file paths to run"),
|
|
427
|
-
project: z.optional(z.string()).describe("Project name to filter"),
|
|
428
|
-
projectRoot: z.optional(z.string()).describe("Explicit Vitest root for this call, used verbatim. Omit it to get the config-anchored default (walk up from the server's boot dir for a vitest/vite config, bounded at the git root). Prefer an absolute path; a relative path is resolved against ctx.cwd, not the server process's cwd. Validated: must be an existing directory in the same git repository as ctx.cwd (same git-common-dir, e.g. a sibling worktree). Rejected with { kind: 'error' } naming both paths otherwise."),
|
|
429
|
-
tags: z.optional(strict({
|
|
430
|
-
all: z.optional(z.array(z.string())).describe("Require every listed tag"),
|
|
431
|
-
any: z.optional(z.array(z.string())).describe("Require at least one listed tag"),
|
|
432
|
-
none: z.optional(z.array(z.string())).describe("Exclude any listed tag")
|
|
433
|
-
})).describe("Structured tag filter; all/any/none AND together with each other and with project/files"),
|
|
434
|
-
passWithNoTests: z.optional(z.boolean()).describe("Per-call override of Vitest's native test.passWithNoTests"),
|
|
435
|
-
timeout: z.optional(z.coerce.number()).describe("Timeout in seconds (default: 120)"),
|
|
436
|
-
_sessionContext: z.optional(strict({
|
|
437
|
-
chatId: z.string(),
|
|
438
|
-
conversationId: z.string(),
|
|
439
|
-
mainAgentId: z.string()
|
|
440
|
-
})).describe("Hook-injected session attribution UUIDs; do not pass manually.")
|
|
441
|
-
}),
|
|
442
|
-
outputSchema: effectToZodSchema(RunTestsResult)
|
|
443
|
-
}, async (args) => {
|
|
444
|
-
const data = await caller.run_tests({
|
|
445
|
-
files: args.files,
|
|
446
|
-
project: args.project,
|
|
447
|
-
tags: args.tags,
|
|
448
|
-
passWithNoTests: args.passWithNoTests,
|
|
449
|
-
timeout: args.timeout,
|
|
450
|
-
...args.projectRoot !== void 0 && { projectRoot: args.projectRoot },
|
|
451
|
-
...args._sessionContext !== void 0 && { _sessionContext: args._sessionContext }
|
|
452
|
-
});
|
|
453
|
-
return structuredResult(Schema.decodeSync(RunTestsAsMarkdown)(data), data);
|
|
454
|
-
});
|
|
455
|
-
server.registerTool("note", {
|
|
456
|
-
description: "Use to manage notes, with a CRUD action discriminator: action='create' writes a scoped note; action='list' (scope?, project?, testFullName?) returns matching notes; action='get' (id) returns a structured note; action='update' (id, ...patch) edits; action='delete' (id) removes; action='search' (query) does FTS5 across title and content. structuredContent always carries the typed result (discriminate on `action`); list/search additionally render markdown in the text channel.",
|
|
457
|
-
inputSchema: strict({
|
|
458
|
-
action: z.enum(NOTE_ACTIONS).describe("CRUD discriminator"),
|
|
459
|
-
id: z.optional(z.coerce.number()).describe("get/update/delete: note id"),
|
|
460
|
-
project: z.optional(z.string()),
|
|
461
|
-
title: z.optional(z.string()),
|
|
462
|
-
content: z.optional(z.string()),
|
|
463
|
-
scope: z.optional(z.enum([
|
|
464
|
-
"global",
|
|
465
|
-
"project",
|
|
466
|
-
"module",
|
|
467
|
-
"suite",
|
|
468
|
-
"test",
|
|
469
|
-
"note"
|
|
470
|
-
])).describe("create: required scope; list: optional filter"),
|
|
471
|
-
testFullName: z.optional(z.string()),
|
|
472
|
-
modulePath: z.optional(z.string()),
|
|
473
|
-
parentNoteId: z.optional(z.coerce.number()),
|
|
474
|
-
createdBy: z.optional(z.string()),
|
|
475
|
-
expiresAt: z.optional(z.string()),
|
|
476
|
-
pinned: z.optional(z.boolean()),
|
|
477
|
-
query: z.optional(z.string()).describe("search: FTS5 query")
|
|
478
|
-
}),
|
|
479
|
-
outputSchema: effectToZodSchema(NoteResult)
|
|
480
|
-
}, async (args) => {
|
|
481
|
-
if (args.action === "create") return structuredJsonResult(await caller.note({
|
|
482
|
-
action: "create",
|
|
483
|
-
title: args.title,
|
|
484
|
-
content: args.content,
|
|
485
|
-
scope: args.scope,
|
|
486
|
-
...args.project !== void 0 && { project: args.project },
|
|
487
|
-
...args.testFullName !== void 0 && { testFullName: args.testFullName },
|
|
488
|
-
...args.modulePath !== void 0 && { modulePath: args.modulePath },
|
|
489
|
-
...args.parentNoteId !== void 0 && { parentNoteId: args.parentNoteId },
|
|
490
|
-
...args.createdBy !== void 0 && { createdBy: args.createdBy },
|
|
491
|
-
...args.expiresAt !== void 0 && { expiresAt: args.expiresAt },
|
|
492
|
-
...args.pinned !== void 0 && { pinned: args.pinned }
|
|
493
|
-
}));
|
|
494
|
-
if (args.action === "list") {
|
|
495
|
-
const data = await caller.note({
|
|
496
|
-
action: "list",
|
|
497
|
-
...args.scope !== void 0 && { scope: args.scope },
|
|
498
|
-
...args.project !== void 0 && { project: args.project },
|
|
499
|
-
...args.testFullName !== void 0 && { testFullName: args.testFullName }
|
|
500
|
-
});
|
|
501
|
-
return structuredResult(formatNoteListMarkdown(data), data);
|
|
502
|
-
}
|
|
503
|
-
if (args.action === "get") return structuredJsonResult(await caller.note({
|
|
504
|
-
action: "get",
|
|
505
|
-
id: args.id
|
|
506
|
-
}));
|
|
507
|
-
if (args.action === "update") return structuredJsonResult(await caller.note({
|
|
508
|
-
action: "update",
|
|
509
|
-
id: args.id,
|
|
510
|
-
...args.title !== void 0 && { title: args.title },
|
|
511
|
-
...args.content !== void 0 && { content: args.content },
|
|
512
|
-
...args.pinned !== void 0 && { pinned: args.pinned },
|
|
513
|
-
...args.expiresAt !== void 0 && { expiresAt: args.expiresAt }
|
|
514
|
-
}));
|
|
515
|
-
if (args.action === "delete") return structuredJsonResult(await caller.note({
|
|
516
|
-
action: "delete",
|
|
517
|
-
id: args.id
|
|
518
|
-
}));
|
|
519
|
-
const searchData = await caller.note({
|
|
520
|
-
action: "search",
|
|
521
|
-
query: args.query
|
|
522
|
-
});
|
|
523
|
-
return structuredResult(formatNoteListMarkdown(searchData), searchData);
|
|
524
|
-
});
|
|
525
|
-
server.registerTool("turn_search", {
|
|
526
|
-
description: "Use when you need to find past turns across sessions by type, time, or session. Returns markdown in content[] and a typed JSON object in structuredContent ({ count, turns[] }).",
|
|
527
|
-
inputSchema: strict({
|
|
528
|
-
sessionId: z.optional(z.coerce.number()).describe("Filter to a specific session id"),
|
|
529
|
-
since: z.optional(z.string()).describe("ISO 8601 cutoff — return turns after this timestamp"),
|
|
530
|
-
type: z.optional(z.enum([
|
|
531
|
-
"user_prompt",
|
|
532
|
-
"tool_call",
|
|
533
|
-
"tool_result",
|
|
534
|
-
"file_edit",
|
|
535
|
-
"hook_fire",
|
|
536
|
-
"note",
|
|
537
|
-
"hypothesis"
|
|
538
|
-
])).describe("Filter by turn type"),
|
|
539
|
-
limit: z.optional(z.coerce.number()).describe("Max turns to return (default 100)")
|
|
540
|
-
}),
|
|
541
|
-
outputSchema: effectToZodSchema(TurnSearchResult)
|
|
542
|
-
}, async (args) => {
|
|
543
|
-
const data = await caller.turn_search({
|
|
544
|
-
sessionId: args.sessionId,
|
|
545
|
-
since: args.since,
|
|
546
|
-
type: args.type,
|
|
547
|
-
limit: args.limit
|
|
548
|
-
});
|
|
549
|
-
return structuredResult(Schema.decodeSync(TurnSearchAsMarkdown)(data), data);
|
|
550
|
-
});
|
|
551
|
-
server.registerTool("failure_signature_get", {
|
|
552
|
-
description: "Use when you have a failure-signature hash and need its first-seen date and occurrence history. Returns markdown in content[] and a typed JSON object in structuredContent ({ found, signatureHash?, firstSeenAt?, occurrenceCount?, recentErrors?[] } or absent variant).",
|
|
553
|
-
inputSchema: strict({ hash: z.string().describe("16-char failure signature hash") }),
|
|
554
|
-
outputSchema: effectToZodSchema(FailureSignatureGetResult)
|
|
555
|
-
}, async (args) => {
|
|
556
|
-
const data = await caller.failure_signature_get({ hash: args.hash });
|
|
557
|
-
return structuredResult(Schema.decodeSync(FailureSignatureGetAsMarkdown)(data), data);
|
|
558
|
-
});
|
|
559
|
-
server.registerTool("tdd_task", {
|
|
560
|
-
description: "Use to manage a TDD task lifecycle, with an action discriminator: action='start' (goal, sessionId|chatId, parentTddTaskId?, startedAt?, runId?) opens a new task; action='end' (tddTaskId, outcome, summaryNoteId?) closes one; action='get' (tddTaskId) returns markdown details; action='resume' (tddTaskId) returns a compact digest.",
|
|
561
|
-
inputSchema: strict({
|
|
562
|
-
action: z.enum(TDD_TASK_ACTIONS).describe("Lifecycle discriminator"),
|
|
563
|
-
tddTaskId: z.optional(z.coerce.number()).describe("end/get/resume: tdd task id"),
|
|
564
|
-
goal: z.optional(z.string()).describe("start: goal text"),
|
|
565
|
-
sessionId: z.optional(z.coerce.number()).describe("start: sessions.id (alternative to chatId)"),
|
|
566
|
-
chatId: z.optional(z.string()).describe("start: host chat UUID"),
|
|
567
|
-
parentTddTaskId: z.optional(z.coerce.number()).describe("start: parent task id when decomposing"),
|
|
568
|
-
startedAt: z.optional(z.string()),
|
|
569
|
-
runId: z.optional(z.string()),
|
|
570
|
-
outcome: z.optional(z.enum([
|
|
571
|
-
"succeeded",
|
|
572
|
-
"blocked",
|
|
573
|
-
"abandoned"
|
|
574
|
-
])).describe("end: final outcome"),
|
|
575
|
-
summaryNoteId: z.optional(z.coerce.number())
|
|
576
|
-
}),
|
|
577
|
-
outputSchema: effectToZodSchema(TddTaskResult)
|
|
578
|
-
}, async (args) => {
|
|
579
|
-
let data;
|
|
580
|
-
if (args.action === "start") data = await caller.tdd_task({
|
|
581
|
-
action: "start",
|
|
582
|
-
goal: args.goal,
|
|
583
|
-
...args.sessionId !== void 0 && { sessionId: args.sessionId },
|
|
584
|
-
...args.chatId !== void 0 && { chatId: args.chatId },
|
|
585
|
-
...args.parentTddTaskId !== void 0 && { parentTddTaskId: args.parentTddTaskId },
|
|
586
|
-
...args.startedAt !== void 0 && { startedAt: args.startedAt },
|
|
587
|
-
...args.runId !== void 0 && { runId: args.runId }
|
|
588
|
-
});
|
|
589
|
-
else if (args.action === "end") data = await caller.tdd_task({
|
|
590
|
-
action: "end",
|
|
591
|
-
tddTaskId: args.tddTaskId,
|
|
592
|
-
outcome: args.outcome,
|
|
593
|
-
...args.summaryNoteId !== void 0 && { summaryNoteId: args.summaryNoteId }
|
|
594
|
-
});
|
|
595
|
-
else if (args.action === "get") data = await caller.tdd_task({
|
|
596
|
-
action: "get",
|
|
597
|
-
tddTaskId: args.tddTaskId
|
|
598
|
-
});
|
|
599
|
-
else data = await caller.tdd_task({
|
|
600
|
-
action: "resume",
|
|
601
|
-
tddTaskId: args.tddTaskId
|
|
602
|
-
});
|
|
603
|
-
return structuredResult(Schema.decodeSync(TddTaskAsMarkdown)(data), data);
|
|
604
|
-
});
|
|
605
|
-
server.registerTool("tdd_phase_transition_request", {
|
|
606
|
-
description: "Use when advancing a TDD cycle and you need a phase transition validated and recorded. Validates goal status, behavior↔goal membership, and D2 artifact-evidence binding rules; returns accept/deny. On accept, auto-promotes a behavior 'pending' → 'in_progress' when behaviorId is supplied. citedArtifactId is OPTIONAL — when omitted, the most recent matching artifact is auto-resolved (kind comes from citedArtifactKind if supplied, otherwise from the transition's required-evidence rule). Transitions like spike→red that require no artifact need neither field. The accepted response echoes citedArtifactId + citedArtifactSource so the caller can see which row was picked.",
|
|
607
|
-
inputSchema: strict({
|
|
608
|
-
tddTaskId: z.coerce.number().describe("tdd_tasks.id"),
|
|
609
|
-
goalId: z.coerce.number().describe("tdd_session_goals.id (required; goal must be in_progress)"),
|
|
610
|
-
requestedPhase: z.enum([
|
|
611
|
-
"spike",
|
|
612
|
-
"red",
|
|
613
|
-
"red.triangulate",
|
|
614
|
-
"green",
|
|
615
|
-
"green.fake-it",
|
|
616
|
-
"refactor",
|
|
617
|
-
"extended-red",
|
|
618
|
-
"green-without-red"
|
|
619
|
-
]).describe("Phase to transition to"),
|
|
620
|
-
citedArtifactId: z.optional(z.coerce.number()).describe("tdd_artifacts.id supplying the evidence. Optional — auto-resolved when omitted."),
|
|
621
|
-
citedArtifactKind: z.optional(z.enum([
|
|
622
|
-
"test_written",
|
|
623
|
-
"test_failed_run",
|
|
624
|
-
"code_written",
|
|
625
|
-
"test_passed_run",
|
|
626
|
-
"refactor",
|
|
627
|
-
"test_weakened"
|
|
628
|
-
])).describe("Kind to look up when citedArtifactId is omitted (defaults to the kind required by the transition)."),
|
|
629
|
-
behaviorId: z.optional(z.coerce.number()).describe("tdd_session_behaviors.id when transitioning a specific behavior (must belong to goalId)"),
|
|
630
|
-
reason: z.optional(z.string()).describe("Free-text reason for the transition")
|
|
631
|
-
}),
|
|
632
|
-
outputSchema: effectToZodSchema(PhaseTransitionResult)
|
|
633
|
-
}, async (args) => structuredJsonResult(await caller.tdd_phase_transition_request({
|
|
634
|
-
tddTaskId: args.tddTaskId,
|
|
635
|
-
goalId: args.goalId,
|
|
636
|
-
requestedPhase: args.requestedPhase,
|
|
637
|
-
...args.citedArtifactId !== void 0 && { citedArtifactId: args.citedArtifactId },
|
|
638
|
-
...args.citedArtifactKind !== void 0 && { citedArtifactKind: args.citedArtifactKind },
|
|
639
|
-
...args.behaviorId !== void 0 && { behaviorId: args.behaviorId },
|
|
640
|
-
...args.reason !== void 0 && { reason: args.reason }
|
|
641
|
-
})));
|
|
642
|
-
server.registerTool("tdd_goal", {
|
|
643
|
-
description: "Use to manage TDD goals, with a CRUD action discriminator: action='create' (tddTaskId, goal) is idempotent on (tddTaskId, goal); action='update' (id, goal?, status?) edits text and/or lifecycle status; action='delete' (id) hard-deletes (prefer status:'abandoned'); action='get' (id) reads with nested behaviors; action='list' (tddTaskId) returns all goals for a TDD task.",
|
|
644
|
-
inputSchema: strict({
|
|
645
|
-
action: z.enum(TDD_GOAL_ACTIONS).describe("CRUD discriminator"),
|
|
646
|
-
id: z.optional(z.coerce.number()).describe("update/delete/get: goal id"),
|
|
647
|
-
tddTaskId: z.optional(z.coerce.number()).describe("create/list: tdd task id"),
|
|
648
|
-
goal: z.optional(z.string()),
|
|
649
|
-
status: z.optional(z.enum([
|
|
650
|
-
"pending",
|
|
651
|
-
"in_progress",
|
|
652
|
-
"done",
|
|
653
|
-
"abandoned"
|
|
654
|
-
]))
|
|
655
|
-
}),
|
|
656
|
-
outputSchema: effectToZodSchema(TddGoalResult)
|
|
657
|
-
}, async (args) => {
|
|
658
|
-
if (args.action === "create") return structuredJsonResult(await caller.tdd_goal({
|
|
659
|
-
action: "create",
|
|
660
|
-
tddTaskId: args.tddTaskId,
|
|
661
|
-
goal: args.goal
|
|
662
|
-
}));
|
|
663
|
-
if (args.action === "update") return structuredJsonResult(await caller.tdd_goal({
|
|
664
|
-
action: "update",
|
|
665
|
-
id: args.id,
|
|
666
|
-
...args.goal !== void 0 && { goal: args.goal },
|
|
667
|
-
...args.status !== void 0 && { status: args.status }
|
|
668
|
-
}));
|
|
669
|
-
if (args.action === "delete") return structuredJsonResult(await caller.tdd_goal({
|
|
670
|
-
action: "delete",
|
|
671
|
-
id: args.id
|
|
672
|
-
}));
|
|
673
|
-
if (args.action === "get") return structuredJsonResult(await caller.tdd_goal({
|
|
674
|
-
action: "get",
|
|
675
|
-
id: args.id
|
|
676
|
-
}));
|
|
677
|
-
return structuredJsonResult(await caller.tdd_goal({
|
|
678
|
-
action: "list",
|
|
679
|
-
tddTaskId: args.tddTaskId
|
|
680
|
-
}));
|
|
681
|
-
});
|
|
682
|
-
server.registerTool("tdd_behavior", {
|
|
683
|
-
description: "Use to manage TDD behaviors, with a CRUD action discriminator: action='create' (goalId, behavior, suggestedTestName?, dependsOnBehaviorIds?) is idempotent on (goalId, behavior); action='update' (id, ...patch) edits; action='delete' (id) hard-deletes; action='get' (id) reads; action='list_by_goal' (goalId) lists one goal's behaviors; action='list_by_tdd_task' (tddTaskId) lists across all goals.",
|
|
684
|
-
inputSchema: strict({
|
|
685
|
-
action: z.enum(TDD_BEHAVIOR_ACTIONS).describe("CRUD discriminator"),
|
|
686
|
-
id: z.optional(z.coerce.number()),
|
|
687
|
-
goalId: z.optional(z.coerce.number()),
|
|
688
|
-
tddTaskId: z.optional(z.coerce.number()),
|
|
689
|
-
behavior: z.optional(z.string()),
|
|
690
|
-
suggestedTestName: z.optional(z.string().nullable()),
|
|
691
|
-
status: z.optional(z.enum([
|
|
692
|
-
"pending",
|
|
693
|
-
"in_progress",
|
|
694
|
-
"done",
|
|
695
|
-
"abandoned"
|
|
696
|
-
])),
|
|
697
|
-
dependsOnBehaviorIds: z.optional(z.array(z.coerce.number()))
|
|
698
|
-
}),
|
|
699
|
-
outputSchema: effectToZodSchema(TddBehaviorResult)
|
|
700
|
-
}, async (args) => {
|
|
701
|
-
if (args.action === "create") return structuredJsonResult(await caller.tdd_behavior({
|
|
702
|
-
action: "create",
|
|
703
|
-
goalId: args.goalId,
|
|
704
|
-
behavior: args.behavior,
|
|
705
|
-
...args.suggestedTestName !== void 0 && args.suggestedTestName !== null && { suggestedTestName: args.suggestedTestName },
|
|
706
|
-
...args.dependsOnBehaviorIds !== void 0 && { dependsOnBehaviorIds: args.dependsOnBehaviorIds }
|
|
707
|
-
}));
|
|
708
|
-
if (args.action === "update") return structuredJsonResult(await caller.tdd_behavior({
|
|
709
|
-
action: "update",
|
|
710
|
-
id: args.id,
|
|
711
|
-
...args.behavior !== void 0 && { behavior: args.behavior },
|
|
712
|
-
...args.suggestedTestName !== void 0 && { suggestedTestName: args.suggestedTestName },
|
|
713
|
-
...args.status !== void 0 && { status: args.status },
|
|
714
|
-
...args.dependsOnBehaviorIds !== void 0 && { dependsOnBehaviorIds: args.dependsOnBehaviorIds }
|
|
715
|
-
}));
|
|
716
|
-
if (args.action === "delete") return structuredJsonResult(await caller.tdd_behavior({
|
|
717
|
-
action: "delete",
|
|
718
|
-
id: args.id
|
|
719
|
-
}));
|
|
720
|
-
if (args.action === "get") return structuredJsonResult(await caller.tdd_behavior({
|
|
721
|
-
action: "get",
|
|
722
|
-
id: args.id
|
|
723
|
-
}));
|
|
724
|
-
if (args.action === "list_by_goal") return structuredJsonResult(await caller.tdd_behavior({
|
|
725
|
-
action: "list_by_goal",
|
|
726
|
-
goalId: args.goalId
|
|
727
|
-
}));
|
|
728
|
-
return structuredJsonResult(await caller.tdd_behavior({
|
|
729
|
-
action: "list_by_tdd_task",
|
|
730
|
-
tddTaskId: args.tddTaskId
|
|
731
|
-
}));
|
|
732
|
-
});
|
|
733
|
-
server.registerTool("tdd_artifact_list", {
|
|
734
|
-
description: "Use when you need the artifact id to cite in tdd_phase_transition_request without querying SQLite directly. Lists TDD artifacts (test_written, test_failed_run, code_written, test_passed_run, refactor, test_weakened) for a tdd_task, newest first. Filters: artifactKind, phaseId, behaviorId, limit (default 50).",
|
|
735
|
-
inputSchema: strict({
|
|
736
|
-
tddTaskId: z.coerce.number().describe("tdd_tasks.id"),
|
|
737
|
-
artifactKind: z.optional(z.enum([
|
|
738
|
-
"test_written",
|
|
739
|
-
"test_failed_run",
|
|
740
|
-
"code_written",
|
|
741
|
-
"test_passed_run",
|
|
742
|
-
"refactor",
|
|
743
|
-
"test_weakened"
|
|
744
|
-
])).describe("Restrict to one artifact kind"),
|
|
745
|
-
phaseId: z.optional(z.coerce.number()).describe("Restrict to artifacts recorded in one phase"),
|
|
746
|
-
behaviorId: z.optional(z.coerce.number()).describe("Restrict to artifacts recorded in phases bound to one behavior"),
|
|
747
|
-
limit: z.optional(z.coerce.number()).describe("Max rows (default 50)")
|
|
748
|
-
}),
|
|
749
|
-
outputSchema: effectToZodSchema(TddArtifactListResult)
|
|
750
|
-
}, async (args) => {
|
|
751
|
-
const data = await caller.tdd_artifact_list({
|
|
752
|
-
tddTaskId: args.tddTaskId,
|
|
753
|
-
...args.artifactKind !== void 0 && { artifactKind: args.artifactKind },
|
|
754
|
-
...args.phaseId !== void 0 && { phaseId: args.phaseId },
|
|
755
|
-
...args.behaviorId !== void 0 && { behaviorId: args.behaviorId },
|
|
756
|
-
...args.limit !== void 0 && { limit: args.limit }
|
|
757
|
-
});
|
|
758
|
-
return structuredResult(Schema.decodeSync(TddArtifactListAsMarkdown)(data), data);
|
|
759
|
-
});
|
|
760
|
-
server.registerTool("hypothesis", {
|
|
761
|
-
description: "Use to manage debugging hypotheses, with a CRUD action discriminator: action='record' (content, tddTaskId?, optional citation ids) writes a hypothesis — the binding session is resolved server-side from the recovered host context (active TDD subagent, else main session); pass tddTaskId (returned by tdd_task action='start') to bind deterministically to that task's session, and do not pass sessionId when recording; action='validate' (id, outcome, validatedAt?) records a validation outcome — validatedAt is optional and defaults server-side to now when omitted, or is honored verbatim when supplied; action='list' (sessionId?, outcome?, limit?) returns matching hypotheses as markdown.",
|
|
762
|
-
inputSchema: strict({
|
|
763
|
-
action: z.enum(HYPOTHESIS_ACTIONS).describe("CRUD discriminator"),
|
|
764
|
-
sessionId: z.optional(z.coerce.number()).describe("list: filter by session id. record: dev/test fallback only — ignored when host context is recovered; never pass a tddTaskId value here"),
|
|
765
|
-
tddTaskId: z.optional(z.coerce.number()).describe("record: tdd task id returned by tdd_task action='start' — binds the hypothesis to that task's session deterministically"),
|
|
766
|
-
content: z.optional(z.string()).describe("Hypothesis content (action=record)"),
|
|
767
|
-
createdTurnId: z.optional(z.coerce.number()),
|
|
768
|
-
citedTestErrorId: z.optional(z.coerce.number()),
|
|
769
|
-
citedStackFrameId: z.optional(z.coerce.number()),
|
|
770
|
-
id: z.optional(z.coerce.number()).describe("Hypothesis id (action=validate)"),
|
|
771
|
-
outcome: z.optional(z.enum([
|
|
772
|
-
"confirmed",
|
|
773
|
-
"refuted",
|
|
774
|
-
"abandoned",
|
|
775
|
-
"open"
|
|
776
|
-
])).describe("validate: 'confirmed'|'refuted'|'abandoned'; list filter may include 'open'"),
|
|
777
|
-
validatedTurnId: z.optional(z.coerce.number()),
|
|
778
|
-
validatedAt: z.optional(z.string()).describe("ISO 8601 timestamp (action=validate)"),
|
|
779
|
-
limit: z.optional(z.coerce.number())
|
|
780
|
-
}),
|
|
781
|
-
outputSchema: effectToZodSchema(HypothesisResult)
|
|
782
|
-
}, async (args) => {
|
|
783
|
-
if (args.action === "record") return structuredJsonResult(await caller.hypothesis({
|
|
784
|
-
action: "record",
|
|
785
|
-
content: args.content,
|
|
786
|
-
...args.tddTaskId !== void 0 && { tddTaskId: args.tddTaskId },
|
|
787
|
-
...args.sessionId !== void 0 && { sessionId: args.sessionId },
|
|
788
|
-
...args.createdTurnId !== void 0 && { createdTurnId: args.createdTurnId },
|
|
789
|
-
...args.citedTestErrorId !== void 0 && { citedTestErrorId: args.citedTestErrorId },
|
|
790
|
-
...args.citedStackFrameId !== void 0 && { citedStackFrameId: args.citedStackFrameId }
|
|
791
|
-
}));
|
|
792
|
-
if (args.action === "validate") return structuredJsonResult(await caller.hypothesis({
|
|
793
|
-
action: "validate",
|
|
794
|
-
id: args.id,
|
|
795
|
-
outcome: args.outcome,
|
|
796
|
-
...args.validatedAt !== void 0 && { validatedAt: args.validatedAt },
|
|
797
|
-
...args.validatedTurnId !== void 0 && { validatedTurnId: args.validatedTurnId }
|
|
798
|
-
}));
|
|
799
|
-
const result = await caller.hypothesis({
|
|
800
|
-
action: "list",
|
|
801
|
-
...args.sessionId !== void 0 && { sessionId: args.sessionId },
|
|
802
|
-
...args.outcome !== void 0 && { outcome: args.outcome },
|
|
803
|
-
...args.limit !== void 0 && { limit: args.limit }
|
|
804
|
-
});
|
|
805
|
-
return structuredResult(formatHypothesisListMarkdown(result), result);
|
|
806
|
-
});
|
|
807
|
-
server.registerTool("tdd_progress_push", {
|
|
808
|
-
description: "Use when a TDD orchestrator needs to report progress to the main agent over a Claude Code channel. The MCP server validates the payload against the ChannelEvent union and resolves goalId/sessionId server-side from behaviorId for behavior-scoped events (so a stale orchestrator context cannot push the wrong tree coordinates). Best-effort — returns { ok: true } regardless of whether channels are active.",
|
|
809
|
-
inputSchema: strict({ payload: z.string().describe("Pre-stringified ChannelEvent JSON (see schemas/ChannelEvent in @vitest-agent/sdk)") })
|
|
810
|
-
}, async (args) => {
|
|
811
|
-
let resolvedPayload = args.payload;
|
|
812
|
-
try {
|
|
813
|
-
const enriched = await resolveChannelEvent(ctx, JSON.parse(args.payload));
|
|
814
|
-
resolvedPayload = JSON.stringify(enriched);
|
|
815
|
-
} catch {}
|
|
816
|
-
try {
|
|
817
|
-
await server.server.notification({
|
|
818
|
-
method: "notifications/claude/channel",
|
|
819
|
-
params: { content: resolvedPayload }
|
|
820
|
-
});
|
|
821
|
-
} catch {}
|
|
822
|
-
return structuredJsonResult({ ok: true });
|
|
823
|
-
});
|
|
824
|
-
server.registerTool("acceptance_metrics", {
|
|
825
|
-
description: "Use when you need the four spec Annex A acceptance metrics computed from the current database. Returns markdown in content[] and a typed JSON object in structuredContent (per-metric { total, ratio, ... }).",
|
|
826
|
-
inputSchema: strict({}),
|
|
827
|
-
outputSchema: effectToZodSchema(AcceptanceMetricsResult)
|
|
828
|
-
}, async () => {
|
|
829
|
-
const data = await caller.acceptance_metrics({});
|
|
830
|
-
return structuredResult(Schema.decodeSync(AcceptanceMetricsAsMarkdown)(data), data);
|
|
831
|
-
});
|
|
832
|
-
server.registerTool("triage_brief", {
|
|
833
|
-
description: "Use when you need to orient on the current test landscape: failing tests, flaky tests, open TDD sessions, and suggested next actions. Returns markdown in content[] and a typed envelope in structuredContent ({ hasContent, markdown }).",
|
|
834
|
-
inputSchema: strict({
|
|
835
|
-
project: z.optional(z.string()).describe("Filter to a specific project"),
|
|
836
|
-
maxLines: z.optional(z.coerce.number()).describe("Soft cap on rendered output lines")
|
|
837
|
-
}),
|
|
838
|
-
outputSchema: effectToZodSchema(TriageBriefResult)
|
|
839
|
-
}, async (args) => {
|
|
840
|
-
const data = await caller.triage_brief({
|
|
841
|
-
project: args.project,
|
|
842
|
-
maxLines: args.maxLines
|
|
843
|
-
});
|
|
844
|
-
return structuredResult(data.markdown, data);
|
|
845
|
-
});
|
|
846
|
-
server.registerTool("wrapup_prompt", {
|
|
847
|
-
description: "Use when a session is ending and you need a tailored wrap-up prompt (Stop / SessionEnd / PreCompact / TDD handoff / UserPromptSubmit nudge variants). Returns markdown in content[] and a typed envelope in structuredContent ({ hasContent, kind, markdown }).",
|
|
848
|
-
inputSchema: strict({
|
|
849
|
-
sessionId: z.optional(z.coerce.number()).describe("sessions.id (integer); omit to use chatId"),
|
|
850
|
-
chatId: z.optional(z.string()).describe("Host chat UUID (alternative to sessionId)"),
|
|
851
|
-
kind: z.optional(z.enum([
|
|
852
|
-
"stop",
|
|
853
|
-
"session_end",
|
|
854
|
-
"pre_compact",
|
|
855
|
-
"tdd_handoff",
|
|
856
|
-
"user_prompt_nudge"
|
|
857
|
-
])).describe("Wrap-up flavor (default: session_end)"),
|
|
858
|
-
userPromptHint: z.optional(z.string()).describe("For user_prompt_nudge: the prompt text to inspect")
|
|
859
|
-
}),
|
|
860
|
-
outputSchema: effectToZodSchema(WrapupPromptResult)
|
|
861
|
-
}, async (args) => {
|
|
862
|
-
const data = await caller.wrapup_prompt({
|
|
863
|
-
sessionId: args.sessionId,
|
|
864
|
-
chatId: args.chatId,
|
|
865
|
-
kind: args.kind,
|
|
866
|
-
userPromptHint: args.userPromptHint
|
|
867
|
-
});
|
|
868
|
-
return structuredResult(data.markdown, data);
|
|
869
|
-
});
|
|
870
|
-
server.registerTool("commit_changes", {
|
|
871
|
-
description: "Use when you need commit metadata and changed files captured by the post-commit hook. Returns up to 20 most-recent when sha is omitted. Returns markdown in content[] and a typed JSON object in structuredContent ({ filterSha?, count, commits[] }).",
|
|
872
|
-
inputSchema: strict({ sha: z.optional(z.string()).describe("Specific commit sha to fetch; omit for recent commits") }),
|
|
873
|
-
outputSchema: effectToZodSchema(CommitChangesResult)
|
|
874
|
-
}, async (args) => {
|
|
875
|
-
const data = await caller.commit_changes({ sha: args.sha });
|
|
876
|
-
return structuredResult(Schema.decodeSync(CommitChangesAsMarkdown)(data), data);
|
|
877
|
-
});
|
|
878
|
-
server.registerTool("ping", {
|
|
879
|
-
description: "Use when you need to verify the MCP server is alive or confirm a hot-patch reload. Returns 'pong'; structuredContent.message carries the constant 'pong' literal.",
|
|
880
|
-
outputSchema: effectToZodSchema(PingResult)
|
|
881
|
-
}, async () => {
|
|
882
|
-
const data = await caller.ping();
|
|
883
|
-
return structuredResult(data.message, data);
|
|
884
|
-
});
|
|
885
|
-
registerAllPrompts(server);
|
|
886
|
-
return server;
|
|
887
|
-
}
|
|
888
|
-
/**
|
|
889
|
-
* Starts the MCP server over stdio, registering all tools and prompts.
|
|
890
|
-
*
|
|
891
|
-
* Builds the server via {@link buildMcpServer}, then connects a
|
|
892
|
-
* `StdioServerTransport`. Returns when the transport disconnects.
|
|
15
|
+
* The server layer: every tool registered under the strict contract, plus
|
|
16
|
+
* the six framing prompts, over `McpServer.layerStdio`. `protocols` is newest-first because the registry
|
|
17
|
+
* falls back to `protocols[0]` for a client offering an unknown version.
|
|
893
18
|
*
|
|
894
|
-
* @param
|
|
19
|
+
* @param options - the advertised server version
|
|
895
20
|
* @public
|
|
896
21
|
*/
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
22
|
+
const ServerLayer = (options) => Layer.mergeAll(registerStrictToolkit(Kit).pipe(Layer.provide(ToolsLayer)), PromptsLayer).pipe(Layer.provide(McpServer.layerStdio({
|
|
23
|
+
name: "vitest-agent",
|
|
24
|
+
version: options.version,
|
|
25
|
+
description: DESCRIPTION,
|
|
26
|
+
protocols: [
|
|
27
|
+
McpProtocol.v2025_11_25,
|
|
28
|
+
McpProtocol.v2025_06_18,
|
|
29
|
+
McpProtocol.v2025_03_26
|
|
30
|
+
]
|
|
31
|
+
})), Layer.provide(Layer.succeed(Logger.LogToStderr, true)), Layer.orDie);
|
|
902
32
|
|
|
903
33
|
//#endregion
|
|
904
|
-
export {
|
|
34
|
+
export { ServerLayer };
|