feat: add delete_tasks tool for removing tasks by ID

Amolith created

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.

Change summary

AGENTS.md                    |   3 
README.md                    |   3 
internal/mcp/server.go       | 102 ++++++++++++++++++++++++++++++++++++++
internal/planning/manager.go |  31 +++++++++++
4 files changed, 137 insertions(+), 2 deletions(-)

Detailed changes

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
 

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
 

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

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
+}