@vitest-agent/mcp 3.0.4 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -1,9 +1,8 @@
1
- import { initTRPC } from "@trpc/server";
2
- import { MutableRef } from "effect";
1
+ import { Context, Layer, MutableRef } from "effect";
3
2
 
4
- //#region src/context.ts
3
+ //#region src/session.ts
5
4
  /**
6
- * Creates a new {@link CurrentSessionIdRef} with an optional initial value.
5
+ * Creates a new `CurrentSessionIdRef` with an optional initial value.
7
6
  *
8
7
  * @param initial - the starting chat id, or `null` when unknown at construction time
9
8
  * @returns a mutable ref holding the current session id
@@ -19,7 +18,7 @@ const createCurrentSessionIdRef = (initial = null) => {
19
18
  };
20
19
  };
21
20
  /**
22
- * Creates a new {@link SessionContextRef} with an optional initial value.
21
+ * Creates a new `SessionContextRef` with an optional initial value.
23
22
  *
24
23
  * When a `recover` thunk is supplied, `get()` invokes it lazily while the
25
24
  * held value is `null` and caches the first non-null result. This is how
@@ -48,14 +47,17 @@ const createSessionContextRef = (initial = null, recover) => {
48
47
  };
49
48
  };
50
49
  /**
51
- * Resolve the boot-time SessionContext from `process.env` (the
52
- * primary path: SessionStart wrote the exports to `CLAUDE_ENV_FILE`
53
- * and Claude Code auto-sources that file into the MCP server child).
50
+ * Resolve the boot-time SessionContext from an environment map (the
51
+ * primary path: SessionStart wrote the exports to `CLAUDE_ENV_FILE` and
52
+ * Claude Code auto-sources that file into the MCP server child).
54
53
  *
55
- * Returns `null` when any required value is absent — callers can
56
- * still attempt the session-map fallback before giving up.
54
+ * Returns `null` when any required value is absent — callers can still
55
+ * attempt the session-map fallback before giving up.
56
+ *
57
+ * @param env - the environment map to read; the bin passes `process.env`
58
+ * @public
57
59
  */
