Skip to content

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.

  1. receive → structured feature fields (summary, intent, flows, contracts, tests)
  2. validate → fields against v1 JSON schema
  3. generate → YAML string and markdown preview
  4. return → structured response with yaml, markdown, validation_status
  5. observe → agent shows markdown to human, human approves or requests changes
  6. 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.

  1. get → feature $id or file path
  2. update → status field (e.g., planned → built)
  3. update → implementation.primary with code file path
  4. update → implementation.test_code and test_code_status if tests written
  5. validate → updated file against schema
  6. 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.

  1. get → feature $id or file path
  2. merge → provided fields into existing feature object
  3. validate → merged result against schema
  4. 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