@protonspy/csdd-mcp 0.1.1

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 ADDED
@@ -0,0 +1,232 @@
1
+ # @protonspy/csdd-mcp
2
+
3
+ **An [MCP](https://modelcontextprotocol.io) server that exposes the [`csdd`](https://github.com/protonspy/csdd) CLI as tools — one tool per subcommand.**
4
+
5
+ `csdd` governs the Claude Code Spec-Driven Development workflow (steering, specs,
6
+ skills, sub-agents, MCP servers) and validates the contract mechanically. This
7
+ server lets an MCP-capable agent drive `csdd` **directly as tools**, instead of
8
+ shelling out to a terminal — same operations, same phase gates, same exit codes.
9
+
10
+ ```
11
+ agent ──(MCP/stdio)──▶ csdd-mcp ──(execFile)──▶ csdd binary ──▶ .claude/ · specs/
12
+ ```
13
+
14
+ Each tool builds a `csdd` argv, runs the binary headlessly (`NO_COLOR=1`, no TTY
15
+ so confirmations auto-decline), and returns its `stdout`/`stderr`. A non-zero
16
+ exit becomes an MCP error result; **exit `2` (validation failure) is surfaced
17
+ distinctly** so the agent can tell "your spec is invalid" from "the command
18
+ broke".
19
+
20
+ ---
21
+
22
+ ## Requirements
23
+
24
+ - **Node.js ≥ 18** (the published package ships compiled JS).
25
+ - **The `csdd` binary**, reachable by the server — see [Locating the csdd binary](#locating-the-csdd-binary).
26
+
27
+ ---
28
+
29
+ ## Install & configure
30
+
31
+ The server runs over **stdio**; point your MCP client at it.
32
+
33
+ ### Claude Code
34
+
35
+ ```bash
36
+ # project scope (writes .mcp.json) — or use --scope user for all projects
37
+ claude mcp add csdd -- npx -y @protonspy/csdd-mcp
38
+ ```
39
+
40
+ Or add it to `.mcp.json` by hand:
41
+
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "csdd": {
46
+ "command": "npx",
47
+ "args": ["-y", "@protonspy/csdd-mcp"],
48
+ "env": { "CSDD_BIN": "/usr/local/bin/csdd" }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ > `env.CSDD_BIN` is optional — drop it if `csdd` is on your `PATH`. See below.
55
+
56
+ ### Any MCP client
57
+
58
+ Launch `npx -y @protonspy/csdd-mcp` (or `csdd-mcp` if installed globally) as a
59
+ stdio server. The process stays alive serving stdio until the client closes the
60
+ pipe.
61
+
62
+ ---
63
+
64
+ ## Locating the csdd binary
65
+
66
+ The server resolves `csdd` once, on first use, in this order (first hit wins):
67
+
68
+ | # | Source | When it applies |
69
+ |---|--------|-----------------|
70
+ | 1 | **`$CSDD_BIN`** | Explicit absolute path. Always wins — use this if in doubt. |
71
+ | 2 | **Platform package** `@protonspy/csdd-<os>-<arch>` | Declared as an `optionalDependency` of this package, so `npx`/`npm i` fetches the prebuilt binary for your OS/arch automatically — the zero-config path. |
72
+ | 3 | **Sibling repo binary** (`../csdd`, `../../csdd`) | When running from a checkout of the csdd repo. |
73
+ | 4 | **`csdd` on `$PATH`** | Last resort, resolved by the OS at spawn time. |
74
+
75
+ If none resolve, calls fail with **exit `127`** and a message telling you to set
76
+ `CSDD_BIN`, install `@protonspy/csdd`, or put `csdd` on your `PATH`.
77
+
78
+ > **Zero-config:** running via `npx -y @protonspy/csdd-mcp` pulls the matching
79
+ > binary through #2 automatically — nothing to install. Set `CSDD_BIN` only to
80
+ > pin a specific build (e.g. a local dev binary).
81
+
82
+ ### Environment
83
+
84
+ | Variable | Effect |
85
+ |----------|--------|
86
+ | `CSDD_BIN` | Absolute path to the `csdd` binary. Highest-priority resolution. |
87
+ | `NO_COLOR` | Forced to `1` for every call so output is ANSI-free (you don't set this). |
88
+
89
+ ---
90
+
91
+ ## Result & error semantics
92
+
93
+ Every tool returns a text result. The mapping from the `csdd` exit code is:
94
+
95
+ | Exit | `isError` | Result text |
96
+ |------|-----------|-------------|
97
+ | `0` | `false` | `stdout` (and any `stderr` as an unlabelled warning); `(ok, no output)` if silent. |
98
+ | `2` | `true` | Prefixed `csdd validation failed (exit 2):` — a contract/validation problem. |
99
+ | other | `true` | Prefixed `csdd failed (exit <n>):`; `stderr` is labelled `[stderr]`. |
100
+ | `127` | `true` | Binary not found — includes guidance to set `CSDD_BIN` / install / fix `PATH`. |
101
+
102
+ ---
103
+
104
+ ## Tool reference
105
+
106
+ **27 tools** covering the csdd **development flow**, grouped by resource.
107
+ Conventions:
108
+
109
+ - Every tool accepts an optional **`root`** — the workspace root (the directory
110
+ containing `.claude/`). Omit it to walk up from the server's working directory.
111
+ - Destructive / gate-breaking tools take **`force`** (boolean). Without it,
112
+ deletes are refused and phase gates hold.
113
+ - `?` marks an optional parameter; everything else is required.
114
+
115
+ > **Scope:** this server exposes only the iterative development-flow resources
116
+ > (steering · spec · skill · agent). **Workspace setup and config management are
117
+ > deliberately not tools** — `csdd init`, `csdd mcp …`, and `csdd export …` are
118
+ > one-time operations a human runs from the CLI, not part of the loop an agent
119
+ > drives. (In fact, `csdd init` is what registers *this* server.)
120
+
121
+ ### Diagnostic
122
+
123
+ | Tool | Parameters | What it does |
124
+ |------|------------|--------------|
125
+ | `csdd_version` | — | Print the underlying `csdd` binary version (diagnostic / connectivity check). |
126
+
127
+ ### 🧭 steering — project memory (`.claude/steering/*.md`)
128
+
129
+ | Tool | Parameters | What it does |
130
+ |------|------------|--------------|
131
+ | `csdd_steering_init` | `root?` | Create `.claude/steering/` and the 6 standard files (product, tech, structure, security, testing, api-conventions). |
132
+ | `csdd_steering_create` | `name`, `inclusion`, `pattern?[]`, `description?`, `title?`, `force?`, `root?` | Create a custom steering file. `inclusion` ∈ `always · fileMatch · manual · auto`. `fileMatch` requires ≥1 `pattern`; `auto` requires a `description`. |
133
+ | `csdd_steering_list` | `root?`, `inclusion?` | List steering files with inclusion mode; optionally filter by `inclusion`. |
134
+ | `csdd_steering_show` | `name`, `root?` | Print a steering file (frontmatter + body). |
135
+ | `csdd_steering_delete` | `name`, `force?`, `root?` | Delete a steering file (`force` required). Foundational files (product, tech, structure) are protected. |
136
+ | `csdd_steering_validate` | `name?`, `root?` | Validate frontmatter/structure. Omit `name` to validate all. Exit 2 on issues. |
137
+
138
+ `inclusion` controls *when* the steering loads: `always` (always-on), `fileMatch`
139
+ (when files match a `pattern`), `manual` (`#name`), `auto` (when its `description`
140
+ matches the context).
141
+
142
+ ### 📐 spec — per-feature contract (`specs/<feature>/`)
143
+
144
+ | Tool | Parameters | What it does |
145
+ |------|------------|--------------|
146
+ | `csdd_spec_init` | `feature`, `language?`, `root?` | Create `specs/<feature>/spec.json` (phase = initial, no approvals). `language` defaults to `en`. |
147
+ | `csdd_spec_list` | `root?` | List specs with current phase, approved phases, and readiness. |
148
+ | `csdd_spec_show` | `feature`, `root?` | Show a spec's `spec.json` metadata and its artifacts. |
149
+ | `csdd_spec_status` | `feature`, `root?` | Combined `show` + `validate` for a spec. |
150
+ | `csdd_spec_generate` | `feature`, `artifact`, `force?`, `root?` | Generate an artifact. `artifact` ∈ `requirements · design · tasks · research · bugfix`. **Phase gates apply** (see below); `force` bypasses them. |
151
+ | `csdd_spec_approve` | `feature`, `phase`, `force?`, `root?` | Approve a phase. `phase` ∈ `requirements · design · tasks`. Validates first; `force` approves despite issues/missing prior approvals. |
152
+ | `csdd_spec_validate` | `feature`, `root?` | Validate EARS phrasing, traceability, task annotations, parallel safety. Exit 2 on issues. |
153
+ | `csdd_spec_delete` | `feature`, `force?`, `root?` | Delete `specs/<feature>/` recursively (`force` required). |
154
+
155
+ > **Phase gates (enforced, not advisory):** `design` needs `requirements`
156
+ > approved; `tasks` needs `design` approved. Generating out of order fails with
157
+ > **exit 2** unless `force` is passed. `ready_for_implementation` flips to `true`
158
+ > only when all three phases are approved. `research` and `bugfix` are ungated.
159
+
160
+ ### 🛠️ skill — workflow bundles (`.claude/skills/<name>/`)
161
+
162
+ | Tool | Parameters | What it does |
163
+ |------|------------|--------------|
164
+ | `csdd_skill_create` | `name`, `description`, `title?`, `root?` | Create `.claude/skills/<name>/` with `SKILL.md` (+ `references/`, `assets/`, `scripts/`). `description` is the one-line activation trigger. |
165
+ | `csdd_skill_list` | `root?` | List skills with their descriptions. |
166
+ | `csdd_skill_show` | `name`, `root?` | List a skill's files and print `SKILL.md`. |
167
+ | `csdd_skill_add_reference` | `skill`, `file`, `root?` | Add a reference file under `references/`. Path traversal is rejected. |
168
+ | `csdd_skill_add_script` | `skill`, `file`, `root?` | Add a script file under `scripts/`. Path traversal is rejected. |
169
+ | `csdd_skill_add_asset` | `skill`, `file`, `root?` | Add an asset file under `assets/`. Path traversal is rejected. |
170
+ | `csdd_skill_validate` | `name`, `root?` | Validate structure + frontmatter; report line/token counts. Exit 2 on issues. |
171
+ | `csdd_skill_delete` | `name`, `force?`, `root?` | Delete `.claude/skills/<name>/` recursively (`force` required). |
172
+
173
+ ### 🤖 agent — custom sub-agents (`.claude/agents/<name>.md`)
174
+
175
+ | Tool | Parameters | What it does |
176
+ |------|------------|--------------|
177
+ | `csdd_agent_create` | `name`, `description`, `tools?[]`, `model?`, `title?`, `force?`, `root?` | Create a least-privilege sub-agent (default tools: `Read`, `Grep`). `description` tells the orchestrator when to pick it. `model` ∈ `sonnet · opus · haiku`. |
178
+ | `csdd_agent_list` | `root?` | List agents with their tools and descriptions. |
179
+ | `csdd_agent_show` | `name`, `root?` | Print an agent file. |
180
+ | `csdd_agent_delete` | `name`, `force?`, `root?` | Delete `.claude/agents/<name>.md` (`force` required). |
181
+
182
+ > **Not here:** managing the `.mcp.json` servers themselves (`csdd mcp add/list/
183
+ > remove/enable/disable/validate`) stays on the CLI — same for `csdd init` and
184
+ > `csdd export`. Keeping setup off the tool surface is intentional (see Scope).
185
+
186
+ ---
187
+
188
+ ## A typical agent flow
189
+
190
+ Setup is a one-time CLI step (`npx @protonspy/csdd init --with-baseline`, which
191
+ also registers this server). From there the agent drives the feature with tools:
192
+
193
+ ```jsonc
194
+ csdd_spec_init { "feature": "photo-albums" }
195
+
196
+ csdd_spec_generate { "feature": "photo-albums", "artifact": "requirements" }
197
+ csdd_spec_validate { "feature": "photo-albums" } // exit 2 → fix what it flags
198
+ csdd_spec_approve { "feature": "photo-albums", "phase": "requirements" }
199
+
200
+ csdd_spec_generate { "feature": "photo-albums", "artifact": "design" } // gated on the approval above
201
+ csdd_spec_approve { "feature": "photo-albums", "phase": "design" }
202
+
203
+ csdd_spec_generate { "feature": "photo-albums", "artifact": "tasks" }
204
+ csdd_spec_approve { "feature": "photo-albums", "phase": "tasks" }
205
+ // → spec.json: ready_for_implementation = true
206
+ ```
207
+
208
+ `csdd_spec_status { "feature": "photo-albums" }` between steps shows phase,
209
+ approvals, and validation issues in one call.
210
+
211
+ ---
212
+
213
+ ## Development
214
+
215
+ ```bash
216
+ npm install
217
+ npm run build # tsc → dist/
218
+ npm test # build + Node's built-in test runner (node:test)
219
+ npm run test:run # tests only, against the current dist/ (no rebuild)
220
+ npm run dev # tsc --watch
221
+ ```
222
+
223
+ Tests are TypeScript run through `node:test` with native **type stripping**, so
224
+ they need **Node ≥ 22.18** (dev-only — the published package still targets Node
225
+ ≥ 18). They exercise the argv builders, result formatting, binary resolution,
226
+ `runCsdd` against a stub binary, and every tool's argv mapping. See `test/`.
227
+
228
+ ---
229
+
230
+ ## License
231
+
232
+ MIT
package/dist/csdd.js ADDED
@@ -0,0 +1,98 @@
1
+ // Resolve the csdd binary and run it headlessly, capturing structured output.
2
+ //
3
+ // Resolution order (first hit wins):
4
+ // 1. $CSDD_BIN — explicit override
5
+ // 2. platform optionalDependency — @protonspy/csdd-<platform>-<arch>
6
+ // (the same packages the npm launcher uses)
7
+ // 3. sibling repo binary — ../csdd (when running from the repo)
8
+ // 4. "csdd" on $PATH — last-resort lookup at spawn time
9
+ import { execFile } from "node:child_process";
10
+ import { createRequire } from "node:module";
11
+ import { existsSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ const require = createRequire(import.meta.url);
15
+ // platform/arch -> npm package name (must match the npm launcher's TARGETS).
16
+ const PKG = {
17
+ "linux-x64": "@protonspy/csdd-linux-x64",
18
+ "linux-arm64": "@protonspy/csdd-linux-arm64",
19
+ "darwin-x64": "@protonspy/csdd-darwin-x64",
20
+ "darwin-arm64": "@protonspy/csdd-darwin-arm64",
21
+ "win32-x64": "@protonspy/csdd-win32-x64",
22
+ };
23
+ const binName = process.platform === "win32" ? "csdd.exe" : "csdd";
24
+ function fromPlatformPackage() {
25
+ const pkgName = PKG[`${process.platform}-${process.arch}`];
26
+ if (!pkgName)
27
+ return null;
28
+ try {
29
+ const pkgJson = require.resolve(`${pkgName}/package.json`);
30
+ const candidate = join(pkgJson, "..", "bin", binName);
31
+ return existsSync(candidate) ? candidate : null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ function fromSiblingRepo() {
38
+ // dist/csdd.js -> packageRoot = mcp-server -> repo root holds ./csdd
39
+ const here = dirname(fileURLToPath(import.meta.url));
40
+ for (const up of ["..", "../.."]) {
41
+ const candidate = join(here, up, binName);
42
+ if (existsSync(candidate))
43
+ return candidate;
44
+ }
45
+ return null;
46
+ }
47
+ let cached = null;
48
+ /** Locate the csdd binary, caching the result. Falls back to a bare "csdd". */
49
+ export function resolveCsddBin() {
50
+ if (cached)
51
+ return cached;
52
+ cached =
53
+ process.env.CSDD_BIN ||
54
+ fromPlatformPackage() ||
55
+ fromSiblingRepo() ||
56
+ binName; // let the OS resolve it on $PATH at spawn time
57
+ return cached;
58
+ }
59
+ /**
60
+ * Run `csdd` with the given argv. Never rejects on a non-zero exit — the exit
61
+ * code is returned so each tool can map it to an MCP error result.
62
+ * `cwd` controls workspace discovery when no --root flag is passed.
63
+ */
64
+ export function runCsdd(argv, cwd) {
65
+ const bin = resolveCsddBin();
66
+ return new Promise((resolve) => {
67
+ execFile(bin, argv, {
68
+ cwd: cwd || process.cwd(),
69
+ // NO_COLOR keeps output free of ANSI escapes; csdd is non-interactive
70
+ // (confirm() returns false) when stdin is not a TTY, which it isn't here.
71
+ env: { ...process.env, NO_COLOR: "1" },
72
+ maxBuffer: 16 * 1024 * 1024,
73
+ windowsHide: true,
74
+ }, (err, stdout, stderr) => {
75
+ const code = err && typeof err.code === "string"
76
+ ? // spawn failure (ENOENT etc.) — surface as exit 127
77
+ 127
78
+ : (err?.code ?? 0);
79
+ if (err && code === 127) {
80
+ resolve({
81
+ ok: false,
82
+ exitCode: 127,
83
+ stdout: stdout?.toString() ?? "",
84
+ stderr: (stderr?.toString() ?? "") +
85
+ `\ncsdd binary not found or not executable: ${bin}\n` +
86
+ `Set CSDD_BIN, install @protonspy/csdd, or put csdd on PATH.`,
87
+ });
88
+ return;
89
+ }
90
+ resolve({
91
+ ok: code === 0,
92
+ exitCode: typeof code === "number" ? code : 1,
93
+ stdout: stdout?.toString() ?? "",
94
+ stderr: stderr?.toString() ?? "",
95
+ });
96
+ });
97
+ });
98
+ }
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ // csdd-mcp — an MCP server (stdio) that exposes the csdd CLI as one tool per
3
+ // subcommand. Each tool shells out to the csdd binary headlessly and returns
4
+ // its stdout/stderr; non-zero exits are surfaced as MCP errors (exit 2 =
5
+ // validation failure).
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { makeHandler } from "./tooldef.js";
9
+ import { allTools } from "./registry.js";
10
+ const version = "0.1.0";
11
+ async function main() {
12
+ const server = new McpServer({ name: "csdd-mcp", version });
13
+ for (const def of allTools) {
14
+ server.registerTool(def.name, {
15
+ title: def.title,
16
+ description: def.description,
17
+ inputSchema: def.inputSchema,
18
+ }, makeHandler(def));
19
+ }
20
+ const transport = new StdioServerTransport();
21
+ await server.connect(transport);
22
+ // Never resolves; the process stays alive serving stdio until the client
23
+ // closes the pipe.
24
+ }
25
+ main().catch((err) => {
26
+ process.stderr.write(`csdd-mcp fatal: ${err?.stack || err}\n`);
27
+ process.exit(1);
28
+ });
@@ -0,0 +1,20 @@
1
+ import { steeringTools } from "./tools/steering.js";
2
+ import { specTools } from "./tools/spec.js";
3
+ import { skillTools } from "./tools/skill.js";
4
+ import { agentTools } from "./tools/agent.js";
5
+ export const miscTools = [
6
+ {
7
+ name: "csdd_version",
8
+ title: "csdd version",
9
+ description: "Print the version of the underlying csdd binary (diagnostic).",
10
+ inputSchema: {},
11
+ toArgs: () => ["version"],
12
+ },
13
+ ];
14
+ export const allTools = [
15
+ ...miscTools,
16
+ ...steeringTools,
17
+ ...specTools,
18
+ ...skillTools,
19
+ ...agentTools,
20
+ ];
@@ -0,0 +1,63 @@
1
+ // Shared types + helpers for declaring csdd-backed MCP tools.
2
+ import { z } from "zod";
3
+ import { runCsdd } from "./csdd.js";
4
+ // --- argv builders ---------------------------------------------------------
5
+ /** `--flag value` when value is a non-empty string/number, else nothing. */
6
+ export function flag(name, value) {
7
+ if (value === undefined || value === null || value === "")
8
+ return [];
9
+ return [name, String(value)];
10
+ }
11
+ /** `--flag` when truthy, else nothing. */
12
+ export function bool(name, value) {
13
+ return value ? [name] : [];
14
+ }
15
+ /** Repeat `--flag value` for each item. */
16
+ export function multi(name, values) {
17
+ if (!Array.isArray(values))
18
+ return [];
19
+ return values.flatMap((v) => [name, String(v)]);
20
+ }
21
+ /** Standard `--root` passthrough (most commands accept it). */
22
+ export function rootArg(params) {
23
+ return flag("--root", params.root);
24
+ }
25
+ // Reusable field schemas -----------------------------------------------------
26
+ export const rootField = z
27
+ .string()
28
+ .optional()
29
+ .describe("Workspace root (the directory containing .claude/). Defaults to walking up from the current directory.");
30
+ export const forceField = z
31
+ .boolean()
32
+ .optional()
33
+ .describe("Bypass safety checks / overwrite or delete without confirmation.");
34
+ // --- result formatting -----------------------------------------------------
35
+ /** Turn a csdd run into an MCP tool result, flagging non-zero exits. */
36
+ export function toMcpResult(r) {
37
+ const parts = [];
38
+ if (r.stdout.trim())
39
+ parts.push(r.stdout.trimEnd());
40
+ if (r.stderr.trim())
41
+ parts.push((r.ok ? "" : "[stderr] ") + r.stderr.trimEnd());
42
+ if (parts.length === 0) {
43
+ parts.push(r.ok ? "(ok, no output)" : `csdd exited with code ${r.exitCode}`);
44
+ }
45
+ // Exit 2 = validation failure; surface it distinctly in the text.
46
+ const header = r.exitCode === 2
47
+ ? "csdd validation failed (exit 2):\n"
48
+ : r.ok
49
+ ? ""
50
+ : `csdd failed (exit ${r.exitCode}):\n`;
51
+ return {
52
+ content: [{ type: "text", text: header + parts.join("\n\n") }],
53
+ isError: !r.ok,
54
+ };
55
+ }
56
+ /** Build the handler that runs a ToolDef's argv and formats the result. */
57
+ export function makeHandler(def) {
58
+ return async (params) => {
59
+ const argv = def.toArgs(params ?? {});
60
+ const result = await runCsdd(argv, params?.root);
61
+ return toMcpResult(result);
62
+ };
63
+ }
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ import { bool, flag, forceField, multi, rootArg, rootField, } from "../tooldef.js";
3
+ const agentName = z.string().describe("Agent name (.claude/agents/<name>.md).");
4
+ export const agentTools = [
5
+ {
6
+ name: "csdd_agent_create",
7
+ title: "Agent create",
8
+ description: "Create a custom sub-agent with least-privilege tools (default: Read, Grep). Description tells the orchestrator when to pick it.",
9
+ inputSchema: {
10
+ name: agentName,
11
+ description: z.string().describe("When the orchestrator should select this agent."),
12
+ tools: z
13
+ .array(z.string())
14
+ .optional()
15
+ .describe("Tool names to grant (e.g. Read, Grep, Bash, Edit). Repeatable."),
16
+ model: z.string().optional().describe("Model override (e.g. sonnet, opus, haiku)."),
17
+ title: z.string().optional().describe("Document title (defaults to Title Case of name)."),
18
+ force: forceField,
19
+ root: rootField,
20
+ },
21
+ toArgs: (p) => [
22
+ "agent",
23
+ "create",
24
+ p.name,
25
+ ...flag("--description", p.description),
26
+ ...multi("--tools", p.tools),
27
+ ...flag("--model", p.model),
28
+ ...flag("--title", p.title),
29
+ ...bool("--force", p.force),
30
+ ...rootArg(p),
31
+ ],
32
+ },
33
+ {
34
+ name: "csdd_agent_list",
35
+ title: "Agent list",
36
+ description: "List agents with their tools and descriptions.",
37
+ inputSchema: { root: rootField },
38
+ toArgs: (p) => ["agent", "list", ...rootArg(p)],
39
+ },
40
+ {
41
+ name: "csdd_agent_show",
42
+ title: "Agent show",
43
+ description: "Print an agent file.",
44
+ inputSchema: { name: agentName, root: rootField },
45
+ toArgs: (p) => ["agent", "show", p.name, ...rootArg(p)],
46
+ },
47
+ {
48
+ name: "csdd_agent_delete",
49
+ title: "Agent delete",
50
+ description: "Delete .claude/agents/<name>.md. Requires force.",
51
+ inputSchema: { name: agentName, force: forceField, root: rootField },
52
+ toArgs: (p) => ["agent", "delete", p.name, ...bool("--force", p.force), ...rootArg(p)],
53
+ },
54
+ ];
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ import { bool, flag, forceField, rootArg, rootField } from "../tooldef.js";
3
+ const skillName = z.string().describe("Skill name (.claude/skills/<name>/).");
4
+ function addArtifact(action, kind) {
5
+ return {
6
+ name: `csdd_skill_add_${action.replace("add-", "")}`,
7
+ title: `Skill add ${kind}`,
8
+ description: `Add a ${kind} file under .claude/skills/<skill>/${kind}s/ with a placeholder. Path traversal is rejected.`,
9
+ inputSchema: {
10
+ skill: skillName,
11
+ file: z.string().describe(`File path relative to the ${kind}s/ subdir.`),
12
+ root: rootField,
13
+ },
14
+ toArgs: (p) => ["skill", action, p.skill, p.file, ...rootArg(p)],
15
+ };
16
+ }
17
+ export const skillTools = [
18
+ {
19
+ name: "csdd_skill_create",
20
+ title: "Skill create",
21
+ description: "Create .claude/skills/<name>/ with SKILL.md (+ references/, assets/, scripts/). Description is the one-line activation trigger.",
22
+ inputSchema: {
23
+ name: skillName,
24
+ description: z.string().describe("One-sentence activation trigger for the skill."),
25
+ title: z.string().optional().describe("Document title (defaults to Title Case of name)."),
26
+ root: rootField,
27
+ },
28
+ toArgs: (p) => [
29
+ "skill",
30
+ "create",
31
+ p.name,
32
+ ...flag("--description", p.description),
33
+ ...flag("--title", p.title),
34
+ ...rootArg(p),
35
+ ],
36
+ },
37
+ {
38
+ name: "csdd_skill_list",
39
+ title: "Skill list",
40
+ description: "List skills with their descriptions.",
41
+ inputSchema: { root: rootField },
42
+ toArgs: (p) => ["skill", "list", ...rootArg(p)],
43
+ },
44
+ {
45
+ name: "csdd_skill_show",
46
+ title: "Skill show",
47
+ description: "List a skill's files and print SKILL.md.",
48
+ inputSchema: { name: skillName, root: rootField },
49
+ toArgs: (p) => ["skill", "show", p.name, ...rootArg(p)],
50
+ },
51
+ addArtifact("add-reference", "reference"),
52
+ addArtifact("add-script", "script"),
53
+ addArtifact("add-asset", "asset"),
54
+ {
55
+ name: "csdd_skill_validate",
56
+ title: "Skill validate",
57
+ description: "Validate skill structure + frontmatter; report line/token counts. Exit 2 on issues.",
58
+ inputSchema: { name: skillName, root: rootField },
59
+ toArgs: (p) => ["skill", "validate", p.name, ...rootArg(p)],
60
+ },
61
+ {
62
+ name: "csdd_skill_delete",
63
+ title: "Skill delete",
64
+ description: "Delete .claude/skills/<name>/ recursively. Requires force.",
65
+ inputSchema: { name: skillName, force: forceField, root: rootField },
66
+ toArgs: (p) => ["skill", "delete", p.name, ...bool("--force", p.force), ...rootArg(p)],
67
+ },
68
+ ];
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ import { bool, flag, forceField, rootArg, rootField } from "../tooldef.js";
3
+ const feature = z.string().describe("Feature name (specs/<feature>/).");
4
+ export const specTools = [
5
+ {
6
+ name: "csdd_spec_init",
7
+ title: "Spec init",
8
+ description: "Create specs/<feature>/spec.json (phase=initial, no approvals, not ready for implementation).",
9
+ inputSchema: {
10
+ feature,
11
+ language: z.string().optional().describe("Spec language (default: en)."),
12
+ root: rootField,
13
+ },
14
+ toArgs: (p) => ["spec", "init", p.feature, ...flag("--language", p.language), ...rootArg(p)],
15
+ },
16
+ {
17
+ name: "csdd_spec_list",
18
+ title: "Spec list",
19
+ description: "List specs with current phase, approved phases, and readiness.",
20
+ inputSchema: { root: rootField },
21
+ toArgs: (p) => ["spec", "list", ...rootArg(p)],
22
+ },
23
+ {
24
+ name: "csdd_spec_show",
25
+ title: "Spec show",
26
+ description: "Show a spec's metadata (spec.json) and its artifacts.",
27
+ inputSchema: { feature, root: rootField },
28
+ toArgs: (p) => ["spec", "show", p.feature, ...rootArg(p)],
29
+ },
30
+ {
31
+ name: "csdd_spec_status",
32
+ title: "Spec status",
33
+ description: "Combined show + validate for a spec.",
34
+ inputSchema: { feature, root: rootField },
35
+ toArgs: (p) => ["spec", "status", p.feature, ...rootArg(p)],
36
+ },
37
+ {
38
+ name: "csdd_spec_generate",
39
+ title: "Spec generate artifact",
40
+ description: "Generate a spec artifact. Phase gates apply: design needs requirements approved, tasks needs design approved (use force to bypass). research/bugfix are ungated.",
41
+ inputSchema: {
42
+ feature,
43
+ artifact: z
44
+ .enum(["requirements", "design", "tasks", "research", "bugfix"])
45
+ .describe("Which artifact to generate."),
46
+ force: forceField,
47
+ root: rootField,
48
+ },
49
+ toArgs: (p) => [
50
+ "spec",
51
+ "generate",
52
+ p.feature,
53
+ ...flag("--artifact", p.artifact),
54
+ ...bool("--force", p.force),
55
+ ...rootArg(p),
56
+ ],
57
+ },
58
+ {
59
+ name: "csdd_spec_approve",
60
+ title: "Spec approve phase",
61
+ description: "Approve a spec phase (requirements|design|tasks). Validates first; force approves despite issues/missing prior approvals. Sets ready_for_implementation only when all three are approved.",
62
+ inputSchema: {
63
+ feature,
64
+ phase: z.enum(["requirements", "design", "tasks"]).describe("Phase to approve."),
65
+ force: forceField,
66
+ root: rootField,
67
+ },
68
+ toArgs: (p) => [
69
+ "spec",
70
+ "approve",
71
+ p.feature,
72
+ ...flag("--phase", p.phase),
73
+ ...bool("--force", p.force),
74
+ ...rootArg(p),
75
+ ],
76
+ },
77
+ {
78
+ name: "csdd_spec_validate",
79
+ title: "Spec validate",
80
+ description: "Validate a spec: EARS phrasing, traceability, task annotations, parallel safety. Exit 2 on issues.",
81
+ inputSchema: { feature, root: rootField },
82
+ toArgs: (p) => ["spec", "validate", p.feature, ...rootArg(p)],
83
+ },
84
+ {
85
+ name: "csdd_spec_delete",
86
+ title: "Spec delete",
87
+ description: "Delete specs/<feature>/ recursively. Requires force.",
88
+ inputSchema: { feature, force: forceField, root: rootField },
89
+ toArgs: (p) => ["spec", "delete", p.feature, ...bool("--force", p.force), ...rootArg(p)],
90
+ },
91
+ ];
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ import { bool, flag, forceField, multi, rootArg, rootField, } from "../tooldef.js";
3
+ const inclusion = z
4
+ .enum(["always", "fileMatch", "manual", "auto"])
5
+ .describe("When the steering loads: always (always-on), fileMatch (when files match --pattern), manual (#name), auto (when description matches context).");
6
+ export const steeringTools = [
7
+ {
8
+ name: "csdd_steering_init",
9
+ title: "Steering init",
10
+ description: "Create .claude/steering/ and populate the 6 standard files (product, tech, structure, security, testing, api-conventions).",
11
+ inputSchema: { root: rootField },
12
+ toArgs: (p) => ["steering", "init", ...rootArg(p)],
13
+ },
14
+ {
15
+ name: "csdd_steering_create",
16
+ title: "Steering create",
17
+ description: "Create a custom steering file with inclusion metadata. fileMatch requires at least one pattern; auto requires a description.",
18
+ inputSchema: {
19
+ name: z.string().describe("Steering file name (no extension)."),
20
+ inclusion,
21
+ pattern: z
22
+ .array(z.string())
23
+ .optional()
24
+ .describe("Glob pattern(s) for fileMatch inclusion. Repeatable."),
25
+ description: z
26
+ .string()
27
+ .optional()
28
+ .describe("One-line description; required for auto inclusion."),
29
+ title: z.string().optional().describe("Document title (defaults to Title Case of name)."),
30
+ force: forceField,
31
+ root: rootField,
32
+ },
33
+ toArgs: (p) => [
34
+ "steering",
35
+ "create",
36
+ p.name,
37
+ ...flag("--inclusion", p.inclusion),
38
+ ...multi("--pattern", p.pattern),
39
+ ...flag("--description", p.description),
40
+ ...flag("--title", p.title),
41
+ ...bool("--force", p.force),
42
+ ...rootArg(p),
43
+ ],
44
+ },
45
+ {
46
+ name: "csdd_steering_list",
47
+ title: "Steering list",
48
+ description: "List steering files with inclusion mode and inclusion-specific extras.",
49
+ inputSchema: {
50
+ root: rootField,
51
+ inclusion: inclusion.optional().describe("Filter by inclusion mode."),
52
+ },
53
+ toArgs: (p) => ["steering", "list", ...rootArg(p), ...flag("--inclusion", p.inclusion)],
54
+ },
55
+ {
56
+ name: "csdd_steering_show",
57
+ title: "Steering show",
58
+ description: "Print a steering file (frontmatter + body).",
59
+ inputSchema: { name: z.string().describe("Steering file name."), root: rootField },
60
+ toArgs: (p) => ["steering", "show", p.name, ...rootArg(p)],
61
+ },
62
+ {
63
+ name: "csdd_steering_delete",
64
+ title: "Steering delete",
65
+ description: "Delete a steering file. Requires force. Foundational files (product, tech, structure) are protected.",
66
+ inputSchema: {
67
+ name: z.string().describe("Steering file name."),
68
+ force: forceField,
69
+ root: rootField,
70
+ },
71
+ toArgs: (p) => ["steering", "delete", p.name, ...bool("--force", p.force), ...rootArg(p)],
72
+ },
73
+ {
74
+ name: "csdd_steering_validate",
75
+ title: "Steering validate",
76
+ description: "Validate steering frontmatter and structure. Omit name to validate all. Exit 2 on issues.",
77
+ inputSchema: {
78
+ name: z.string().optional().describe("Validate only this file; omit for all."),
79
+ root: rootField,
80
+ },
81
+ toArgs: (p) => ["steering", "validate", ...(p.name ? [p.name] : []), ...rootArg(p)],
82
+ },
83
+ ];
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@protonspy/csdd-mcp",
3
+ "version": "0.1.1",
4
+ "description": "MCP server exposing the csdd CLI over stdio (one tool per subcommand).",
5
+ "homepage": "https://github.com/protonspy/csdd/tree/main/mcp-server#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/protonspy/csdd/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/protonspy/csdd.git",
12
+ "directory": "mcp-server"
13
+ },
14
+ "type": "module",
15
+ "bin": {
16
+ "csdd-mcp": "dist/index.js"
17
+ },
18
+ "main": "dist/index.js",
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "model-context-protocol",
29
+ "csdd",
30
+ "claude-code",
31
+ "sdd"
32
+ ],
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.12.0",
36
+ "zod": "^3.23.8"
37
+ },
38
+ "optionalDependencies": {
39
+ "@protonspy/csdd-linux-x64": "0.1.1",
40
+ "@protonspy/csdd-linux-arm64": "0.1.1",
41
+ "@protonspy/csdd-darwin-x64": "0.1.1",
42
+ "@protonspy/csdd-darwin-arm64": "0.1.1",
43
+ "@protonspy/csdd-win32-x64": "0.1.1"
44
+ }
45
+ }