58
- const sessionContextFromEnv = (env = process.env) => {
60
+ const sessionContextFromEnv = (env) => {
59
61
  const chatId = env.VITEST_AGENT_CHAT_ID;
60
62
  const conversationId = env.VITEST_AGENT_CONVERSATION_ID;
61
63
  const mainAgentId = env.VITEST_AGENT_MAIN_AGENT_ID ?? env.VITEST_AGENT_AGENT_ID;
@@ -67,20 +69,34 @@ const sessionContextFromEnv = (env = process.env) => {
67
69
  mainAgentId
68
70
  };
69
71
  };
70
- const t = initTRPC.context().create();
71
- const router = t.router;
72
- const publicProcedure = t.procedure;
73
72
  /**
74
- * Factory for creating server-side tRPC callers for the MCP router.
75
- *
76
- * Use with {@link appRouter} in tests or programmatic contexts to invoke
77
- * tool procedures without starting the MCP server.
73
+ * The per-process MCP session service consumed by tool handlers.
78
74
  *
79
75
  * @public
80
76
  */
81
- const createCallerFactory = t.createCallerFactory;
82
- /** Exported so middleware modules can attach to the same tRPC instance. */
83
- const middleware = t.middleware;
77
+ var McpSession = class McpSession extends Context.Service()("@vitest-agent/mcp/McpSession") {
78
+ /**
79
+ * The live session: seeded from the bin's boot-time recovery, healing
80
+ * lazily through `recover` on the first `sessionContext.get()` that
81
+ * finds a `null` context.
82
+ */
83
+ static layer = (options) => Layer.succeed(McpSession, {
84
+ cwd: options.cwd,
85
+ currentSessionId: createCurrentSessionIdRef(options.initialSessionId),
86
+ sessionContext: createSessionContextRef(options.initialContext, options.recover)
87
+ });
88
+ /**
89
+ * A test session: `cwd` is the caller's (this module is process-free,
90
+ * so the test harness passes `process.cwd()` itself), both refs start
91
+ * `null`, and either can be overridden.
92
+ */
93
+ static layerTest = (overrides) => McpSession.layer({
94
+ cwd: overrides.cwd,
95
+ initialSessionId: overrides.initialSessionId ?? null,
96
+ initialContext: overrides.initialContext ?? null,
97
+ ...overrides.recover === void 0 ? {} : { recover: overrides.recover }
98
+ });
99
+ };
84
100
 
85
101
  //#endregion
86
- export { createCallerFactory, createCurrentSessionIdRef, createSessionContextRef, middleware, publicProcedure, router, sessionContextFromEnv };
102
+ export { McpSession, createCurrentSessionIdRef, createSessionContextRef, sessionContextFromEnv };
package/toolkit.js ADDED
@@ -0,0 +1,86 @@
1
+ import { withIdempotency } from "./idempotency.js";
2
+ import { acceptanceMetricsTool, handleAcceptanceMetrics } from "./tools/acceptance-metrics.js";
3
+ import { cacheHealthTool, handleCacheHealth } from "./tools/cache-health.js";
4
+ import { commitChangesTool, handleCommitChanges } from "./tools/commit-changes.js";
5
+ import { configureTool, handleConfigure } from "./tools/configure.js";
6
+ import { handleTestCoverage, testCoverageTool } from "./tools/coverage.js";
7
+ import { handleTestErrors, testErrorsTool } from "./tools/errors.js";
8
+ import { failureSignatureGetTool, handleFailureSignatureGet } from "./tools/failure-signature-get.js";
9
+ import { fileCoverageTool, handleFileCoverage } from "./tools/file-coverage.js";
10
+ import { handleHelp, helpTool } from "./tools/help.js";
11
+ import { handleTestHistory, testHistoryTool } from "./tools/history.js";
12
+ import { handleHypothesis, hypothesisTool } from "./tools/hypothesis.js";
13
+ import { handleInventory, inventoryTool } from "./tools/inventory.js";
14
+ import { handleNote, noteTool } from "./tools/note.js";
15
+ import { handleTestOverview, testOverviewTool } from "./tools/overview.js";
16
+ import { handlePing, pingTool } from "./tools/ping.js";
17
+ import { handleRegisterAgent, registerAgentTool } from "./tools/register-agent.js";
18
+ import { handleRunTests, runTestsTool } from "./tools/run-tests.js";
19
+ import { handleSettingsList, settingsListTool } from "./tools/settings-list.js";
20
+ import { handleTestStatus, testStatusTool } from "./tools/status.js";
21
+ import { handleTddArtifactList, tddArtifactListTool } from "./tools/tdd-artifact.js";
22
+ import { handleTddBehavior, tddBehaviorTool } from "./tools/tdd-behavior.js";
23
+ import { handleTddGoal, tddGoalTool } from "./tools/tdd-goal.js";
24
+ import { handlePhaseTransitionRequest, tddPhaseTransitionRequestTool } from "./tools/tdd-phase-transition-request.js";
25
+ import { handleTddProgressPush, tddProgressPushTool } from "./tools/tdd-progress-push.js";
26
+ import { handleTddTask, tddTaskTool } from "./tools/tdd-task.js";
27
+ import { handleTest, testTool } from "./tools/test.js";
28
+ import { handleTestTrends, testTrendsTool } from "./tools/trends.js";
29
+ import { handleTriageBrief, triageBriefTool } from "./tools/triage-brief.js";
30
+ import { handleTurnSearch, turnSearchTool } from "./tools/turn-search.js";
31
+ import { handleWrapupPrompt, wrapupPromptTool } from "./tools/wrapup-prompt.js";
32
+ import { Toolkit } from "effect/unstable/ai";
33
+
34
+ //#region src/toolkit.ts
35
+ /**
36
+ * Every tool the server registers.
37
+ *
38
+ * @public
39
+ */
40
+ const Kit = Toolkit.make(pingTool, helpTool, testStatusTool, testOverviewTool, testCoverageTool, testHistoryTool, testTrendsTool, testErrorsTool, fileCoverageTool, settingsListTool, cacheHealthTool, configureTool, commitChangesTool, turnSearchTool, failureSignatureGetTool, acceptanceMetricsTool, triageBriefTool, wrapupPromptTool, inventoryTool, testTool, registerAgentTool, noteTool, hypothesisTool, tddTaskTool, tddPhaseTransitionRequestTool, tddGoalTool, tddBehaviorTool, tddArtifactListTool, tddProgressPushTool, runTestsTool);
41
+ /**
42
+ * The handler for each tool in {@link Kit}, keyed by tool name.
43
+ *
44
+ * @public
45
+ */
46
+ const toolHandlers = {
47
+ ping: handlePing,
48
+ help: handleHelp,
49
+ test_status: handleTestStatus,
50
+ test_overview: handleTestOverview,
51
+ test_coverage: handleTestCoverage,
52
+ test_history: handleTestHistory,
53
+ test_trends: handleTestTrends,
54
+ test_errors: handleTestErrors,
55
+ file_coverage: handleFileCoverage,
56
+ settings_list: handleSettingsList,
57
+ cache_health: handleCacheHealth,
58
+ configure: handleConfigure,
59
+ commit_changes: handleCommitChanges,
60
+ turn_search: handleTurnSearch,
61
+ failure_signature_get: handleFailureSignatureGet,
62
+ acceptance_metrics: handleAcceptanceMetrics,
63
+ triage_brief: handleTriageBrief,
64
+ wrapup_prompt: handleWrapupPrompt,
65
+ inventory: handleInventory,
66
+ test: handleTest,
67
+ register_agent: handleRegisterAgent,
68
+ note: handleNote,
69
+ hypothesis: withIdempotency("hypothesis", handleHypothesis),
70
+ tdd_task: withIdempotency("tdd_task", handleTddTask),
71
+ tdd_phase_transition_request: handlePhaseTransitionRequest,
72
+ tdd_goal: withIdempotency("tdd_goal", handleTddGoal),
73
+ tdd_behavior: withIdempotency("tdd_behavior", handleTddBehavior),
74
+ tdd_artifact_list: handleTddArtifactList,
75
+ tdd_progress_push: handleTddProgressPush,
76
+ run_tests: handleRunTests
77
+ };
78
+ /**
79
+ * The handlers layer: provides `Tool.HandlersFor<typeof Kit.tools>`.
80
+ *
81
+ * @public
82
+ */
83
+ const ToolsLayer = Kit.toLayer(toolHandlers);
84
+
85
+ //#endregion
86
+ export { Kit, ToolsLayer, toolHandlers };
@@ -1,20 +1,16 @@
1
- import { publicProcedure } from "../context.js";
1
+ import { RenderText } from "../annotations.js";
2
2
  import { Effect, Schema, SchemaGetter } from "effect";
3
- import { DataReader } from "@vitest-agent/sdk";
3
+ import { DataReader } from "@vitest-agent/engine";
4
+ import { Tool } from "effect/unstable/ai";
4
5
 
5
6
  //#region src/tools/acceptance-metrics.ts
7
+ const totalAnnotation = { description: "Sample size — number of observations the metric ratio is computed over." };
8
+ const ratioAnnotation = { description: "Compliance ratio in [0, 1]. Multiply by 100 for the percentage form rendered in the markdown view." };
6
9
  /**
7
- * `acceptance_metrics` MCP tool Schema-driven implementation.
8
- *
9
- * Mirrors `DataReader.AcceptanceMetrics` as an Effect Schema so the
10
- * structured payload the agent receives, the markdown rendering on
11
- * the text channel, and the SDK-side `outputSchema` all derive from
12
- * one canonical contract.
10
+ * The `acceptance_metrics` tool's success payload.
13
11
  *
14
- * @packageDocumentation
12
+ * @public
15
13
  */
16
- const totalAnnotation = { description: "Sample size — number of observations the metric ratio is computed over." };
17
- const ratioAnnotation = { description: "Compliance ratio in [0, 1]. Multiply by 100 for the percentage form rendered in the markdown view." };
18
14
  const AcceptanceMetricsResult = Schema.Struct({
19
15
  phaseEvidenceIntegrity: Schema.Struct({
20
16
  total: Schema.Finite.annotate(totalAnnotation),
@@ -66,9 +62,25 @@ const AcceptanceMetricsAsMarkdown = AcceptanceMetricsResult.pipe(Schema.decodeTo
66
62
  decode: SchemaGetter.transform((data) => formatAcceptanceMetricsMarkdown(data)),
67
63
  encode: SchemaGetter.forbidden(() => "AcceptanceMetricsAsMarkdown is one-way: markdown cannot be parsed back to AcceptanceMetricsResult.")
68
64
  }));
69
- const acceptanceMetrics = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({}))).query(async ({ ctx }) => ctx.runtime.runPromise(Effect.gen(function* () {
65
+ /**
66
+ * Handler for {@link acceptanceMetricsTool}.
67
+ *
68
+ * @public
69
+ */
70
+ const handleAcceptanceMetrics = () => Effect.gen(function* () {
70
71
  return yield* (yield* DataReader).computeAcceptanceMetrics();
71
- })));
72
+ }).pipe(Effect.orDie);
73
+ /**
74
+ * The Effect-native `acceptance_metrics` tool. No parameters (the default
75
+ * `Tool.EmptyParams` serves as a strict empty object).
76
+ *
77
+ * @public
78
+ */
79
+ const acceptanceMetricsTool = Tool.make("acceptance_metrics", {
80
+ 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, ... }).",
81
+ success: AcceptanceMetricsResult,
82
+ dependencies: [DataReader]
83
+ }).annotate(Tool.Title, "Acceptance metrics").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatAcceptanceMetricsMarkdown(encoded));
72
84
 
