@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
package/server.js CHANGED
@@ -1,904 +1,34 @@
1
- import { createCallerFactory } from "./context.js";
2
- import { AcceptanceMetricsAsMarkdown, AcceptanceMetricsResult } from "./tools/acceptance-metrics.js";
3
- import { CacheHealthAsMarkdown, CacheHealthResult } from "./tools/cache-health.js";
4
- import { CommitChangesAsMarkdown, CommitChangesResult } from "./tools/commit-changes.js";
5
- import { ConfigureAsMarkdown, ConfigureResult } from "./tools/configure.js";
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
- * Build a strict (unknown-keys-rejected) zod object from a raw shape, with
44
- * an `unrecognized_keys` error message that names the accepted params —
45
- * plain `z.strictObject(shape)` only reports the offending key(s), leaving
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
- function structuredResult(text, structured) {
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
- * Shorthand for mutation/CRUD tools whose text channel is just the
132
- * JSON-stringified form of the same object that travels in
133
- * `structuredContent`. Replaces the legacy `jsonResult` helper for
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 ctx - the MCP context carrying the shared ManagedRuntime and session refs
19
+ * @param options - the advertised server version
895
20
  * @public
896
21
  */
897
- async function startMcpServer(ctx) {
898
- const server = buildMcpServer(ctx);
899
- const transport = new StdioServerTransport();
900
- await server.connect(transport);
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 { buildMcpServer, startMcpServer };
34
+ export { ServerLayer };