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:
mika kuns
2026-08-07 09:25:45 +02:00
parent 6e9a7cea87
commit c792765ed3
16 changed files with 295 additions and 282 deletions
+39 -35
View File
@@ -3,8 +3,14 @@ using ModelContextProtocol.Server;
namespace ClaudeDo.Worker.External;
public sealed record BatchAddTaskInput(string Title, string? Description = null, string? Model = null);
public sealed record BatchSetMyDayInput(string TaskId, bool IsMyDay, int? SortOrder = null);
public sealed record BatchAddTaskInput(
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);
// 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;
[McpServerTool, Description(
"Fetch a snapshot of many tasks in one call (overview / polling a fan-out). " +
"Returns one result per id: { id, found, task, taskFull, error }. " +
"includeDescription=false (default): found tasks come back in `task` (lean reference, no " +
"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.")]
"Fetch a snapshot of many tasks in one call — use for an overview or polling a fan-out instead of " +
"calling get_task per id. A missing id comes back as found=false, not an error; error is only set " +
"for an unexpected failure." + McpToolDocs.MaxBatch)]
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));
@@ -75,19 +81,15 @@ public sealed class BatchMcpTools
}
[McpServerTool, Description(
"Create many tasks in one list at once. Each item: { title, description?, model? } " +
"(model: haiku|sonnet|opus, blank = inherit list/global default). " +
"queueImmediately enqueues every created task. " +
"Returns one result per item: { index, title, ok, task, possibleDuplicates, error }; task is " +
"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.")]
"Create many tasks in one list at once — use instead of repeated add_task calls when seeding a list. " +
"Every item is still created even if it looks like a duplicate; possibleDuplicates is a non-blocking " +
"heads-up (up to 3 similar open tasks in the list) worth mentioning to the caller, not an error." +
McpToolDocs.LeanTaskRef + McpToolDocs.MaxBatch)]
public async Task<IReadOnlyList<BatchAddTaskResult>> BatchAddTasks(
string listId,
BatchAddTaskInput[] tasks,
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)
{
EnsureWithinCap(tasks, nameof(tasks));
@@ -113,11 +115,13 @@ public sealed class BatchMcpTools
}
[McpServerTool, Description(
"Set the status of many tasks at once. status is 'Idle', 'Queued', 'Cancelled' or 'Done' only — " +
"same rule as update_task_status ('Done' is refused per-item for a task with an active worktree). " +
"Returns one result per id: { taskId, ok, error }. Max 100 ids.")]
"Set the status of many tasks at once — use for bulk queue/cancel/done actions instead of calling " +
"update_task_status per task. 'Done' is refused per-item for a task with an active worktree." +
McpToolDocs.MaxBatch)]
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));
return await RunPerTaskAsync(taskIds,
@@ -125,9 +129,8 @@ public sealed class BatchMcpTools
}
[McpServerTool, Description(
"Cancel many running tasks at once. Returns one result per id: " +
"{ taskId, ok, cancelled, error }. cancelled=false means the task was not running. " +
"Max 100 ids.")]
"Cancel many running tasks at once — use to bulk-stop tasks instead of calling cancel_task per id. " +
"ok=true with cancelled=false just means the task wasn't running." + McpToolDocs.MaxBatch)]
public async Task<IReadOnlyList<BatchCancelResult>> BatchCancelTasks(
string[] taskIds, CancellationToken cancellationToken)
{
@@ -151,8 +154,8 @@ public sealed class BatchMcpTools
}
[McpServerTool, Description(
"Delete many tasks at once. A Running task is refused (cancel it first) and reported " +
"as ok=false with its error. Returns one result per id: { taskId, ok, error }. Max 100 ids.")]
"Delete many tasks at once — use for bulk cleanup instead of calling delete_task per id." +
McpToolDocs.NotWhileRunning + McpToolDocs.MaxBatch)]
public async Task<IReadOnlyList<BatchTaskResult>> BatchDeleteTasks(
string[] taskIds, CancellationToken cancellationToken)
{
@@ -162,9 +165,9 @@ public sealed class BatchMcpTools
}
[McpServerTool, Description(
"Daily prep: set/clear MyDay for many tasks at once. Each item: { taskId, isMyDay, sortOrder? }. " +
"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.")]
"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 " +
"(ok=false) without blocking the rest." + McpToolDocs.MaxBatch)]
public async Task<IReadOnlyList<BatchTaskResult>> BatchSetMyDay(
BatchSetMyDayInput[] items, CancellationToken cancellationToken)
{
@@ -188,12 +191,13 @@ public sealed class BatchMcpTools
}
[McpServerTool, Description(
"Remove the worktrees of many tasks at once (directory + git branch). " +
"force=false refuses a dirty or Running worktree (reported ok=false); force=true removes " +
"even a dirty worktree (uncommitted changes lost), still refusing Running tasks. " +
"Returns one result per id: { taskId, ok, removed, branchDeleted, error }. Max 100 ids.")]
"Remove the worktrees (directory + git branch) of many tasks at once — use for bulk cleanup instead " +
"of calling cleanup_task_worktree per id." + McpToolDocs.NotWhileRunning + McpToolDocs.MaxBatch)]
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));