73
85
  //#endregion
74
- export { AcceptanceMetricsAsMarkdown, AcceptanceMetricsResult, acceptanceMetrics, formatAcceptanceMetricsMarkdown };
86
+ export { AcceptanceMetricsResult, acceptanceMetricsTool, formatAcceptanceMetricsMarkdown, handleAcceptanceMetrics };
@@ -1,20 +1,10 @@
1
- import { publicProcedure } from "../context.js";
1
+ import { RenderText } from "../annotations.js";
2
2
  import { Effect, Option, Schema, SchemaGetter } from "effect";
3
- import { CacheManifest, DataReader } from "@vitest-agent/sdk";
3
+ import { DataReader } from "@vitest-agent/engine";
4
+ import { Tool } from "effect/unstable/ai";
5
+ import { CacheManifest } from "@vitest-agent/sdk";
4
6
 
5
7
  //#region src/tools/cache-health.ts
6
- /**
7
- * `cache_health` MCP tool — Schema-driven implementation.
8
- *
9
- * Wraps the cache manifest in a `CacheHealthResult` Schema that
10
- * captures both the present and absent cases. The text channel
11
- * renders the same markdown the previous implementation produced;
12
- * the structured payload now exposes `manifestPresent` plus the
13
- * computed `ageMs` so agents can branch on freshness without parsing
14
- * prose.
15
- *
16
- * @packageDocumentation
17
- */
18
8
  const ManifestPresent = Schema.Struct({
19
9
  manifestPresent: Schema.Literal(true).annotate({ description: "Discriminant — `true` when a cache manifest exists." }),
20
10
  manifest: CacheManifest.annotate({ description: "Full cache manifest content as written by the reporter." }),
@@ -28,6 +18,11 @@ const ManifestAbsent = Schema.Struct({ manifestPresent: Schema.Literal(false).an
28
18
  identifier: "CacheHealthAbsent",
29
19
  title: "Cache manifest absent"
30
20
  });
21
+ /**
22
+ * The `cache_health` tool's success payload.
23
+ *
24
+ * @public
25
+ */
31
26
  const CacheHealthResult = Schema.Union([ManifestPresent, ManifestAbsent]).annotate({
32
27
  identifier: "CacheHealthResult",
33
28
  title: "cache_health result",
@@ -65,7 +60,12 @@ const CacheHealthAsMarkdown = CacheHealthResult.pipe(Schema.decodeTo(Schema.Stri
65
60
  decode: SchemaGetter.transform((data) => formatCacheHealthMarkdown(data)),
66
61
  encode: SchemaGetter.forbidden(() => "CacheHealthAsMarkdown is one-way: markdown cannot be parsed back to CacheHealthResult.")
67
62
  }));
68
- const cacheHealth = publicProcedure.query(async ({ ctx }) => ctx.runtime.runPromise(Effect.gen(function* () {
63
+ /**
64
+ * Handler for {@link cacheHealthTool}.
65
+ *
66
+ * @public
67
+ */
68
+ const handleCacheHealth = () => Effect.gen(function* () {
69
69
  const manifestOpt = yield* (yield* DataReader).getManifest();
70
70
  if (Option.isNone(manifestOpt)) return { manifestPresent: false };
71
71
  const manifest = manifestOpt.value;
@@ -76,7 +76,18 @@ const cacheHealth = publicProcedure.query(async ({ ctx }) => ctx.runtime.runProm
76
76
  ageMs,
77
77
  stale: ageMs > STALE_AFTER_MS
78
78
  };
79
- })));
79
+ }).pipe(Effect.orDie);
80
+ /**
81
+ * The Effect-native `cache_health` tool. No parameters (the default
82
+ * `Tool.EmptyParams` serves as a strict empty object).
83
+ *
84
+ * @public
85
+ */
86
+ const cacheHealthTool = Tool.make("cache_health", {
87
+ 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? }).",
88
+ success: CacheHealthResult,
89
+ dependencies: [DataReader]
90
+ }).annotate(Tool.Title, "Cache health").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatCacheHealthMarkdown(encoded));
80
91
 
