Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,27 @@ jobs:

- name: test (race + coverage)
run: go test -race -coverprofile=coverage.out ./...

mcp-server:
name: mcp-server (build & test)
runs-on: ubuntu-latest
defaults:
run:
working-directory: mcp-server
steps:
- uses: actions/checkout@v4

# Node 24: the unit tests are TypeScript run through node:test with native
# type stripping (Node >= 22.18). The published package ships compiled JS
# (dist/) and still supports Node >= 18 — see package.json "engines".
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: npm
cache-dependency-path: mcp-server/package-lock.json

- name: install
run: npm ci

- name: test (build + node:test)
run: npm test
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,32 @@ Creating a steering automatically inserts `@.claude/steering/<name>` into a mana

---

## 🔌 MCP server — drive csdd as native tools

Prefer your agent to call **tools** over shelling out to a terminal?
[`@protonspy/csdd-mcp`](mcp-server/) is an MCP server (stdio) that exposes the csdd
**development flow** as tools — `csdd_spec_generate`, `csdd_steering_create`,
`csdd_spec_approve`, … **27 in total.** It wraps the same CLI, so the contract is
intact: phase gates still block, the validator still runs, and **exit 2 surfaces
as a distinct "validation failed" result** the agent can branch on. Typed
parameters (enums for `artifact`/`phase`/`inclusion`) mean the agent picks valid
inputs and the server builds the argv — more precise than hand-written commands.

`csdd init` registers the server in `.mcp.json` for you (pass `--no-mcp` to skip):

```bash
# already wired by `csdd init`; to add it to an existing workspace:
claude mcp add csdd -- npx -y @protonspy/csdd-mcp
```

- **Dev-flow only**, grouped by resource (steering · spec · skill · agent), plus `csdd_version`. **Setup and config management stay on the CLI** — `init`, `mcp`, and `export` are one-time human operations, not agent-loop tools.
- **Same binary, same rules.** The server just builds the argv and runs `csdd` headlessly (`NO_COLOR`, no TTY) — no logic of its own, so the CLI stays the single source of truth.
- **Zero-config binary** via `npx` (the matching prebuilt `csdd` is an `optionalDependency`); override with `CSDD_BIN`.

Full tool reference and configuration: [`mcp-server/README.md`](mcp-server/README.md).

---

## Interop — export to Kiro / Codex

`csdd` is Claude Code-native, but the SDD artifacts aren't locked in. `csdd export`
Expand Down
48 changes: 48 additions & 0 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,54 @@ func TestInitWithoutBaseline(t *testing.T) {
}
}

func TestInitRegistersMCPServer(t *testing.T) {
dir := t.TempDir()
code, out, _ := run(t, "init", "--root", dir)
if code != 0 {
t.Fatalf("init failed: %d", code)
}
if !strings.Contains(out, "registered the csdd MCP server") {
t.Errorf("init should report registering the csdd MCP server: %s", out)
}
cfg, err := loadMCP(filepath.Join(dir, ".mcp.json"))
if err != nil {
t.Fatalf("load .mcp.json: %v", err)
}
srv, ok := cfg.MCPServers[csddMCPServerName]
if !ok {
t.Fatalf("csdd MCP server not registered: %+v", cfg.MCPServers)
}
if srv.Command != "npx" || strings.Join(srv.Args, " ") != "-y @protonspy/csdd-mcp" {
t.Errorf("unexpected server entry: command=%q args=%v", srv.Command, srv.Args)
}
if srv.URL != "" || srv.Disabled || len(srv.AutoApprove) > 0 {
t.Errorf("expected a plain enabled stdio server, got %+v", srv)
}
// Idempotent: a second init must not re-register (and so not re-announce it).
_, out2, _ := run(t, "init", "--root", dir)
if strings.Contains(out2, "registered the csdd MCP server") {
t.Errorf("second init should not re-register the MCP server: %s", out2)
}
}

