refactor(mcp): rewrite external MCP tool descriptions for trigger clarity
Every tool description now leads with what the tool does AND when to reach for it, since MCP clients rank tools by that text. Per-parameter prose moved onto the parameters as [Description], exhaustive result-shape enumerations and design/history rationale dropped, and the repeated boilerplate clauses (lean-task-ref, batch cap, refused-while-Running) pulled into McpToolDocs, which also documents the style for future tools. Tool-level description text: 20494 -> 13605 chars (-34%); combined with the new parameter descriptions 18517 (-10%). Closes gaps that caused wrong calls rather than just verbose ones: - list_task_attachments returns metadata only, no file content - run_task_now shares continue_task's single override slot and throws when busy - list_runs is ordered oldest-first and feeds get_run - workingDir on create_list/update_list is an existing local git repo path, unvalidated until the first task run - get_task_worktree's behind=0 also means the main ref was unreachable Removes get_task_status_values: a whole tool entry for static reference text. GetTask's description is now the canonical place for status meanings.
This commit is contained in:
@@ -32,7 +32,14 @@ session internals, or app-settings writes. Auth via an optional `X-ClaudeDo-Key`
|
|||||||
`ListTasksResult`/`BatchGetTaskResult`, where exactly one of the lean (`TaskRefDto`) and
|
`ListTasksResult`/`BatchGetTaskResult`, where exactly one of the lean (`TaskRefDto`) and
|
||||||
full (`TaskDto`, incl. Description/Result) fields is populated per the flag — keeps a
|
full (`TaskDto`, incl. Description/Result) fields is populated per the flag — keeps a
|
||||||
list of verbosely-described tasks from blowing past the response size limit by default.
|
list of verbosely-described tasks from blowing past the response size limit by default.
|
||||||
3. `ExternalMcpExceptionFilter.Wrap` is registered as a call-tool filter so
|
3. **Description style is documented in `McpToolDocs`** (same folder) and shared boilerplate
|
||||||
|
lives there as `const` strings. Rules: the first sentence says what the tool does *and* when
|
||||||
|
to reach for it (MCP clients rank tools by that text, so the trigger must not sit behind
|
||||||
|
return-shape prose); parameters are documented with `[Description]` **on the parameter**, not
|
||||||
|
in the tool description; result fields appear only where the caller must branch on them
|
||||||
|
before calling (`isEmpty`, `truncated`, `conflicts`, `available`); no design rationale or
|
||||||
|
"since this feature was introduced" history. Not test-enforced — review it in PRs.
|
||||||
|
4. `ExternalMcpExceptionFilter.Wrap` is registered as a call-tool filter so
|
||||||
`InvalidOperationException` / `ArgumentException` messages survive as `McpException` —
|
`InvalidOperationException` / `ArgumentException` messages survive as `McpException` —
|
||||||
otherwise the SDK's catch-all replaces any non-`McpException` with a generic
|
otherwise the SDK's catch-all replaces any non-`McpException` with a generic
|
||||||
*"An error occurred invoking 'X'."*
|
*"An error occurred invoking 'X'."*
|
||||||
@@ -42,8 +49,9 @@ session internals, or app-settings writes. Auth via an optional `X-ClaudeDo-Key`
|
|||||||
### `ExternalMcpService` — task CRUD, execution, git
|
### `ExternalMcpService` — task CRUD, execution, git
|
||||||
|
|
||||||
Task: `ListTaskLists`, `ListTasks`, `GetTask`, `AddTask`, `AddSubtask`, `UpdateTask`,
|
Task: `ListTaskLists`, `ListTasks`, `GetTask`, `AddTask`, `AddSubtask`, `UpdateTask`,
|
||||||
`UpdateTaskStatus`, `GetTaskStatusValues`, `ReviewTask`, `RunTaskNow`, `ContinueTask`,
|
`UpdateTaskStatus`, `ReviewTask`, `RunTaskNow`, `ContinueTask`, `CancelTask`, `DeleteTask`.
|
||||||
`CancelTask`, `DeleteTask`.
|
(`GetTaskStatusValues` was removed — a whole tool entry for static reference text. `GetTask`'s
|
||||||
|
description is now the canonical place for what each status means.)
|
||||||
|
|
||||||
Worktree/git: `GetTaskWorktree`, `GetTaskDiff`, `MergeTask`, `ContinueMerge`, `AbortMerge`,
|
Worktree/git: `GetTaskWorktree`, `GetTaskDiff`, `MergeTask`, `ContinueMerge`, `AbortMerge`,
|
||||||
`PreviewMerge`, `PreviewMergeSet`, `RevertMerge`, `ListWorktrees`, `CleanupTaskWorktree`.
|
`PreviewMerge`, `PreviewMergeSet`, `RevertMerge`, `ListWorktrees`, `CleanupTaskWorktree`.
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ subfolder within their area; the namespace stays the area namespace.
|
|||||||
- **RunCancellationRegistry** — taskId → running-run CTS. Lets `TaskStateService.CancelAsync` kill a cancelled task's process without a DI cycle.
|
- **RunCancellationRegistry** — taskId → running-run CTS. Lets `TaskStateService.CancelAsync` kill a cancelled task's process without a DI cycle.
|
||||||
- **OverrideSlotService** — owns `RunNow` / `ContinueTask`; goes through `TaskStateService.StartRunningAsync` (caller-driven, serialized by slot lock).
|
- **OverrideSlotService** — owns `RunNow` / `ContinueTask`; goes through `TaskStateService.StartRunningAsync` (caller-driven, serialized by slot lock).
|
||||||
- **StaleTaskRecovery** — startup-only; calls `TaskStateService.RecoverStaleRunningAsync` to flip orphaned `Running` rows to `Failed`.
|
- **StaleTaskRecovery** — startup-only; calls `TaskStateService.RecoverStaleRunningAsync` to flip orphaned `Running` rows to `Failed`.
|
||||||
- **External/*** — always-on MCP tools for general Claude sessions, scoped to *starting* and *observing* sessions (no multi-turn, planning internals, or app-settings writes). Auth via optional `X-ClaudeDo-Key`. **Two hard conventions** (both test-enforced): every optional parameter needs a C# default value, and no tool returns bare `Task`/a nullable payload. Full tool inventory + per-tool behaviour → [external-mcp](../../docs/explore-notes/external-mcp.md).
|
- **External/*** — always-on MCP tools for general Claude sessions, scoped to *starting* and *observing* sessions (no multi-turn, planning internals, or app-settings writes). Auth via optional `X-ClaudeDo-Key`. **Two hard conventions** (both test-enforced): every optional parameter needs a C# default value, and no tool returns bare `Task`/a nullable payload. **Tool-description style** (not test-enforced) is documented in `External/McpToolDocs.cs`, which also holds the shared boilerplate clauses — read it before adding or editing a tool description. Full tool inventory + per-tool behaviour → [external-mcp](../../docs/explore-notes/external-mcp.md).
|
||||||
|
|
||||||
## Status Model
|
## Status Model
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ public sealed class AgentMcpTools
|
|||||||
|
|
||||||
public AgentMcpTools(AgentFileService agents) => _agents = agents;
|
public AgentMcpTools(AgentFileService agents) => _agents = agents;
|
||||||
|
|
||||||
[McpServerTool, Description("List available agent definition files (name, description, path) for use as a task's agent path.")]
|
[McpServerTool, Description("List available agent definition files (name, description, path) to pick a value for a task's or list's agentPath override.")]
|
||||||
public async Task<IReadOnlyList<AgentInfo>> ListAgents(CancellationToken cancellationToken)
|
public async Task<IReadOnlyList<AgentInfo>> ListAgents(CancellationToken cancellationToken)
|
||||||
=> await _agents.ScanAsync(cancellationToken);
|
=> await _agents.ScanAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ public sealed class AppSettingsMcpTools
|
|||||||
|
|
||||||
public AppSettingsMcpTools(IDbContextFactory<ClaudeDoDbContext> dbFactory) => _dbFactory = dbFactory;
|
public AppSettingsMcpTools(IDbContextFactory<ClaudeDoDbContext> dbFactory) => _dbFactory = dbFactory;
|
||||||
|
|
||||||
[McpServerTool, Description("Read the worker's app-level defaults (model, max turns, permission mode, max parallel execution slots, worktree strategy). Read-only.")]
|
[McpServerTool, Description("Read the worker's global defaults (model, max turns, permission mode, max parallel execution slots, worktree strategy) that apply when a task/list doesn't override them. Read-only.")]
|
||||||
public async Task<AppSettingsReadDto> GetAppSettings(CancellationToken cancellationToken)
|
public async Task<AppSettingsReadDto> GetAppSettings(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|||||||
+11
-12
@@ -33,17 +33,15 @@ public sealed class AttachmentMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Attach a read-only reference file to a task. These files are handed to the agent at run time, " +
|
"Attach a read-only reference file to a task so the agent receives it at run time — use to prepare " +
|
||||||
"making them useful to prepare context for a task that will run later (e.g. plans, scripts, specs). " +
|
"context (plans, scripts, specs) for a task that will run later. Exactly one of textContent/" +
|
||||||
"Pass textContent for plain-text files (plans, markdown, scripts). " +
|
"base64Content is required. Re-attaching the same fileName overwrites the previous version." +
|
||||||
"Pass base64Content only for binary files (images, archives). Exactly one of the two must be provided. " +
|
McpToolDocs.NotWhileRunning)]
|
||||||
"Re-attaching a file with the same fileName overwrites the previous version. " +
|
|
||||||
"Refuses if the task is currently Running — cancel it first.")]
|
|
||||||
public async Task<AttachmentDto> AddTaskAttachment(
|
public async Task<AttachmentDto> AddTaskAttachment(
|
||||||
string taskId,
|
string taskId,
|
||||||
string fileName,
|
[Description("Name to store the attachment under; reusing an existing name overwrites it.")] string fileName,
|
||||||
string? textContent = null,
|
[Description("Plain-text content (plans, markdown, scripts). Provide this or base64Content, not both.")] string? textContent = null,
|
||||||
string? base64Content = null,
|
[Description("Base64-encoded content for binary files (images, archives). Provide this or textContent, not both.")] string? base64Content = null,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var task = await _tasks.GetByIdAsync(taskId, ct)
|
var task = await _tasks.GetByIdAsync(taskId, ct)
|
||||||
@@ -94,7 +92,8 @@ public sealed class AttachmentMcpTools
|
|||||||
return new AttachmentDto(fileName, byteSize, existing?.CreatedAt ?? DateTime.UtcNow);
|
return new AttachmentDto(fileName, byteSize, existing?.CreatedAt ?? DateTime.UtcNow);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("List all attachments on a task (fileName, byteSize, createdAt).")]
|
[McpServerTool, Description(
|
||||||
|
"List all attachments on a task — use to check what reference files are already attached before adding more.")]
|
||||||
public async Task<IReadOnlyList<AttachmentDto>> ListTaskAttachments(
|
public async Task<IReadOnlyList<AttachmentDto>> ListTaskAttachments(
|
||||||
string taskId, CancellationToken ct = default)
|
string taskId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -103,8 +102,8 @@ public sealed class AttachmentMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Remove a single attachment from a task. Deletes both the file on disk and the database record. " +
|
"Remove a single attachment from a task, deleting both the file on disk and its database record." +
|
||||||
"Refuses if the task is currently Running — cancel it first. Returns { removed: true, taskId, fileName } on success.")]
|
McpToolDocs.NotWhileRunning)]
|
||||||
public async Task<RemoveAttachmentResult> RemoveTaskAttachment(
|
public async Task<RemoveAttachmentResult> RemoveTaskAttachment(
|
||||||
string taskId, string fileName, CancellationToken ct = default)
|
string taskId, string fileName, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
|||||||
+39
-35
@@ -3,8 +3,14 @@ using ModelContextProtocol.Server;
|
|||||||
|
|
||||||
namespace ClaudeDo.Worker.External;
|
namespace ClaudeDo.Worker.External;
|
||||||
|
|
||||||
public sealed record BatchAddTaskInput(string Title, string? Description = null, string? Model = null);
|
public sealed record BatchAddTaskInput(
|
||||||
public sealed record BatchSetMyDayInput(string TaskId, bool IsMyDay, int? SortOrder = null);
|
string Title,
|
||||||
|
[property: Description("Task description/instructions for the agent.")] string? Description = null,
|
||||||
|
[property: Description("Model override: haiku|sonnet|opus. Blank inherits the list/global default.")] string? Model = null);
|
||||||
|
public sealed record BatchSetMyDayInput(
|
||||||
|
string TaskId,
|
||||||
|
[property: Description("true to add the task to My Day, false to remove it.")] bool IsMyDay,
|
||||||
|
[property: Description("Position within My Day; omit to append at the end.")] int? SortOrder = null);
|
||||||
|
|
||||||
// task is populated when found and includeDescription=false (the default, lean reference);
|
// task is populated when found and includeDescription=false (the default, lean reference);
|
||||||
// taskFull is populated when found and includeDescription=true (full task incl.
|
// taskFull is populated when found and includeDescription=true (full task incl.
|
||||||
@@ -34,14 +40,14 @@ public sealed class BatchMcpTools
|
|||||||
public BatchMcpTools(ExternalMcpService svc) => _svc = svc;
|
public BatchMcpTools(ExternalMcpService svc) => _svc = svc;
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Fetch a snapshot of many tasks in one call (overview / polling a fan-out). " +
|
"Fetch a snapshot of many tasks in one call — use for an overview or polling a fan-out instead of " +
|
||||||
"Returns one result per id: { id, found, task, taskFull, error }. " +
|
"calling get_task per id. A missing id comes back as found=false, not an error; error is only set " +
|
||||||
"includeDescription=false (default): found tasks come back in `task` (lean reference, no " +
|
"for an unexpected failure." + McpToolDocs.MaxBatch)]
|
||||||
"Description/Result). includeDescription=true: found tasks come back in `taskFull` (incl. " +
|
|
||||||
"Description/Result) instead. A missing id is found=false (not an error; task and taskFull both null); " +
|
|
||||||
"error is only set for an unexpected failure. Max 100 ids.")]
|
|
||||||
public async Task<IReadOnlyList<BatchGetTaskResult>> BatchGetTasks(
|
public async Task<IReadOnlyList<BatchGetTaskResult>> BatchGetTasks(
|
||||||
string[] taskIds, bool includeDescription = false, CancellationToken cancellationToken = default)
|
string[] taskIds,
|
||||||
|
[Description("If true, return the full task (incl. Description/Result) in `taskFull`; if false " +
|
||||||
|
"(default), return a lean reference in `task`.")] bool includeDescription = false,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
EnsureWithinCap(taskIds, nameof(taskIds));
|
EnsureWithinCap(taskIds, nameof(taskIds));
|
||||||
|
|
||||||
@@ -75,19 +81,15 @@ public sealed class BatchMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Create many tasks in one list at once. Each item: { title, description?, model? } " +
|
"Create many tasks in one list at once — use instead of repeated add_task calls when seeding a list. " +
|
||||||
"(model: haiku|sonnet|opus, blank = inherit list/global default). " +
|
"Every item is still created even if it looks like a duplicate; possibleDuplicates is a non-blocking " +
|
||||||
"queueImmediately enqueues every created task. " +
|
"heads-up (up to 3 similar open tasks in the list) worth mentioning to the caller, not an error." +
|
||||||
"Returns one result per item: { index, title, ok, task, possibleDuplicates, error }; task is " +
|
McpToolDocs.LeanTaskRef + McpToolDocs.MaxBatch)]
|
||||||
"a lean reference (id, listId, title, status, sortOrder, isMyDay), not the description you just " +
|
|
||||||
"sent. Each item is always created — possibleDuplicates is a non-blocking heads-up (up to 3 open " +
|
|
||||||
"tasks in the same list with a strongly overlapping title, id/title/status only); check it and " +
|
|
||||||
"mention any hit to the caller, but do not treat it as an error. Max 100 items.")]
|
|
||||||
public async Task<IReadOnlyList<BatchAddTaskResult>> BatchAddTasks(
|
public async Task<IReadOnlyList<BatchAddTaskResult>> BatchAddTasks(
|
||||||
string listId,
|
string listId,
|
||||||
BatchAddTaskInput[] tasks,
|
BatchAddTaskInput[] tasks,
|
||||||
string? createdBy = null,
|
string? createdBy = null,
|
||||||
bool queueImmediately = false,
|
[Description("If true, enqueue every created task immediately instead of leaving it Idle.")] bool queueImmediately = false,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
EnsureWithinCap(tasks, nameof(tasks));
|
EnsureWithinCap(tasks, nameof(tasks));
|
||||||
@@ -113,11 +115,13 @@ public sealed class BatchMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Set the status of many tasks at once. status is 'Idle', 'Queued', 'Cancelled' or 'Done' only — " +
|
"Set the status of many tasks at once — use for bulk queue/cancel/done actions instead of calling " +
|
||||||
"same rule as update_task_status ('Done' is refused per-item for a task with an active worktree). " +
|
"update_task_status per task. 'Done' is refused per-item for a task with an active worktree." +
|
||||||
"Returns one result per id: { taskId, ok, error }. Max 100 ids.")]
|
McpToolDocs.MaxBatch)]
|
||||||
public async Task<IReadOnlyList<BatchTaskResult>> BatchUpdateTaskStatus(
|
public async Task<IReadOnlyList<BatchTaskResult>> BatchUpdateTaskStatus(
|
||||||
string[] taskIds, string status, CancellationToken cancellationToken)
|
string[] taskIds,
|
||||||
|
[Description("One of 'Idle', 'Queued', 'Cancelled', or 'Done'.")] string status,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
EnsureWithinCap(taskIds, nameof(taskIds));
|
EnsureWithinCap(taskIds, nameof(taskIds));
|
||||||
return await RunPerTaskAsync(taskIds,
|
return await RunPerTaskAsync(taskIds,
|
||||||
@@ -125,9 +129,8 @@ public sealed class BatchMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Cancel many running tasks at once. Returns one result per id: " +
|
"Cancel many running tasks at once — use to bulk-stop tasks instead of calling cancel_task per id. " +
|
||||||
"{ taskId, ok, cancelled, error }. cancelled=false means the task was not running. " +
|
"ok=true with cancelled=false just means the task wasn't running." + McpToolDocs.MaxBatch)]
|
||||||
"Max 100 ids.")]
|
|
||||||
public async Task<IReadOnlyList<BatchCancelResult>> BatchCancelTasks(
|
public async Task<IReadOnlyList<BatchCancelResult>> BatchCancelTasks(
|
||||||
string[] taskIds, CancellationToken cancellationToken)
|
string[] taskIds, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -151,8 +154,8 @@ public sealed class BatchMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Delete many tasks at once. A Running task is refused (cancel it first) and reported " +
|
"Delete many tasks at once — use for bulk cleanup instead of calling delete_task per id." +
|
||||||
"as ok=false with its error. Returns one result per id: { taskId, ok, error }. Max 100 ids.")]
|
McpToolDocs.NotWhileRunning + McpToolDocs.MaxBatch)]
|
||||||
public async Task<IReadOnlyList<BatchTaskResult>> BatchDeleteTasks(
|
public async Task<IReadOnlyList<BatchTaskResult>> BatchDeleteTasks(
|
||||||
string[] taskIds, CancellationToken cancellationToken)
|
string[] taskIds, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -162,9 +165,9 @@ public sealed class BatchMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Daily prep: set/clear MyDay for many tasks at once. Each item: { taskId, isMyDay, sortOrder? }. " +
|
"Set or clear MyDay (daily prep) for many tasks at once — use instead of calling set_my_day per task. " +
|
||||||
"Still cap-guarded — items that would exceed DailyPrepMaxTasks open MyDay tasks fail individually " +
|
"Still cap-guarded: items that would exceed DailyPrepMaxTasks open MyDay tasks fail individually " +
|
||||||
"(ok=false) without blocking the rest. Returns one result per item: { taskId, ok, error }. Max 100 items.")]
|
"(ok=false) without blocking the rest." + McpToolDocs.MaxBatch)]
|
||||||
public async Task<IReadOnlyList<BatchTaskResult>> BatchSetMyDay(
|
public async Task<IReadOnlyList<BatchTaskResult>> BatchSetMyDay(
|
||||||
BatchSetMyDayInput[] items, CancellationToken cancellationToken)
|
BatchSetMyDayInput[] items, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -188,12 +191,13 @@ public sealed class BatchMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Remove the worktrees of many tasks at once (directory + git branch). " +
|
"Remove the worktrees (directory + git branch) of many tasks at once — use for bulk cleanup instead " +
|
||||||
"force=false refuses a dirty or Running worktree (reported ok=false); force=true removes " +
|
"of calling cleanup_task_worktree per id." + McpToolDocs.NotWhileRunning + McpToolDocs.MaxBatch)]
|
||||||
"even a dirty worktree (uncommitted changes lost), still refusing Running tasks. " +
|
|
||||||
"Returns one result per id: { taskId, ok, removed, branchDeleted, error }. Max 100 ids.")]
|
|
||||||
public async Task<IReadOnlyList<BatchCleanupResult>> BatchCleanupTaskWorktrees(
|
public async Task<IReadOnlyList<BatchCleanupResult>> BatchCleanupTaskWorktrees(
|
||||||
string[] taskIds, bool force = false, CancellationToken cancellationToken = default)
|
string[] taskIds,
|
||||||
|
[Description("If true, also remove a dirty worktree, losing uncommitted changes; a Running task " +
|
||||||
|
"is still refused either way.")] bool force = false,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
EnsureWithinCap(taskIds, nameof(taskIds));
|
EnsureWithinCap(taskIds, nameof(taskIds));
|
||||||
|
|
||||||
|
|||||||
+11
-13
@@ -46,7 +46,7 @@ public sealed class ConfigMcpTools
|
|||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Get a list's default config (model, system prompt, agent path). Returns { found: false, config: null } if no config is set.")]
|
[McpServerTool, Description("Read a list's default run config — the fallback used by tasks in this list that don't set their own overrides. Returns { found: false, config: null } if none is set.")]
|
||||||
public async Task<TaskConfigResult> GetListConfig(string listId, CancellationToken cancellationToken)
|
public async Task<TaskConfigResult> GetListConfig(string listId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var cfg = await _lists.GetConfigAsync(listId, cancellationToken);
|
var cfg = await _lists.GetConfigAsync(listId, cancellationToken);
|
||||||
@@ -56,9 +56,8 @@ public sealed class ConfigMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Set a list's default model/system prompt/agent path/max turns. Passing all four as null clears the list " +
|
"Set a list's default model/system prompt/agent path/max turns — the fallback for tasks in this list " +
|
||||||
"config. Returns { ok, listId, config } — config is null when the config was cleared, otherwise it echoes " +
|
"that don't override them. Passing all four as null clears the list config instead of setting one.")]
|
||||||
"the fields that were set (a field is null there if it was individually left unset/cleared).")]
|
|
||||||
public async Task<SetListConfigResult> SetListConfig(
|
public async Task<SetListConfigResult> SetListConfig(
|
||||||
string listId, string? model = null, string? systemPrompt = null, string? agentPath = null,
|
string listId, string? model = null, string? systemPrompt = null, string? agentPath = null,
|
||||||
int? maxTurns = null, CancellationToken cancellationToken = default)
|
int? maxTurns = null, CancellationToken cancellationToken = default)
|
||||||
@@ -90,9 +89,8 @@ public sealed class ConfigMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Set per-task config overrides (model/system prompt/agent path/max turns). Pass null for any field to " +
|
"Set per-task overrides for model/system prompt/agent path/max turns; these take precedence over the " +
|
||||||
"clear that override. Returns { ok, taskId, config } — config echoes the resulting overrides (a field is " +
|
"list's default config for this one task. Pass null for any field to clear that override.")]
|
||||||
"null there if it was cleared or never set).")]
|
|
||||||
public async Task<SetTaskConfigResult> SetTaskConfig(
|
public async Task<SetTaskConfigResult> SetTaskConfig(
|
||||||
string taskId, string? model = null, string? systemPrompt = null, string? agentPath = null,
|
string taskId, string? model = null, string? systemPrompt = null, string? agentPath = null,
|
||||||
int? maxTurns = null, CancellationToken cancellationToken = default)
|
int? maxTurns = null, CancellationToken cancellationToken = default)
|
||||||
@@ -109,7 +107,7 @@ public sealed class ConfigMcpTools
|
|||||||
return new SetTaskConfigResult(true, taskId, new TaskConfigDto(m, sp, ap, maxTurns));
|
return new SetTaskConfigResult(true, taskId, new TaskConfigDto(m, sp, ap, maxTurns));
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Get per-task config overrides (model/system prompt/agent path/max turns). Returns { found: false, config: null } if no override is set on this task.")]
|
[McpServerTool, Description("Read this task's per-task overrides (model/system prompt/agent path/max turns), which take precedence over the list's default config. Returns { found: false, config: null } if none is set.")]
|
||||||
public async Task<TaskConfigResult> GetTaskConfig(string taskId, CancellationToken cancellationToken)
|
public async Task<TaskConfigResult> GetTaskConfig(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
@@ -120,11 +118,11 @@ public sealed class ConfigMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Get the config a task will ACTUALLY run with — model, max turns, effort, permission mode, agent path, " +
|
"Report the config a task will ACTUALLY run with — model, max turns, effort, permission mode, agent " +
|
||||||
"whether a system prompt is set, and skill names — with each field's source (task/list/preset/global). " +
|
"path, whether a system prompt is set, and skill names — each tagged with its source " +
|
||||||
"Uses the exact same resolution TaskRunner runs with, so this never drifts from get_app_settings/" +
|
"(task/list/preset/global). Use this over get_task_config/get_app_settings when you need resolved " +
|
||||||
"get_task_config's raw, possibly-unused values. maxTurns also reports the raw requested value and " +
|
"values, not raw overrides. maxTurns also reports the raw requested value and whether it was clamped " +
|
||||||
"whether it was clamped to the global ceiling. Read-only, no side effects.")]
|
"to the global ceiling.")]
|
||||||
public async Task<EffectiveRunConfigDto> GetEffectiveRunConfig(string taskId, CancellationToken cancellationToken)
|
public async Task<EffectiveRunConfigDto> GetEffectiveRunConfig(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
|
|||||||
+134
-160
@@ -26,7 +26,6 @@ public sealed record CancelTaskResult(bool Cancelled, string Id);
|
|||||||
// review range (worktree ahead, or HandlerBaseCommit..HandlerHeadCommit for a worktree-less
|
// review range (worktree ahead, or HandlerBaseCommit..HandlerHeadCommit for a worktree-less
|
||||||
// child) contributed nothing, so a reviewer sees them before approving instead of after.
|
// child) contributed nothing, so a reviewer sees them before approving instead of after.
|
||||||
public sealed record ReviewTaskResult(TaskRefDto Task, string? MergeStatus, IReadOnlyList<string> MergeConflicts, string? MergeMessage, string? RepoPath = null, IReadOnlyList<TaskRefDto>? EmptyChildren = null);
|
public sealed record ReviewTaskResult(TaskRefDto Task, string? MergeStatus, IReadOnlyList<string> MergeConflicts, string? MergeMessage, string? RepoPath = null, IReadOnlyList<TaskRefDto>? EmptyChildren = null);
|
||||||
public sealed record StatusValueDto(string Status, string Meaning);
|
|
||||||
public sealed record RunTaskNowResult(bool Started, string TaskId);
|
public sealed record RunTaskNowResult(bool Started, string TaskId);
|
||||||
|
|
||||||
public sealed record TaskDto(
|
public sealed record TaskDto(
|
||||||
@@ -161,7 +160,8 @@ public sealed class ExternalMcpService
|
|||||||
_planningMerge = planningMerge;
|
_planningMerge = planningMerge;
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("List all task lists available in ClaudeDo.")]
|
[McpServerTool, Description(
|
||||||
|
"List all task lists available in ClaudeDo. Start here — every task tool needs a listId from this call.")]
|
||||||
public async Task<IReadOnlyList<TaskListDto>> ListTaskLists(CancellationToken cancellationToken)
|
public async Task<IReadOnlyList<TaskListDto>> ListTaskLists(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var lists = await _lists.GetAllAsync(cancellationToken);
|
var lists = await _lists.GetAllAsync(cancellationToken);
|
||||||
@@ -169,17 +169,17 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"List tasks in a given list. Optionally filter by creator (createdBy) and/or status. " +
|
"List the tasks in one list — the usual way to find a taskId. Optionally filter by creator and/or status.")]
|
||||||
"Valid status values: Idle, Queued, Running, WaitingForReview, WaitingForChildren, Done, Failed, Cancelled. " +
|
|
||||||
"includeDescription=false (default): returns lean task references in `tasks` (no Description/Result) — " +
|
|
||||||
"use this unless you actually need the description text, since a list of verbosely-described tasks can " +
|
|
||||||
"otherwise blow past the response size limit. " +
|
|
||||||
"includeDescription=true: returns full tasks (incl. Description/Result) in `tasksFull` instead; `tasks` is " +
|
|
||||||
"null in that case.")]
|
|
||||||
public async Task<ListTasksResult> ListTasks(
|
public async Task<ListTasksResult> ListTasks(
|
||||||
string listId,
|
string listId,
|
||||||
|
[Description("Only return tasks with this CreatedBy value.")]
|
||||||
string? createdBy = null,
|
string? createdBy = null,
|
||||||
|
[Description("Only return tasks in this status: Idle, Queued, Running, WaitingForReview, " +
|
||||||
|
"WaitingForChildren, Done, Failed or Cancelled.")]
|
||||||
string? status = null,
|
string? status = null,
|
||||||
|
[Description("false (default): lean references in `tasks`, no Description/Result — keep this unless you " +
|
||||||
|
"need the description text, since verbosely-described tasks can blow past the response size " +
|
||||||
|
"limit. true: full tasks in `tasksFull` instead (`tasks` is then null).")]
|
||||||
bool includeDescription = false,
|
bool includeDescription = false,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -206,10 +206,12 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Get a single task by id, including its current status and result. " +
|
"Get a single task by id, including its current status and result — the canonical reference for what a " +
|
||||||
"Status lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled. " +
|
"status means. Lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled. " +
|
||||||
"A successful run lands in WaitingForReview; use review_task to approve, reject, or cancel. " +
|
"A successful run lands in WaitingForReview; use review_task to approve, reject or cancel it. " +
|
||||||
"Done/Failed/Cancelled tasks can be reset to Idle for re-execution.")]
|
"Done/Failed/Cancelled tasks can be reset to Idle for re-execution. A Queued task with a blocker waits " +
|
||||||
|
"for its predecessor before the picker will claim it, and WaitingForChildren is a parent whose own work " +
|
||||||
|
"is done but whose children are still running.")]
|
||||||
public async Task<TaskDto> GetTask(string taskId, CancellationToken cancellationToken)
|
public async Task<TaskDto> GetTask(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
@@ -227,21 +229,19 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Create a new task in the given list. Set queueImmediately=true to enqueue it for agent execution. " +
|
"Create a new task in the given list. The task is always created — possibleDuplicates is a non-blocking " +
|
||||||
"Set model to the cheapest model that can do the task well — 'haiku' for trivial/mechanical work, " +
|
"heads-up (up to 3 open tasks in the same list with a strongly overlapping title); check it and mention " +
|
||||||
"'sonnet' for normal coding (the default), 'opus' only for complex or cross-cutting work. " +
|
"any hit to the caller, but do not treat it as an error." + McpToolDocs.LeanTaskRef)]
|
||||||
"Leave model null to inherit the list/global default. " +
|
|
||||||
"Returns { task, possibleDuplicates }: task is a lean reference (id, listId, title, status, " +
|
|
||||||
"sortOrder, isMyDay) — not the description you just sent. The task is always created — " +
|
|
||||||
"possibleDuplicates is a non-blocking heads-up (up to 3 open tasks in the same list with a " +
|
|
||||||
"strongly overlapping title, id/title/status only); check it and mention any hit to the caller, " +
|
|
||||||
"but do not treat it as an error.")]
|
|
||||||
public async Task<AddTaskResult> AddTask(
|
public async Task<AddTaskResult> AddTask(
|
||||||
string listId,
|
string listId,
|
||||||
string title,
|
string title,
|
||||||
string? description = null,
|
string? description = null,
|
||||||
string? createdBy = null,
|
string? createdBy = null,
|
||||||
|
[Description("true: enqueue the task for agent execution right away.")]
|
||||||
bool queueImmediately = false,
|
bool queueImmediately = false,
|
||||||
|
[Description("Cheapest model that can do the task well: 'haiku' for trivial/mechanical work, 'sonnet' " +
|
||||||
|
"for normal coding, 'opus' only for complex or cross-cutting work. null inherits the " +
|
||||||
|
"list/global default (normally sonnet).")]
|
||||||
string? model = null,
|
string? model = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -354,9 +354,8 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Update an existing task's title, description, and/or commit type. Pass null to leave a field unchanged. " +
|
"Update an existing task's title, description, and/or commit type. Pass null to leave a field unchanged." +
|
||||||
"Refuses if the task is currently Running. Returns a lean task reference (id, listId, title, status, " +
|
McpToolDocs.NotWhileRunning + McpToolDocs.LeanTaskRef)]
|
||||||
"sortOrder, isMyDay) — not the description you just sent.")]
|
|
||||||
public async Task<TaskRefDto> UpdateTask(
|
public async Task<TaskRefDto> UpdateTask(
|
||||||
string taskId,
|
string taskId,
|
||||||
string? title = null,
|
string? title = null,
|
||||||
@@ -380,12 +379,12 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Append a subtask (step) to a task. orderNum defaults to the end. " +
|
"Append a subtask (step) to a task. Subtasks are surfaced to the agent at run time and shown in the " +
|
||||||
"Refuses if the task is currently Running. Subtasks are surfaced to the agent at run time and shown in the task's Steps list. " +
|
"task's Steps list." + McpToolDocs.NotWhileRunning + McpToolDocs.LeanTaskRef)]
|
||||||
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
|
||||||
public async Task<TaskRefDto> AddSubtask(
|
public async Task<TaskRefDto> AddSubtask(
|
||||||
string taskId,
|
string taskId,
|
||||||
string title,
|
string title,
|
||||||
|
[Description("Position among the existing steps; defaults to the end.")]
|
||||||
int? orderNum = null,
|
int? orderNum = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -419,16 +418,14 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Update a task's status. Only 'Idle', 'Queued', 'Cancelled' and 'Done' are permitted externally — " +
|
"Move a task between the statuses a caller may set directly. Use run_task_now for execution control and " +
|
||||||
"use run_task_now for execution control, and review_task to act on a WaitingForReview task. " +
|
"review_task to act on a WaitingForReview task — neither is reachable from here." + McpToolDocs.LeanTaskRef)]
|
||||||
"Settable: Idle (reset to editable), Queued (enqueue for execution), " +
|
|
||||||
"Cancelled (retire the task without deleting it; it can be reset to Idle later), " +
|
|
||||||
"Done (mark complete; refused if the task has an active worktree — use review_task to approve " +
|
|
||||||
"and merge that worktree instead). " +
|
|
||||||
"Full lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled. " +
|
|
||||||
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
|
||||||
public async Task<TaskRefDto> UpdateTaskStatus(
|
public async Task<TaskRefDto> UpdateTaskStatus(
|
||||||
string taskId,
|
string taskId,
|
||||||
|
[Description("'Idle' (reset to editable), 'Queued' (enqueue for execution), 'Cancelled' (retire without " +
|
||||||
|
"deleting; can be reset to Idle later) or 'Done' (mark complete; refused if the task has an " +
|
||||||
|
"active worktree — use review_task to approve and merge that worktree instead). No other " +
|
||||||
|
"value is settable externally.")]
|
||||||
string status,
|
string status,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -482,27 +479,29 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Review a task that is WaitingForReview. " +
|
"Act on a task that is WaitingForReview — the only way to approve, reject or retire a reviewed run. " +
|
||||||
"decision='approve' → review+merge, exactly like the UI's Approve: a childless task merges its worktree into " +
|
"'approve' is review+merge, exactly like the UI's Approve: a childless task merges its worktree into " +
|
||||||
"targetBranch (default: the repo's current branch) then goes Done; a task with children drives the unit merge " +
|
"targetBranch then goes Done; a task with children drives the unit merge (parent worktree if active + each " +
|
||||||
"(parent worktree if active + each Done child in order); a task without an active worktree approves straight to Done. " +
|
"Done child in order); a task without an active worktree approves straight to Done. Fails if the task is " +
|
||||||
"mergeStatus 'conflict' means the merge stopped on conflicts (files listed) — by default the merge is cleanly " +
|
"not WaitingForReview (except 'cancel', which also works while Running/Queued). mergeStatus 'conflict' " +
|
||||||
"aborted and you resolve in the ClaudeDo UI; pass leaveConflictsInTree=true to instead leave the conflict " +
|
"means the merge stopped on conflicts, with the files listed. emptyChildren (parent approve only) lists " +
|
||||||
"markers in the working tree (repoPath in the result) so you can resolve them and call continue_merge, " +
|
"the Done children about to be unit-merged whose own review range contributed nothing (e.g. a child that " +
|
||||||
"or abort_merge to cancel. " +
|
"reported CLAUDEDO_BLOCKED and committed no code) — check it before trusting that every child actually " +
|
||||||
"decision='reject_rerun' → Queued and re-runs, resuming the agent's session with your feedback as the next turn (feedback is required). " +
|
"delivered something." + McpToolDocs.LeanTaskRef)]
|
||||||
"decision='reject_park' → Idle for manual editing (feedback ignored). " +
|
|
||||||
"decision='cancel' → Cancelled. " +
|
|
||||||
"Fails if the task is not currently WaitingForReview (except cancel, which also works while Running/Queued). " +
|
|
||||||
"The result's task field is a lean reference (id, listId, title, status, sortOrder, isMyDay), not the task's description. " +
|
|
||||||
"emptyChildren (parent approve only) lists the Done children about to be unit-merged whose own review range " +
|
|
||||||
"contributed nothing (e.g. a child that reported CLAUDEDO_BLOCKED and committed no code) — check it before " +
|
|
||||||
"trusting that every child actually delivered something.")]
|
|
||||||
public async Task<ReviewTaskResult> ReviewTask(
|
public async Task<ReviewTaskResult> ReviewTask(
|
||||||
string taskId,
|
string taskId,
|
||||||
|
[Description("'approve', 'reject_rerun', 'reject_park' or 'cancel'.")]
|
||||||
string decision,
|
string decision,
|
||||||
|
[Description("Rejection comment. Required for 'reject_rerun', where the task goes Queued and re-runs with " +
|
||||||
|
"this text as the next turn of the agent's resumed session; ignored for 'reject_park', which " +
|
||||||
|
"just returns the task to Idle for manual editing.")]
|
||||||
string? feedback = null,
|
string? feedback = null,
|
||||||
|
[Description("Branch an approve merges into; defaults to the repo's current branch.")]
|
||||||
string? targetBranch = null,
|
string? targetBranch = null,
|
||||||
|
[Description("What an approve does when the merge hits conflicts. false (default): abort cleanly, leaving " +
|
||||||
|
"no half-merged state, and you resolve in the ClaudeDo UI. true: leave the conflict markers " +
|
||||||
|
"in the working tree (repoPath in the result) so you can resolve them and call continue_merge, " +
|
||||||
|
"or abort_merge to cancel.")]
|
||||||
bool leaveConflictsInTree = false,
|
bool leaveConflictsInTree = false,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -634,7 +633,10 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Immediately run a task in the override execution slot (bypasses the agent queue). Returns { started: true, taskId } on success.")]
|
[McpServerTool, Description(
|
||||||
|
"Run a task immediately in the override execution slot, bypassing the agent queue. That slot is single-" +
|
||||||
|
"occupancy and shared with continue_task — throws \"Override slot busy\" if something else holds it; " +
|
||||||
|
"enqueue via update_task_status instead of retrying in a loop.")]
|
||||||
public async Task<RunTaskNowResult> RunTaskNow(string taskId, CancellationToken cancellationToken)
|
public async Task<RunTaskNowResult> RunTaskNow(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -653,7 +655,8 @@ public sealed class ExternalMcpService
|
|||||||
return new RunTaskNowResult(true, taskId);
|
return new RunTaskNowResult(true, taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Cancel a running task. Returns { cancelled: true, id } if the task was running and cancellation was requested; cancelled is false if the task was not running.")]
|
[McpServerTool, Description(
|
||||||
|
"Cancel a running task, killing its agent process. cancelled=false means the task was not running.")]
|
||||||
public async Task<CancelTaskResult> CancelTask(string taskId, CancellationToken cancellationToken)
|
public async Task<CancelTaskResult> CancelTask(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var cancelled = _queue.CancelTask(taskId);
|
var cancelled = _queue.CancelTask(taskId);
|
||||||
@@ -661,7 +664,9 @@ public sealed class ExternalMcpService
|
|||||||
return new CancelTaskResult(cancelled, taskId);
|
return new CancelTaskResult(cancelled, taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Delete a task. Returns { deleted: true, id } on success. Throws if the task is not found or is currently Running — cancel it first.")]
|
[McpServerTool, Description(
|
||||||
|
"Delete a task permanently. Prefer update_task_status 'Cancelled' to retire a task you may want back." +
|
||||||
|
McpToolDocs.NotWhileRunning)]
|
||||||
public async Task<DeleteTaskResult> DeleteTask(string taskId, CancellationToken cancellationToken)
|
public async Task<DeleteTaskResult> DeleteTask(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
@@ -676,31 +681,12 @@ public sealed class ExternalMcpService
|
|||||||
return new DeleteTaskResult(true, taskId);
|
return new DeleteTaskResult(true, taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Status reference ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
[McpServerTool, Description("Returns all valid task status values and their meanings. Use before filtering by status or interpreting task state.")]
|
|
||||||
public Task<IReadOnlyList<StatusValueDto>> GetTaskStatusValues() =>
|
|
||||||
Task.FromResult<IReadOnlyList<StatusValueDto>>([
|
|
||||||
new("Idle", "Not yet queued; task is editable and will not run until enqueued."),
|
|
||||||
new("Queued", "Waiting for an agent execution slot. Tasks with a blocker (BlockedByTaskId) are skipped by the queue picker until their predecessor finishes."),
|
|
||||||
new("Running", "Agent is actively executing the task; cannot be edited or deleted until cancelled."),
|
|
||||||
new("WaitingForReview", "Run finished successfully and awaits review. Use review_task: approve (→ Done), reject_rerun (→ Queued, resumes the session with feedback), reject_park (→ Idle), or cancel (→ Cancelled)."),
|
|
||||||
new("WaitingForChildren", "Planning parent whose child tasks are still running. The parent resumes once all children reach a terminal state."),
|
|
||||||
new("Done", "Completed successfully and approved; result text is available in the result field. Can be reset to Idle for re-execution."),
|
|
||||||
new("Failed", "Execution ended with an error; task can be reset to Idle or re-queued directly."),
|
|
||||||
new("Cancelled", "Cancelled by the user; task can be reset to Idle or re-queued directly."),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// ── Worktree / git tools ──────────────────────────────────────────────────
|
// ── Worktree / git tools ──────────────────────────────────────────────────
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Get git worktree details for a task: path, branch, headCommit (current HEAD SHA), " +
|
"Get a task's git worktree state — path, branch, base/head commit, ahead/behind counts, isDirty, and the " +
|
||||||
"baseCommit (SHA where the branch was created), ahead (commits on branch since base), " +
|
"mergeCommit its branch produced once merged. behind is 0 when the 'main' ref is unreachable, so do not " +
|
||||||
"behind (commits on main not yet on this branch; 0 if 'main' ref is unreachable), " +
|
"read 0 as \"up to date\" without checking. A null mergeCommit means revert_merge cannot act on this task. " +
|
||||||
"isDirty (has uncommitted changes in the worktree directory), " +
|
|
||||||
"mergeCommit (SHA of the merge commit this worktree's branch produced on the target branch, " +
|
|
||||||
"if it has been merged and that succeeded after this field was introduced; null otherwise — " +
|
|
||||||
"required by revert_merge). " +
|
|
||||||
"Throws if the task or its worktree does not exist.")]
|
"Throws if the task or its worktree does not exist.")]
|
||||||
public async Task<WorktreeInfoDto> GetTaskWorktree(string taskId, CancellationToken cancellationToken)
|
public async Task<WorktreeInfoDto> GetTaskWorktree(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -718,16 +704,17 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Get the diff for a task's worktree relative to its base commit. For a worktree-less " +
|
"Read what a task actually changed — the diff of its worktree against its base commit (for a worktree-less " +
|
||||||
"list-handler host task (Mission Control's \"Let Claude handle it\"), returns the fixed " +
|
"list-handler host task, the fixed HandlerBaseCommit..HandlerHeadCommit range over the list's working dir " +
|
||||||
"HandlerBaseCommit..HandlerHeadCommit range over the list's working dir instead. " +
|
"instead). files lists the changed paths in either mode; truncated=true means the diff was capped and " +
|
||||||
"stat=false (default): returns the full unified diff, capped at 200 KB (truncated=true when larger). " +
|
"totalBytes holds its real size. Throws if the task has no worktree/review range, or the relevant " +
|
||||||
"stat=true: returns a --stat summary (changed files with insertion/deletion counts). " +
|
"directory is missing from disk.")]
|
||||||
"files always lists the changed file paths regardless of stat mode. " +
|
|
||||||
"totalBytes is the uncapped diff size (useful when truncated=true). " +
|
|
||||||
"Throws if the task has no worktree/review range, or the relevant directory is missing from disk.")]
|
|
||||||
public async Task<TaskDiffDto> GetTaskDiff(
|
public async Task<TaskDiffDto> GetTaskDiff(
|
||||||
string taskId, bool stat = false, CancellationToken cancellationToken = default)
|
string taskId,
|
||||||
|
[Description("false (default): the full unified diff, capped at 200 KB. true: a --stat summary with " +
|
||||||
|
"per-file insertion/deletion counts — start here when the diff may be large.")]
|
||||||
|
bool stat = false,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var (repoPath, baseCommit, headCommit) = await LoadDiffRangeAsync(taskId, cancellationToken);
|
var (repoPath, baseCommit, headCommit) = await LoadDiffRangeAsync(taskId, cancellationToken);
|
||||||
|
|
||||||
@@ -782,21 +769,21 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Merge a task's worktree branch into targetBranch (default: main). " +
|
"Merge a Done task's worktree branch into targetBranch. For a task still in WaitingForReview prefer " +
|
||||||
"noFf=true (default): always creates a merge commit (--no-ff). " +
|
"review_task, which merges as part of approving. merged=true carries the new mergeCommit SHA; on conflict " +
|
||||||
"dryRun=true: validates preconditions only, does not perform the merge; merged=false in the result means 'not actually merged'. " +
|
"merged=false and conflicts lists the affected files.")]
|
||||||
"allowWaitingForReview=true: also allows merging a task in WaitingForReview (default false, which only allows Done). " +
|
|
||||||
"On success: merged=true, mergeCommit contains the new merge commit SHA. " +
|
|
||||||
"On conflict: by default the merge is cleanly aborted (no half-merged state left); merged=false and conflicts lists the affected files. " +
|
|
||||||
"leaveConflictsInTree=true: on conflict the merge is NOT aborted — conflict markers are left in the working " +
|
|
||||||
"tree at repoPath (conflictsInTree=true in the result) so you can resolve them there and call continue_merge, " +
|
|
||||||
"or abort_merge to cancel.")]
|
|
||||||
public async Task<MergeTaskResultDto> MergeTask(
|
public async Task<MergeTaskResultDto> MergeTask(
|
||||||
string taskId,
|
string taskId,
|
||||||
string targetBranch = "main",
|
string targetBranch = "main",
|
||||||
|
[Description("true (default): always create a merge commit (--no-ff).")]
|
||||||
bool noFf = true,
|
bool noFf = true,
|
||||||
|
[Description("true: validate preconditions only and do not merge — merged=false then means \"not attempted\".")]
|
||||||
bool dryRun = false,
|
bool dryRun = false,
|
||||||
|
[Description("true: also allow merging a task in WaitingForReview; false (default) allows Done only.")]
|
||||||
bool allowWaitingForReview = false,
|
bool allowWaitingForReview = false,
|
||||||
|
[Description("What to do on conflict. false (default): abort cleanly, leaving no half-merged state. true: " +
|
||||||
|
"leave the conflict markers in the working tree at repoPath (conflictsInTree=true) so you can " +
|
||||||
|
"resolve them there and call continue_merge, or abort_merge to cancel.")]
|
||||||
bool leaveConflictsInTree = false,
|
bool leaveConflictsInTree = false,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -848,11 +835,9 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Finish an in-progress conflicted merge after the conflict markers in the working tree (repoPath from " +
|
"Finish an in-progress conflicted merge once you have resolved the conflict markers in the working tree " +
|
||||||
"merge_task/review_task) have been resolved. Handles both a single task's merge and a parent/children unit " +
|
"(repoPath from merge_task/review_task). Pass the PARENT task id to continue a parent/children unit merge. " +
|
||||||
"merge — pass the PARENT task id to continue a unit merge. On success merged=true and the task reaches its " +
|
"merged=false with conflicts listed means markers are still present — resolve them and call again. " +
|
||||||
"post-merge status (Done when approving). If conflict markers are still present, merged=false and conflicts " +
|
|
||||||
"lists the affected files — resolve them and call continue_merge again. " +
|
|
||||||
"Throws if there is no in-progress merge for the task; use abort_merge to cancel a paused merge instead.")]
|
"Throws if there is no in-progress merge for the task; use abort_merge to cancel a paused merge instead.")]
|
||||||
public async Task<MergeContinuationResultDto> ContinueMerge(string taskId, CancellationToken cancellationToken)
|
public async Task<MergeContinuationResultDto> ContinueMerge(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -920,10 +905,8 @@ public sealed class ExternalMcpService
|
|||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Abort an in-progress conflicted merge, discarding the conflict markers and restoring a clean working tree. " +
|
"Abort an in-progress conflicted merge, discarding the conflict markers and restoring a clean working tree. " +
|
||||||
"Handles both a single task's merge and a parent/children unit merge — pass the PARENT task id to abort a " +
|
"Pass the PARENT task id to abort a parent/children unit merge. The task keeps its pre-merge status " +
|
||||||
"unit merge. The task keeps its pre-merge status (e.g. WaitingForReview). " +
|
"(e.g. WaitingForReview). Throws if there is no in-progress merge for the task." + McpToolDocs.LeanTaskRef)]
|
||||||
"Throws if there is no in-progress merge for the task. " +
|
|
||||||
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
|
||||||
public async Task<TaskRefDto> AbortMerge(string taskId, CancellationToken cancellationToken)
|
public async Task<TaskRefDto> AbortMerge(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
@@ -945,21 +928,17 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Non-destructive merge preview for a task's worktree branch against targetBranch (default: the repo's " +
|
"Check whether a task would merge cleanly before touching anything — `git merge-tree --write-tree`, so the " +
|
||||||
"current branch), via `git merge-tree --write-tree` — does NOT touch the working tree, index, or HEAD. " +
|
"working tree, index and HEAD are untouched. status is 'clean' or 'conflict' (conflictFiles lists where git " +
|
||||||
"status: 'clean' (mergeable; changedFileCount is the size of that merge) or 'conflict' (conflictFiles " +
|
"would stop); behind counts commits on targetBranch not yet on this branch, which flags a stale branch even " +
|
||||||
"lists the paths git would stop on). behind = commits on targetBranch not yet on this task's branch, so " +
|
"when the preview is clean. IMPORTANT: a clean preview says nothing about whether the result compiles or " +
|
||||||
"you can spot a stale branch even when the preview itself is clean. " +
|
"passes tests — git can merge two changes cleanly (one file deleting a symbol another still references) and " +
|
||||||
"IMPORTANT: a clean preview says nothing about whether the merged result compiles or passes tests — git " +
|
"still break the build. isEmpty=true means the task's review range contributed nothing; check that flag " +
|
||||||
"can merge two changes cleanly (e.g. one file deletes a symbol another file still references) and still " +
|
"rather than reading a small changedFileCount as empty. Throws if the task has neither an active worktree " +
|
||||||
"break the build. " +
|
"nor a handler commit range, or the list's working directory is missing from disk.")]
|
||||||
"isEmpty=true means the task's review range contributed nothing (no commits ahead of base, or — for a " +
|
|
||||||
"worktree-less list-handler host task — HandlerBaseCommit == HandlerHeadCommit); do not mistake a small " +
|
|
||||||
"changedFileCount for an empty one, check isEmpty instead. " +
|
|
||||||
"Throws a clear error if the task has neither an active worktree nor a handler commit range, or the " +
|
|
||||||
"list's working directory is missing from disk.")]
|
|
||||||
public async Task<MergePreviewToolDto> PreviewMerge(
|
public async Task<MergePreviewToolDto> PreviewMerge(
|
||||||
string taskId,
|
string taskId,
|
||||||
|
[Description("Branch to preview against; defaults to the repo's current branch.")]
|
||||||
string? targetBranch = null,
|
string? targetBranch = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -968,18 +947,16 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Merge preview plus file-overlap check across several tasks at once, all previewed against the same " +
|
"Plan a batch merge: preview_merge for several tasks against the same targetBranch, plus a file-overlap " +
|
||||||
"targetBranch (default: the repo's current branch). For each taskId returns the same fields as " +
|
"check between them. Per entry you get preview_merge's fields, or error instead when that task could not " +
|
||||||
"preview_merge (status/conflictFiles/changedFileCount/behind; error is set instead if that task could not " +
|
"be previewed (it is then left out of the overlap computation). overlaps names, for each file touched by " +
|
||||||
"be previewed, and it is then excluded from the overlap computation). overlaps lists, for each file " +
|
"MORE THAN ONE of the given tasks, which tasks touch it — a single taskId always yields no overlaps. " +
|
||||||
"touched by MORE THAN ONE of the given tasks (via each task's own diff, not the merge preview itself), " +
|
"IMPORTANT: overlap is a HINT and its absence is not safety — two tasks touching entirely different files " +
|
||||||
"which tasks touch it — passing a single taskId always yields an empty overlaps list. " +
|
"(one deleting a symbol, another still referencing it) can still collide unflagged, and as with " +
|
||||||
"IMPORTANT: file-name overlap is a HINT, not a guarantee of a real collision, and its absence is not a " +
|
"preview_merge a clean result does not mean the merge builds.")]
|
||||||
"guarantee of safety — two tasks touching different files entirely (e.g. one deletes a symbol, another " +
|
|
||||||
"still references it elsewhere) can still collide, and this tool will not flag that case. " +
|
|
||||||
"isEmpty=true (per entry) means that task's review range contributed nothing — see preview_merge.")]
|
|
||||||
public async Task<MergePreviewSetResultDto> PreviewMergeSet(
|
public async Task<MergePreviewSetResultDto> PreviewMergeSet(
|
||||||
IReadOnlyList<string> taskIds,
|
IReadOnlyList<string> taskIds,
|
||||||
|
[Description("Branch to preview every task against; defaults to the repo's current branch.")]
|
||||||
string? targetBranch = null,
|
string? targetBranch = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -1076,18 +1053,17 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Revert a previously merged task's merge commit on targetBranch (default: main), via `git revert -m 1` — " +
|
"Undo a merged task by reverting its merge commit — `git revert -m 1`, always a new commit and never a " +
|
||||||
"a new commit, never a reset/rewrite (the target working directory is shared with other sessions). " +
|
"reset/rewrite, since the target working directory is shared with other sessions. Requires the task to be " +
|
||||||
"Requires the task to be Done with a Merged worktree that has a recorded merge commit; tasks merged " +
|
"Done with a Merged worktree that has a recorded merge commit (check get_task_worktree's mergeCommit " +
|
||||||
"before this feature existed have no recorded commit and are refused rather than guessed via git log. " +
|
"first). On success the task returns to WaitingForReview so it can be reconsidered. On conflict the revert " +
|
||||||
"On success: reverted=true, revertCommit is the new commit's SHA, and the task returns to " +
|
"is aborted immediately and conflicts lists the files. Throws if there is no recorded merge commit, the " +
|
||||||
"WaitingForReview so it can be reconsidered. " +
|
"repo is mid-merge/mid-revert, or the target working tree has uncommitted changes from another session.")]
|
||||||
"On a conflicting revert: reverted=false, the revert is aborted immediately (no half-resolved state " +
|
|
||||||
"left in the tree) and conflicts lists the files that would have conflicted. " +
|
|
||||||
"Throws if there is no recorded merge commit, the repo is mid-merge/mid-revert, or the target working " +
|
|
||||||
"tree has uncommitted changes from another session.")]
|
|
||||||
public async Task<RevertMergeResultDto> RevertMerge(
|
public async Task<RevertMergeResultDto> RevertMerge(
|
||||||
string taskId, string targetBranch = "main", CancellationToken cancellationToken = default)
|
string taskId,
|
||||||
|
[Description("Branch carrying the merge commit; defaults to main.")]
|
||||||
|
string targetBranch = "main",
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var result = await _merge.RevertMergeAsync(taskId, targetBranch, cancellationToken);
|
var result = await _merge.RevertMergeAsync(taskId, targetBranch, cancellationToken);
|
||||||
|
|
||||||
@@ -1104,10 +1080,8 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"List all ClaudeDo-tracked worktrees. " +
|
"Survey every worktree ClaudeDo tracks — use it to find leftovers to clean up. Only worktrees recorded in " +
|
||||||
"Each entry: taskId, path, branch, headCommit (empty if path missing on disk), " +
|
"the ClaudeDo database appear here, and headCommit is empty when the path is missing from disk.")]
|
||||||
"isDirty (has uncommitted changes), mergedIntoMain (worktree state is Merged). " +
|
|
||||||
"Only worktrees recorded in the ClaudeDo database are returned.")]
|
|
||||||
public async Task<IReadOnlyList<WorktreeListItemDto>> ListWorktrees(CancellationToken cancellationToken)
|
public async Task<IReadOnlyList<WorktreeListItemDto>> ListWorktrees(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var rows = await _maintenance.GetOverviewAsync(null, cancellationToken);
|
var rows = await _maintenance.GetOverviewAsync(null, cancellationToken);
|
||||||
@@ -1125,12 +1099,14 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Remove a task's worktree directory and delete its git branch. " +
|
"Remove a task's worktree directory and delete its git branch. branchDeleted reports whether the branch " +
|
||||||
"force=false (default): refuses if the worktree has uncommitted changes or the task is Running. " +
|
"went too." + McpToolDocs.NotWhileRunning)]
|
||||||
"force=true: removes even a dirty worktree (uncommitted changes are lost); task must not be Running. " +
|
|
||||||
"Returns removed=true on success; branchDeleted reflects whether the branch was also removed.")]
|
|
||||||
public async Task<CleanupWorktreeResult> CleanupTaskWorktree(
|
public async Task<CleanupWorktreeResult> CleanupTaskWorktree(
|
||||||
string taskId, bool force = false, CancellationToken cancellationToken = default)
|
string taskId,
|
||||||
|
[Description("false (default): refuse a worktree with uncommitted changes. true: remove it anyway, losing " +
|
||||||
|
"those changes.")]
|
||||||
|
bool force = false,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var ctx = _dbFactory.CreateDbContext();
|
using var ctx = _dbFactory.CreateDbContext();
|
||||||
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, cancellationToken)
|
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, cancellationToken)
|
||||||
@@ -1155,10 +1131,9 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Send a follow-up prompt to an existing Claude session (multi-turn continuation). " +
|
"Send a follow-up prompt to a task's existing Claude session instead of starting a fresh run — the agent " +
|
||||||
"The agent resumes using --resume with the session ID from the task's last run. " +
|
"resumes via --resume with the session id from the task's last run, so its prior context is kept. Uses the " +
|
||||||
"Runs in the override execution slot; throws if the slot is busy — try again later. " +
|
"same single-occupancy override slot as run_task_now and throws \"Override slot busy\" when that is taken.")]
|
||||||
"Returns a status string from the execution slot.")]
|
|
||||||
public async Task<string> ContinueTask(
|
public async Task<string> ContinueTask(
|
||||||
string taskId,
|
string taskId,
|
||||||
string followUpPrompt,
|
string followUpPrompt,
|
||||||
@@ -1187,10 +1162,10 @@ public sealed class ExternalMcpService
|
|||||||
// ── Daily prep ───────────────────────────────────────────────────────────
|
// ── Daily prep ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Daily prep: returns the open tasks eligible for today's MyDay selection. " +
|
"Daily prep: the open tasks eligible for today's MyDay selection. candidates are Idle, unblocked, " +
|
||||||
"candidates = Idle, not blocked, in a git repo not excluded from the weekly report, and not already in MyDay. " +
|
"non-manual and in a git repo not excluded from the weekly report; currentMyDay are Idle tasks already " +
|
||||||
"currentMyDay = Idle tasks already flagged IsMyDay (count them toward the cap). " +
|
"flagged and count toward maxTasks, the hard cap on open MyDay tasks. Add your picks with set_my_day and " +
|
||||||
"maxTasks = the hard cap on total open MyDay tasks. Use set_my_day to add tasks (never exceed maxTasks).")]
|
"never exceed maxTasks.")]
|
||||||
public async Task<DailyPrepDataDto> GetDailyPrepCandidates(CancellationToken cancellationToken)
|
public async Task<DailyPrepDataDto> GetDailyPrepCandidates(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||||
@@ -1225,14 +1200,13 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Daily prep: set or clear a task's MyDay flag, optionally setting its sortOrder " +
|
"Daily prep: set or clear a task's MyDay flag. Setting it is rejected once the MyDay cap " +
|
||||||
"(use consecutive sortOrder values to keep related tasks together). " +
|
"(DailyPrepMaxTasks open MyDay tasks) would be exceeded; clearing is always allowed." +
|
||||||
"Setting isMyDay=true is rejected if it would exceed the MyDay cap (DailyPrepMaxTasks open MyDay tasks); " +
|
McpToolDocs.LeanTaskRef)]
|
||||||
"clearing (isMyDay=false) is always allowed. " +
|
|
||||||
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
|
||||||
public async Task<TaskRefDto> SetMyDay(
|
public async Task<TaskRefDto> SetMyDay(
|
||||||
string taskId,
|
string taskId,
|
||||||
bool isMyDay,
|
bool isMyDay,
|
||||||
|
[Description("Position in the MyDay list; use consecutive values to keep related tasks together.")]
|
||||||
int? sortOrder = null,
|
int? sortOrder = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
+8
-6
@@ -20,13 +20,15 @@ public sealed class HandoffMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"End of Phase 2 for the list handler (\"Let Claude handle it\"): hand this run off to a fresh " +
|
"Call at the end of Phase 2 of the list handler (\"Let Claude handle it\") to hand this run off " +
|
||||||
"ConPTY session that carries out Phases 3-5, without dragging along this session's dedupe/rewrite " +
|
"to a fresh ConPTY session that carries out Phases 3-5, without dragging along this session's " +
|
||||||
"context. taskId is this session's own handler task id; survivingTaskIds are the tasks that made " +
|
"dedupe/rewrite context. Reuses the SAME handler task -- no new task is created, and " +
|
||||||
"it past dedupe, in the order to run them. Reuses the SAME handler task -- no new task is created, " +
|
"HandlerBaseCommit is untouched. The current tile stays open; you must end your own turn " +
|
||||||
"and HandlerBaseCommit is untouched. The current tile stays open; end your own turn after calling this.")]
|
"immediately after calling this.")]
|
||||||
public async Task<HandoffListHandlerResult> HandoffListHandler(
|
public async Task<HandoffListHandlerResult> HandoffListHandler(
|
||||||
string taskId, IReadOnlyList<string> survivingTaskIds, CancellationToken cancellationToken)
|
[Description("This session's own handler task id.")] string taskId,
|
||||||
|
[Description("The tasks that made it past dedupe, in the order to run them.")] IReadOnlyList<string> survivingTaskIds,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (survivingTaskIds.Count == 0)
|
if (survivingTaskIds.Count == 0)
|
||||||
throw new InvalidOperationException("survivingTaskIds must contain at least one task id.");
|
throw new InvalidOperationException("survivingTaskIds must contain at least one task id.");
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ public sealed class LifecycleMcpTools
|
|||||||
_reset = reset;
|
_reset = reset;
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Reset a failed task: discards its worktree and returns it to Idle so it can be run again. Only Failed tasks are accepted. Returns { reset: true, taskId } on success.")]
|
[McpServerTool, Description("Reset a failed task back to Idle so it can be run again, discarding its now-stale worktree. Only tasks with Status=Failed are accepted; other statuses throw.")]
|
||||||
public async Task<ResetFailedTaskResult> ResetFailedTask(string taskId, CancellationToken cancellationToken)
|
public async Task<ResetFailedTaskResult> ResetFailedTask(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
|
|||||||
+15
-5
@@ -21,9 +21,14 @@ public sealed class ListMcpTools
|
|||||||
_broadcaster = broadcaster;
|
_broadcaster = broadcaster;
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Create a new task list. workingDir sets the git repo tasks run against; commitType defaults to 'chore'.")]
|
[McpServerTool, Description("Create a new task list — the top-level grouping tasks belong to, with its own working dir, commit type, and default run config.")]
|
||||||
public async Task<ListSummaryDto> CreateList(
|
public async Task<ListSummaryDto> CreateList(
|
||||||
string name, string? workingDir = null, string? commitType = null, CancellationToken cancellationToken = default)
|
string name,
|
||||||
|
[Description("Absolute local path to an existing git repository this list's tasks will run against. Not validated here — the first task run fails if the path isn't an actual git repo. Omit to run this list's tasks in a throwaway sandbox with no worktree.")]
|
||||||
|
string? workingDir = null,
|
||||||
|
[Description("Conventional-commit-style type prefix for this list's task commits (e.g. 'feat', 'fix'). Defaults to 'chore'.")]
|
||||||
|
string? commitType = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
throw new InvalidOperationException("name is required.");
|
throw new InvalidOperationException("name is required.");
|
||||||
@@ -41,9 +46,14 @@ public sealed class ListMcpTools
|
|||||||
return ToDto(entity);
|
return ToDto(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Rename a list and/or change its working dir and default commit type. Pass null to leave a field unchanged.")]
|
[McpServerTool, Description("Rename a list, or change its working dir / default commit type without recreating it. Pass null for any field to leave it unchanged.")]
|
||||||
public async Task<ListSummaryDto> UpdateList(
|
public async Task<ListSummaryDto> UpdateList(
|
||||||
string listId, string? name = null, string? workingDir = null, string? commitType = null,
|
string listId,
|
||||||
|
string? name = null,
|
||||||
|
[Description("Absolute local path to an existing git repository this list's tasks will run against; not validated until the next task runs. Null leaves it unchanged; pass an empty string to clear it and switch this list to sandbox-only task runs.")]
|
||||||
|
string? workingDir = null,
|
||||||
|
[Description("New default commit type prefix for this list's task commits. Null leaves it unchanged.")]
|
||||||
|
string? commitType = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var entity = await _lists.GetByIdAsync(listId, cancellationToken)
|
var entity = await _lists.GetByIdAsync(listId, cancellationToken)
|
||||||
@@ -62,7 +72,7 @@ public sealed class ListMcpTools
|
|||||||
return ToDto(entity);
|
return ToDto(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Delete a list and its tasks. Irreversible. Returns { deleted: true, id } on success.")]
|
[McpServerTool, Description("Permanently delete a list and all its tasks — no undo. Only for removing the whole list, not a single task within it.")]
|
||||||
public async Task<DeleteListResult> DeleteList(string listId, CancellationToken cancellationToken)
|
public async Task<DeleteListResult> DeleteList(string listId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_ = await _lists.GetByIdAsync(listId, cancellationToken)
|
_ = await _lists.GetByIdAsync(listId, cancellationToken)
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
namespace ClaudeDo.Worker.External;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Boilerplate clauses shared by several external MCP tool descriptions. Every tool description is
|
||||||
|
/// still emitted in full to the client — these constants only stop the wording from drifting apart
|
||||||
|
/// across ~50 attributes.
|
||||||
|
///
|
||||||
|
/// Description style (keep new tools in line with it):
|
||||||
|
/// 1. First sentence says what the tool does AND when to reach for it — MCP clients rank tools by
|
||||||
|
/// this text, so the trigger must not be buried behind return-shape prose.
|
||||||
|
/// 2. Then only non-obvious preconditions and refusals.
|
||||||
|
/// 3. Document parameters with [Description] on the parameter, not in the tool description.
|
||||||
|
/// 4. Describe result fields only where the caller must branch on them (isEmpty, truncated,
|
||||||
|
/// conflicts, …). Everything else is visible in the first actual response.
|
||||||
|
/// 5. No design rationale or "since this feature was introduced" history.
|
||||||
|
/// Budget: ~400 chars for a simple tool, ~800 for the merge/review family.
|
||||||
|
/// </summary>
|
||||||
|
internal static class McpToolDocs
|
||||||
|
{
|
||||||
|
/// <summary>Warns that the payload is the lean reference, not the task's description/result.</summary>
|
||||||
|
public const string LeanTaskRef = " Returns a lean task reference, not the task's description.";
|
||||||
|
|
||||||
|
/// <summary>Batch-size cap shared by every BatchMcpTools entry point.</summary>
|
||||||
|
public const string MaxBatch = " Max 100 per call.";
|
||||||
|
|
||||||
|
/// <summary>Mutations that refuse to touch a task while its agent is running.</summary>
|
||||||
|
public const string NotWhileRunning = " Refused while the task is Running — cancel it first.";
|
||||||
|
}
|
||||||
+6
-9
@@ -28,15 +28,12 @@ public sealed class QueueStateMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Read-only snapshot of the execution queue -- observe slot occupancy instead of inferring " +
|
"Read-only snapshot of the execution queue -- call this to observe slot occupancy instead " +
|
||||||
"it from maxParallelExecutions. Result: { configuredSlots, effectiveSlots, activeSlots: " +
|
"of inferring it from maxParallelExecutions. effectiveSlots is configuredSlots stepped down " +
|
||||||
"[{ slot, taskId, startedAt }], waitingTaskIds }. configuredSlots is Settings -> " +
|
"by the usage throttle (lower when the 5h/7d usage window fills up), so comparing the two " +
|
||||||
"MaxParallelExecutions; effectiveSlots is that value stepped down by the usage throttle " +
|
"shows whether throttling is currently active. Each active slot is \"queue\" (a normal " +
|
||||||
"(lower when the 5h/7d usage window is filling up) -- compare the two to see whether " +
|
"queue slot) or \"override\" (the single run_task_now/continue_task slot). waitingTaskIds " +
|
||||||
"throttling is currently active. activeSlots lists every task presently holding an " +
|
"lists queued, unblocked, non-manual, due tasks in the order the queue would pick them next.")]
|
||||||
"execution slot, with slot \"queue\" for a normal queue slot or \"override\" for the single " +
|
|
||||||
"run_task_now/continue_task slot. waitingTaskIds lists queued, unblocked, non-manual, due " +
|
|
||||||
"tasks in the order the queue would pick them next.")]
|
|
||||||
public async Task<GetQueueStateResult> GetQueueState(CancellationToken cancellationToken = default)
|
public async Task<GetQueueStateResult> GetQueueState(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var (configured, effective) = await _queue.GetSlotCountsAsync(cancellationToken);
|
var (configured, effective) = await _queue.GetSlotCountsAsync(cancellationToken);
|
||||||
|
|||||||
+11
-10
@@ -24,14 +24,17 @@ public sealed class RunHistoryMcpTools
|
|||||||
|
|
||||||
public RunHistoryMcpTools(TaskRunRepository runs) => _runs = runs;
|
public RunHistoryMcpTools(TaskRunRepository runs) => _runs = runs;
|
||||||
|
|
||||||
[McpServerTool, Description("List all execution runs for a task (newest run metadata, tokens, turns, result, error).")]
|
[McpServerTool, Description(
|
||||||
|
"List all execution runs for a task — metadata, tokens, turns, result, and error per run — ordered " +
|
||||||
|
"oldest to newest by run number, so the last entry is the most recent. Use a run's id from here with " +
|
||||||
|
"get_run to fetch it individually.")]
|
||||||
public async Task<IReadOnlyList<RunDto>> ListRuns(string taskId, CancellationToken cancellationToken)
|
public async Task<IReadOnlyList<RunDto>> ListRuns(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var runs = await _runs.GetByTaskIdAsync(taskId, cancellationToken);
|
var runs = await _runs.GetByTaskIdAsync(taskId, cancellationToken);
|
||||||
return runs.Select(ToDto).ToList();
|
return runs.Select(ToDto).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Get a single execution run by its run id.")]
|
[McpServerTool, Description("Get one execution run's full detail by its run id, obtained from list_runs.")]
|
||||||
public async Task<RunDto> GetRun(string runId, CancellationToken cancellationToken)
|
public async Task<RunDto> GetRun(string runId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var run = await _runs.GetByIdAsync(runId, cancellationToken)
|
var run = await _runs.GetByIdAsync(runId, cancellationToken)
|
||||||
@@ -40,18 +43,16 @@ public sealed class RunHistoryMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Fetch log entries from a task's latest run. " +
|
"Fetch NDJSON log lines from a task's latest run — use this to check progress or debug a task without " +
|
||||||
"Returns { available, entries, totalLines, truncated }. " +
|
"opening the log file. Defaults to the last 50 lines. available=false means no log exists yet (queued " +
|
||||||
"available=false means no log exists yet (task is queued or just started — not an error). " +
|
"or just started — not an error); truncated=true when fewer entries are returned than totalLines.")]
|
||||||
"entries are the individual lines (NDJSON messages) from Claude's streaming output. " +
|
|
||||||
"Default: returns the last 50 entries (tail=50). " +
|
|
||||||
"tail: override the number of trailing entries to return. " +
|
|
||||||
"offset+limit: return entries starting at position offset (0-based); overrides tail when provided. " +
|
|
||||||
"truncated=true when fewer entries are returned than totalLines.")]
|
|
||||||
public async Task<TaskLogResult> GetTaskLog(
|
public async Task<TaskLogResult> GetTaskLog(
|
||||||
string taskId,
|
string taskId,
|
||||||
|
[Description("Number of trailing entries to return; ignored if offset or limit is set. Default 50.")]
|
||||||
int? tail = null,
|
int? tail = null,
|
||||||
|
[Description("0-based entry index to start from; overrides tail when set. Combine with limit to page through the log.")]
|
||||||
int? offset = null,
|
int? offset = null,
|
||||||
|
[Description("Max entries to return starting at offset. Omit to return everything from offset to the end.")]
|
||||||
int? limit = null,
|
int? limit = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
+17
-12
@@ -29,19 +29,24 @@ public sealed class TaskWaitMcpTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Blocks until at least one of the given tasks leaves Queued/Running, or until timeoutSeconds elapses " +
|
"Blocks until at least one of the given tasks leaves Queued/Running -- use this instead of " +
|
||||||
"(clamped server-side to 900s). Returns immediately if any task is already outside Queued/Running " +
|
"polling get_task in a loop. Returns immediately if a task is already outside Queued/Running " +
|
||||||
"when called (an unknown id is reported as status \"NotFound\" and counts as changed). Use this instead " +
|
"(an unknown id reports status \"NotFound\" and counts as changed). Pitfall: a planning parent " +
|
||||||
"of polling get_task in a loop. Pitfall: a planning parent with children goes Running -> " +
|
"goes Running -> WaitingForChildren while its children are still working, so by default " +
|
||||||
"WaitingForChildren while its children are still working, and by default that counts as \"changed\" -- " +
|
"waiting on a parent returns early; see treatWaitingForChildrenAsBusy. Requires the calling " +
|
||||||
"so waiting on a parent returns immediately even though the work isn't done. Set " +
|
"claude process to run with MCP_TOOL_TIMEOUT >= 930000 (ms) for a long wait to actually be " +
|
||||||
"treatWaitingForChildrenAsBusy=true to keep waiting through WaitingForChildren; the call then only " +
|
"held open -- ClaudeDo's own launchers already set this.")]
|
||||||
"returns once the parent reaches WaitingForReview or a terminal status (default: false, unchanged " +
|
|
||||||
"legacy behavior). Requires the calling claude process to run with MCP_TOOL_TIMEOUT >= 930000 (ms) for " +
|
|
||||||
"a long wait to actually be held open -- ClaudeDo's own launchers already set this. " +
|
|
||||||
"Result: { changed: [{ taskId, status }], timedOut }.")]
|
|
||||||
public async Task<WaitForTaskChangeResult> WaitForTaskChange(
|
public async Task<WaitForTaskChangeResult> WaitForTaskChange(
|
||||||
string[] taskIds, int timeoutSeconds = 60, bool treatWaitingForChildrenAsBusy = false,
|
string[] taskIds,
|
||||||
|
[Description(
|
||||||
|
"How long to wait, in seconds, before giving up. Clamped server-side to 900s (15 min) " +
|
||||||
|
"regardless of what's passed.")]
|
||||||
|
int timeoutSeconds = 60,
|
||||||
|
[Description(
|
||||||
|
"When true, WaitingForChildren still counts as busy, so waiting on a planning parent " +
|
||||||
|
"continues until it reaches WaitingForReview or a terminal status instead of returning " +
|
||||||
|
"as soon as it leaves Running.")]
|
||||||
|
bool treatWaitingForChildrenAsBusy = false,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (taskIds.Length == 0)
|
if (taskIds.Length == 0)
|
||||||
|
|||||||
@@ -989,19 +989,6 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
|||||||
Assert.Equal(10, result.Config.MaxTurns);
|
Assert.Equal(10, result.Config.MaxTurns);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── GetTaskStatusValues ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetTaskStatusValues_ContainsAllStatuses()
|
|
||||||
{
|
|
||||||
var sut = NewService();
|
|
||||||
var values = await sut.GetTaskStatusValues();
|
|
||||||
var names = values.Select(v => v.Status).ToHashSet();
|
|
||||||
|
|
||||||
foreach (var status in Enum.GetValues<TaskStatus>())
|
|
||||||
Assert.Contains(status.ToString(), names);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── ListTasks status filter ───────────────────────────────────────────────
|
// ── ListTasks status filter ───────────────────────────────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user