From 0794c141167dbde8a8c6a22deb899d0b0bab2b79 Mon Sep 17 00:00:00 2001 From: Amolith Date: Sun, 3 Aug 2025 14:15:38 -0700 Subject: [PATCH] feat: add delete_tasks tool for removing tasks by ID Add new MCP tool to delete one or more tasks by their IDs and return the updated task list. Includes atomic validation to ensure all task IDs exist before deleting any tasks. --- AGENTS.md | 3 +- README.md | 3 +- internal/mcp/server.go | 102 +++++++++++++++++++++++++++++++++++ internal/planning/manager.go | 31 +++++++++++ 4 files changed, 137 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e603fda88f0ed0e81ba280cf9c53b4ab3fa029e8..5ab0b9d57d448599e38baaf6a36f40adbfd8e738 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,11 +52,12 @@ The project requires license headers (SPDX format) on all source files and uses ### MCP Tool Implementation -The server exposes four MCP tools that map directly to planning manager methods: +The server exposes five MCP tools that map directly to planning manager methods: - `update_goal(goal: string)`: Sets overarching goal with length validation - `add_tasks(tasks: []TaskInput)`: Batch task creation with duplicate detection. Encourages breaking tasks down into smallest units of work and regular progress tracking. Output behavior depends on existing tasks: shows verbose instructions + task list when no tasks existed previously, shows brief task list (like `get_tasks`) when tasks already existed. - `get_tasks()`: Returns markdown-formatted task list with legend and sorted by creation time. Should be called frequently to stay organized. - `update_tasks(tasks: []TaskUpdate)`: Updates status of one or more tasks and returns full list. Helps maintain planning workflow by tracking progress. +- `delete_tasks(task_ids: []string)`: Deletes one or more tasks by their IDs and returns the resulting task list. Validates all task IDs exist before deleting any. ### Configuration System diff --git a/README.md b/README.md index 4f5e98a769fb2e2030c43237caaba5c913b31801..a2372f98ce3ef5c4442d2683d2da68441dfcef8e 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,13 @@ This MCP server enables your AI assistant to: ## Core Capabilities -The server provides four essential planning tools: +The server provides five essential planning tools: - **`update_goal`**: Set or update the overarching goal for your planning session - **`add_tasks`**: Add one or more tasks to work on. Break tasks down into the smallest units of work possible and track progress. - **`get_tasks`**: Get current task list with status indicators and legend. Call frequently to stay organized. - **`update_tasks`**: Update the status of one or more tasks. Maintain your planning workflow by updating statuses regularly. +- **`delete_tasks`**: Delete one or more tasks by their IDs and return the updated task list. ## Installation diff --git a/internal/mcp/server.go b/internal/mcp/server.go index f520c2f7403844b5e71d9cf6daa53dc9192c1967..b83aa12fac4908a7fb5f6a01f0fdef9d4638ef6f 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -121,6 +121,19 @@ func (s *Server) registerTools(mcpServer *server.MCPServer) { ), ) mcpServer.AddTool(updateTasksTool, s.handleUpdateTasks) + + // Register delete_tasks tool + deleteTasksTool := mcp.NewTool("delete_tasks", + mcp.WithDescription("Delete one or more tasks by their IDs. After deletion, respond with the resulting task list."), + mcp.WithArray("task_ids", + mcp.Required(), + mcp.Description("Array of task IDs to delete"), + mcp.Items(map[string]any{ + "type": "string", + }), + ), + ) + mcpServer.AddTool(deleteTasksTool, s.handleDeleteTasks) } // handleUpdateGoal handles the update_goal tool call @@ -416,6 +429,95 @@ func (s *Server) handleUpdateTasks(ctx context.Context, request mcp.CallToolRequ }, nil } +// handleDeleteTasks handles the delete_tasks tool call +func (s *Server) handleDeleteTasks(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + s.logger.Info("Received delete_tasks tool call") + + // Extract parameters + arguments := request.GetArguments() + taskIDsRaw, ok := arguments["task_ids"] + if !ok { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: "Error: task_ids parameter is required", + }, + }, + IsError: true, + }, nil + } + + // Convert to slice of interfaces + taskIDsSlice, ok := taskIDsRaw.([]any) + if !ok { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: "Error: task_ids parameter must be an array", + }, + }, + IsError: true, + }, nil + } + + if len(taskIDsSlice) == 0 { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: "Error: at least one task ID is required", + }, + }, + IsError: true, + }, nil + } + + // Parse task IDs + taskIDs := make([]string, 0, len(taskIDsSlice)) + for _, taskIDRaw := range taskIDsSlice { + taskID, ok := taskIDRaw.(string) + if !ok || taskID == "" { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: "Error: each task ID must be a non-empty string", + }, + }, + IsError: true, + }, nil + } + taskIDs = append(taskIDs, taskID) + } + + // Delete tasks + if err := s.planner.DeleteTasks(taskIDs); err != nil { + s.logger.Error("Failed to delete tasks", "error", err) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("Error deleting tasks: %v", err), + }, + }, + IsError: true, + }, nil + } + + // Return full task list + taskList := s.planner.GetTasks() + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: taskList, + }, + }, + }, nil +} + // GetServer returns the underlying MCP server func (s *Server) GetServer() *server.MCPServer { return s.server diff --git a/internal/planning/manager.go b/internal/planning/manager.go index 0e6ad1d62940793ce1e64ac5072932724d4f0904..13ab903b04c578ed0a1b8242a7607f3d14cc8bd6 100644 --- a/internal/planning/manager.go +++ b/internal/planning/manager.go @@ -239,3 +239,34 @@ type TaskUpdate struct { TaskID string `json:"task_id"` Status TaskStatus `json:"status"` } + +// DeleteTasks deletes one or more tasks by their IDs +func (m *Manager) DeleteTasks(taskIDs []string) error { + m.mu.Lock() + defer m.mu.Unlock() + + if len(taskIDs) == 0 { + return fmt.Errorf("at least one task ID is required") + } + + // First validate all task IDs exist + notFound := make([]string, 0) + for _, taskID := range taskIDs { + if _, exists := m.tasks[taskID]; !exists { + notFound = append(notFound, taskID) + } + } + + if len(notFound) > 0 { + return fmt.Errorf("task(s) not found: %s", strings.Join(notFound, ", ")) + } + + // If all validations pass, delete all tasks + for _, taskID := range taskIDs { + delete(m.tasks, taskID) + m.logger.Info("Task deleted", "id", taskID) + } + + m.logger.Info("Tasks deleted", "count", len(taskIDs)) + return nil +}