@vitest-agent/mcp 3.0.3 → 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
package/README.md CHANGED
@@ -6,16 +6,17 @@
6
6
 
7
7
  > **Part of the [vitest-agent](https://vitest-agent.dev) ecosystem.** Most users want **[@vitest-agent/plugin](https://www.npmjs.com/package/@vitest-agent/plugin)**, which pulls this package in automatically. Install `@vitest-agent/mcp` directly only if you run the MCP server standalone.
8
8
 
9
- The `vitest-agent-mcp` MCP server bin. Exposes action-keyed tools over stdio that give LLM agents structured access to test data, coverage, history, failure signatures, TDD lifecycle state and more. Also surfaces six framing-only prompts.
9
+ The `vitest-agent-mcp` MCP server bin, built on Effect's native `McpServer` (`effect/unstable/ai`). Exposes action-keyed tools over stdio that give LLM agents structured access to test data, coverage, history, failure signatures, TDD lifecycle state and more. Also surfaces six framing-only prompts.
10
10
 
11
11
  ## Features
12
12
 
13
- - **29 action-keyed tools** — per-CRUD families collapse into single tools dispatching on an `action` discriminator; covers `test_status`, `test_overview`, `test_coverage`, `test_errors`, `run_tests`, `note`, `hypothesis`, `tdd_task`, `tdd_goal`, `tdd_behavior` and more
13
+ - **30 action-keyed tools** — one `Tool.make` per file, assembled into a single `Toolkit`; per-CRUD families collapse into single tools dispatching on an `action` discriminator; covers `test_status`, `test_overview`, `test_coverage`, `test_errors`, `run_tests`, `note`, `hypothesis`, `tdd_task`, `tdd_goal`, `tdd_behavior`, `tdd_progress_push` and more
14
14
  - **Six framing prompts** — `triage`, `why-flaky`, `regression-since-pass`, `explain-failure`, `tdd-resume`, `wrapup`
15
- - **Idempotency middleware** — `tdd_task`, `tdd_goal`, `tdd_behavior` and `hypothesis` create-actions are idempotent on derived keys
16
- - **Strict tool inputs** — an unknown key is rejected with an error naming it and listing the accepted params, instead of being stripped and running a wider query than the caller asked for
17
- - **Session-surviving error handling** — a resolver that throws returns a structured `UnexpectedToolError` envelope, and a stray unhandled rejection after the transport connects is logged to stderr rather than killing the server mid-session
18
- - **Programmatic API** — `buildMcpServer` constructs the server without connecting a transport; `parseSessionEnvExports` and `recoverSessionContextFromSessionEnv` recover the host session context from the Claude Code plugin's session-env files, so agent attribution survives a plugin reload
15
+ - **Idempotent writes** — `tdd_task`, `tdd_goal`, `tdd_behavior` and `hypothesis` create-actions are idempotent on derived keys via the `withIdempotency` combinator; a replay carries `_idempotentReplay: true`
16
+ - **Strict tool inputs** — every served `inputSchema` is strict at every object level; an unknown key is rejected with an error naming it and listing the accepted params, instead of being stripped and running a wider query than the caller asked for
17
+ - **Session-surviving error handling** — a handler that dies returns a structured `UnexpectedToolError` envelope, every log line goes to stderr (stdout is the JSON-RPC wire), and a stray unhandled rejection after the transport connects is logged rather than killing the server mid-session
18
+ - **Programmatic API** — `ServerLayer({ version })` is the whole server as an Effect `Layer` over any `Stdio` implementation (the test harness runs it over in-memory queues); `Kit`, `toolHandlers`, `PromptsLayer`, `registerStrictToolkit` and `McpSession` are exported for embedding or extension
19
+ - **No MCP SDK, tRPC or zod** — the wire protocol, JSON Schema generation and input validation all come from `effect`; tool inputs and outputs are Effect `Schema` values end to end
19
20
 
20
21
  ## Install
21
22
 
package/annotations.js ADDED
@@ -0,0 +1,18 @@
1
+ import { Context } from "effect";
2
+
3
+ //#region src/annotations.ts
4
+ /**
5
+ * Renders a tool's encoded result as the human-readable `content[0].text`
6
+ * channel (markdown, typically). Returning `undefined` — or leaving the
7
+ * annotation unset — falls back to `JSON.stringify(encoded)`.
8
+ *
9
+ * Attach with `tool.annotate(RenderText, (encoded) => ...)`; the renderer
10
+ * receives the wire-encoded result (the same value that becomes
11
+ * `structuredContent`), never the decoded domain value.
12
+ *
13
+ * @public
14
+ */
15
+ const RenderText = Context.Reference("@vitest-agent/mcp/RenderText", { defaultValue: () => void 0 });
16
+
17
+ //#endregion
18
+ export { RenderText };
@@ -1,149 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { createCurrentSessionIdRef, createSessionContextRef, sessionContextFromEnv } from "../context.js";
3
- import { McpLive } from "../layers/McpLive.js";
4
- import { startMcpServer } from "../server.js";
5
- import { recoverSessionContextFromSessionEnv } from "../session-env.js";
6
- import { shouldExitOnUncaughtException } from "../utils/crash-guards.js";
7
- import { safeFormatFatalError } from "../utils/safe-format-fatal-error.js";
8
- import { Effect, ManagedRuntime } from "effect";
9
- import * as NodeServices from "@effect/platform-node/NodeServices";
10
- import { PathResolutionLive, resolveDataPath, resolveLogFile, resolveLogLevel } from "@vitest-agent/sdk";
2
+ import { main } from "../main.js";
11
3
 
12
4
  //#region src/bin.ts
13
5
  /**
14
- * Whether `server.connect(transport)` has resolved for this process.
15
- * Read by the `uncaughtException` handler below via
16
- * {@link shouldExitOnUncaughtException} — see that function's doc
17
- * comment for the judgment call this flag backs (issue #191).
18
- */
19
- let transportConnected = false;
20
- /**
21
- * Issue #191, sub-item A: the MCP server used to be a bare
22
- * `main().catch(...)` with no process-level guards. Under Node >=15 an
23
- * unhandled promise rejection anywhere in the async chain — a
24
- * fire-and-forgotten Effect fiber, a background timer, anything that
25
- * never flows through a tool call's own await chain — crashes the
26
- * process by default, closing the stdio transport and silently
27
- * deregistering every tool from the client's perspective mid session,
28
- * with no recovery path. Register both guards at module scope, before
29
- * any async work in `main()` starts, so they also cover the dbPath
30
- * resolution / ManagedRuntime construction phase.
31
- *
32
- * Every throw or rejection *inside* a single tool call is already
33
- * caught by the MCP SDK's own `CallToolRequestSchema` handler (see
34
- * `server.ts`'s `registerTool` shadow for the structured-envelope
35
- * layer on top of that). Anything that reaches these handlers
36
- * therefore originated outside any tool-call boundary and, by
37
- * definition, has no in-flight caller waiting on it — logging and
38
- * continuing is safe because there is no request context to roll back
39
- * and no benefit to dropping every *other* in-flight and future tool
40
- * call over it.
41
- */
42
- process.on("unhandledRejection", (reason) => {
43
- process.stderr.write(`vitest-agent-mcp: unhandledRejection: ${safeFormatFatalError(reason)}\n`);
44
- });
45
- /**
46
- * Node's own guidance for `uncaughtException` is "do not resume normal
47
- * operation" — arbitrary in-process state may be corrupt. This process
48
- * accepts that residual risk once the transport is connected: see
49
- * `shouldExitOnUncaughtException`'s doc comment for the reasoning
50
- * (no long-lived mutable state outside SQLite's own transactions, and
51
- * silent process death mid-session is strictly worse). Before the
52
- * transport connects there is no client session to preserve by staying
53
- * alive, so this exits loudly instead.
54
- */
55
- process.on("uncaughtException", (err, origin) => {
56
- process.stderr.write(`vitest-agent-mcp: uncaughtException (${origin}): ${safeFormatFatalError(err)}\n`);
57
- if (shouldExitOnUncaughtException(transportConnected)) {
58
- process.exitCode = 1;
59
- process.exit(1);
60
- }
61
- });
62
- /**
63
- * Test-only crash injection, gated by an env var so it can never fire
64
- * in a normal install. Exists so `__test__/bin-crash-resilience.e2e.test.ts`
65
- * can exercise the guards above against a *real* child process (a
66
- * crash in the test's own process is not something a unit test can
67
- * safely simulate). Fires exactly once, on the next event-loop turn
68
- * after the transport is known to be connected, so ordering relative
69
- * to `transportConnected` is deterministic regardless of client-side
70
- * handshake timing.
71
- */
72
- function scheduleTestCrashInjection() {
73
- const kind = process.env.VITEST_AGENT_MCP_TEST_INJECT_CRASH;
74
- if (kind !== "unhandledRejection" && kind !== "uncaughtException") return;
75
- setImmediate(() => {
76
- if (kind === "unhandledRejection") Promise.reject(/* @__PURE__ */ new Error("[test-injected] unhandledRejection"));
77
- else throw new Error("[test-injected] uncaughtException");
78
- });
79
- }
80
- /**
81
- * Resolve the user's project directory.
82
- *
83
- * Precedence (most explicit wins):
84
- *
85
- * 1. `VITEST_AGENT_REPORTER_PROJECT_DIR` — set by the Claude Code plugin
86
- * loader (`plugins/claude-code/bin/mcp-server.mjs`) to the resolved project root.
87
- * The loader controls this end-to-end so the value is reliable when
88
- * set.
89
- * 2. `CLAUDE_PROJECT_DIR` — exported by Claude Code for hook scripts and
90
- * (per docs hints) MCP server subprocesses. Used when the loader is
91
- * bypassed (e.g. someone wires the MCP binary up manually).
92
- * 3. `process.cwd()` — fall-through for direct invocation outside Claude
93
- * Code, where the user is presumably running from their project root.
94
- */
95
- function resolveProjectDir() {
96
- return process.env.VITEST_AGENT_REPORTER_PROJECT_DIR ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
97
- }
98
- /**
99
- * Optional first positional argument: an initial Claude Code chat UUID
100
- * (the host's `chatId`) to seed the MCP server's session association.
6
+ * MCP server entry point for vitest-agent. Thin shim over `main.ts` — the
7
+ * assembled program that owns the process.
101
8
  *
102
- * The plugin manifest (`plugins/claude-code/.claude-plugin/plugin.json`) can pass this
103
- * via Claude Code variable substitution if such a variable exists for
104
- * sessions (the documented substitutions are `${CLAUDE_PLUGIN_ROOT}` and
105
- * `${CLAUDE_PLUGIN_DATA}`; testing whether `${CLAUDE_SESSION_ID}` or a
106
- * similar name is honored in `mcpServers.args` is part of the reason
107
- * this seed path exists). When the seed is empty the agent is expected
108
- * to recover the chat id at boot. The legacy `set_current_session_id`
109
- * MCP tool was removed in Phase 3.
9
+ * @packageDocumentation
110
10
  */
111
- function resolveInitialSessionId() {
112
- const argv = process.argv[2];
113
- if (argv === void 0) return null;
114
- const trimmed = argv.trim();
115
- if (trimmed.length === 0) return null;
116
- if (trimmed.startsWith("${") && trimmed.endsWith("}")) return null;
117
- return trimmed;
118
- }
119
- async function main() {
120
- const projectDir = resolveProjectDir();
121
- const initialSessionId = resolveInitialSessionId();
122
- const dbPath = await Effect.runPromise(resolveDataPath(projectDir).pipe(Effect.provide(PathResolutionLive(projectDir)), Effect.provide(NodeServices.layer)));
123
- const logLevel = resolveLogLevel();
124
- const logFile = resolveLogFile();
125
- const runtime = ManagedRuntime.make(McpLive(dbPath, logLevel, logFile));
126
- const recoveredContext = sessionContextFromEnv(process.env);
127
- const ctx = {
128
- runtime,
129
- cwd: projectDir,
130
- currentSessionId: createCurrentSessionIdRef(initialSessionId ?? recoveredContext?.chatId ?? null),
131
- sessionContext: createSessionContextRef(recoveredContext, () => recoverSessionContextFromSessionEnv({ projectDir }))
132
- };
133
- const chatIdResolved = initialSessionId ?? recoveredContext?.chatId ?? null;
134
- console.error("[vitest-agent-mcp] Starting...");
135
- console.error(`[vitest-agent-mcp] Project: ${projectDir}`);
136
- console.error(`[vitest-agent-mcp] Database: ${dbPath}`);
137
- console.error(`[vitest-agent-mcp] Initial chat id: ${chatIdResolved !== null ? "(set)" : "(none — SessionStart hook had not written CLAUDE_ENV_FILE yet)"}`);
138
- if (recoveredContext !== null) console.error("[vitest-agent-mcp] Recovered session context: agent=(set) conversation=(set)");
139
- await startMcpServer(ctx);
140
- transportConnected = true;
141
- scheduleTestCrashInjection();
142
- }
143
- main().catch((err) => {
144
- process.stderr.write(`vitest-agent-mcp: ${safeFormatFatalError(err)}\n`);
145
- process.exit(1);
146
- });
11
+ main();
147
12
 
148
13
  //#endregion
149
14
  export { };
@@ -1,16 +1,17 @@
1
- import { middleware, publicProcedure } from "../context.js";
2
1
  import { Effect, Option } from "effect";
3
- import { DataReader, DataStore } from "@vitest-agent/sdk";
2
+ import { DataReader, DataStore } from "@vitest-agent/engine";
4
3
 
5
- //#region src/middleware/idempotency.ts
4
+ //#region src/idempotency.ts
6
5
  /**
7
- * Registered idempotency specs for mutation procedures.
6
+ * Registered idempotency specs for mutation tools.
8
7
  *
9
8
  * `hypothesis validate` is covered (key: `validate:${id}:${outcome}`).
10
9
  * `hypothesis record` is deliberately not covered — see the note in the
11
10
  * hypothesis spec below.
12
11
  *
13
12
  * Add an entry here whenever a new idempotent mutation is introduced.
13
+ *
14
+ * @public
14
15
  */
15
16
  const idempotencyKeys = [
16
17
  {
@@ -68,61 +69,59 @@ const idempotencyKeys = [
68
69
  }
69
70
  }
70
71
  ];
71
- /** Lookup table keyed by procedure path for O(1) spec retrieval. */
72
+ /** Lookup table keyed by tool name for O(1) spec retrieval. */
72
73
  const keySpecByPath = new Map(idempotencyKeys.map((s) => [s.procedurePath, s]));
74
+ /** Merges the `_idempotentReplay` marker into an object payload; passes any other shape through unchanged. */
75
+ const withReplayMarker = (parsed) => parsed !== null && typeof parsed === "object" ? {
76
+ ...parsed,
77
+ _idempotentReplay: true
78
+ } : parsed;
73
79
  /**
74
- * tRPC middleware that caches mutation results in `mcp_idempotent_responses`.
80
+ * Wraps `handler` with idempotent-response caching keyed on `path`.
81
+ *
82
+ * Semantics (unchanged from the retired tRPC middleware):
75
83
  *
76
- * On every mutation call where the procedure path has a registered
77
- * `IdempotencyKeySpec`:
84
+ * 1. Look up the `IdempotencyKeySpec` registered for `path`, and
85
+ * derive a key from `params` (already decoded — strict registration
86
+ * guarantees no key was stripped). No registered spec, or a `null`
87
+ * key, runs `handler` untouched with nothing cached.
88
+ * 2. Cache HIT — `DataReader.findIdempotentResponse` returns the stored
89
+ * JSON. A row that parses falls through to step 2a; a row that fails
90
+ * to parse (corrupt or truncated) is treated as step 3, a cache MISS —
91
+ * it is not a reason to fail the call.
92
+ * 2a. The parsed value is merged with `_idempotentReplay: true` for an
93
+ * object payload (a non-object payload passes through unchanged).
94
+ * `handler` does not run.
95
+ * 3. Cache MISS — run `handler`, then persist the result via
96
+ * `DataStore.recordIdempotentResponse` best-effort: a persistence
97
+ * failure (or a cache-lookup failure) never surfaces to the caller, it
98
+ * is swallowed so a transient DB failure doesn't turn into a tool
99
+ * error.
78
100
  *
79
- * 1. Derive the idempotency key from the raw input.
80
- * 2. Look up `DataReader.findIdempotentResponse(path, key)`.
81
- * - Cache HIT → return the stored JSON result immediately (no handler).
82
- * - Cache MISS → call `next()` to run the handler, then persist the result
83
- * via `DataStore.recordIdempotentResponse`. Persistence errors are
84
- * swallowed (best-effort) so a transient DB failure doesn't surface to
85
- * the caller as a tool error.
101
+ * The combinator reads no ambient state `path` and `params` are its
102
+ * only inputs beyond the `DataReader` / `DataStore` services it adds to
103
+ * `handler`'s requirements.
86
104
  *
87
- * Procedures without a registered spec, or where key derivation returns
88
- * `null`, pass straight through to `next()` without any caching.
105
+ * @public
89
106
  */
90
- const idempotent = middleware(async (opts) => {
91
- const { ctx, path, type, getRawInput, next } = opts;
92
- if (type !== "mutation") return next();
107
+ const withIdempotency = (path, handler) => (params) => Effect.gen(function* () {
93
108
  const spec = keySpecByPath.get(path);
94
- if (!spec) return next();
95
- const rawInput = await getRawInput();
96
- const key = spec.deriveKey(rawInput);
97
- if (key === null) return next();
98
- const cached = await ctx.runtime.runPromise(Effect.gen(function* () {
99
- return yield* (yield* DataReader).findIdempotentResponse(path, key);
100
- }));
109
+ const key = spec === void 0 ? null : spec.deriveKey(params);
110
+ if (key === null) return yield* handler(params);
111
+ const cached = yield* (yield* DataReader).findIdempotentResponse(path, key).pipe(Effect.orElseSucceed(() => Option.none()));
101
112
  if (Option.isSome(cached)) {
102
- const parsed = JSON.parse(cached.value);
103
- return {
104
- ok: true,
105
- data: parsed !== null && typeof parsed === "object" ? {
106
- ...parsed,
107
- _idempotentReplay: true
108
- } : parsed,
109
- marker: "middlewareMarker",
110
- ctx
111
- };
113
+ const parsed = yield* Effect.try(() => JSON.parse(cached.value)).pipe(Effect.map(Option.some), Effect.orElseSucceed(() => Option.none()));
114
+ if (Option.isSome(parsed)) return withReplayMarker(parsed.value);
112
115
  }
113
- const result = await next();
114
- if (result.ok) await ctx.runtime.runPromise(Effect.gen(function* () {
115
- yield* (yield* DataStore).recordIdempotentResponse({
116
- procedurePath: path,
117
- key,
118
- resultJson: JSON.stringify(result.data),
119
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
120
- });
121
- }).pipe(Effect.orElseSucceed(() => void 0)));
116
+ const result = yield* handler(params);
117
+ yield* (yield* DataStore).recordIdempotentResponse({
118
+ procedurePath: path,
119
+ key,
120
+ resultJson: JSON.stringify(result),
121
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
122
+ }).pipe(Effect.orElseSucceed(() => void 0));
122
123
  return result;
123
124
  });
124
- /** Drop-in replacement for `publicProcedure` on idempotent mutations. */
125
- const idempotentProcedure = publicProcedure.use(idempotent);
126
125
 
127
126
  //#endregion
128
- export { idempotencyKeys, idempotentProcedure };
127
+ export { idempotencyKeys, withIdempotency };