func TestInitNoMCP(t *testing.T) {
dir := t.TempDir()
code, out, _ := run(t, "init", "--root", dir, "--no-mcp")
if code != 0 {
t.Fatalf("init failed: %d", code)
}
if strings.Contains(out, "registered the csdd MCP server") {
t.Errorf("--no-mcp should not register the server: %s", out)
}
cfg, err := loadMCP(filepath.Join(dir, ".mcp.json"))
if err != nil {
t.Fatalf("load .mcp.json: %v", err)
}
if _, ok := cfg.MCPServers[csddMCPServerName]; ok {
t.Errorf("--no-mcp should leave .mcp.json without the csdd server: %+v", cfg.MCPServers)
}
}

// ---------- steering ----------

func TestSteeringInit(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions cmd/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ func TestExportCodex(t *testing.T) {

func TestExportCodexNoMCP(t *testing.T) {
dir := freshWorkspace(t)
// init registers the csdd server by default; remove it to exercise the no-server path.
_, _, _ = run(t, "mcp", "remove", csddMCPServerName, "--force", "--root", dir)
code, out, errOut := run(t, "export", "codex", "--root", dir)
if code != 0 {
t.Fatalf("export codex failed: %s", errOut)
Expand Down
37 changes: 37 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ func runInit(args []string, templates embed.FS) int {
fs := flag.NewFlagSet("init", flag.ContinueOnError)
var root string
var withBaseline bool
var noMCP bool
fs.StringVar(&root, "root", "", "Target directory (default: cwd).")
fs.BoolVar(&withBaseline, "with-baseline", false, "Also scaffold product.md, tech.md, structure.md.")
fs.BoolVar(&noMCP, "no-mcp", false, "Do not register the csdd MCP server in .mcp.json.")
if err := fs.Parse(args); err != nil {
return failOnFlagParse(err)
}
Expand Down Expand Up @@ -47,6 +49,13 @@ func runInit(args []string, templates embed.FS) int {
render.OK("Initialized Claude Code workspace at " + root)
render.Info("directories created: " + intStr(created.dirs))
render.Info("files created: " + intStr(created.files))
if !noMCP {
if added, err := ensureCsddMCPServer(root); err != nil {
render.Warn("could not register csdd MCP server: " + err.Error())
} else if added {
render.Info("registered the csdd MCP server (drive the dev flow as tools) in " + workspace.Relative(root, paths.MCP(root)))
}
}
offerGitignore(root)
render.Info("Enable the pre-push test gate: `git config core.hooksPath .githooks`")
if !withBaseline {
Expand Down Expand Up @@ -78,6 +87,34 @@ func offerGitignore(root string) {
}
}

// csddMCPServerName is the key under which `csdd init` registers the csdd MCP
// server in .mcp.json.
const csddMCPServerName = "csdd"

// ensureCsddMCPServer registers the csdd MCP server (a stdio server launched via
// npx) in .mcp.json, unless an entry of that name already exists. The npm
// package resolves the matching prebuilt csdd binary through its
// optionalDependencies, so the entry is portable across machines — no absolute
// paths. Idempotent; returns whether a new entry was written.
func ensureCsddMCPServer(root string) (bool, error) {
path := paths.MCP(root)
cfg, err := loadMCP(path)
if err != nil {
return false, err
}
if _, exists := cfg.MCPServers[csddMCPServerName]; exists {
return false, nil
}
cfg.MCPServers[csddMCPServerName] = MCPServer{
Command: "npx",
Args: []string{"-y", "@protonspy/csdd-mcp"},
}
if err := saveMCP(path, cfg); err != nil {
return false, err
}
return true, nil
}

type initCounts struct {
dirs, files int
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ func TestMCPListAndShow(t *testing.T) {

func TestMCPListEmpty(t *testing.T) {
dir := freshWorkspace(t)
// init registers the csdd server by default; remove it to exercise the empty state.
_, _, _ = run(t, "mcp", "remove", csddMCPServerName, "--force", "--root", dir)
code, out, _ := run(t, "mcp", "list", "--root", dir)
if code != 0 || !strings.Contains(out, "no mcp servers") {
t.Errorf("empty list should report none:\n%s", out)
Expand Down
24 changes: 23 additions & 1 deletion internal/templater/templates/root/CLAUDE.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,34 @@

This repository follows a Spec-Driven Development (SDD) + Test-Driven
Development (TDD) workflow, native to **Claude Code**. Steering, specs, skills,
and custom sub-agents are managed by the `csdd` CLI.
and custom sub-agents are managed by the `csdd` CLI — or, preferably, its MCP
tools (see *Driving csdd* below).

**Read [`csdd.md`](./csdd.md) before producing any artifact.** It is the
operational guide that tells you how to drive `csdd` without reading source.
Everything below is a quick map; `csdd.md` is the contract.

## Driving csdd — prefer the MCP tools for the dev flow

`csdd init` registers a **`csdd` MCP server** in `.mcp.json`. When it is
connected, drive the development flow with its **`csdd_*` tools** instead of
shelling out:

| Resource | Tools | CLI fallback |
|---|---|---|
| steering | `csdd_steering_*` | `npx @protonspy/csdd steering …` |
| spec | `csdd_spec_*` | `npx @protonspy/csdd spec …` |
| skill | `csdd_skill_*` | `npx @protonspy/csdd skill …` |
| agent | `csdd_agent_*` | `npx @protonspy/csdd agent …` |

Why prefer the tools: typed parameters (the `artifact`, `phase`, and `inclusion`
enums, required fields) stop you from passing invalid values, and the server
builds the exact argv — more precise than a hand-written command. The **phase
gates, validators, and exit codes are identical** either way.

Use the **CLI** when the MCP server isn't connected, and always for **setup and
management** — `init`, `mcp`, and `export` are intentionally *not* tools.

## Project memory (steering)

Steering files are always-on project memory, loaded via the `@`-imports below.
Expand Down
12 changes: 12 additions & 0 deletions internal/templater/templates/root/csdd.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ The CLI enforces this mechanically. `npx @protonspy/csdd spec generate <feature>

## 2. Cheat sheet — every command

> **MCP tools (preferred when available).** If the `csdd` MCP server is connected
> (it is registered automatically by `csdd init`), call the development-flow
> commands as **tools** — `csdd_steering_*`, `csdd_spec_*`, `csdd_skill_*`,
> `csdd_agent_*` — instead of shelling out. Typed parameters keep the arguments
> valid and the server builds the argv, so the call is more precise than a
> hand-written command. **Same phase gates, same validation, same exit codes.**
> The commands below are the equivalent CLI surface and the source of truth for
> each command's semantics. **Setup and management — `init`, `mcp`, `export` —
> are CLI-only and are *not* exposed as tools.**

Run `npx @protonspy/csdd --help` for the top-level surface. The subcommands you will actually use:

```bash
Expand Down Expand Up @@ -395,6 +405,8 @@ npx @protonspy/csdd agent create security-reviewer \

Model Context Protocol servers give the agent extra tools (filesystem access, issue trackers, search, internal APIs…). They are declared in `.mcp.json` and managed exclusively with `npx @protonspy/csdd mcp` — **do not hand-edit the JSON**; the CLI keeps it well-formed and reviewable.

> `csdd init` registers one server out of the box: **`csdd`** (stdio, `npx -y @protonspy/csdd-mcp`), which exposes the development-flow commands as the `csdd_*` tools described in §2. Pass `csdd init --no-mcp` to skip it. Manage it like any other server with `npx @protonspy/csdd mcp` (this command surface is **not** itself a tool).

### Two transports

| Transport | Use | Required flags |
Expand Down
3 changes: 3 additions & 0 deletions mcp-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
*.log
6 changes: 6 additions & 0 deletions mcp-server/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
src/
test/
tsconfig.json
node_modules/
*.log
.gitignore
Loading
Loading