@precode/mcp 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -15,14 +15,28 @@ At the end it writes an **`IMPLEMENTATION.md`** (what was built) and a **`TODO_F
15
15
 
16
16
  ## Install
17
17
 
18
- Runs over stdio. Point any MCP host at it.
18
+ Runs over stdio. Point any MCP host at `npx -y @precode/mcp`. Full per-host copy-paste configs: [useprecode.vercel.app/mcp](https://useprecode.vercel.app/mcp).
19
19
 
20
- **Cursor** `.cursor/mcp.json`:
20
+ | Host | Config location | How |
21
+ |------|-----------------|-----|
22
+ | **Cursor** | `.cursor/mcp.json` | JSON — see below |
23
+ | **Claude Code** | `.mcp.json` (project) | `claude mcp add --env PRECODE_TELEMETRY_DISABLED=1 --transport stdio --scope project precode -- npx -y @precode/mcp` |
24
+ | **Claude Desktop** | `~/Library/Application Support/Claude/claude_desktop_config.json` | JSON — same shape as Cursor |
25
+ | **Codex CLI** | `~/.codex/config.toml` | `codex mcp add precode -- npx -y @precode/mcp` |
26
+ | **VS Code** | `.vscode/mcp.json` | JSON — uses `"servers"` not `"mcpServers"` |
27
+ | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | JSON — same shape as Cursor |
28
+ | **Antigravity** | `~/.gemini/antigravity/mcp_config.json` | Agent panel → Manage MCP Servers → View raw config |
29
+
30
+ **Cursor / Claude Desktop / Windsurf / Antigravity** — `mcpServers` JSON:
21
31
 
22
32
  ```json
23
33
  {
24
34
  "mcpServers": {
25
- "precode": { "command": "npx", "args": ["-y", "@precode/mcp"] }
35
+ "precode": {
36
+ "command": "npx",
37
+ "args": ["-y", "@precode/mcp"],
38
+ "env": { "PRECODE_TELEMETRY_DISABLED": "1" }
39
+ }
26
40
  }
27
41
  }
28
42
  ```
@@ -30,10 +44,30 @@ Runs over stdio. Point any MCP host at it.
30
44
  **Claude Code:**
31
45
 
32
46
  ```bash
33
- claude mcp add precode -- npx -y @precode/mcp
47
+ claude mcp add --env PRECODE_TELEMETRY_DISABLED=1 --transport stdio --scope project precode -- npx -y @precode/mcp
48
+ ```
49
+
50
+ **Codex CLI:**
51
+
52
+ ```bash
53
+ codex mcp add precode -- npx -y @precode/mcp
34
54
  ```
35
55
 
36
- **Codex / Antigravity / VS Code** — add the same `command: npx`, `args: ["-y", "@precode/mcp"]` to their MCP config.
56
+ **VS Code** — note `"servers"` wrapper and optional `"cwd": "${workspaceFolder}"`:
57
+
58
+ ```json
59
+ {
60
+ "servers": {
61
+ "precode": {
62
+ "type": "stdio",
63
+ "command": "npx",
64
+ "args": ["-y", "@precode/mcp"],
65
+ "cwd": "${workspaceFolder}",
66
+ "env": { "PRECODE_TELEMETRY_DISABLED": "1" }
67
+ }
68
+ }
69
+ }
70
+ ```
37
71
 
38
72
  The server reads `.precode/` from the directory the host runs it in (`PRECODE_ROOT` overrides). Local stdio only — the build loop needs your filesystem.
39
73
 
@@ -52,13 +86,14 @@ Telemetry is off unless `PRECODE_TELEMETRY_URL` is set. When enabled, the server
52
86
  npm run test:stdio
53
87
  ```
54
88
 
55
- The smoke test launches the built MCP server over stdio, verifies the tool list, confirms failed checks keep a task open, confirms passing checks mark it done, reads `precode://docs`, and writes the final `TODO_FOR_YOU.md`.
89
+ The smoke test launches the built MCP server over stdio, verifies the tool list, runs a real executable check, confirms a failing check (and a fabricated passing report) keep the task open, confirms the task closes only once the real check passes, reads `precode://docs`, and writes the final `TODO_FOR_YOU.md`. `npm test` runs the full edge-case suite (`test/run.mjs`).
56
90
 
