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

Why this exists ​

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.

Design 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

How it works ​

Draft and approve a feature spec (draft-feature-from-discussion) ​

The spec-first loop the write tools exist for: the human and agent discuss a feature, the agent drafts and shows the markdown, and the human reviews before anything is written.

  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. Review — the markdown preview the agent shows — approve or request changes
  6. Write — if approved, agent calls write_feature to persist the .usm file

Update status pipeline (update-feature-status) ​

System pipeline behind update_feature_status: transition enforcement and atomic persistence.

  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 fields pipeline (update-feature-fields) ​

System pipeline behind update_feature: merge provided fields, validate, persist.

  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

Guarantees ​

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)

write-returns-docs-url ​

Write and update tools return the canonical docs link so agents don't guess ports or URL shapes (issue #34)

Acceptance criteria:

  • [ ] write_feature, update_feature and update_feature_status include docs_path on success
  • [ ] docs_url is a full localhost URL when a docs server port is recorded
  • [ ] docs_hint explains how to get a live URL when none is running

Test specifications ​

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