81
92
  //#endregion
82
- export { CacheHealthAsMarkdown, CacheHealthResult, cacheHealth, formatCacheHealthMarkdown };
93
+ export { CacheHealthResult, cacheHealthTool, formatCacheHealthMarkdown, handleCacheHealth };
@@ -1,13 +1,9 @@
1
- import { publicProcedure } from "../context.js";
1
+ import { RenderText } from "../annotations.js";
2
2
  import { Effect, Schema, SchemaGetter } from "effect";
3
- import { DataReader } from "@vitest-agent/sdk";
3
+ import { DataReader } from "@vitest-agent/engine";
4
+ import { Tool } from "effect/unstable/ai";
4
5
 
5
6
  //#region src/tools/commit-changes.ts
6
- /**
7
- * `commit_changes` MCP tool — Schema-driven implementation.
8
- *
9
- * @packageDocumentation
10
- */
11
7
  const FileRow = Schema.Struct({
12
8
  filePath: Schema.String.annotate({ description: "Repo-relative path of the changed file." }),
13
9
  changeKind: Schema.Literals([
@@ -27,6 +23,11 @@ const CommitRow = Schema.Struct({
27
23
  branch: Schema.NullOr(Schema.String).annotate({ description: "Branch the commit was recorded on at hook fire time." }),
28
24
  files: Schema.Array(FileRow).annotate({ description: "Files this commit changed, with per-file change kinds." })
29
25
  }).annotate({ identifier: "CommitRow" });
26
+ /**
27
+ * The `commit_changes` tool's success payload.
28
+ *
29
+ * @public
30
+ */
30
31
  const CommitChangesResult = Schema.Struct({
31
32
  filterSha: Schema.optional(Schema.String).annotate({ description: "Echo of the optional `sha` filter the caller passed; absent when no filter was applied (recent commits returned)." }),
32
33
  count: Schema.Finite.annotate({ description: "Number of commit rows returned." }),
@@ -56,14 +57,36 @@ const CommitChangesAsMarkdown = CommitChangesResult.pipe(Schema.decodeTo(Schema.
56
57
  decode: SchemaGetter.transform((data) => formatCommitChangesMarkdown(data)),
57
58
  encode: SchemaGetter.forbidden(() => "CommitChangesAsMarkdown is one-way: markdown cannot be parsed back to CommitChangesResult.")
58
59
  }));
59
- const commitChanges = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({ sha: Schema.optional(Schema.String) }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
60
+ /**
61
+ * The `commit_changes` tool's parameters.
62
+ *
63
+ * @public
64
+ */
65
+ const CommitChangesInput = Schema.Struct({ sha: Schema.optionalKey(Schema.String).annotate({ description: "Specific commit sha to fetch; omit for recent commits" }) });
66
+ /**
67
+ * Handler for {@link commitChangesTool}.
68
+ *
69
+ * @public
70
+ */
71
+ const handleCommitChanges = (input) => Effect.gen(function* () {
60
72
  const entries = yield* (yield* DataReader).getCommitChanges(input.sha);
61
73
  return {
62
74
  ...input.sha !== void 0 && { filterSha: input.sha },
63
75
  count: entries.length,
64
76
  commits: entries
65
77
  };
66
- })));
78
+ }).pipe(Effect.orDie);
79
+ /**
80
+ * The Effect-native `commit_changes` tool.
81
+ *
82
+ * @public
83
+ */
84
+ const commitChangesTool = Tool.make("commit_changes", {
85
+ 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[] }).",
86
+ parameters: CommitChangesInput,
87
+ success: CommitChangesResult,
88
+ dependencies: [DataReader]
89
+ }).annotate(Tool.Title, "Commit changes").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatCommitChangesMarkdown(encoded));
67
90
 
