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