using System.ComponentModel; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Hub; using ModelContextProtocol.Server; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Planning; public sealed record ChildTaskDto(string TaskId, string Title, string? Description, string Status, IReadOnlyList Tags); public sealed record CreatedChildDto(string TaskId, string Status); [McpServerToolType] public sealed class PlanningMcpService { private readonly TaskRepository _tasks; private readonly PlanningMcpContextAccessor _contextAccessor; private readonly HubBroadcaster _broadcaster; public PlanningMcpService( TaskRepository tasks, PlanningMcpContextAccessor contextAccessor, HubBroadcaster broadcaster) { _tasks = tasks; _contextAccessor = contextAccessor; _broadcaster = broadcaster; } private Task BroadcastTaskUpdatedAsync(string taskId, CancellationToken ct) => _broadcaster.TaskUpdated(taskId); [McpServerTool, Description("Create a new draft child task under the current planning session's parent task.")] public async Task CreateChildTask( string title, string? description, IReadOnlyList? tags, string? commitType, CancellationToken cancellationToken) { var ctx = _contextAccessor.Current; var child = await _tasks.CreateChildAsync(ctx.ParentTaskId, title, description, tags, commitType, cancellationToken); await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken); return new CreatedChildDto(child.Id, "Draft"); } [McpServerTool, Description("List all child tasks under the current planning session's parent task.")] public async Task> ListChildTasks( CancellationToken cancellationToken) { var ctx = _contextAccessor.Current; var children = await _tasks.GetChildrenAsync(ctx.ParentTaskId, cancellationToken); var list = new List(children.Count); foreach (var c in children) { var tagList = await _tasks.GetTagsAsync(c.Id, cancellationToken); list.Add(new ChildTaskDto(c.Id, c.Title, c.Description, c.Status.ToString(), tagList.Select(t => t.Name).ToList())); } return list; } [McpServerTool, Description("Update an existing draft child task. Only Draft tasks may be modified.")] public async Task UpdateChildTask( string taskId, string? title, string? description, IReadOnlyList? tags, string? commitType, CancellationToken cancellationToken) { var ctx = _contextAccessor.Current; var child = await _tasks.GetByIdAsync(taskId, cancellationToken) ?? throw new InvalidOperationException($"Task {taskId} not found."); if (child.ParentTaskId != ctx.ParentTaskId) throw new InvalidOperationException("Task is not a child of this planning session."); if (child.Status != TaskStatus.Draft) throw new InvalidOperationException("Cannot modify a finalized task."); if (title is not null) child.Title = title; if (description is not null) child.Description = description; if (commitType is not null) child.CommitType = commitType; await _tasks.UpdateAsync(child, cancellationToken); var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!; var tagList = await _tasks.GetTagsAsync(reload.Id, cancellationToken); await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken); return new ChildTaskDto(reload.Id, reload.Title, reload.Description, reload.Status.ToString(), tagList.Select(t => t.Name).ToList()); } [McpServerTool, Description("Delete a draft child task. Only Draft tasks may be deleted.")] public async Task DeleteChildTask( string taskId, CancellationToken cancellationToken) { var ctx = _contextAccessor.Current; var child = await _tasks.GetByIdAsync(taskId, cancellationToken) ?? throw new InvalidOperationException($"Task {taskId} not found."); if (child.ParentTaskId != ctx.ParentTaskId) throw new InvalidOperationException("Task is not a child of this planning session."); if (child.Status != TaskStatus.Draft) throw new InvalidOperationException("Cannot delete a finalized task."); await _tasks.DeleteAsync(taskId, cancellationToken); await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken); } [McpServerTool, Description("Update the title and/or description of the parent planning task itself.")] public async Task UpdatePlanningTask( string? title, string? description, CancellationToken cancellationToken) { var ctx = _contextAccessor.Current; await _tasks.UpdatePlanningTaskAsync(ctx.ParentTaskId, title, description, cancellationToken); await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken); } [McpServerTool, Description("Finalize the planning session, promoting all draft child tasks to queued or manual status.")] public async Task Finalize( bool queueAgentTasks, CancellationToken cancellationToken) { var ctx = _contextAccessor.Current; var count = await _tasks.FinalizePlanningAsync(ctx.ParentTaskId, queueAgentTasks, cancellationToken); await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken); return count; } }