68
91
  //#endregion
69
- export { CommitChangesAsMarkdown, CommitChangesResult, commitChanges, formatCommitChangesMarkdown };
92
+ export { CommitChangesInput, CommitChangesResult, commitChangesTool, formatCommitChangesMarkdown, handleCommitChanges };
@@ -1,13 +1,9 @@
1
- import { publicProcedure } from "../context.js";
1
+ import { RenderText } from "../annotations.js";
2
2
  import { Effect, Option, Schema, SchemaGetter } from "effect";
3
- import { DataReader } from "@vitest-agent/sdk";
3
+ import { DataReader } from "@vitest-agent/engine";
4
+ import { Tool } from "effect/unstable/ai";
4
5
 
5
6
  //#region src/tools/configure.ts
6
- /**
7
- * `configure` MCP tool — Schema-driven implementation.
8
- *
9
- * @packageDocumentation
10
- */
11
7
  const SettingsRowSchema = Schema.Struct({
12
8
  hash: Schema.String.annotate({ description: "Stable SHA-1 of the captured Vitest settings; `test_runs.settings_hash` foreign key." }),
13
9
  reporters: Schema.NullOr(Schema.String).annotate({ description: "Comma-separated reporter list as resolved from the user's vitest config." }),
@@ -35,6 +31,11 @@ const SettingsAbsent = Schema.Struct({
35
31
  source: Schema.Literals(["requested", "latest"]),
36
32
  requestedHash: Schema.optional(Schema.String).annotate({ description: "Echo of the hash the caller passed; absent when the empty `latest` lookup found nothing." })
37
33
  }).annotate({ identifier: "ConfigureAbsent" });
34
+ /**
35
+ * The `configure` tool's success payload.
36
+ *
37
+ * @public
38
+ */
38
39
  const ConfigureResult = Schema.Union([SettingsFound, SettingsAbsent]).annotate({
39
40
  identifier: "ConfigureResult",
40
41
  title: "configure result",
@@ -74,7 +75,18 @@ const ConfigureAsMarkdown = ConfigureResult.pipe(Schema.decodeTo(Schema.String,
74
75
  decode: SchemaGetter.transform((data) => formatConfigureMarkdown(data)),
75
76
  encode: SchemaGetter.forbidden(() => "ConfigureAsMarkdown is one-way: markdown cannot be parsed back to ConfigureResult.")
76
77
  }));
77
- const configure = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({ settingsHash: Schema.optional(Schema.String) }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
78
+ /**
79
+ * The `configure` tool's parameters.
80
+ *
81
+ * @public
82
+ */
83
+ const ConfigureInput = Schema.Struct({ settingsHash: Schema.optionalKey(Schema.String).annotate({ description: "Settings hash from a manifest entry or test run" }) });
84
+ /**
85
+ * Handler for {@link configureTool}.
86
+ *
87
+ * @public
88
+ */
89
+ const handleConfigure = (input) => Effect.gen(function* () {
78
90
  const reader = yield* DataReader;
79
91
  if (input.settingsHash === void 0) {
80
92
  const latestOpt = yield* reader.getLatestSettings();
@@ -97,7 +109,18 @@ const configure = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct(
97
109
  source: "requested",
98
110
  settings: settingsOpt.value
99
111
  };
100
- })));
112
+ }).pipe(Effect.orDie);
113
+ /**
114
+ * The Effect-native `configure` tool.
115
+ *
116
+ * @public
117
+ */
118
+ const configureTool = Tool.make("configure", {
119
+ 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? }).",
120
+ parameters: ConfigureInput,
121
+ success: ConfigureResult,
122
+ dependencies: [DataReader]
123
+ }).annotate(Tool.Title, "Configure").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatConfigureMarkdown(encoded));
101
124
 
