From b6ce01b0126cd36dac7a78ae7b789e9d058e68ff Mon Sep 17 00:00:00 2001 From: Amolith Date: Sat, 20 Sep 2025 15:29:24 -0600 Subject: [PATCH] feat: add modify_task tool for task updates Add new MCP tool to modify task title and/or description with automatic ID regeneration. The tool accepts task_id (required) and optional title/description fields, updating only provided fields while maintaining existing content for omitted ones. Includes proper validation, logging, and returns updated task list. Updates documentation across AGENTS.md and README.md. Co-Authored-By: Crush --- AGENTS.md | 8 +++--- README.md | 4 ++- internal/mcp/server.go | 42 ++++++++++++++++++++++++++++++ internal/mcp/types.go | 7 +++++ internal/mcp/validator.go | 22 ++++++++++++++++ internal/planning/manager.go | 50 ++++++++++++++++++++++++++++++++++++ internal/planning/types.go | 13 ++++++++++ 7 files changed, 142 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a179033797f8200ab130d149ced18a46121df948..6912fe18ef47fd841123cfc64cd3815fb681b773 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,13 +10,14 @@ This file provides guidance to AI coding assistants when working with code in th ## planning-mcp-server capabilities -The server provides six essential planning tools: +The server provides seven essential planning tools: - **`set_goal`**: Set the initial goal for your planning session (title and description required). Returns error if goal already exists. - **`change_goal`**: Change an existing goal with a required reason for the change. Only used when operator explicitly requests clearing/changing the goal. -- **`add_tasks`**: Add one or more tasks to work on. Break tasks down into the smallest units of work possible and track progress. Each task requires a title (and optional description). +- **`add_tasks`**: Add one or more tasks to work on. Break them down into the smallest units of work possible and track progress. Each task requires a title (and optional description). - **`get_tasks`**: Get task list with optional status filtering and indicators. Call frequently to stay organized. - **`update_task_statuses`**: Update the status of one or more tasks. Maintain your planning workflow by updating statuses regularly. +- **`modify_task`**: Modify the title and/or description of a task. ID and at least one of title/description are required. When one or the other is omitted, that field will not be modified. - **`delete_tasks`**: Delete one or more tasks by their IDs and return the updated task list. Only use if the operator explicitly requests clearing the board. ## Development Commands @@ -84,13 +85,14 @@ The server uses in-memory storage only. All goals and tasks are lost when the se ### MCP Tool Implementation -The server exposes six MCP tools that map directly to planning manager methods: +The server exposes seven MCP tools that map directly to planning manager methods: - `set_goal(title: string, description: string)`: Sets initial goal with title and description (both required). Returns error if goal already exists. - `change_goal(title: string, description: string, reason: string)`: Changes existing goal (all parameters required). Only used when operator explicitly requests clearing/changing the goal. - `add_tasks(tasks: []TaskInput)`: Batch task creation with duplicate detection. Each task requires `title` (required) and `description` (optional). 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(status: string)`: Returns markdown-formatted task list with optional status filter (all, pending, in_progress, completed, cancelled, failed). Default is "all". Should be called frequently to stay organized. - `update_task_statuses(tasks: []TaskUpdate)`: Updates status of one or more tasks and returns full list. Never cancels tasks autonomously - marks as failed on errors and asks operator for guidance. +- `modify_task(task_id: string, title?: string, description?: string)`: Modifies the title and/or description of a task. ID and at least one of title/description are required. When one or the other is omitted, that field will not be modified. Regenerates the task ID based on new content and returns the updated task list. - `delete_tasks(task_ids: []string)`: Deletes one or more tasks by their IDs. Only used when operator explicitly requests clearing the board. Otherwise, tasks should be marked as cancelled/failed. Returns the resulting task list. ### Configuration System diff --git a/README.md b/README.md index 401afc04cab6ce8450a4e35fcf31d046d9016e08..773e70b36f0ca27f1b0fe1e21ba4389ad3c9442d 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,14 @@ A Model Context Protocol (MCP) server that provides planning tools for LLMs to t ## What this gives your AI assistant -The server provides six essential planning tools: +The server provides seven essential planning tools: - **`set_goal`**: Set the initial goal for your planning session (title and description required). Returns error if goal already exists. - **`change_goal`**: Change an existing goal with a required reason for the change. Only used when operator explicitly requests clearing/changing the goal. - **`add_tasks`**: Add one or more tasks to work on. Break tasks down into the smallest units of work possible and track progress. Each task requires a title (and optional description). - **`get_tasks`**: Get task list with optional status filtering and indicators. Call frequently to stay organized. - **`update_task_statuses`**: Update the status of one or more tasks. Maintain your planning workflow by updating statuses regularly. +- **`modify_task`**: Modify the title and/or description of a task. ID and at least one of title/description are required. When one or the other is omitted, that field will not be modified. - **`delete_tasks`**: Delete one or more tasks by their IDs and return the updated task list. Only use if the operator explicitly requests clearing the board. ## Installation @@ -127,6 +128,7 @@ Task IDs are deterministically generated based on the task title and description - Same task content always gets the same ID within a session - No collisions for different tasks - Consistent references during the current session +- When modifying tasks, IDs are regenerated to reflect the new content ## Data Storage diff --git a/internal/mcp/server.go b/internal/mcp/server.go index cfea4c8da585f3114f201ae00ba87f262ee61b8f..88a377a10787201aa12ff4533c13c4d9d513099e 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -157,6 +157,22 @@ func (s *Server) registerTools(mcpServer *server.MCPServer) { ), ) mcpServer.AddTool(deleteTasksTool, s.handleDeleteTasks) + + // Register modify_task tool + modifyTaskTool := mcp.NewTool("modify_task", + mcp.WithDescription("Modify the title and/or description of a task. ID and at least one of title/description are required. When one or the other is omitted, that field will not be modified."), + mcp.WithString("task_id", + mcp.Required(), + mcp.Description("ID of the task to modify"), + ), + mcp.WithString("title", + mcp.Description("New title for the task (optional - if omitted, title remains unchanged)"), + ), + mcp.WithString("description", + mcp.Description("New description for the task (optional - if omitted, description remains unchanged)"), + ), + ) + mcpServer.AddTool(modifyTaskTool, s.handleModifyTask) } // handleSetGoal handles the set_goal tool call @@ -346,6 +362,32 @@ func (s *Server) handleDeleteTasks(ctx context.Context, request mcp.CallToolRequ return createSuccessResult(taskList), nil } +// handleModifyTask handles the modify_task tool call +func (s *Server) handleModifyTask(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + s.logger.Info("Received modify_task tool call") + + // Parse request + var req ModifyTaskRequest + if err := parseRequest(request.GetArguments(), &req); err != nil { + return createErrorResult(fmt.Sprintf("Invalid request format: %v", err)), nil + } + + // Validate request + if err := s.validator.ValidateModifyTaskRequest(req); err != nil { + return createErrorResult(fmt.Sprintf("Validation error: %v", err)), nil + } + + // Modify task + if err := s.planner.ModifyTask(req.TaskID, req.Title, req.Description); err != nil { + s.logger.Error("Failed to modify task", "error", err) + return createErrorResult(fmt.Sprintf("Error modifying task: %v", err)), nil + } + + // Return full task list + taskList := s.planner.GetTasks() + return createSuccessResult(taskList), nil +} + // GetServer returns the underlying MCP server func (s *Server) GetServer() *server.MCPServer { return s.server diff --git a/internal/mcp/types.go b/internal/mcp/types.go index e73cd14cfe74a5ea44c008938bb95b27733b3315..25c164b69064581d13e40e1f70419e070c91b9f0 100644 --- a/internal/mcp/types.go +++ b/internal/mcp/types.go @@ -58,6 +58,13 @@ type DeleteTasksRequest struct { TaskIDs []string `json:"task_ids" validate:"required,min=1"` } +// ModifyTaskRequest represents the request structure for modify_task +type ModifyTaskRequest struct { + TaskID string `json:"task_id" validate:"required"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` +} + // parseRequest is a generic helper function to parse map[string]any to struct without validation func parseRequest[T any](arguments map[string]any, dest *T) error { // Convert map to JSON then unmarshal to struct diff --git a/internal/mcp/validator.go b/internal/mcp/validator.go index 95c8bb0d4bc09bb0b32d7df976dca9a92f36ea4b..117baa2b4e470b448b1292cf9dd6caa952f1618f 100644 --- a/internal/mcp/validator.go +++ b/internal/mcp/validator.go @@ -19,6 +19,7 @@ type Validator interface { ValidateGetTasksRequest(req GetTasksRequest) error ValidateUpdateTaskStatusesRequest(req UpdateTaskStatusesRequest) error ValidateDeleteTasksRequest(req DeleteTasksRequest) error + ValidateModifyTaskRequest(req ModifyTaskRequest) error } // PlanningValidator implements the Validator interface with configuration-based validation @@ -151,3 +152,24 @@ func (v *PlanningValidator) ValidateDeleteTasksRequest(req DeleteTasksRequest) e } return nil } + +// ValidateModifyTaskRequest validates a modify task request +func (v *PlanningValidator) ValidateModifyTaskRequest(req ModifyTaskRequest) error { + if req.TaskID == "" { + return errors.New("task_id is required") + } + + if req.Title == "" && req.Description == "" { + return errors.New("at least one of title or description must be provided") + } + + if req.Title != "" && len(req.Title) > v.config.Planning.MaxTaskLength { + return fmt.Errorf("title too long (max %d characters)", v.config.Planning.MaxTaskLength) + } + + if req.Description != "" && len(req.Description) > v.config.Planning.MaxTaskLength { + return fmt.Errorf("description too long (max %d characters)", v.config.Planning.MaxTaskLength) + } + + return nil +} diff --git a/internal/planning/manager.go b/internal/planning/manager.go index f0005e086743c3487d3ae522c2996bff8e9306e1..eb5bc02bb0dec8a65562c2b460f568061d430393 100644 --- a/internal/planning/manager.go +++ b/internal/planning/manager.go @@ -331,6 +331,56 @@ type TaskUpdate struct { Status TaskStatus `json:"status"` } +// ModifyTask updates the title and/or description of a task and regenerates its ID +func (m *Manager) ModifyTask(taskID, title, description string) error { + if title == "" && description == "" { + return fmt.Errorf("at least one of title or description must be provided") + } + + m.mu.Lock() + defer m.mu.Unlock() + + task, exists := m.tasks[taskID] + if !exists { + return fmt.Errorf("task not found: %s", taskID) + } + + // Validate new title if provided + if title != "" { + if len(title) > m.config.Planning.MaxTaskLength { + return fmt.Errorf("task title too long (max %d characters)", m.config.Planning.MaxTaskLength) + } + } + + // Validate new description if provided + if description != "" { + if len(description) > m.config.Planning.MaxTaskLength { + return fmt.Errorf("task description too long (max %d characters)", m.config.Planning.MaxTaskLength) + } + } + + // Store old values for logging + oldTitle := task.Title + oldDescription := task.Description + + // Update content and regenerate ID + task.UpdateContent(title, description) + + // Update the task in the map with the new ID + delete(m.tasks, taskID) + m.tasks[task.ID] = task + + m.logger.Info("Task modified", + "old_id", taskID, + "new_id", task.ID, + "old_title", oldTitle, + "new_title", task.Title, + "old_description", oldDescription, + "new_description", task.Description) + + return nil +} + // DeleteTasks deletes one or more tasks by their IDs func (m *Manager) DeleteTasks(taskIDs []string) error { m.mu.Lock() diff --git a/internal/planning/types.go b/internal/planning/types.go index d18fff97207cfa1b6d33405050ecbe6622611221..6d31fd759e5d1dc999f13176ae1a61019bf1f708 100644 --- a/internal/planning/types.go +++ b/internal/planning/types.go @@ -95,6 +95,19 @@ func (t *Task) UpdateStatus(status TaskStatus) { t.UpdatedAt = time.Now() } +// UpdateContent updates the task title and/or description and regenerates ID +func (t *Task) UpdateContent(title, description string) { + if title != "" { + t.Title = title + } + if description != "" { + t.Description = description + } + // Regenerate ID based on new content + t.ID = generateTaskID(t.Title, t.Description) + t.UpdatedAt = time.Now() +} + // generateTaskID creates a deterministic 8-character ID based on task content func generateTaskID(title, description string) string { content := fmt.Sprintf("%s:%s", title, description)