@vitest-agent/mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/bin/vitest-agent-mcp.js +93 -0
  4. package/context.js +72 -0
  5. package/index.d.ts +1577 -0
  6. package/index.js +19 -0
  7. package/layers/McpLive.js +30 -0
  8. package/middleware/idempotency.js +128 -0
  9. package/package.json +58 -0
  10. package/prompts/explain-failure.js +27 -0
  11. package/prompts/index.js +89 -0
  12. package/prompts/regression-since-pass.js +28 -0
  13. package/prompts/tdd-resume.js +28 -0
  14. package/prompts/triage.js +24 -0
  15. package/prompts/why-flaky.js +30 -0
  16. package/prompts/wrapup.js +19 -0
  17. package/resources/index.js +155 -0
  18. package/resources/indexes.js +77 -0
  19. package/resources/manifest-schema.js +46 -0
  20. package/resources/paths.js +20 -0
  21. package/resources/patterns.js +22 -0
  22. package/resources/upstream-docs.js +22 -0
  23. package/router.js +74 -0
  24. package/server.js +838 -0
  25. package/tools/_tdd-error-envelope.js +98 -0
  26. package/tools/acceptance-metrics.js +75 -0
  27. package/tools/cache-health.js +83 -0
  28. package/tools/commit-changes.js +64 -0
  29. package/tools/configure.js +107 -0
  30. package/tools/coverage.js +76 -0
  31. package/tools/errors.js +151 -0
  32. package/tools/failure-signature-get.js +73 -0
  33. package/tools/file-coverage.js +106 -0
  34. package/tools/help.js +146 -0
  35. package/tools/history.js +121 -0
  36. package/tools/hypothesis.js +127 -0
  37. package/tools/inventory.js +377 -0
  38. package/tools/note.js +208 -0
  39. package/tools/overview.js +92 -0
  40. package/tools/ping.js +22 -0
  41. package/tools/register-agent.js +135 -0
  42. package/tools/run-tests.js +359 -0
  43. package/tools/settings-list.js +48 -0
  44. package/tools/status.js +74 -0
  45. package/tools/tdd-artifact.js +101 -0
  46. package/tools/tdd-behavior.js +177 -0
  47. package/tools/tdd-goal.js +147 -0
  48. package/tools/tdd-phase-transition-request.js +212 -0
  49. package/tools/tdd-task.js +278 -0
  50. package/tools/test.js +281 -0
  51. package/tools/trends.js +112 -0
  52. package/tools/triage-brief.js +42 -0
  53. package/tools/turn-search.js +60 -0
  54. package/tools/wrapup-prompt.js +49 -0
  55. package/tsdoc-metadata.json +11 -0
  56. package/utils/effect-to-zod.js +81 -0