102
125
  //#endregion
103
- export { ConfigureAsMarkdown, ConfigureResult, configure, formatConfigureMarkdown };
126
+ export { ConfigureInput, ConfigureResult, configureTool, formatConfigureMarkdown, handleConfigure };
package/tools/coverage.js CHANGED
@@ -1,6 +1,8 @@
1
- import { publicProcedure } from "../context.js";
1
+ import { RenderText } from "../annotations.js";
2
2
  import { Effect, Option, Schema, SchemaGetter } from "effect";
3
- import { CoverageReport, DataReader } from "@vitest-agent/sdk";
3
+ import { DataReader } from "@vitest-agent/engine";
4
+ import { Tool } from "effect/unstable/ai";
5
+ import { CoverageReport } from "@vitest-agent/sdk";
4
6
 
5
7
  //#region src/tools/coverage.ts
6
8
  const CoverageAvailable = Schema.Struct({
@@ -12,6 +14,11 @@ const CoverageAbsent = Schema.Struct({
12
14
  dataAvailable: Schema.Literal(false),
13
15
  project: Schema.String
14
16
  }).annotate({ identifier: "TestCoverageAbsent" });
17
+ /**
18
+ * The `test_coverage` tool's success payload.
19
+ *
20
+ * @public
21
+ */
15
22
  const TestCoverageResult = Schema.Union([CoverageAvailable, CoverageAbsent]).annotate({
16
23
  identifier: "TestCoverageResult",
17
24
  title: "test_coverage result",
@@ -66,7 +73,18 @@ const TestCoverageAsMarkdown = TestCoverageResult.pipe(Schema.decodeTo(Schema.St
66
73
  decode: SchemaGetter.transform((data) => formatTestCoverageMarkdown(data)),
67
74
  encode: SchemaGetter.forbidden(() => "TestCoverageAsMarkdown is one-way.")
68
75
  }));
69
- const testCoverage = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({ project: Schema.optional(Schema.String) }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
76
+ /**
77
+ * The `test_coverage` tool's parameters.
78
+ *
79
+ * @public
80
+ */
81
+ const TestCoverageInput = Schema.Struct({ project: Schema.optionalKey(Schema.String).annotate({ description: "Project name" }) });
82
+ /**
83
+ * Handler for {@link testCoverageTool}.
84
+ *
85
+ * @public
86
+ */
87
+ const handleTestCoverage = (input) => Effect.gen(function* () {
70
88
  const reader = yield* DataReader;
71
89
  const project = input.project ?? "default";
72
90
  const coverageOpt = yield* reader.getCoverage(project);
@@ -79,7 +97,18 @@ const testCoverage = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Stru
79
97
  project,
80
98
  coverage: coverageOpt.value
81
99
  };
82
- })));
100
+ }).pipe(Effect.orDie);
101
+ /**
102
+ * The Effect-native `test_coverage` tool.
103
+ *
104
+ * @public
105
+ */
106
+ const testCoverageTool = Tool.make("test_coverage", {
107
+ 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).",
108
+ parameters: TestCoverageInput,
109
+ success: TestCoverageResult,
110
+ dependencies: [DataReader]
111
+ }).annotate(Tool.Title, "Test coverage").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestCoverageMarkdown(encoded));
83
112
 
