@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
@@ -0,0 +1,377 @@
1
+ import { publicProcedure } from "../context.js";
2
+ import { DataReader } from "@vitest-agent/sdk";
3
+ import { Effect, Match, Option, ParseResult, Schema } from "effect";
4
+
5
+ //#region src/tools/inventory.ts
6
+ /**
7
+ * Consolidated `inventory` MCP tool — Schema-driven implementation.
8
+ *
9
+ * Each `kind` produces a structured result that the boundary in
10
+ * server.ts can render as markdown via the exported
11
+ * `formatInventoryMarkdown` helper. The structured payload uses an
12
+ * `inventoryKind` discriminant (named separately from the input
13
+ * `kind` because `session` collapses to two output shapes — one for
14
+ * single-id lookup and one for list).
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+ const ProjectRunSummary = Schema.Struct({
19
+ project: Schema.String,
20
+ lastRun: Schema.NullOr(Schema.String),
21
+ lastResult: Schema.NullOr(Schema.Literal("passed", "failed", "interrupted")),
22
+ total: Schema.Number,
23
+ passed: Schema.Number,
24
+ failed: Schema.Number,
25
+ skipped: Schema.Number
26
+ }).annotations({ identifier: "InventoryProjectRow" });
27
+ const ModuleRow = Schema.Struct({
28
+ id: Schema.Number,
29
+ file: Schema.String,
30
+ state: Schema.String,
31
+ testCount: Schema.Number,
32
+ duration: Schema.NullOr(Schema.Number)
33
+ }).annotations({ identifier: "InventoryModuleRow" });
34
+ const SuiteRow = Schema.Struct({
35
+ id: Schema.Number,
36
+ name: Schema.String,
37
+ module: Schema.String,
38
+ state: Schema.String,
39
+ testCount: Schema.Number
40
+ }).annotations({ identifier: "InventorySuiteRow" });
41
+ const SessionRow = Schema.Struct({
42
+ id: Schema.Number,
43
+ chatId: Schema.String,
44
+ project: Schema.String,
45
+ cwd: Schema.String,
46
+ agentKind: Schema.Literal("main", "subagent"),
47
+ agentType: Schema.NullOr(Schema.String),
48
+ parentSessionId: Schema.NullOr(Schema.Number),
49
+ triageWasNonEmpty: Schema.Boolean,
50
+ startedAt: Schema.String,
51
+ endedAt: Schema.NullOr(Schema.String),
52
+ endReason: Schema.NullOr(Schema.String)
53
+ }).annotations({ identifier: "InventorySessionRow" });
54
+ const ProjectInventory = Schema.Struct({
55
+ inventoryKind: Schema.Literal("project"),
56
+ count: Schema.Number,
57
+ projects: Schema.Array(ProjectRunSummary)
58
+ }).annotations({ identifier: "ProjectInventory" });
59
+ const ModuleGroup = Schema.Struct({
60
+ project: Schema.String,
61
+ modules: Schema.Array(ModuleRow)
62
+ });
63
+ const ModuleInventory = Schema.Struct({
64
+ inventoryKind: Schema.Literal("module"),
65
+ count: Schema.Number,
66
+ groups: Schema.Array(ModuleGroup)
67
+ }).annotations({ identifier: "ModuleInventory" });
68
+ const SuiteGroup = Schema.Struct({
69
+ project: Schema.String,
70
+ suites: Schema.Array(SuiteRow)
71
+ });
72
+ const SuiteInventory = Schema.Struct({
73
+ inventoryKind: Schema.Literal("suite"),
74
+ count: Schema.Number,
75
+ groups: Schema.Array(SuiteGroup)
76
+ }).annotations({ identifier: "SuiteInventory" });
77
+ const SessionDetailFound = Schema.Struct({
78
+ inventoryKind: Schema.Literal("session_detail"),
79
+ found: Schema.Literal(true),
80
+ session: SessionRow
81
+ }).annotations({ identifier: "SessionDetailFound" });
82
+ const SessionDetailMissing = Schema.Struct({
83
+ inventoryKind: Schema.Literal("session_detail"),
84
+ found: Schema.Literal(false),
85
+ id: Schema.Number
86
+ }).annotations({ identifier: "SessionDetailMissing" });
87
+ const SessionListInventory = Schema.Struct({
88
+ inventoryKind: Schema.Literal("session_list"),
89
+ count: Schema.Number,
90
+ sessions: Schema.Array(SessionRow)
91
+ }).annotations({ identifier: "SessionListInventory" });
92
+ const TagProjectBreakdown = Schema.Struct({
93
+ project: Schema.String,
94
+ moduleCount: Schema.Number,
95
+ testCount: Schema.Number
96
+ }).annotations({ identifier: "TagProjectBreakdown" });
97
+ const TagRowScoped = Schema.Struct({
98
+ tag: Schema.String,
99
+ moduleCount: Schema.Number,
100
+ testCount: Schema.Number
101
+ }).annotations({ identifier: "TagRowScoped" });
102
+ const TagRowUnscoped = Schema.Struct({
103
+ tag: Schema.String,
104
+ moduleCount: Schema.Number,
105
+ testCount: Schema.Number,
106
+ byProject: Schema.Array(TagProjectBreakdown)
107
+ }).annotations({ identifier: "TagRowUnscoped" });
108
+ const TagInventoryScoped = Schema.Struct({
109
+ inventoryKind: Schema.Literal("tag_scoped"),
110
+ project: Schema.String,
111
+ count: Schema.Number,
112
+ tags: Schema.Array(TagRowScoped)
113
+ }).annotations({ identifier: "TagInventoryScoped" });
114
+ const TagInventoryUnscoped = Schema.Struct({
115
+ inventoryKind: Schema.Literal("tag_unscoped"),
116
+ count: Schema.Number,
117
+ tags: Schema.Array(TagRowUnscoped)
118
+ }).annotations({ identifier: "TagInventoryUnscoped" });
119
+ const InventoryResult = Schema.Union(ProjectInventory, ModuleInventory, SuiteInventory, SessionDetailFound, SessionDetailMissing, SessionListInventory, TagInventoryScoped, TagInventoryUnscoped).annotations({
120
+ identifier: "InventoryResult",
121
+ title: "inventory result",
122
+ description: "Discriminate on `inventoryKind`. project/module/suite carry counted lists; session_detail discriminates further on `found`; session_list returns the matching sessions; tag_scoped and tag_unscoped carry per-tag counts (the unscoped form also carries a `byProject` breakdown per tag)."
123
+ });
124
+ const formatInventoryMarkdown = (data) => {
125
+ if (data.inventoryKind === "project") {
126
+ if (data.count === 0) return "No projects found. Run tests first.";
127
+ const lines = [
128
+ "## Projects",
129
+ "",
130
+ "| Project | Last Run | Result | Total | Passed | Failed | Skipped |",
131
+ "| --- | --- | --- | --- | --- | --- | --- |"
132
+ ];
133
+ for (const p of data.projects) {
134
+ const lastRun = p.lastRun ? p.lastRun.split("T")[0] : "—";
135
+ const result = p.lastResult ?? "—";
136
+ lines.push(`| ${p.project} | ${lastRun} | ${result} | ${p.total} | ${p.passed} | ${p.failed} | ${p.skipped} |`);
137
+ }
138
+ return lines.join("\n");
139
+ }
140
+ if (data.inventoryKind === "module") {
141
+ if (data.count === 0) return "No modules found. Run run_tests({}) to execute tests and populate the database.";
142
+ const lines = ["## Modules", ""];
143
+ for (const g of data.groups) {
144
+ lines.push(`### ${g.project}`, "", "| ID | File | State | Tests | Duration |", "| --- | --- | --- | --- | --- |");
145
+ for (const m of g.modules) {
146
+ const duration = m.duration !== null ? `${m.duration}ms` : "—";
147
+ lines.push(`| ${m.id} | ${m.file} | ${m.state} | ${m.testCount} | ${duration} |`);
148
+ }
149
+ lines.push("");
150
+ }
151
+ return lines.join("\n").trimEnd();
152
+ }
153
+ if (data.inventoryKind === "suite") {
154
+ if (data.count === 0) return "No suites found. Run run_tests({}) to execute tests and populate the database.";
155
+ const lines = ["## Suites", ""];
156
+ for (const g of data.groups) {
157
+ lines.push(`### ${g.project}`, "", "| ID | Name | Module | State | Tests |", "| --- | --- | --- | --- | --- |");
158
+ for (const s of g.suites) lines.push(`| ${s.id} | ${s.name} | ${s.module} | ${s.state} | ${s.testCount} |`);
159
+ lines.push("");
160
+ }
161
+ return lines.join("\n").trimEnd();
162
+ }
163
+ if (data.inventoryKind === "session_detail") {
164
+ if (!data.found) return `No session with id=${data.id}.`;
165
+ const s = data.session;
166
+ const lines = [
167
+ `# Session ${s.id}`,
168
+ "",
169
+ `- chatId: \`${s.chatId}\``,
170
+ `- project: ${s.project}`,
171
+ `- agentKind: ${s.agentKind}${s.agentType !== null ? ` (${s.agentType})` : ""}`,
172
+ `- started: ${s.startedAt}`,
173
+ `- ended: ${s.endedAt ?? "still open"}`,
174
+ `- triageWasNonEmpty: ${s.triageWasNonEmpty}`
175
+ ];
176
+ if (s.parentSessionId !== null) lines.push(`- parentSessionId: ${s.parentSessionId}`);
177
+ return lines.join("\n");
178
+ }
179
+ if (data.inventoryKind === "tag_scoped") {
180
+ if (data.count === 0) return `No tags recorded for project \`${data.project}\`. Run run_tests({}) to populate.`;
181
+ const lines = [
182
+ `## Tags — ${data.project}`,
183
+ "",
184
+ "| Tag | Modules | Tests |",
185
+ "| --- | --- | --- |"
186
+ ];
187
+ for (const t of data.tags) lines.push(`| ${t.tag} | ${t.moduleCount} | ${t.testCount} |`);
188
+ return lines.join("\n");
189
+ }
190
+ if (data.inventoryKind === "tag_unscoped") {
191
+ if (data.count === 0) return "No tags recorded. Run run_tests({}) to populate.";
192
+ const lines = [
193
+ "## Tags",
194
+ "",
195
+ "| Tag | Modules | Tests | Projects |",
196
+ "| --- | --- | --- | --- |"
197
+ ];
198
+ for (const t of data.tags) {
199
+ const projectsBreakdown = t.byProject.map((p) => `${p.project} (${p.testCount})`).join(", ");
200
+ lines.push(`| ${t.tag} | ${t.moduleCount} | ${t.testCount} | ${projectsBreakdown} |`);
201
+ }
202
+ return lines.join("\n");
203
+ }
204
+ if (data.count === 0) return "No sessions recorded yet.";
205
+ const lines = ["# Sessions", ""];
206
+ for (const s of data.sessions) {
207
+ const ended = s.endedAt ? `ended ${s.endedAt}` : "open";
208
+ lines.push(`- **${s.chatId}** [${s.agentKind}] project=${s.project} started=${s.startedAt} ${ended}`);
209
+ }
210
+ return lines.join("\n");
211
+ };
212
+ const InventoryAsMarkdown = Schema.transformOrFail(InventoryResult, Schema.String, {
213
+ strict: true,
214
+ decode: (data) => ParseResult.succeed(formatInventoryMarkdown(data)),
215
+ encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "InventoryAsMarkdown is one-way."))
216
+ });
217
+ const ProjectVariant = Schema.Struct({ kind: Schema.Literal("project") });
218
+ const ModuleVariant = Schema.Struct({
219
+ kind: Schema.Literal("module"),
220
+ project: Schema.optional(Schema.String)
221
+ });
222
+ const SuiteVariant = Schema.Struct({
223
+ kind: Schema.Literal("suite"),
224
+ project: Schema.optional(Schema.String),
225
+ module: Schema.optional(Schema.String)
226
+ });
227
+ const SessionVariant = Schema.Struct({
228
+ kind: Schema.Literal("session"),
229
+ id: Schema.optional(Schema.Number),
230
+ project: Schema.optional(Schema.String),
231
+ agentKind: Schema.optional(Schema.Literal("main", "subagent")),
232
+ limit: Schema.optional(Schema.Number)
233
+ });
234
+ const TagVariant = Schema.Struct({
235
+ kind: Schema.Literal("tag"),
236
+ project: Schema.optional(Schema.String)
237
+ });
238
+ const InventoryInput = Schema.Union(ProjectVariant, ModuleVariant, SuiteVariant, SessionVariant, TagVariant);
239
+ const inventory = publicProcedure.input(Schema.standardSchemaV1(InventoryInput)).query(async ({ ctx, input }) => {
240
+ return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("kind")({
241
+ project: () => Effect.gen(function* () {
242
+ const projects = yield* (yield* DataReader).getRunsByProject();
243
+ return {
244
+ inventoryKind: "project",
245
+ count: projects.length,
246
+ projects: projects.map((p) => ({
247
+ project: p.project,
248
+ lastRun: p.lastRun,
249
+ lastResult: p.lastResult,
250
+ total: p.total,
251
+ passed: p.passed,
252
+ failed: p.failed,
253
+ skipped: p.skipped
254
+ }))
255
+ };
256
+ }),
257
+ module: (variant) => Effect.gen(function* () {
258
+ const reader = yield* DataReader;
259
+ const targets = variant.project ? [{ project: variant.project }] : yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs.map((r) => ({ project: r.project }))));
260
+ const groups = [];
261
+ let total = 0;
262
+ for (const t of targets) {
263
+ const modules = yield* reader.listModules(t.project);
264
+ if (modules.length > 0) {
265
+ groups.push({
266
+ project: t.project,
267
+ modules
268
+ });
269
+ total += modules.length;
270
+ }
271
+ }
272
+ return {
273
+ inventoryKind: "module",
274
+ count: total,
275
+ groups
276
+ };
277
+ }),
278
+ suite: (variant) => Effect.gen(function* () {
279
+ const reader = yield* DataReader;
280
+ const opts = {};
281
+ if (variant.module !== void 0) opts.module = variant.module;
282
+ const targets = variant.project ? [{ project: variant.project }] : yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs.map((r) => ({ project: r.project }))));
283
+ const groups = [];
284
+ let total = 0;
285
+ for (const t of targets) {
286
+ const suites = yield* reader.listSuites(t.project, opts);
287
+ if (suites.length > 0) {
288
+ groups.push({
289
+ project: t.project,
290
+ suites
291
+ });
292
+ total += suites.length;
293
+ }
294
+ }
295
+ return {
296
+ inventoryKind: "suite",
297
+ count: total,
298
+ groups
299
+ };
300
+ }),
301
+ session: (variant) => Effect.gen(function* () {
302
+ const reader = yield* DataReader;
303
+ if (variant.id !== void 0) {
304
+ const opt = yield* reader.getSessionById(variant.id);
305
+ return Option.isNone(opt) ? {
306
+ inventoryKind: "session_detail",
307
+ found: false,
308
+ id: variant.id
309
+ } : {
310
+ inventoryKind: "session_detail",
311
+ found: true,
312
+ session: opt.value
313
+ };
314
+ }
315
+ const rows = yield* reader.listSessions({
316
+ ...variant.project !== void 0 && { project: variant.project },
317
+ ...variant.agentKind !== void 0 && { agentKind: variant.agentKind },
318
+ ...variant.limit !== void 0 && { limit: variant.limit }
319
+ });
320
+ return {
321
+ inventoryKind: "session_list",
322
+ count: rows.length,
323
+ sessions: rows
324
+ };
325
+ }),
326
+ tag: (variant) => Effect.gen(function* () {
327
+ const reader = yield* DataReader;
328
+ if (variant.project !== void 0) {
329
+ const rows = yield* reader.listTagInventory({ project: variant.project });
330
+ return {
331
+ inventoryKind: "tag_scoped",
332
+ project: variant.project,
333
+ count: rows.length,
334
+ tags: rows.map((r) => ({
335
+ tag: r.tag,
336
+ moduleCount: r.moduleCount,
337
+ testCount: r.testCount
338
+ }))
339
+ };
340
+ }
341
+ const rows = yield* reader.listTagInventory();
342
+ const byTag = /* @__PURE__ */ new Map();
343
+ for (const r of rows) {
344
+ let entry = byTag.get(r.tag);
345
+ if (entry === void 0) {
346
+ entry = {
347
+ moduleCount: 0,
348
+ testCount: 0,
349
+ byProject: []
350
+ };
351
+ byTag.set(r.tag, entry);
352
+ }
353
+ entry.moduleCount += r.moduleCount;
354
+ entry.testCount += r.testCount;
355
+ entry.byProject.push({
356
+ project: r.project,
357
+ moduleCount: r.moduleCount,
358
+ testCount: r.testCount
359
+ });
360
+ }
361
+ const tags = Array.from(byTag.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([tag, e]) => ({
362
+ tag,
363
+ moduleCount: e.moduleCount,
364
+ testCount: e.testCount,
365
+ byProject: e.byProject
366
+ }));
367
+ return {
368
+ inventoryKind: "tag_unscoped",
369
+ count: tags.length,
370
+ tags
371
+ };
372
+ })
373
+ })));
374
+ });
375
+
376
+ //#endregion
377
+ export { InventoryAsMarkdown, InventoryResult, inventory };
package/tools/note.js ADDED
@@ -0,0 +1,208 @@
1
+ import { publicProcedure } from "../context.js";
2
+ import { DataReader, DataStore } from "@vitest-agent/sdk";
3
+ import { Effect, Match, Option, Schema } from "effect";
4
+
5
+ //#region src/tools/note.ts
6
+ const NoteScope = Schema.Literal("global", "project", "module", "suite", "test", "note");
7
+ const NoteRowSchema = Schema.Struct({
8
+ id: Schema.Number.annotations({ description: "Note primary key." }),
9
+ title: Schema.String,
10
+ content: Schema.String,
11
+ scope: NoteScope.annotations({ description: "`global` (project-agnostic), `project`, `module`, `suite`, `test` (scoped), or `note` (child note attached via `parentNoteId`)." }),
12
+ project: Schema.NullOr(Schema.String),
13
+ testFullName: Schema.NullOr(Schema.String),
14
+ modulePath: Schema.NullOr(Schema.String),
15
+ parentNoteId: Schema.NullOr(Schema.Number),
16
+ createdBy: Schema.NullOr(Schema.String),
17
+ expiresAt: Schema.NullOr(Schema.String),
18
+ pinned: Schema.Boolean,
19
+ createdAt: Schema.String,
20
+ updatedAt: Schema.String
21
+ }).annotations({ identifier: "NoteRow" });
22
+ const NoteCreateOk = Schema.Struct({
23
+ action: Schema.Literal("create"),
24
+ id: Schema.Number.annotations({ description: "Primary key of the newly inserted note." })
25
+ });
26
+ const NoteListOk = Schema.Struct({
27
+ action: Schema.Literal("list"),
28
+ count: Schema.Number,
29
+ notes: Schema.Array(NoteRowSchema).annotations({ description: "Notes matching the optional scope/project/test filters." })
30
+ });
31
+ const NoteGetFound = Schema.Struct({
32
+ action: Schema.Literal("get"),
33
+ found: Schema.Literal(true),
34
+ note: NoteRowSchema
35
+ });
36
+ const NoteGetMissing = Schema.Struct({
37
+ action: Schema.Literal("get"),
38
+ found: Schema.Literal(false),
39
+ id: Schema.Number
40
+ });
41
+ const NoteUpdateOk = Schema.Struct({
42
+ action: Schema.Literal("update"),
43
+ success: Schema.Literal(true)
44
+ });
45
+ const NoteDeleteOk = Schema.Struct({
46
+ action: Schema.Literal("delete"),
47
+ success: Schema.Literal(true)
48
+ });
49
+ const NoteSearchOk = Schema.Struct({
50
+ action: Schema.Literal("search"),
51
+ query: Schema.String,
52
+ count: Schema.Number,
53
+ notes: Schema.Array(NoteRowSchema).annotations({ description: "Notes whose title or content match the FTS5 query." })
54
+ });
55
+ const NoteResult = Schema.Union(NoteCreateOk, NoteListOk, NoteGetFound, NoteGetMissing, NoteUpdateOk, NoteDeleteOk, NoteSearchOk).annotations({
56
+ identifier: "NoteResult",
57
+ title: "note result",
58
+ description: "Discriminate on `action`. `get` further discriminates on `found`."
59
+ });
60
+ const renderNoteTable = (notes) => {
61
+ const lines = ["| ID | Title | Scope | Project | Created |", "| --- | --- | --- | --- | --- |"];
62
+ for (const n of notes) {
63
+ const proj = n.project ?? "—";
64
+ const created = n.createdAt.split("T")[0];
65
+ lines.push(`| ${n.id} | ${n.title} | ${n.scope} | ${proj} | ${created} |`);
66
+ }
67
+ return lines.join("\n");
68
+ };
69
+ /**
70
+ * Markdown rendering used at the boundary for note list/search
71
+ * results. Mutations (create/get/update/delete) get JSON-stringify
72
+ * via `structuredJsonResult` instead of a markdown view.
73
+ */
74
+ const formatNoteListMarkdown = (data) => {
75
+ if (data.action === "list") {
76
+ if (data.notes.length === 0) return "No notes found. Use note({ action: \"create\", ... }) to add notes.";
77
+ return [
78
+ "## Notes",
79
+ "",
80
+ renderNoteTable(data.notes)
81
+ ].join("\n");
82
+ }
83
+ if (data.action === "search") {
84
+ if (data.notes.length === 0) return "No notes matched.";
85
+ return [
86
+ `## Notes matching "${data.query}"`,
87
+ "",
88
+ renderNoteTable(data.notes)
89
+ ].join("\n");
90
+ }
91
+ return JSON.stringify(data, null, 2);
92
+ };
93
+ const CreateVariant = Schema.Struct({
94
+ action: Schema.Literal("create"),
95
+ title: Schema.String,
96
+ content: Schema.String,
97
+ scope: NoteScope,
98
+ project: Schema.optional(Schema.String),
99
+ testFullName: Schema.optional(Schema.String),
100
+ modulePath: Schema.optional(Schema.String),
101
+ parentNoteId: Schema.optional(Schema.Number),
102
+ createdBy: Schema.optional(Schema.String),
103
+ expiresAt: Schema.optional(Schema.String),
104
+ pinned: Schema.optional(Schema.Boolean)
105
+ });
106
+ const ListVariant = Schema.Struct({
107
+ action: Schema.Literal("list"),
108
+ scope: Schema.optional(Schema.String),
109
+ project: Schema.optional(Schema.String),
110
+ testFullName: Schema.optional(Schema.String)
111
+ });
112
+ const GetVariant = Schema.Struct({
113
+ action: Schema.Literal("get"),
114
+ id: Schema.Number
115
+ });
116
+ const UpdateVariant = Schema.Struct({
117
+ action: Schema.Literal("update"),
118
+ id: Schema.Number,
119
+ title: Schema.optional(Schema.String),
120
+ content: Schema.optional(Schema.String),
121
+ pinned: Schema.optional(Schema.Boolean),
122
+ expiresAt: Schema.optional(Schema.String)
123
+ });
124
+ const DeleteVariant = Schema.Struct({
125
+ action: Schema.Literal("delete"),
126
+ id: Schema.Number
127
+ });
128
+ const SearchVariant = Schema.Struct({
129
+ action: Schema.Literal("search"),
130
+ query: Schema.String
131
+ });
132
+ const NoteInputUnion = Schema.Union(CreateVariant, ListVariant, GetVariant, UpdateVariant, DeleteVariant, SearchVariant);
133
+ const note = publicProcedure.input(Schema.standardSchemaV1(NoteInputUnion)).mutation(async ({ ctx, input }) => {
134
+ return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
135
+ create: (variant) => Effect.gen(function* () {
136
+ const store = yield* DataStore;
137
+ const noteInput = {
138
+ title: variant.title,
139
+ content: variant.content,
140
+ scope: variant.scope,
141
+ ...variant.project !== void 0 && { project: variant.project },
142
+ ...variant.testFullName !== void 0 && { testFullName: variant.testFullName },
143
+ ...variant.modulePath !== void 0 && { modulePath: variant.modulePath },
144
+ ...variant.parentNoteId !== void 0 && { parentNoteId: variant.parentNoteId },
145
+ ...variant.createdBy !== void 0 && { createdBy: variant.createdBy },
146
+ ...variant.expiresAt !== void 0 && { expiresAt: variant.expiresAt },
147
+ ...variant.pinned !== void 0 && { pinned: variant.pinned }
148
+ };
149
+ return {
150
+ action: "create",
151
+ id: yield* store.writeNote(noteInput)
152
+ };
153
+ }),
154
+ list: (variant) => Effect.gen(function* () {
155
+ const notes = yield* (yield* DataReader).getNotes(variant.scope, variant.project, variant.testFullName);
156
+ return {
157
+ action: "list",
158
+ count: notes.length,
159
+ notes
160
+ };
161
+ }),
162
+ get: (variant) => Effect.gen(function* () {
163
+ const noteOpt = yield* (yield* DataReader).getNoteById(variant.id);
164
+ return Option.isNone(noteOpt) ? {
165
+ action: "get",
166
+ found: false,
167
+ id: variant.id
168
+ } : {
169
+ action: "get",
170
+ found: true,
171
+ note: noteOpt.value
172
+ };
173
+ }),
174
+ update: (variant) => Effect.gen(function* () {
175
+ const store = yield* DataStore;
176
+ const fields = {
177
+ ...variant.title !== void 0 && { title: variant.title },
178
+ ...variant.content !== void 0 && { content: variant.content },
179
+ ...variant.pinned !== void 0 && { pinned: variant.pinned },
180
+ ...variant.expiresAt !== void 0 && { expiresAt: variant.expiresAt }
181
+ };
182
+ yield* store.updateNote(variant.id, fields);
183
+ return {
184
+ action: "update",
185
+ success: true
186
+ };
187
+ }),
188
+ delete: (variant) => Effect.gen(function* () {
189
+ yield* (yield* DataStore).deleteNote(variant.id);
190
+ return {
191
+ action: "delete",
192
+ success: true
193
+ };
194
+ }),
195
+ search: (variant) => Effect.gen(function* () {
196
+ const notes = yield* (yield* DataReader).searchNotes(variant.query);
197
+ return {
198
+ action: "search",
199
+ query: variant.query,
200
+ count: notes.length,
201
+ notes
202
+ };
203
+ })
204
+ })));
205
+ });
206
+
207
+ //#endregion
208
+ export { NoteResult, formatNoteListMarkdown, note };
@@ -0,0 +1,92 @@
1
+ import { publicProcedure } from "../context.js";
2
+ import { DataReader } from "@vitest-agent/sdk";
3
+ import { Effect, Option, ParseResult, Schema } from "effect";
4
+
5
+ //#region src/tools/overview.ts
6
+ /**
7
+ * `test_overview` MCP tool — Schema-driven implementation.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ const ProjectRunSummary = Schema.Struct({
12
+ project: Schema.String,
13
+ lastRun: Schema.NullOr(Schema.String),
14
+ lastResult: Schema.NullOr(Schema.Literal("passed", "failed", "interrupted")),
15
+ total: Schema.Number,
16
+ passed: Schema.Number,
17
+ failed: Schema.Number,
18
+ skipped: Schema.Number
19
+ }).annotations({
20
+ identifier: "ProjectRunSummary",
21
+ description: "One row per project's most recent run summary."
22
+ });
23
+ const OverviewAvailable = Schema.Struct({
24
+ dataAvailable: Schema.Literal(true),
25
+ projectFilter: Schema.optional(Schema.String),
26
+ runs: Schema.Array(ProjectRunSummary)
27
+ }).annotations({ identifier: "TestOverviewAvailable" });
28
+ const OverviewAbsent = Schema.Struct({
29
+ dataAvailable: Schema.Literal(false),
30
+ projectFilter: Schema.optional(Schema.String),
31
+ reason: Schema.Literal("no_runs", "project_filter_empty")
32
+ }).annotations({ identifier: "TestOverviewAbsent" });
33
+ const TestOverviewResult = Schema.Union(OverviewAvailable, OverviewAbsent).annotations({
34
+ identifier: "TestOverviewResult",
35
+ title: "test_overview result",
36
+ description: "Per-project run metrics. Discriminate on `dataAvailable` for cold-start handling."
37
+ });
38
+ const iconForResult = (r) => {
39
+ if (r === "passed") return "✅";
40
+ if (r === "failed") return "❌";
41
+ if (r === "interrupted") return "⚠️";
42
+ return "⬜";
43
+ };
44
+ const formatTestOverviewMarkdown = (data) => {
45
+ if (!data.dataAvailable) {
46
+ if (data.reason === "project_filter_empty") return `No test data found for project \`${data.projectFilter ?? "(unknown)"}\`. Run tests first.`;
47
+ return "No test data available. Run tests first.";
48
+ }
49
+ const lines = ["# Test Overview", ""];
50
+ const projectGroups = /* @__PURE__ */ new Map();
51
+ for (const run of data.runs) {
52
+ const group = projectGroups.get(run.project) ?? [];
53
+ group.push(run);
54
+ projectGroups.set(run.project, group);
55
+ }
56
+ for (const [projectName, projectRuns] of projectGroups) {
57
+ lines.push(`## ${projectName}`, "");
58
+ for (const run of projectRuns) {
59
+ const lastRun = run.lastRun ? new Date(run.lastRun).toLocaleString() : "never";
60
+ lines.push(`### ${iconForResult(run.lastResult)} ${run.project}`, "", "| Metric | Count |", "| --- | --- |", `| Total | ${run.total} |`, `| Passed | ${run.passed} |`, `| Failed | ${run.failed} |`, `| Skipped | ${run.skipped} |`, `| Last run | ${lastRun} |`, "");
61
+ }
62
+ }
63
+ return lines.join("\n");
64
+ };
65
+ const TestOverviewAsMarkdown = Schema.transformOrFail(TestOverviewResult, Schema.String, {
66
+ strict: true,
67
+ decode: (data) => ParseResult.succeed(formatTestOverviewMarkdown(data)),
68
+ encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "TestOverviewAsMarkdown is one-way."))
69
+ });
70
+ const testOverview = publicProcedure.input(Schema.standardSchemaV1(Schema.Struct({ project: Schema.optional(Schema.String) }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
71
+ const reader = yield* DataReader;
72
+ const [manifestOpt, runs] = yield* Effect.all([reader.getManifest(), reader.getRunsByProject()]);
73
+ if (Option.isNone(manifestOpt) || runs.length === 0) return {
74
+ dataAvailable: false,
75
+ reason: "no_runs",
76
+ ...input.project !== void 0 && { projectFilter: input.project }
77
+ };
78
+ const filteredRuns = input.project === void 0 ? runs : runs.filter((r) => r.project === input.project);
79
+ if (filteredRuns.length === 0) return {
80
+ dataAvailable: false,
81
+ reason: "project_filter_empty",
82
+ ...input.project !== void 0 && { projectFilter: input.project }
83
+ };
84
+ return {
85
+ dataAvailable: true,
86
+ ...input.project !== void 0 && { projectFilter: input.project },
87
+ runs: filteredRuns
88
+ };
89
+ })));
90
+
91
+ //#endregion
92
+ export { TestOverviewAsMarkdown, TestOverviewResult, testOverview };
package/tools/ping.js ADDED
@@ -0,0 +1,22 @@
1
+ import { publicProcedure } from "../context.js";
2
+ import { Schema } from "effect";
3
+
4
+ //#region src/tools/ping.ts
5
+ /**
6
+ * `ping` MCP tool — Schema-driven implementation.
7
+ *
8
+ * Trivial liveness probe used to verify hot-patch reload of the MCP
9
+ * server. Returns the canonical `pong` payload so callers can assert
10
+ * a healthy round-trip.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+ const PingResult = Schema.Struct({ message: Schema.Literal("pong").annotations({ description: "Constant `pong`. Presence confirms the MCP server responded." }) }).annotations({
15
+ identifier: "PingResult",
16
+ title: "ping result",
17
+ description: "Liveness probe. Carries no data beyond the constant `pong` discriminant."
18
+ });
19
+ const ping = publicProcedure.query(async () => ({ message: "pong" }));
20
+
21
+ //#endregion
22
+ export { PingResult, ping };