57
91
  ## Tools
58
92
 
59
93
  | Tool | Does |
60
94
  |------|------|
61
95
  | `precode.get_goal` | Re-anchor on the goal + progress |
96
+ | `precode.status` | Read-only snapshot of tasks, checks, and deferred work |
62
97
  | `precode.next_phase` | Alias for the next phased build step |
63
98
  | `precode.next_task` | Pull the next step + acceptance criteria |
64
99
  | `precode.verify` | **Runs** the task's `checks.json` checks; marks done only on real exit-0 |
@@ -84,9 +119,9 @@ Declare checks in `.precode/checks.json`. Each `auto` check is **run by the MCP*
84
119
  ```
85
120
 
86
121
  - `kind: "auto"` — executed; pass = exit 0 (and `expect` substring/`/regex/` matches output if set). Optional `timeoutMs` (default 120s).
87
- - `kind: "manual"` — never auto-passed; surfaced as a human gate in `TODO_FOR_YOU.md`.
122
+ - `kind: "manual"` — never auto-passed; surfaced as a human gate in `TODO_FOR_YOU.md` (a manual-only task closes with its gate routed to you).
88
123
  - `taskIndex` — scope a check to one task; omit for a global check that runs on every task.
89
- - No auto check for a task? `verify` falls back to your self-reported results but stamps the pass **UNVERIFIED**, so it's never mistaken for a machine-verified one.
124
+ - **A task never closes on a self-reported pass.** Reported `checkResults` are advisory only. A check declared with an empty `cmd` is treated as unconfigured and **HOLDs** the task — wire a real command, mark it `manual`, or `precode.defer_task`. `adopt_spec` writes such placeholders on purpose so a fresh spec can't be "verified" until you point the gate at something real.
90
125
 
91
126
  **Security:** the runner only executes commands you put in your own repo's `checks.json` — the same trust level as the agent already running your build. Commands run with a timeout and captured output. Because `checks.json` lives in your repo, gate tampering shows up in your diff.
92
127
 
package/dist/index.js CHANGED
@@ -2,9 +2,14 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
+ import { createRequire } from "node:module";
5
6
  import { PrecodeStore, adoptSpec } from "./store.js";
6
7
  import { checkKind, runChecks } from "./checks.js";
7
8
  import { recordTelemetry, taskTelemetry } from "./telemetry.js";
9
+ // Single source of truth for the version the handshake reports — read from
10
+ // package.json so it can never drift from the published package again.
11
+ const VERSION = createRequire(import.meta.url)("../package.json")
12
+ .version ?? "0.0.0";
8
13
  /**
9
14
  * @precode/mcp — drives a phased build → recheck → fix loop over a `.precode/`
10
15
  * package. Fixes the loop/goal failure other spec MCPs ship with:
@@ -22,7 +27,15 @@ function text(s) {
22
27
  }
23
28
  const NO_STORE = text("No `.precode/` package found in this directory. Either drop a PreCode export here, " +
24
29
  "or call `precode.adopt_spec` to map a plain SPEC.md / .specify / .spec-workflow folder into one.");
25
- const server = new McpServer({ name: "precode", version: "0.1.0" });
30
+ const server = new McpServer({ name: "precode", version: VERSION }, {
31
+ instructions: "PreCode drives a spec → build → verify loop with a hard, SELF-EXECUTING done-gate. " +
32
+ "Flow: ensure a .precode/ package exists (precode.adopt_spec maps any SPEC.md / .specify / .spec-workflow). " +
33
+ "Then loop: precode.get_goal to anchor on the goal → precode.next_task → build ONLY that task → precode.verify. " +
34
+ "On verify the MCP itself RUNS the commands in .precode/checks.json; a task closes only on real exit-0, so self-reported results cannot fake a pass. " +
35
+ "A failing or unconfigured check keeps the task OPEN with concrete fixes. If a check genuinely cannot pass here (needs a secret/deploy/external service), call precode.defer_task with a reason. " +
36
+ "When every task is done, call precode.finalize. Call precode.status anytime for a read-only progress snapshot. " +
37
+ "Resources precode://goal, ://tasks, ://docs, ://acceptance, ://progress are re-readable each phase.",
38
+ });
26
39
  // --- Tool: get the goal (anti-drift anchor) -------------------------------
27
40
  server.tool("precode.get_goal", "Re-read the project goal and progress. Call this at the start of every phase so the build does not drift from the spec.", {}, async () => {
28
41
  const s = await store();
@@ -31,6 +44,37 @@ server.tool("precode.get_goal", "Re-read the project goal and progress. Call thi
31
44
  await recordTelemetry({ eventName: "mcp_get_goal", root: ROOT });
32
45
  return text(await s.goal());
33
46
  });
47
+ // --- Tool: status (read-only snapshot) ------------------------------------
48
+ server.tool("precode.status", "Read-only snapshot of the build: task counts, the current open task, deferred tasks, and whether real checks are configured. Has no side effects — safe to call anytime.", {}, async () => {
49
+ const s = await store();
50
+ if (!s)
51
+ return NO_STORE;
52
+ const tasks = await s.tasks();
53
+ const deferred = await s.deferredEntries();
54
+ const deferredSet = new Set(deferred.map((e) => e.index));
55
+ const done = tasks.filter((t) => t.done).length;
56
+ const open = tasks.filter((t) => !t.done && !deferredSet.has(t.index));
57
+ const next = open[0];
58
+ const allChecks = await s.checks();
59
+ const autoConfigured = allChecks.filter((c) => checkKind(c) === "auto").length;
60
+ const placeholders = allChecks.filter((c) => checkKind(c) === "skip").length;
61
+ const manual = allChecks.filter((c) => checkKind(c) === "manual").length;
62
+ return text([
63
+ `Tasks: ${done}/${tasks.length} done, ${open.length} open, ${deferred.length} deferred.`,
64
+ next ? `Next open task: #${next.index} — ${next.text}` : "No open tasks — call precode.finalize.",
65
+ `Checks in checks.json: ${autoConfigured} auto (executed), ${manual} manual, ${placeholders} unconfigured placeholder(s).`,
66
+ placeholders > 0
67
+ ? "WARNING: placeholder checks have no command — tasks they apply to will HOLD until you set a real cmd, mark them manual, or defer."
68
+ : autoConfigured === 0 && manual === 0
69
+ ? "WARNING: no checks defined — add an auto check with a real cmd so the gate can verify."
70
+ : "Gate is backed by real executed checks.",
71
+ deferred.length
72
+ ? `Deferred: ${deferred.map((e) => `#${e.index} (${e.reason})`).join("; ")}`
73
+ : "",
74
+ ]
75
+ .filter(Boolean)
76
+ .join("\n"));
77
+ });
34
78
  // --- Tool: next task ------------------------------------------------------