84
113
  //#endregion
85
- export { TestCoverageAsMarkdown, TestCoverageResult, formatTestCoverageMarkdown, testCoverage };
114
+ export { TestCoverageInput, TestCoverageResult, formatTestCoverageMarkdown, handleTestCoverage, testCoverageTool };
package/tools/errors.js CHANGED
@@ -1,25 +1,9 @@
1
- import { publicProcedure } from "../context.js";
1
+ import { RenderText } from "../annotations.js";
2
2
  import { Effect, Schema, SchemaGetter } from "effect";
3
- import { DataReader } from "@vitest-agent/sdk";
3
+ import { DataReader } from "@vitest-agent/engine";
4
+ import { Tool } from "effect/unstable/ai";
4
5
 
5
6
  //#region src/tools/errors.ts
6
- /**
7
- * `test_errors` MCP tool — Schema-driven implementation.
8
- *
9
- * The Effect Schema `TestErrorsResult` is the canonical contract for
10
- * the tool's output. The same Schema:
11
- * - types the procedure's return value;
12
- * - drives `formatTestErrorsMarkdown` (input typed via `Schema.Type`);
13
- * - composes into `TestErrorsAsMarkdown`, a one-way
14
- * `Schema.decodeTo` whose `decode` direction renders the
15
- * markdown the text channel carries (encode is forbidden because
16
- * markdown rendering is lossy);
17
- * - bridges to zod via `effectToZodSchema` for the SDK's
18
- * `outputSchema` field, so the structured shape we declare to MCP
19
- * stays in lockstep with what the procedure actually emits.
20
- *
21
- * @packageDocumentation
22
- */
23
7
  /** One annotation attached to a failing test, surfaced with its error. */