package/server.js ADDED
@@ -0,0 +1,838 @@
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 { HypothesisResult, formatHypothesisListMarkdown } from "./tools/hypothesis.js";
13
+ import { InventoryAsMarkdown, InventoryResult } from "./tools/inventory.js";
14
+ import { 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 { TddBehaviorResult } from "./tools/tdd-behavior.js";
23
+ import { TddGoalResult } from "./tools/tdd-goal.js";
24
+ import { PhaseTransitionResult } from "./tools/tdd-phase-transition-request.js";
25
+ import { TddTaskAsMarkdown, TddTaskResult } from "./tools/tdd-task.js";
26
+ import { 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 { registerAllResources } from "./resources/index.js";
34
+ import { effectToZodSchema } from "./utils/effect-to-zod.js";
35
+ import { ChannelEvent, DataReader } from "@vitest-agent/sdk";
36
+ import { Effect, Option, Schema } from "effect";
37
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
38
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
39
+ import { z } from "zod";
40
+
41
+ //#region src/server.ts
42
+ /**
43
+ * For behavior-scoped events, resolve goalId/sessionId server-side from
44
+ * behaviorId so a stale orchestrator context cannot push the wrong tree
45
+ * coordinates. Goal-scoped events get sessionId resolved from goalId.
46
+ * Returns the enriched event object or the original on resolution failure.
47
+ */
48
+ async function resolveChannelEvent(ctx, raw) {
49
+ const decoded = Schema.decodeUnknownEither(ChannelEvent)(raw);
50
+ if (decoded._tag === "Left") return raw;
51
+ const event = decoded.right;
52
+ return ctx.runtime.runPromise(Effect.gen(function* () {
53
+ const reader = yield* DataReader;
54
+ switch (event.type) {
55
+ case "behavior_started":
56
+ case "phase_transition":
57
+ case "behavior_completed":
58
+ case "behavior_abandoned":
59
+ case "blocked": {
60
+ const goalIdOpt = yield* reader.resolveGoalIdForBehavior(event.behaviorId);
61
+ if (Option.isNone(goalIdOpt)) return event;
62
+ const goalDetailOpt = yield* reader.getGoalById(goalIdOpt.value);
63
+ if (Option.isNone(goalDetailOpt)) return event;
64
+ return {
65
+ ...event,
66
+ goalId: goalIdOpt.value,
67
+ sessionId: goalDetailOpt.value.sessionId
68
+ };
69
+ }
70
+ case "goal_started":
71
+ case "goal_completed":
72
+ case "goal_abandoned": {
73
+ const goalDetailOpt = yield* reader.getGoalById(event.goalId);
74
+ if (Option.isNone(goalDetailOpt)) return event;
75
+ return {
76
+ ...event,
77
+ sessionId: goalDetailOpt.value.sessionId
78
+ };
79
+ }
80
+ default: return event;
81
+ }
82
+ }));
83
+ }
84
+ /**
85
+ * Emit both a human-readable text block (`content[]`) and a typed
86
+ * structured payload (`structuredContent`) per the MCP 2025-06-18
87
+ * tool-result contract.
88
+ *
89
+ * Per the spec, "for backwards compatibility, a tool that returns
90
+ * structured content SHOULD also return the serialized JSON in a
91
+ * TextContent block." The helper keeps the existing markdown/JSON
92
+ * text exactly as today (so the human-facing transcript is unchanged)
93
+ * and adds `structuredContent` on top so the LLM can parse the
94
+ * tool's data without inferring it from the rendered text.
95
+ *
96
+ * `structuredContent` MUST be a JSON object — not an array, not a
97
+ * primitive. Tools that conceptually return a list wrap it as
98
+ * `{ items: [...] }` (or a more specific key like `artifacts: [...]`).
99
+ *
100
+ * @internal
101
+ */
102
+ function structuredResult(text, structured) {
103
+ return {
104
+ content: [{
105
+ type: "text",
106
+ text
107
+ }],
108
+ structuredContent: structured
109
+ };
110
+ }
111
+ /**
112
+ * Shorthand for mutation/CRUD tools whose text channel is just the
113
+ * JSON-stringified form of the same object that travels in
114
+ * `structuredContent`. Replaces the legacy `jsonResult` helper for
115
+ * tools that previously rendered their result as JSON.stringify; the
116
+ * structured payload is identical, so the agent gets the typed object
117
+ * via MCP's structuredContent channel without paying the markdown
118
+ * formatter ceremony of `Schema.transformOrFail`.
119
+ *
120
+ * @internal
121
+ */
122
+ function structuredJsonResult(value) {
123
+ return structuredResult(JSON.stringify(value, null, 2), value);
124
+ }
125
+ /**
126
+ * Starts the MCP server over stdio, registering all tools, resources, and prompts.
127
+ *
128
+ * Constructs the MCP server instance, registers all tRPC-backed tools (wired through
129
+ * `ctx.runtime`), calls `registerAllResources` and `registerAllPrompts`, then connects
130
+ * a `StdioServerTransport`. Returns when the transport disconnects.
131
+ *
132
+ * @param ctx - the MCP context carrying the shared ManagedRuntime and session refs
133
+ * @public
134
+ */
135
+ async function startMcpServer(ctx) {
136
+ const server = new McpServer({
137
+ name: "vitest-agent",
138
+ version: "0.1.0"
139
+ }, { capabilities: { experimental: { "claude/channel": {} } } });
140
+ const caller = createCallerFactory(appRouter)(ctx);
141
+ server.registerTool("help", {
142
+ description: "Use when you need the catalog of available MCP tools and their parameters. Markdown in content[]; same string available as structuredContent.helpText.",
143
+ outputSchema: effectToZodSchema(HelpResult)
144
+ }, async () => {
145
+ const data = await caller.help();
146
+ return structuredResult(data.helpText, data);
147
+ });
148
+ server.registerTool("test_status", {
149
+ 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).",
150
+ inputSchema: { project: z.optional(z.string()).describe("Filter to a specific project") },
151
+ outputSchema: effectToZodSchema(TestStatusResult)
152
+ }, async (args) => {
153
+ const data = await caller.test_status({ project: args.project });
154
+ return structuredResult(Schema.decodeSync(TestStatusAsMarkdown)(data), data);
155
+ });
156
+ server.registerTool("test_overview", {
157
+ 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).",
158
+ inputSchema: { project: z.optional(z.string()).describe("Filter to a specific project") },
159
+ outputSchema: effectToZodSchema(TestOverviewResult)
160
+ }, async (args) => {
161
+ const data = await caller.test_overview({ project: args.project });
162
+ return structuredResult(Schema.decodeSync(TestOverviewAsMarkdown)(data), data);
163
+ });
164
+ server.registerTool("test_coverage", {
165
+ 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).",
166
+ inputSchema: { project: z.optional(z.string()).describe("Project name") },
167
+ outputSchema: effectToZodSchema(TestCoverageResult)
168
+ }, async (args) => {
169
+ const data = await caller.test_coverage({ project: args.project });
170
+ return structuredResult(Schema.decodeSync(TestCoverageAsMarkdown)(data), data);
171
+ });
172
+ server.registerTool("test_history", {
173
+ 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[]).",
174
+ inputSchema: { project: z.string().describe("Project name (required)") },
175
+ outputSchema: effectToZodSchema(TestHistoryResult)
176
+ }, async (args) => {
177
+ const data = await caller.test_history({ project: args.project });
178
+ return structuredResult(Schema.decodeSync(TestHistoryAsMarkdown)(data), data);
179
+ });
180
+ server.registerTool("test_trends", {
181
+ 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? }).",
182
+ inputSchema: {
183
+ project: z.string().describe("Project name (required)"),
184
+ limit: z.optional(z.coerce.number()).describe("Max number of trend entries to return")
185
+ },
186
+ outputSchema: effectToZodSchema(TestTrendsResult)
187
+ }, async (args) => {
188
+ const data = await caller.test_trends({
189
+ project: args.project,
190
+ limit: args.limit
191
+ });
192
+ return structuredResult(Schema.decodeSync(TestTrendsAsMarkdown)(data), data);
193
+ });
194
+ server.registerTool("test_errors", {
195
+ 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[].",
196
+ inputSchema: {
197
+ project: z.string().describe("Project name (required)"),
198
+ errorName: z.optional(z.string()).describe("Filter to a specific error name")
199
+ },
200
+ outputSchema: effectToZodSchema(TestErrorsResult)
201
+ }, async (args) => {
202
+ const data = await caller.test_errors({
203
+ project: args.project,
204
+ ...args.errorName !== void 0 && { errorName: args.errorName }
205
+ });
206
+ return structuredResult(Schema.decodeSync(TestErrorsAsMarkdown)(data), data);
207
+ });
208
+ server.registerTool("test", {
209
+ description: "Use to inspect tests, with an action discriminator: action='list' (project?, state?, module?, limit?) returns matching tests; action='get' (fullName, project?) returns details + errors + run history; action='for_file' (filePath) returns test modules covering a source file. structuredContent carries the typed payload (discriminate on `action`, then on `found` for get).",
210
+ inputSchema: {
211
+ action: z.enum([
212
+ "list",
213
+ "get",
214
+ "for_file"
215
+ ]).describe("Inspection discriminator"),
216
+ project: z.optional(z.string()),
217
+ state: z.optional(z.string()).describe("list: filter by state"),
218
+ module: z.optional(z.string()).describe("list: filter by module path"),
219
+ limit: z.optional(z.coerce.number()).describe("list: max rows to return"),
220
+ fullName: z.optional(z.string()).describe("get: full test name"),
221
+ filePath: z.optional(z.string()).describe("for_file: source file path")
222
+ },
223
+ outputSchema: effectToZodSchema(TestResult)
224
+ }, async (args) => {
225
+ let data;
226
+ if (args.action === "list") data = await caller.test({
227
+ action: "list",
228
+ ...args.project !== void 0 && { project: args.project },
229
+ ...args.state !== void 0 && { state: args.state },
230
+ ...args.module !== void 0 && { module: args.module },
231
+ ...args.limit !== void 0 && { limit: args.limit }
232
+ });
233
+ else if (args.action === "get") data = await caller.test({
234
+ action: "get",
235
+ fullName: args.fullName,
236
+ ...args.project !== void 0 && { project: args.project }
237
+ });
238
+ else data = await caller.test({
239
+ action: "for_file",
240
+ filePath: args.filePath
241
+ });
242
+ return structuredResult(Schema.decodeSync(TestAsMarkdown)(data), data);
243
+ });
244
+ server.registerTool("file_coverage", {
245
+ 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[] }).",
246
+ inputSchema: {
247
+ filePath: z.string().describe("Source file path to check coverage for"),
248
+ project: z.optional(z.string()).describe("Project name")
249
+ },
250
+ outputSchema: effectToZodSchema(FileCoverageResult)
251
+ }, async (args) => {
252
+ const data = await caller.file_coverage({
253
+ filePath: args.filePath,
254
+ project: args.project
255
+ });
256
+ return structuredResult(Schema.decodeSync(FileCoverageAsMarkdown)(data), data);
257
+ });
258
+ server.registerTool("configure", {
259
+ 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? }).",
260
+ inputSchema: { settingsHash: z.optional(z.string()).describe("Settings hash from a manifest entry or test run") },
261
+ outputSchema: effectToZodSchema(ConfigureResult)
262
+ }, async (args) => {
263
+ const data = await caller.configure({ settingsHash: args.settingsHash });
264
+ return structuredResult(Schema.decodeSync(ConfigureAsMarkdown)(data), data);
265
+ });
266
+ server.registerTool("cache_health", {
267
+ 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? }).",
268
+ outputSchema: effectToZodSchema(CacheHealthResult)
269
+ }, async () => {
270
+ const data = await caller.cache_health();
271
+ return structuredResult(Schema.decodeSync(CacheHealthAsMarkdown)(data), data);
272
+ });
273
+ server.registerTool("inventory", {
274
+ description: "Use to discover what exists in the workspace, with a kind discriminator: project / module / suite / session. structuredContent discriminates on `inventoryKind` (project, module, suite, session_detail, session_list) so callers can branch on the response shape without parsing markdown.",
275
+ inputSchema: {
276
+ kind: z.enum([
277
+ "project",
278
+ "module",
279
+ "suite",
280
+ "session"
281
+ ]).describe("Inventory entity"),
282
+ id: z.optional(z.coerce.number()).describe("session: single-row lookup by id"),
283
+ project: z.optional(z.string()),
284
+ module: z.optional(z.string()).describe("suite: filter by module path"),
285
+ agentKind: z.optional(z.enum(["main", "subagent"])).describe("session: filter by agent kind"),
286
+ limit: z.optional(z.coerce.number()).describe("session: max rows")
287
+ },
288
+ outputSchema: effectToZodSchema(InventoryResult)
289
+ }, async (args) => {
290
+ let data;
291
+ if (args.kind === "project") data = await caller.inventory({ kind: "project" });
292
+ else if (args.kind === "module") data = await caller.inventory({
293
+ kind: "module",
294
+ ...args.project !== void 0 && { project: args.project }
295
+ });
296
+ else if (args.kind === "suite") data = await caller.inventory({
297
+ kind: "suite",
298
+ ...args.project !== void 0 && { project: args.project },
299
+ ...args.module !== void 0 && { module: args.module }
300
+ });
301
+ else data = await caller.inventory({
302
+ kind: "session",
303
+ ...args.id !== void 0 && { id: args.id },
304
+ ...args.project !== void 0 && { project: args.project },
305
+ ...args.agentKind !== void 0 && { agentKind: args.agentKind },
306
+ ...args.limit !== void 0 && { limit: args.limit }
307
+ });
308
+ return structuredResult(Schema.decodeSync(InventoryAsMarkdown)(data), data);
309
+ });
310
+ server.registerTool("settings_list", {
311
+ 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[] }).",
312
+ outputSchema: effectToZodSchema(SettingsListResult)
313
+ }, async () => {
314
+ const data = await caller.settings_list({});
315
+ return structuredResult(Schema.decodeSync(SettingsListAsMarkdown)(data), data);
316
+ });
317
+ server.registerTool("register_agent", {
318
+ 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').",
319
+ inputSchema: {
320
+ chatId: z.string().describe("Host's chat UUID (session_id from CC hook payload, etc.)"),
321
+ conversationId: z.optional(z.string()).describe("Canonical conversation UUID (from session-map mapConversation)"),
322
+ hostKind: z.optional(z.string()).describe("Host vendor identifier; defaults to 'claude-code'"),
323
+ agentType: z.string().describe("Agent type; must begin with the host-kind prefix"),
324
+ parentAgentId: z.optional(z.string()).describe("Parent agent UUID for subagent registrations"),
325
+ 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"),
326
+ startGitBranch: z.optional(z.string()),
327
+ startGitCommitSha: z.optional(z.string()),
328
+ startWorktreeDir: z.optional(z.string())
329
+ },
330
+ outputSchema: effectToZodSchema(RegisterAgentResult)
331
+ }, async (args) => {
332
+ const result = await caller.register_agent({
333
+ chatId: args.chatId,
334
+ agentType: args.agentType,
335
+ ...args.conversationId !== void 0 && { conversationId: args.conversationId },
336
+ ...args.hostKind !== void 0 && { hostKind: args.hostKind },
337
+ ...args.parentAgentId !== void 0 && { parentAgentId: args.parentAgentId },
338
+ ...args.clientNonce !== void 0 && { clientNonce: args.clientNonce },
339
+ ...args.startGitBranch !== void 0 && { startGitBranch: args.startGitBranch },
340
+ ...args.startGitCommitSha !== void 0 && { startGitCommitSha: args.startGitCommitSha },
341
+ ...args.startWorktreeDir !== void 0 && { startWorktreeDir: args.startWorktreeDir }
342
+ });
343
+ return {
344
+ content: [{
345
+ type: "text",
346
+ text: JSON.stringify(result)
347
+ }],
348
+ isError: result.ok === false,
349
+ structuredContent: result
350
+ };
351
+ });
352
+ server.registerTool("run_tests", {
353
+ description: "Use to run Vitest tests, with optional file and project filters. structuredContent carries the typed AgentReport plus per-test classifications (discriminate on `kind`: ok, timeout, error). The legacy format=json arg is dropped — structuredContent supersedes it.",
354
+ inputSchema: {
355
+ files: z.optional(z.array(z.string())).describe("Test file paths to run"),
356
+ project: z.optional(z.string()).describe("Project name to filter"),
357
+ timeout: z.optional(z.coerce.number()).describe("Timeout in seconds (default: 120)"),
358
+ _sessionContext: z.optional(z.object({
359
+ chatId: z.string(),
360
+ conversationId: z.string(),
361
+ mainAgentId: z.string()
362
+ })).describe("Hook-injected session attribution UUIDs; do not pass manually.")
363
+ },
364
+ outputSchema: effectToZodSchema(RunTestsResult)
365
+ }, async (args) => {
366
+ const data = await caller.run_tests({
367
+ files: args.files,
368
+ project: args.project,
369
+ timeout: args.timeout,
370
+ ...args._sessionContext !== void 0 && { _sessionContext: args._sessionContext }
371
+ });
372
+ return structuredResult(Schema.decodeSync(RunTestsAsMarkdown)(data), data);
373
+ });
374
+ server.registerTool("note", {
375
+ 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.",
376
+ inputSchema: {
377
+ action: z.enum([
378
+ "create",
379
+ "list",
380
+ "get",
381
+ "update",
382
+ "delete",
383
+ "search"
384
+ ]).describe("CRUD discriminator"),
385
+ id: z.optional(z.coerce.number()).describe("get/update/delete: note id"),
386
+ project: z.optional(z.string()),
387
+ title: z.optional(z.string()),
388
+ content: z.optional(z.string()),
389
+ scope: z.optional(z.enum([
390
+ "global",
391
+ "project",
392
+ "module",
393
+ "suite",
394
+ "test",
395
+ "note"
396
+ ])).describe("create: required scope; list: optional filter"),
397
+ testFullName: z.optional(z.string()),
398
+ modulePath: z.optional(z.string()),
399
+ parentNoteId: z.optional(z.coerce.number()),
400
+ createdBy: z.optional(z.string()),
401
+ expiresAt: z.optional(z.string()),
402
+ pinned: z.optional(z.boolean()),
403
+ query: z.optional(z.string()).describe("search: FTS5 query")
404
+ },
405
+ outputSchema: effectToZodSchema(NoteResult)
406
+ }, async (args) => {
407
+ if (args.action === "create") return structuredJsonResult(await caller.note({
408
+ action: "create",
409
+ title: args.title,
410
+ content: args.content,
411
+ scope: args.scope,
412
+ ...args.project !== void 0 && { project: args.project },
413
+ ...args.testFullName !== void 0 && { testFullName: args.testFullName },
414
+ ...args.modulePath !== void 0 && { modulePath: args.modulePath },
415
+ ...args.parentNoteId !== void 0 && { parentNoteId: args.parentNoteId },
416
+ ...args.createdBy !== void 0 && { createdBy: args.createdBy },
417
+ ...args.expiresAt !== void 0 && { expiresAt: args.expiresAt },
418
+ ...args.pinned !== void 0 && { pinned: args.pinned }
419
+ }));
420
+ if (args.action === "list") {
421
+ const data = await caller.note({
422
+ action: "list",
423
+ ...args.scope !== void 0 && { scope: args.scope },
424
+ ...args.project !== void 0 && { project: args.project },
425
+ ...args.testFullName !== void 0 && { testFullName: args.testFullName }
426
+ });
427
+ return structuredResult(formatNoteListMarkdown(data), data);
428
+ }
429
+ if (args.action === "get") return structuredJsonResult(await caller.note({
430
+ action: "get",
431
+ id: args.id
432
+ }));
433
+ if (args.action === "update") return structuredJsonResult(await caller.note({
434
+ action: "update",
435
+ id: args.id,
436
+ ...args.title !== void 0 && { title: args.title },
437
+ ...args.content !== void 0 && { content: args.content },
438
+ ...args.pinned !== void 0 && { pinned: args.pinned },
439
+ ...args.expiresAt !== void 0 && { expiresAt: args.expiresAt }
440
+ }));
441
+ if (args.action === "delete") return structuredJsonResult(await caller.note({
442
+ action: "delete",
443
+ id: args.id
444
+ }));
445
+ const searchData = await caller.note({
446
+ action: "search",
447
+ query: args.query
448
+ });
449
+ return structuredResult(formatNoteListMarkdown(searchData), searchData);
450
+ });
451
+ server.registerTool("turn_search", {
452
+ 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[] }).",
453
+ inputSchema: {
454
+ sessionId: z.optional(z.coerce.number()).describe("Filter to a specific session id"),
455
+ since: z.optional(z.string()).describe("ISO 8601 cutoff — return turns after this timestamp"),
456
+ type: z.optional(z.enum([
457
+ "user_prompt",
458
+ "tool_call",
459
+ "tool_result",
460
+ "file_edit",
461
+ "hook_fire",
462
+ "note",
463
+ "hypothesis"
464
+ ])).describe("Filter by turn type"),
465
+ limit: z.optional(z.coerce.number()).describe("Max turns to return (default 100)")
466
+ },
467
+ outputSchema: effectToZodSchema(TurnSearchResult)
468
+ }, async (args) => {
469
+ const data = await caller.turn_search({
470
+ sessionId: args.sessionId,
471
+ since: args.since,
472
+ type: args.type,
473
+ limit: args.limit
474
+ });
475
+ return structuredResult(Schema.decodeSync(TurnSearchAsMarkdown)(data), data);
476
+ });
477
+ server.registerTool("failure_signature_get", {
478
+ 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).",
479
+ inputSchema: { hash: z.string().describe("16-char failure signature hash") },
480
+ outputSchema: effectToZodSchema(FailureSignatureGetResult)
481
+ }, async (args) => {
482
+ const data = await caller.failure_signature_get({ hash: args.hash });
483
+ return structuredResult(Schema.decodeSync(FailureSignatureGetAsMarkdown)(data), data);
484
+ });
485
+ server.registerTool("tdd_task", {
486
+ 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.",
487
+ inputSchema: {
488
+ action: z.enum([
489
+ "start",
490
+ "end",
491
+ "get",
492
+ "resume"
493
+ ]).describe("Lifecycle discriminator"),
494
+ tddTaskId: z.optional(z.coerce.number()).describe("end/get/resume: tdd task id"),
495
+ goal: z.optional(z.string()).describe("start: goal text"),
496
+ sessionId: z.optional(z.coerce.number()).describe("start: sessions.id (alternative to chatId)"),
497
+ chatId: z.optional(z.string()).describe("start: host chat UUID"),
498
+ parentTddTaskId: z.optional(z.coerce.number()).describe("start: parent task id when decomposing"),
499
+ startedAt: z.optional(z.string()),
500
+ runId: z.optional(z.string()),
501
+ outcome: z.optional(z.enum([
502
+ "succeeded",
503
+ "blocked",
504
+ "abandoned"
505
+ ])).describe("end: final outcome"),
506
+ summaryNoteId: z.optional(z.coerce.number())
507
+ },
508
+ outputSchema: effectToZodSchema(TddTaskResult)
509
+ }, async (args) => {
510
+ let data;
511
+ if (args.action === "start") data = await caller.tdd_task({
512
+ action: "start",
513
+ goal: args.goal,
514
+ ...args.sessionId !== void 0 && { sessionId: args.sessionId },
515
+ ...args.chatId !== void 0 && { chatId: args.chatId },
516
+ ...args.parentTddTaskId !== void 0 && { parentTddTaskId: args.parentTddTaskId },
517
+ ...args.startedAt !== void 0 && { startedAt: args.startedAt },
518
+ ...args.runId !== void 0 && { runId: args.runId }
519
+ });
520
+ else if (args.action === "end") data = await caller.tdd_task({
521
+ action: "end",
522
+ tddTaskId: args.tddTaskId,
523
+ outcome: args.outcome,
524
+ ...args.summaryNoteId !== void 0 && { summaryNoteId: args.summaryNoteId }
525
+ });
526
+ else if (args.action === "get") data = await caller.tdd_task({
527
+ action: "get",
528
+ tddTaskId: args.tddTaskId
529
+ });
530
+ else data = await caller.tdd_task({
531
+ action: "resume",
532
+ tddTaskId: args.tddTaskId
533
+ });
534
+ return structuredResult(Schema.decodeSync(TddTaskAsMarkdown)(data), data);
535
+ });
536
+ server.registerTool("tdd_phase_transition_request", {
537
+ 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.",
538
+ inputSchema: {
539
+ tddTaskId: z.coerce.number().describe("tdd_tasks.id"),
540
+ goalId: z.coerce.number().describe("tdd_session_goals.id (required; goal must be in_progress)"),
541
+ requestedPhase: z.enum([
542
+ "spike",
543
+ "red",
544
+ "red.triangulate",
545
+ "green",
546
+ "green.fake-it",
547
+ "refactor",
548
+ "extended-red",
549
+ "green-without-red"
550
+ ]).describe("Phase to transition to"),
551
+ citedArtifactId: z.optional(z.coerce.number()).describe("tdd_artifacts.id supplying the evidence. Optional — auto-resolved when omitted."),
552
+ citedArtifactKind: z.optional(z.enum([
553
+ "test_written",
554
+ "test_failed_run",
555
+ "code_written",
556
+ "test_passed_run",
557
+ "refactor",
558
+ "test_weakened"
559
+ ])).describe("Kind to look up when citedArtifactId is omitted (defaults to the kind required by the transition)."),
560
+ behaviorId: z.optional(z.coerce.number()).describe("tdd_session_behaviors.id when transitioning a specific behavior (must belong to goalId)"),
561
+ reason: z.optional(z.string()).describe("Free-text reason for the transition")
562
+ },
563
+ outputSchema: effectToZodSchema(PhaseTransitionResult)
564
+ }, async (args) => structuredJsonResult(await caller.tdd_phase_transition_request({
565
+ tddTaskId: args.tddTaskId,
566
+ goalId: args.goalId,
567
+ requestedPhase: args.requestedPhase,
568
+ ...args.citedArtifactId !== void 0 && { citedArtifactId: args.citedArtifactId },
569
+ ...args.citedArtifactKind !== void 0 && { citedArtifactKind: args.citedArtifactKind },
570
+ ...args.behaviorId !== void 0 && { behaviorId: args.behaviorId },
571
+ ...args.reason !== void 0 && { reason: args.reason }
572
+ })));
573
+ server.registerTool("tdd_goal", {
574
+ 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.",
575
+ inputSchema: {
576
+ action: z.enum([
577
+ "create",
578
+ "update",
579
+ "delete",
580
+ "get",
581
+ "list"
582
+ ]).describe("CRUD discriminator"),
583
+ id: z.optional(z.coerce.number()).describe("update/delete/get: goal id"),
584
+ tddTaskId: z.optional(z.coerce.number()).describe("create/list: tdd task id"),
585
+ goal: z.optional(z.string()),
586
+ status: z.optional(z.enum([
587
+ "pending",
588
+ "in_progress",
589
+ "done",
590
+ "abandoned"
591
+ ]))
592
+ },
593
+ outputSchema: effectToZodSchema(TddGoalResult)
594
+ }, async (args) => {
595
+ if (args.action === "create") return structuredJsonResult(await caller.tdd_goal({
596
+ action: "create",
597
+ tddTaskId: args.tddTaskId,
598
+ goal: args.goal
599
+ }));
600
+ if (args.action === "update") return structuredJsonResult(await caller.tdd_goal({
601
+ action: "update",
602
+ id: args.id,
603
+ ...args.goal !== void 0 && { goal: args.goal },
604
+ ...args.status !== void 0 && { status: args.status }
605
+ }));
606
+ if (args.action === "delete") return structuredJsonResult(await caller.tdd_goal({
607
+ action: "delete",
608
+ id: args.id
609
+ }));
610
+ if (args.action === "get") return structuredJsonResult(await caller.tdd_goal({
611
+ action: "get",
612
+ id: args.id
613
+ }));
614
+ return structuredJsonResult(await caller.tdd_goal({
615
+ action: "list",
616
+ tddTaskId: args.tddTaskId
617
+ }));
618
+ });
619
+ server.registerTool("tdd_behavior", {
620
+ 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.",
621
+ inputSchema: {
622
+ action: z.enum([
623
+ "create",
624
+ "update",
625
+ "delete",
626
+ "get",
627
+ "list_by_goal",
628
+ "list_by_tdd_task"
629
+ ]).describe("CRUD discriminator"),
630
+ id: z.optional(z.coerce.number()),
631
+ goalId: z.optional(z.coerce.number()),
632
+ tddTaskId: z.optional(z.coerce.number()),
633
+ behavior: z.optional(z.string()),
634
+ suggestedTestName: z.optional(z.string().nullable()),
635
+ status: z.optional(z.enum([
636
+ "pending",
637
+ "in_progress",
638
+ "done",
639
+ "abandoned"
640
+ ])),
641
+ dependsOnBehaviorIds: z.optional(z.array(z.coerce.number()))
642
+ },
643
+ outputSchema: effectToZodSchema(TddBehaviorResult)
644
+ }, async (args) => {
645
+ if (args.action === "create") return structuredJsonResult(await caller.tdd_behavior({
646
+ action: "create",
647
+ goalId: args.goalId,
648
+ behavior: args.behavior,
649
+ ...args.suggestedTestName !== void 0 && args.suggestedTestName !== null && { suggestedTestName: args.suggestedTestName },
650
+ ...args.dependsOnBehaviorIds !== void 0 && { dependsOnBehaviorIds: args.dependsOnBehaviorIds }
651
+ }));
652
+ if (args.action === "update") return structuredJsonResult(await caller.tdd_behavior({
653
+ action: "update",
654
+ id: args.id,
655
+ ...args.behavior !== void 0 && { behavior: args.behavior },
656
+ ...args.suggestedTestName !== void 0 && { suggestedTestName: args.suggestedTestName },
657
+ ...args.status !== void 0 && { status: args.status },
658
+ ...args.dependsOnBehaviorIds !== void 0 && { dependsOnBehaviorIds: args.dependsOnBehaviorIds }
659
+ }));
660
+ if (args.action === "delete") return structuredJsonResult(await caller.tdd_behavior({
661
+ action: "delete",
662
+ id: args.id
663
+ }));
664
+ if (args.action === "get") return structuredJsonResult(await caller.tdd_behavior({
665
+ action: "get",
666
+ id: args.id
667
+ }));
668
+ if (args.action === "list_by_goal") return structuredJsonResult(await caller.tdd_behavior({
669
+ action: "list_by_goal",
670
+ goalId: args.goalId
671
+ }));
672
+ return structuredJsonResult(await caller.tdd_behavior({
673
+ action: "list_by_tdd_task",
674
+ tddTaskId: args.tddTaskId
675
+ }));
676
+ });
677
+ server.registerTool("tdd_artifact_list", {
678
+ 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).",
679
+ inputSchema: {
680
+ tddTaskId: z.coerce.number().describe("tdd_tasks.id"),
681
+ artifactKind: z.optional(z.enum([
682
+ "test_written",
683
+ "test_failed_run",
684
+ "code_written",
685
+ "test_passed_run",
686
+ "refactor",
687
+ "test_weakened"
688
+ ])).describe("Restrict to one artifact kind"),
689
+ phaseId: z.optional(z.coerce.number()).describe("Restrict to artifacts recorded in one phase"),
690
+ behaviorId: z.optional(z.coerce.number()).describe("Restrict to artifacts recorded in phases bound to one behavior"),
691
+ limit: z.optional(z.coerce.number()).describe("Max rows (default 50)")
692
+ },
693
+ outputSchema: effectToZodSchema(TddArtifactListResult)
694
+ }, async (args) => {
695
+ const data = await caller.tdd_artifact_list({
696
+ tddTaskId: args.tddTaskId,
697
+ ...args.artifactKind !== void 0 && { artifactKind: args.artifactKind },
698
+ ...args.phaseId !== void 0 && { phaseId: args.phaseId },
699
+ ...args.behaviorId !== void 0 && { behaviorId: args.behaviorId },
700
+ ...args.limit !== void 0 && { limit: args.limit }
701
+ });
702
+ return structuredResult(Schema.decodeSync(TddArtifactListAsMarkdown)(data), data);
703
+ });
704
+ server.registerTool("hypothesis", {
705
+ description: "Use to manage debugging hypotheses, with a CRUD action discriminator: action='record' (sessionId, content, optional citation ids) writes a hypothesis; action='validate' (id, outcome, validatedAt) records a validation outcome; action='list' (sessionId?, outcome?, limit?) returns matching hypotheses as markdown.",
706
+ inputSchema: {
707
+ action: z.enum([
708
+ "record",
709
+ "validate",
710
+ "list"
711
+ ]).describe("CRUD discriminator"),
712
+ sessionId: z.optional(z.coerce.number()).describe("Session id (required for record; filter for list)"),
713
+ content: z.optional(z.string()).describe("Hypothesis content (action=record)"),
714
+ createdTurnId: z.optional(z.coerce.number()),
715
+ citedTestErrorId: z.optional(z.coerce.number()),
716
+ citedStackFrameId: z.optional(z.coerce.number()),
717
+ id: z.optional(z.coerce.number()).describe("Hypothesis id (action=validate)"),
718
+ outcome: z.optional(z.enum([
719
+ "confirmed",
720
+ "refuted",
721
+ "abandoned",
722
+ "open"
723
+ ])).describe("validate: 'confirmed'|'refuted'|'abandoned'; list filter may include 'open'"),
724
+ validatedTurnId: z.optional(z.coerce.number()),
725
+ validatedAt: z.optional(z.string()).describe("ISO 8601 timestamp (action=validate)"),
726
+ limit: z.optional(z.coerce.number())
727
+ },
728
+ outputSchema: effectToZodSchema(HypothesisResult)
729
+ }, async (args) => {
730
+ if (args.action === "record") return structuredJsonResult(await caller.hypothesis({
731
+ action: "record",
732
+ sessionId: args.sessionId,
733
+ content: args.content,
734
+ ...args.createdTurnId !== void 0 && { createdTurnId: args.createdTurnId },
735
+ ...args.citedTestErrorId !== void 0 && { citedTestErrorId: args.citedTestErrorId },
736
+ ...args.citedStackFrameId !== void 0 && { citedStackFrameId: args.citedStackFrameId }
737
+ }));
738
+ if (args.action === "validate") return structuredJsonResult(await caller.hypothesis({
739
+ action: "validate",
740
+ id: args.id,
741
+ outcome: args.outcome,
742
+ validatedAt: args.validatedAt,
743
+ ...args.validatedTurnId !== void 0 && { validatedTurnId: args.validatedTurnId }
744
+ }));
745
+ const result = await caller.hypothesis({
746
+ action: "list",
747
+ ...args.sessionId !== void 0 && { sessionId: args.sessionId },
748
+ ...args.outcome !== void 0 && { outcome: args.outcome },
749
+ ...args.limit !== void 0 && { limit: args.limit }
750
+ });
751
+ return structuredResult(formatHypothesisListMarkdown(result), result);
752
+ });
753
+ server.registerTool("tdd_progress_push", {
754
+ 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.",
755
+ inputSchema: { payload: z.string().describe("Pre-stringified ChannelEvent JSON (see schemas/ChannelEvent in @vitest-agent/sdk)") }
756
+ }, async (args) => {
757
+ let resolvedPayload = args.payload;
758
+ try {
759
+ const enriched = await resolveChannelEvent(ctx, JSON.parse(args.payload));
760
+ resolvedPayload = JSON.stringify(enriched);
761
+ } catch {}
762
+ try {
763
+ await server.server.notification({
764
+ method: "notifications/claude/channel",
765
+ params: { content: resolvedPayload }
766
+ });
767
+ } catch {}
768
+ return structuredJsonResult({ ok: true });
769
+ });
770
+ server.registerTool("acceptance_metrics", {
771
+ 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, ... }).",
772
+ inputSchema: {},
773
+ outputSchema: effectToZodSchema(AcceptanceMetricsResult)
774
+ }, async () => {
775
+ const data = await caller.acceptance_metrics({});
776
+ return structuredResult(Schema.decodeSync(AcceptanceMetricsAsMarkdown)(data), data);
777
+ });
778
+ server.registerTool("triage_brief", {
779
+ 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 }).",
780
+ inputSchema: {
781
+ project: z.optional(z.string()).describe("Filter to a specific project"),
782
+ maxLines: z.optional(z.coerce.number()).describe("Soft cap on rendered output lines")
783
+ },
784
+ outputSchema: effectToZodSchema(TriageBriefResult)
785
+ }, async (args) => {
786
+ const data = await caller.triage_brief({
787
+ project: args.project,
788
+ maxLines: args.maxLines
789
+ });
790
+ return structuredResult(data.markdown, data);
791
+ });
792
+ server.registerTool("wrapup_prompt", {
793
+ 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 }).",
794
+ inputSchema: {
795
+ sessionId: z.optional(z.coerce.number()).describe("sessions.id (integer); omit to use chatId"),
796
+ chatId: z.optional(z.string()).describe("Host chat UUID (alternative to sessionId)"),
797
+ kind: z.optional(z.enum([
798
+ "stop",
799
+ "session_end",
800
+ "pre_compact",
801
+ "tdd_handoff",
802
+ "user_prompt_nudge"
803
+ ])).describe("Wrap-up flavor (default: session_end)"),
804
+ userPromptHint: z.optional(z.string()).describe("For user_prompt_nudge: the prompt text to inspect")
805
+ },
806
+ outputSchema: effectToZodSchema(WrapupPromptResult)
807
+ }, async (args) => {
808
+ const data = await caller.wrapup_prompt({
809
+ sessionId: args.sessionId,
810
+ chatId: args.chatId,
811
+ kind: args.kind,
812
+ userPromptHint: args.userPromptHint
813
+ });
814
+ return structuredResult(data.markdown, data);
815
+ });
816
+ server.registerTool("commit_changes", {
817
+ 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[] }).",
818
+ inputSchema: { sha: z.optional(z.string()).describe("Specific commit sha to fetch; omit for recent commits") },
819
+ outputSchema: effectToZodSchema(CommitChangesResult)
820
+ }, async (args) => {
821
+ const data = await caller.commit_changes({ sha: args.sha });
822
+ return structuredResult(Schema.decodeSync(CommitChangesAsMarkdown)(data), data);
823
+ });
824
+ server.registerTool("ping", {
825
+ 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.",
826
+ outputSchema: effectToZodSchema(PingResult)
827
+ }, async () => {
828
+ const data = await caller.ping();
829
+ return structuredResult(data.message, data);
830
+ });
831
+ registerAllResources(server);
832
+ registerAllPrompts(server);
833
+ const transport = new StdioServerTransport();
834
+ await server.connect(transport);
835
+ }
836
+
837
+ //#endregion
838
+ export { startMcpServer };