usm/mcp-write [built]
MCP write tools — let agents author and update .usm feature specs as part of the spec-first workflow. Adds draft_feature, update_feature, and update_feature_status tools to the MCP server, closing the loop between human discussion and agent implementation.
Status: built
Intent
The current MCP server is read-only — agents can list, read, search, and query existing .usm files, but cannot create or update them. The spec-first workflow requires the agent to write a feature .usm spec from a human discussion, show the human the generated markdown for review, then update the feature after implementation. Without write tools, this loop is broken: the human must manually write YAML, which defeats the purpose of agent-first authoring.
Decisions
write-tools-as-mcp-not-cli [accepted]
Decision: Implement authoring as MCP tools rather than CLI commands only
Rationale: Agents work through MCP in Cursor, Claude Desktop, and other tools. A CLI command requires the agent to shell out, parse output, and handle errors externally. An MCP tool returns structured JSON the agent can reason about directly. CLI wrappers can be added later for human use.
Alternatives considered:
- CLI commands only (usm feature draft, usm feature update) — rejected: Agents must shell out, parse stdout, handle errors externally — MCP returns structured JSON directly
- File system writes only (agent writes YAML directly) — rejected: No validation, no markdown preview, no status transition enforcement — agents would produce invalid .usm files
Consequences: MCP server gains write capability — needs careful validation to prevent corruption
draft-returns-preview [accepted]
Decision: draft_feature returns both YAML and generated markdown
Rationale: The human reviews the markdown, not the YAML. Returning both lets the agent show the human the readable preview immediately without a separate generate step. The agent can also iterate on the draft based on feedback before writing to disk.
Consequences: draft_feature is heavier than a pure YAML generator, but the review step is the whole point
validate-before-write [accepted]
Decision: All write operations validate against the v1 schema before persisting
Rationale: An invalid .usm file breaks downstream generators and MCP read tools. Validation at write time prevents corruption. The tool returns structured validation errors so the agent can fix them before retrying.
Consequences: Write operations are slower due to validation, but prevent corrupt .usm files from entering the system
Flows
Draft a feature spec from human discussion (draft-feature-from-discussion)
Agent and human discuss a feature in chat. Agent calls draft_feature with structured fields derived from the discussion. Tool validates, generates YAML + markdown preview. Agent shows markdown to human for review.
- receive → structured feature fields (summary, intent, flows, contracts, tests)
- validate → fields against v1 JSON schema
- generate → YAML string and markdown preview
- return → structured response with yaml, markdown, validation_status
- observe → agent shows markdown to human, human approves or requests changes
- write → if approved, agent calls write_feature to persist the .usm file
Update feature status after implementation (update-feature-status)
After the agent implements the feature in code, it updates the .usm file to reflect the new status and implementation paths.
- get → feature $id or file path
- update → status field (e.g., planned → built)
- update → implementation.primary with code file path
- update → implementation.test_code and test_code_status if tests written
- validate → updated file against schema
- write → persisted .usm file with $last_updated bumped
Update arbitrary fields on an existing feature (update-feature-fields)
Agent partially updates a feature spec — e.g., adding a flow, revising a contract, or updating tests based on what was actually built.
- get → feature $id or file path
- merge → provided fields into existing feature object
- validate → merged result against schema
- write → persisted .usm file if valid, error if not
Contracts
draft-validates-before-returning
draft_feature must validate the spec before returning it
Acceptance criteria:
- [ ] Returns validation_status: valid or invalid
- [ ] If invalid, returns structured errors with field paths
- [ ] YAML is only generated if validation passes
write-is-atomic
write_feature must not leave partial/corrupt files on disk
Acceptance criteria:
- [ ] Validates full file before writing
- [ ] Writes to temp then renames (atomic on POSIX)
- [ ] Returns error if write fails — original file untouched
update-preserves-unspecified-fields
update_feature merges provided fields without losing existing ones
Acceptance criteria:
- [ ] Fields not in the update payload are preserved
- [ ] Array fields are replaced, not merged (explicit intent)
- [ ] $id, $type, $schema are immutable — cannot be changed via update
status-transitions-are-sane
update_feature_status enforces valid status transitions
Acceptance criteria:
- [ ] planned → in-progress → built is valid
- [ ] built → deprecated is valid
- [ ] built → planned is rejected (use a new feature instead)
Tests
draft-minimal-feature
Given:
- minimal_fields:
Then:
- assertion: validation_status is valid
- assertion: returned YAML contains all required fields
- assertion: returned markdown has summary and intent sections
draft-invalid-feature
Given:
- invalid_fields:
Then:
- assertion: validation_status is invalid
- assertion: errors array contains summary and intent validation messages
- assertion: no YAML or markdown in response
write-creates-new-file
Given:
- valid_feature_yaml: true
- target_path_does_not_exist: true
Then:
- assertion: file created at target path
- assertion: file content matches provided YAML
update-status-to-built
Given:
- existing_feature_status: "in-progress"
- update_payload: {"status":"built","implementation":{"primary":"src/features/my-feature.ts"}}
Then:
- assertion: status field is built
- assertion: implementation.primary is set
- assertion: $last_updated is bumped to today
- assertion: summary and intent preserved unchanged
update-status-rejects-backwards
Given:
- existing_feature_status: "built"
- update_payload:
Then:
- assertion: error returned with message about invalid transition
- assertion: original file unchanged
Implementation
- Primary: src/mcp/write.ts
- Test code status: none
See Also
- usm/mcp
- usm/mcp-validate
- usm/gen-feature-review