Files
ClaudeDo/docs/superpowers/specs/2026-04-25-external-mcp-crud-extensions-design.md
Mika Kuns 10b2ca817b docs(superpowers): add external MCP CRUD extensions spec and plan
Capture the design and execution plan for the AddTask/UpdateTask/DeleteTask/
SetTaskTags external MCP work that landed in commits 1a74e1c..59dc1e2.
2026-04-27 10:16:19 +02:00

8.7 KiB

External MCP — CRUD Extensions

Date: 2026-04-25 Status: Approved

Goal

Give a normal (non-planning) Claude CLI session full control over the ClaudeDo task inbox via the existing always-on ExternalMcpService. Primary use case: when a chat session produces scope-creep work, Claude can spin up a fully-formed task — title, description, tags (including the agent tag for auto-execution) — without leaving the session.

The work is purely additive: the ExternalMcpService endpoint is already wired, authenticated by the optional X-ClaudeDo-Key header, and exposes ListTaskLists, ListTasks, GetTask, AddTask, UpdateTaskStatus, RunTaskNow, CancelTask. Missing for "full CRUD" are tag handling, content updates, deletion, and tag discovery.

Scope

Tool Status Notes
ListTaskLists exists unchanged
ListTasks exists unchanged
GetTask exists unchanged
AddTask extend add optional tags parameter
UpdateTaskStatus exists unchanged (Manual ↔ Queued)
RunTaskNow exists unchanged
CancelTask exists unchanged
UpdateTask new patch title/description/commitType/tags
DeleteTask new delete a task (cascades)
SetTaskTags new replace the full tag set on a task
ListTags new enumerate all known tag names

Out of scope:

  • List CRUD (creating/renaming/deleting lists) — out of scope for this iteration; UI remains the source of truth for list management.
  • ListConfig / agent settings overrides — handled by the UI, not surfaced via MCP here.
  • Tag CRUD beyond auto-creation during AddTask / UpdateTask / SetTaskTags. There is no DeleteTag tool; tag rows live as long as some task references them.

Authentication

No change. The endpoint continues to be gated by ExternalMcpAuthMiddleware — if WorkerConfig.ExternalMcpApiKey is set, callers must include X-ClaudeDo-Key; otherwise the loopback-only worker is open to local processes.

Tool specifications

AddTask (extended)

AddTask(
    listId: string,
    title: string,
    description: string?,
    createdBy: string,
    queueImmediately: bool,
    tags: string[]?,
    cancellationToken)
    -> TaskDto

Behavior:

  • Existing behavior preserved. New tags parameter, when non-null, attaches the named tags to the new task.
  • Tag names are matched case-insensitively against existing rows; missing tag rows are auto-created (mirrors TaskRepository.CreateChildAsync).
  • Empty/whitespace tag names are skipped; duplicates are deduplicated.
  • tags is the LAST parameter before CancellationToken so existing positional callers are unaffected (CancellationToken is bound by name in MCP runtime; defensive — see Migration).

UpdateTask (new)

UpdateTask(
    taskId: string,
    title: string?,
    description: string?,
    commitType: string?,
    tags: string[]?,
    cancellationToken)
    -> TaskDto

Behavior:

  • Loads the task; throws InvalidOperationException if not found.
  • Refuses if status is Running — protects in-flight worktrees and the streaming log.
  • Does NOT change status (use UpdateTaskStatus) and does NOT change createdBy, listId, or parentTaskId (audit + structural fields, immutable here).
  • For each non-null parameter, applies the update. Null means "leave unchanged".
  • tags semantics: full replacement of the tag set (same as SetTaskTags). Auto-creates missing tag rows.
  • Broadcasts TaskUpdated on the SignalR hub on success.

DeleteTask (new)

DeleteTask(taskId: string, cancellationToken) -> void

Behavior:

  • Loads the task; throws if not found.
  • Refuses if status is Running — caller must CancelTask first.
  • Calls TaskRepository.DeleteAsync (FK cascades remove task_tags, worktrees, task_runs, subtasks).
  • Broadcasts TaskUpdated(taskId) so UIs drop the row.

SetTaskTags (new)

SetTaskTags(taskId: string, tags: string[], cancellationToken) -> TaskDto