24
8
  const TestErrorAnnotation = Schema.Struct({
25
9
  type: Schema.String.annotate({ description: "Annotation type as the test author wrote it — an arbitrary string, not an enum." }),
@@ -60,7 +44,11 @@ const TestErrorRow = Schema.Struct({
60
44
  title: "Test error row",
61
45
  description: "Single error captured during a test run, joined with stack frame and source-location context."
62
46
  });
63
- /** Top-level structured payload — populates `structuredContent`. */
47
+ /**
48
+ * The `test_errors` tool's success payload — populates `structuredContent`.
49
+ *
50
+ * @public
51
+ */
64
52
  const TestErrorsResult = Schema.Struct({
65
53
  project: Schema.String.annotate({
66
54
  title: "Project name",
@@ -153,10 +141,21 @@ const TestErrorsAsMarkdown = TestErrorsResult.pipe(Schema.decodeTo(Schema.String
153
141
  decode: SchemaGetter.transform((data) => formatTestErrorsMarkdown(data)),
154
142
  encode: SchemaGetter.forbidden(() => "TestErrorsAsMarkdown is one-way: markdown cannot be parsed back to TestErrorsResult. Consume the procedure's structured output (or MCP structuredContent) directly.")
155
143
  }));
156
- const testErrors = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
157
- project: Schema.String,
158
- errorName: Schema.optional(Schema.String)
159
- }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
144
+ /**
145
+ * The `test_errors` tool's parameters.
146
+ *
147
+ * @public
148
+ */
149
+ const TestErrorsInput = Schema.Struct({
150
+ project: Schema.String.annotate({ description: "Project name (required)" }),
151
+ errorName: Schema.optionalKey(Schema.String).annotate({ description: "Filter to a specific error name" })
152
+ });
153
+ /**
154
+ * Handler for {@link testErrorsTool}.
155
+ *
156
+ * @public
157
+ */
158
+ const handleTestErrors = (input) => Effect.gen(function* () {
160
159
  const reader = yield* DataReader;
161
160
  const errors = yield* reader.getErrors(input.project, input.errorName);
162
161
  const cache = /* @__PURE__ */ new Map();
@@ -190,7 +189,18 @@ const testErrors = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct
190
189
  count: rows.length,
191
190
  errors: rows
192
191
  };
193
- })));
192
+ }).pipe(Effect.orDie);
193
+ /**
194
+ * The Effect-native `test_errors` tool.
195
+ *
196
+ * @public
197
+ */
198
+ const testErrorsTool = Tool.make("test_errors", {
199
+ 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[].",
200
+ parameters: TestErrorsInput,
201
+ success: TestErrorsResult,
202
+ dependencies: [DataReader]
203
+ }).annotate(Tool.Title, "Test errors").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestErrorsMarkdown(encoded));
194
204
 
195
205
  //#endregion
196
- export { TestErrorAnnotation, TestErrorRow, TestErrorsAsMarkdown, TestErrorsResult, formatTestErrorsMarkdown, testErrors };
206
+ export { TestErrorAnnotation, TestErrorRow, TestErrorsInput, TestErrorsResult, formatTestErrorsMarkdown, handleTestErrors, testErrorsTool };