35
79
  server.tool("precode.next_task", "Pull the next unchecked build step plus the goal and acceptance criteria. Build ONLY this task, then call precode.verify. Do not skip ahead.", {}, async () => {
36
80
  const s = await store();
@@ -91,7 +135,7 @@ server.tool("precode.next_phase", "Alias for precode.next_task. Pulls the next p
91
135
  ].join("\n"));
92
136
  });
93
137
  // --- Tool: verify (the hard done-gate + fix loop) -------------------------
94
- server.tool("precode.verify", "Close the done-gate for the current task. If the project declares checks in .precode/checks.json, the MCP RUNS them itself and the verdict comes from real exit codes — your reported results cannot fake a pass. A task is marked done only when every auto check it runs passes. Otherwise the gaps come back and the task stays OPEN fix and call precode.verify again.", {
138
+ server.tool("precode.verify", "Close the done-gate for the current task. The MCP RUNS the task's auto checks from .precode/checks.json itself and the verdict comes from real exit codes — your reported results CANNOT fake a pass and never close a task on their own. A task is marked done only when every auto check passes (or it has only manual gates, which route to TODO). If a declared check has no command yet, or no check is defined, you get a HOLD telling you to wire a real command, mark it manual, or precode.defer_task. Fix and call precode.verify again.", {
95
139
  checkResults: z
96
140
  .array(z.object({
97
141
  name: z.string().describe("e.g. 'build', 'typecheck', 'lint', 'smoke test'"),
@@ -99,7 +143,7 @@ server.tool("precode.verify", "Close the done-gate for the current task. If the
99
143
  detail: z.string().optional(),
100
144
  }))
101
145
  .optional()
102
- .describe("Optional supplementary notes about checks you ran. Authoritative only as a FALLBACK when checks.json defines no auto check for this task."),
146
+ .describe("ADVISORY ONLY. Never used to mark a task done a task closes only when the MCP executes its checks.json checks and they pass (or via a manual gate / precode.defer_task)."),
103
147
  notes: z.string().optional(),
104
148
  task: z.string().optional().describe("Optional host-side task identifier or title."),
105
149
  }, async ({ checkResults, notes, task: taskLabel }) => {
@@ -116,6 +160,7 @@ server.tool("precode.verify", "Close the done-gate for the current task. If the
116
160
  const specs = await s.checksForTask(task.index);
117
161
  const autoSpecs = specs.filter((c) => checkKind(c) === "auto");
118
162
  const manualSpecs = specs.filter((c) => checkKind(c) === "manual");
163
+ const placeholderSpecs = specs.filter((c) => checkKind(c) === "skip");
119
164
  // Route manual checks to the human TODO when the task closes — never auto-passed.
120
165
  const routeManual = async () => {
121
166
  if (!manualSpecs.length)
@@ -184,26 +229,48 @@ server.tool("precode.verify", "Close the done-gate for the current task. If the
184
229
  });
185
230
  return await closeTask(`Verified by MCP execution: ${runs.map((r) => `${r.name} ✓ (exit ${r.exitCode})`).join(", ")}`);
186
231
  }
187
- // --- Fallback: no auto check defined for this task. Self-report is
188
- // accepted but explicitly stamped UNVERIFIED so it is never mistaken
189
- // for a machine-verified pass. ---
232
+ // --- No auto check ran. A task NEVER closes on a self-reported pass. ---
190
233
  const reported = checkResults ?? [];
191
- if (reported.length === 0) {
192
- return text(`HOLD. Task #${task.index} has no executable check in .precode/checks.json and you reported none. ` +
193
- "Add a real command to checks.json (preferred — then the gate runs it), or report the checks you ran. " +
194
- "Nothing is marked done on an empty report.");
234
+ const reportedNote = reported.length
235
+ ? `Agent-reported (advisory, NOT used to close): ${reported
236
+ .map((c) => `${c.name}=${c.passed ? "pass" : "fail"}`)
237
+ .join(", ")}`
238
+ : "";
239
+ // Unconfigured placeholder checks (declared but no command) — hold and
240
+ // tell the agent to wire a real command, mark it manual, or defer.
241
+ if (placeholderSpecs.length > 0) {
242
+ return text([
243
+ `HOLD. Task #${task.index} is NOT done.`,
244
+ `${placeholderSpecs.length} declared check(s) have no command yet: ${placeholderSpecs
245
+ .map((c) => c.name)
246
+ .join(", ")}.`,
247
+ "A task never closes on a self-reported pass. Do ONE of these in .precode/checks.json:",
248
+ "- set a real shell `cmd` for each check (preferred — the gate then RUNS it), or",
249
+ '- set its `kind` to `manual` if only a human can verify it (routed to TODO_FOR_YOU.md), or',
250
+ "- call precode.defer_task if it genuinely cannot be verified in this environment.",
251
+ reportedNote,
252
+ ]
253
+ .filter(Boolean)
254
+ .join("\n"));
195
255
  }
196
- const failedReported = reported.filter((c) => !c.passed);
197
- if (failedReported.length > 0) {
198
- return await gateFailure(failedReported.map((c) => `${c.name}: FAILED${c.detail ? ` — ${c.detail}` : ""}`), failedReported.map((c) => `${c.name}: ${fixHint(c.name)}`));
256
+ // Only explicit manual gates apply — a legitimate human-verified close.
257
+ if (manualSpecs.length > 0) {
258
+ await recordTelemetry({
259
+ eventName: "mcp_verify_pass",
260
+ root: ROOT,
261
+ task: taskTelemetry(task),
262
+ metadata: { checkCount: manualSpecs.length, executed: false, manualOnly: true },
263
+ });
264
+ return await closeTask("Closed with MANUAL gates only (no automatable check) — routed to TODO_FOR_YOU.md for your verification.");
199
265
  }
200
- await recordTelemetry({
201
- eventName: "mcp_verify_pass",
202
- root: ROOT,
203
- task: taskTelemetry(task),
204
- metadata: { checkCount: reported.length, executed: false },
205
- });
206
- return await closeTask(`UNVERIFIED (self-reported, no checks.json command): ${reported.map((c) => `${c.name} ✓`).join(", ")}`);
266
+ // No check defined at all for this task.
267
+ return text([
268
+ `HOLD. Task #${task.index} has no check defined in .precode/checks.json, so it cannot be verified.`,
269
+ "A task never closes on a self-reported pass. Add at least one check (preferred: an `auto` check with a real `cmd` the gate runs), mark it `manual`, or call precode.defer_task.",
270
+ reportedNote,
271
+ ]
272
+ .filter(Boolean)
273
+ .join("\n"));
207
274
  }
208
275
  finally {
209
276
  await s.releaseLock();
package/dist/store.js CHANGED
@@ -193,10 +193,14 @@ export class PrecodeStore {
193
193
  const project = manifest?.project ?? {};
194
194
  const tasks = await this.tasks();
195
195
  const done = tasks.filter((t) => t.done).length;
196
+ const deferred = await this.deferredIndices();
197
+ const docs = await this.listDocFiles();
196
198
  return [
197
199
  `Project: ${project.name ?? "(unknown)"} (${project.appType ?? "app"})`,
198
- `Progress: ${done}/${tasks.length} tasks complete.`,
199
- "Build strictly to the spec in `.precode/docs/`. Do not invent scope. Re-read the relevant doc before each task.",
200
+ `Progress: ${done}/${tasks.length} tasks complete${deferred.length ? `, ${deferred.length} deferred` : ""}.`,
201
+ docs.length
202
+ ? `Build strictly to the spec docs in .precode/: ${docs.join(", ")}. Re-read the relevant one before each task; do not invent scope.`
203
+ : "No spec docs found in .precode/docs/. Work from the tasks and acceptance criteria; do not invent scope.",
200
204
  ].join("\n");
201
205
  }
202
206
  async appendImplementation(entry) {
@@ -324,9 +328,13 @@ export async function adoptSpec(searchRoot, specPathHint) {
324
328
  error: "No spec found. Provide a SPEC.md, a .specify/.spec-workflow folder, or a docs/ folder of markdown.",
325
329
  };
326
330
  }
327
- // Derive tasks: prefer explicit checkboxes; fall back to headings only when
328
- // the spec has none, so heading text never pollutes a real task list.
331
+ // Derive tasks, in priority order so a real list is never collapsed into one:
332
+ // 1. explicit checkboxes - [ ] ...
333
+ // 2. plain bullets / numbered items - x * x 1. x
334
+ // 3. section headings ## / ###
335
+ // 4. a single catch-all when the spec has no structure at all.
329
336
  const checkboxes = [];
337
+ const bullets = [];
330
338
  const headings = [];
331
339
  for (const line of specBody.split("\n")) {
332
340
  const cb = TASK_RE.exec(line);
@@ -334,11 +342,21 @@ export async function adoptSpec(searchRoot, specPathHint) {
334
342
  checkboxes.push(`- [ ] ${cb[3]}`);
335
343
  continue;
336
344
  }
345
+ const bullet = /^\s*(?:[-*]|\d+[.)])\s+(.*\S)\s*$/.exec(line);
346
+ if (bullet) {
347
+ if (bullets.length < 40)
348
+ bullets.push(`- [ ] ${bullet[1]}`);
349
+ continue;
350
+ }
337
351
  const h = /^#{2,3}\s+(.*\S)\s*$/.exec(line);
338
352
  if (h && headings.length < 40)
339
353
  headings.push(`- [ ] Implement: ${h[1]}`);
340
354
  }
341
- const tasks = checkboxes.length ? checkboxes : headings;
355
+ const tasks = checkboxes.length
356
+ ? checkboxes
357
+ : bullets.length
358
+ ? bullets
359
+ : headings;
342
360
  if (!tasks.length)
343
361
  tasks.push("- [ ] Implement the full spec, then verify.");
344
362
  await fs.mkdir(path.join(dir, "progress"), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@precode/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Open, agent-agnostic MCP server that turns a spec (any SPEC.md, best with a PreCode .precode/ package) into a self-correcting, verified build. Drives a phased build → recheck → fix loop with hard definition-of-done gates and an implemented-vs-todo ledger.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://useprecode.vercel.app/mcp",