feat: replace update_task_status with update_tasks for batch updates

Amolith created

- Replace single task update with batch task update functionality
- Add UpdateTasks method to planning manager for multiple task updates
- Update MCP tool definition to accept array of task updates
- Support both single and multiple task updates in one call
- Update documentation to reflect new update_tasks tool

Change summary

AGENTS.md                       |   2 
README.md                       |   2 
cmd/planning-mcp-server/main.go |   2 
internal/mcp/server.go          | 121 +++++++++++++++++++++++++++-------
internal/planning/manager.go    |  28 ++++++++
5 files changed, 127 insertions(+), 28 deletions(-)

Detailed changes

AGENTS.md 🔗

@@ -56,7 +56,7 @@ The server exposes four 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_task_status(task_id: string, status: string)`: Updates task status and returns full list. Helps maintain planning workflow by tracking progress.
+- `update_tasks(tasks: []TaskUpdate)`: Updates status of one or more tasks and returns full list. Helps maintain planning workflow by tracking progress.
 
 ### Configuration System
 

README.md 🔗

@@ -19,7 +19,7 @@ The server provides four 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_task_status`**: Update the status of a specific task. Maintain your planning workflow by updating statuses regularly.
+- **`update_tasks`**: Update the status of one or more tasks. Maintain your planning workflow by updating statuses regularly.
 
 ## Installation
 

cmd/planning-mcp-server/main.go 🔗

@@ -54,7 +54,7 @@ The server provides tools for goal setting, task management, and progress tracki
 - update_goal: Set or update the overarching goal
 - add_tasks: Add one or more tasks to work on
 - get_tasks: Get current task list with status indicators
