@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 +43 -8
- package/dist/index.js +87 -20
- package/dist/store.js +23 -5
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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": {
|
|
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
|
-
**
|
|
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
|
|
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
|
-
-
|
|
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:
|
|
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.
|
|
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("
|
|
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
|
-
// ---
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"
|
|
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
|
-
|
|
197
|
-
if (
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
task:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
|
328
|
-
//
|
|
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
|
|
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.
|
|
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",
|