Behavior:

  • Convenience wrapper for "I just want to (re)set tags". Equivalent to UpdateTask(taskId, null, null, null, tags).
  • Same validation: refuses if Running.
  • Returns the updated TaskDto (with status; tags are not included in TaskDto today — see Open Decisions).

ListTags (new)

ListTags(cancellationToken) -> { Id: long, Name: string }[]

Behavior:

  • Returns every row in the tags table. No filter, no pagination — the table is small (seed values + user-defined).
  • Lets Claude discover existing tag names (agent, manual, plus any user-defined) before tagging, avoiding duplicates that differ only by case/whitespace.

Repository changes

src/ClaudeDo.Data/Repositories/TaskRepository.cs:

  • Add public Task SetTagsAsync(string taskId, IReadOnlyList<string> tagNames, CancellationToken ct = default) — replaces the tag set, auto-creates missing rows. Implementation pattern matches the tag block already inside CreateChildAsync and the new UpdateChildAsync from the planning-MCP work; consider extracting a private helper ApplyTagsAsync(TaskEntity, IReadOnlyList<string>, CancellationToken) shared by both.

src/ClaudeDo.Data/Repositories/TagRepository.cs:

  • Add public Task<List<TagEntity>> GetAllAsync(CancellationToken ct = default) if it does not already exist. (Matches ListRepository.GetAllAsync style.)

No new tables, no migrations.

Service changes

src/ClaudeDo.Worker/External/ExternalMcpService.cs:

  • Add TagRepository to the constructor (DI registration is already in place since the planning service uses it).
  • Extend AddTask signature with IReadOnlyList<string>? tags and apply via the repository.
  • Add UpdateTask, DeleteTask, SetTaskTags, ListTags methods, each annotated [McpServerTool, Description("…")].
  • Each new mutating tool calls _broadcaster.TaskUpdated(taskId) on success (matches existing pattern in this file).

DI: ExternalMcpService is already registered. If TagRepository is not already registered (it is — used by ListRepository), no change. If a constructor parameter is added, Program.cs does not need changes because services are scoped/transient.

Error handling

All errors raised as InvalidOperationException with a human-readable message — matches the existing pattern in ExternalMcpService and PlanningMcpService. The MCP SDK serializes these to the JSON-RPC error channel; Claude sees the message text directly.

Specific cases:

  • Task not found → "Task {id} not found."
  • Running-task guard → "Cannot {update|delete} a running task. Cancel it first."
  • Unknown status (in UpdateTaskStatus, unchanged) → "Unknown status '{x}'."

Testing

Add tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs (or extend if it exists) with:

Test Asserts
AddTask_WithTags_AttachesTags tags param creates and attaches tag rows
AddTask_WithUnknownTag_AutoCreatesTagRow new tag name produces a row in tags table
UpdateTask_PatchesNonNullFields only non-null fields change
UpdateTask_OnRunning_Throws InvalidOperationException
UpdateTask_BroadcastsTaskUpdated hub broadcast received
UpdateTask_TagsReplaceFullSet passing tags=[…] replaces existing tags wholesale
DeleteTask_RemovesTaskAndTagJoins task and task_tags rows gone
DeleteTask_OnRunning_Throws InvalidOperationException
SetTaskTags_ReplacesAndBroadcasts replacement semantics + broadcast
ListTags_ReturnsSeedAndCustomTags agent + manual + any user-defined

Existing test infrastructure (DbFixture, FakeHubContext) is reused. No new fakes required.

Caveat: the test assembly currently fails to compile on main because of pre-existing in-progress work on PlanningChainCoordinator (missing constructor argument in WorkerHub/TaskRunner test instantiations). Tests will pass only after that work lands; do not block this design on it.

Open decisions (defaults chosen, easy to flip)

  1. TaskDto does not currently include tags. For consistency, the spec keeps TaskDto as-is and ships a separate ListTags tool. If preferred, we could add Tags: string[] to TaskDto so every tool response includes them — small DB cost (one extra SelectMany), one struct field added. Default: leave TaskDto alone, defer.
  2. Per-tag AddTaskTag / RemoveTaskTag micro-tools. Skipped — SetTaskTags covers the use case, and it's idempotent. Add later if granular ops are wanted.
  3. List CRUD via MCP. Out of scope. UI owns lists.

Migration / compatibility

AddTask gains an optional parameter. The MCP server SDK sends parameters by name in JSON-RPC params, so existing clients that omit tags continue to work without code changes. No version bump required.