-- update_task_status: Update the status of a specific task`,
+- update_tasks: Update the status of one or more tasks`,
 		SilenceUsage: true,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if showVersion {

internal/mcp/server.go 🔗

@@ -98,19 +98,29 @@ func (s *Server) registerTools(mcpServer *server.MCPServer) {
 	)
 	mcpServer.AddTool(getTasksTool, s.handleGetTasks)
 
-	// Register update_task_status tool
-	updateTaskStatusTool := mcp.NewTool("update_task_status",
-		mcp.WithDescription("Update the status of a specific task. Maintain your planning workflow by regularly updating task statuses as you make progress."),
-		mcp.WithString("task_id",
-			mcp.Required(),
-			mcp.Description("The task ID to update"),
-		),
-		mcp.WithString("status",
+	// Register update_tasks tool
+	updateTasksTool := mcp.NewTool("update_tasks",
+		mcp.WithDescription("Update the status of one or more tasks. Maintain your planning workflow by regularly updating task statuses as you make progress."),
+		mcp.WithArray("tasks",
 			mcp.Required(),
-			mcp.Description("New status: pending, in_progress, completed, or failed"),
+			mcp.Description("Array of task updates"),
+			mcp.Items(map[string]any{
+				"type": "object",
+				"properties": map[string]any{
+					"task_id": map[string]any{
+						"type":        "string",
+						"description": "The task ID to update",
+					},
+					"status": map[string]any{
+						"type":        "string",
+						"description": "New status: pending, in_progress, completed, or failed",
+					},
+				},
+				"required": []string{"task_id", "status"},
+			}),
 		),
 	)
-	mcpServer.AddTool(updateTaskStatusTool, s.handleUpdateTaskStatus)
+	mcpServer.AddTool(updateTasksTool, s.handleUpdateTasks)
 }
 
 // handleUpdateGoal handles the update_goal tool call
@@ -284,49 +294,110 @@ func (s *Server) handleGetTasks(ctx context.Context, request mcp.CallToolRequest
 	}, nil
 }
 
-// handleUpdateTaskStatus handles the update_task_status tool call
-func (s *Server) handleUpdateTaskStatus(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	s.logger.Info("Received update_task_status tool call")
+// handleUpdateTasks handles the update_tasks tool call
+func (s *Server) handleUpdateTasks(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	s.logger.Info("Received update_tasks tool call")
 
 	// Extract parameters
 	arguments := request.GetArguments()
-	taskID, ok := arguments["task_id"].(string)
-	if !ok || taskID == "" {
+	tasksRaw, ok := arguments["tasks"]
+	if !ok {
 		return &mcp.CallToolResult{
 			Content: []mcp.Content{
 				mcp.TextContent{
 					Type: "text",
-					Text: "Error: task_id parameter is required and must be a string",
+					Text: "Error: tasks parameter is required",
 				},
 			},
 			IsError: true,
 		}, nil
 	}
 
-	statusStr, ok := arguments["status"].(string)
-	if !ok || statusStr == "" {
+	// Convert to slice of interfaces
+	tasksSlice, ok := tasksRaw.([]any)
+	if !ok {
+		return &mcp.CallToolResult{
+			Content: []mcp.Content{
+				mcp.TextContent{
+					Type: "text",
+					Text: "Error: tasks parameter must be an array",
+				},
+			},
+			IsError: true,
+		}, nil
+	}
+
+	if len(tasksSlice) == 0 {
 		return &mcp.CallToolResult{
 			Content: []mcp.Content{
 				mcp.TextContent{
 					Type: "text",
-					Text: "Error: status parameter is required and must be a string",
+					Text: "Error: at least one task update is required",
 				},
 			},
 			IsError: true,
 		}, nil
 	}
 
-	// Parse status
-	status := planning.ParseStatus(statusStr)
+	// Parse task updates
+	updates := make([]planning.TaskUpdate, 0, len(tasksSlice))
+	for _, taskRaw := range tasksSlice {
+		taskMap, ok := taskRaw.(map[string]any)
+		if !ok {
+			return &mcp.CallToolResult{
+				Content: []mcp.Content{
+					mcp.TextContent{
+						Type: "text",
+						Text: "Error: each task update must be an object",
+					},
+				},
+				IsError: true,
+			}, nil
+		}
+
+		taskID, ok := taskMap["task_id"].(string)
+		if !ok || taskID == "" {
+			return &mcp.CallToolResult{
+				Content: []mcp.Content{
+					mcp.TextContent{
+						Type: "text",
+						Text: "Error: each task update must have a non-empty task_id",
+					},
+				},
+				IsError: true,
+			}, nil
+		}
+
+		statusStr, ok := taskMap["status"].(string)
+		if !ok || statusStr == "" {
+			return &mcp.CallToolResult{
+				Content: []mcp.Content{
+					mcp.TextContent{
+						Type: "text",
+						Text: "Error: each task update must have a non-empty status",
+					},
+				},
+				IsError: true,
+			}, nil
+		}
+
+		// Parse status
+		status := planning.ParseStatus(statusStr)
+
+		updates = append(updates, planning.TaskUpdate{
+			TaskID: taskID,
+			Status: status,
+		})
+	}
 
-	// Update task status
-	if err := s.planner.UpdateTaskStatus(taskID, status); err != nil {
-		s.logger.Error("Failed to update task status", "error", err)
+	// Update task statuses
+	if err := s.planner.UpdateTasks(updates); err != nil {
+		s.logger.Error("Failed to update task statuses", "error", err)
 		return &mcp.CallToolResult{
 			Content: []mcp.Content{
 				mcp.TextContent{
 					Type: "text",
-					Text: fmt.Sprintf("Error updating task status: %v", err),
+					Text: fmt.Sprintf("Error updating task statuses: %v", err),
 				},
 			},
 			IsError: true,

internal/planning/manager.go 🔗

@@ -191,6 +191,28 @@ func (m *Manager) UpdateTaskStatus(taskID string, status TaskStatus) error {
 	return nil
 }
 
+// UpdateTasks updates the status of multiple tasks in a single operation
+func (m *Manager) UpdateTasks(updates []TaskUpdate) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	// First validate all task IDs exist
+	for _, update := range updates {
+		if _, exists := m.tasks[update.TaskID]; !exists {
+			return fmt.Errorf("task not found: %s", update.TaskID)
+		}
+	}
+
+	// If all validations pass, apply all updates
+	for _, update := range updates {
+		task := m.tasks[update.TaskID]
+		task.UpdateStatus(update.Status)
+		m.logger.Info("Task status updated", "id", update.TaskID, "status", update.Status.String())
+	}
+
+	return nil
+}
+
 // GetGoal returns the current goal
 func (m *Manager) GetGoal() *Goal {
 	m.mu.RLock()
@@ -211,3 +233,9 @@ type TaskInput struct {
 	Title       string `json:"title"`
 	Description string `json:"description"`
 }
+
+// TaskUpdate represents a task status update
+type TaskUpdate struct {
+	TaskID string     `json:"task_id"`
+	Status TaskStatus `json:"status"`
+}