@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 +232 -0
- package/dist/csdd.js +98 -0
- package/dist/index.js +28 -0
- package/dist/registry.js +20 -0
- package/dist/tooldef.js +63 -0
- package/dist/tools/agent.js +54 -0
- package/dist/tools/skill.js +68 -0
- package/dist/tools/spec.js +91 -0
- package/dist/tools/steering.js +83 -0
- package/package.json +45 -0
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
|
+
});
|
package/dist/registry.js
ADDED
|
@@ -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
|
+
];
|
package/dist/tooldef.js
ADDED
|
@@ -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
|
+
}
|