Detailed changes
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+}
@@ -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()
@@ -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)