@@ -58,19 +58,41 @@ func New(cfg *config.Config, logger *slog.Logger, planner *planning.Manager) (*S
// registerTools registers all planning tools
func (s *Server) registerTools(mcpServer *server.MCPServer) {
- // Register update_goal tool
- updateGoalTool := mcp.NewTool("update_goal",
- mcp.WithDescription("Set or update the overarching goal for your planning session"),
- mcp.WithString("goal",
+ // Register project_management__set_goal tool
+ setGoalTool := mcp.NewTool("project_management__set_goal",
+ mcp.WithDescription("Set the initial project goal. Returns error if already set and encourages calling project_management__change_goal"),
+ mcp.WithString("title",
mcp.Required(),
- mcp.Description("The goal text to set"),
+ mcp.Description("The goal title"),
+ ),
+ mcp.WithString("description",
+ mcp.Required(),
+ mcp.Description("The goal description"),
),
)
- mcpServer.AddTool(updateGoalTool, s.handleUpdateGoal)
+ mcpServer.AddTool(setGoalTool, s.handleSetGoal)
- // Register add_tasks tool
- addTasksTool := mcp.NewTool("add_tasks",
- mcp.WithDescription("Add one or more tasks to work on. Break tasks down into the smallest units of work possible. If there are more than one, use me to keep track of where you are in fulfilling the user's request. Call get_tasks often to stay on track."),
+ // Register project_management__change_goal tool
+ changeGoalTool := mcp.NewTool("project_management__change_goal",
+ mcp.WithDescription("Change an existing project goal. Only use if the operator explicitly requests clearing the board/list/goal and doing something else"),
+ mcp.WithString("title",
+ mcp.Required(),
+ mcp.Description("The new goal title"),
+ ),
+ mcp.WithString("description",
+ mcp.Required(),
+ mcp.Description("The new goal description"),
+ ),
+ mcp.WithString("reason",
+ mcp.Required(),
+ mcp.Description("The reason for changing the goal"),
+ ),
+ )
+ mcpServer.AddTool(changeGoalTool, s.handleChangeGoal)
+
+ // Register project_management__add_tasks tool
+ addTasksTool := mcp.NewTool("project_management__add_tasks",
+ mcp.WithDescription("Add one or more tasks to work on. Break tasks down into the smallest units of work possible. If there are more than one, use me to keep track of where you are in fulfilling the user's request. Call project_management__get_tasks often to stay on track."),
mcp.WithArray("tasks",
mcp.Required(),
mcp.Description("Array of tasks to add"),
@@ -92,15 +114,18 @@ func (s *Server) registerTools(mcpServer *server.MCPServer) {
)
mcpServer.AddTool(addTasksTool, s.handleAddTasks)
- // Register get_tasks tool
- getTasksTool := mcp.NewTool("get_tasks",
- mcp.WithDescription("Get current task list with status indicators. Call this frequently to stay organized and track your progress."),
+ // Register project_management__get_tasks tool
+ getTasksTool := mcp.NewTool("project_management__get_tasks",
+ mcp.WithDescription("Get task list with status indicators. Call this frequently to stay organized and track your progress. Prefer to call with 'all' or 'pending', only 'completed' if unsure, only 'cancelled' or 'failed' if the operator explicitly asks."),
+ mcp.WithString("status",
+ mcp.Description("Filter tasks by status: all, pending, in_progress, completed, cancelled, or failed (default: all)"),
+ ),
)
mcpServer.AddTool(getTasksTool, s.handleGetTasks)
- // 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."),
+ // Register project_management__update_task_statuses tool
+ updateTasksTool := mcp.NewTool("project_management__update_task_statuses",
+ mcp.WithDescription("Update the status of one or more tasks. Never cancel tasks on your own. If something doesn't work or there's an error, mark it failed and decide whether to ask the operator for guidance or continue on your own. Usually prefer to ask the operator."),
mcp.WithArray("tasks",
mcp.Required(),
mcp.Description("Array of task updates"),
@@ -113,18 +138,18 @@ func (s *Server) registerTools(mcpServer *server.MCPServer) {
},
"status": map[string]any{
"type": "string",
- "description": "New status: pending, in_progress, completed, or failed",
+ "description": "New status: pending, in_progress, completed, cancelled, or failed",
},
},
"required": []string{"task_id", "status"},
}),
),
)
- mcpServer.AddTool(updateTasksTool, s.handleUpdateTasks)
+ mcpServer.AddTool(updateTasksTool, s.handleUpdateTaskStatuses)
- // 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."),
+ // Register project_management__delete_tasks tool
+ deleteTasksTool := mcp.NewTool("project_management__delete_tasks",
+ mcp.WithDescription("Delete one or more tasks by their IDs. Only use if the operator explicitly requests clearing the board/list/goal and doing something else. Otherwise, update statuses to 'cancelled', 'failed', etc. as appropriate. After deletion, respond with the resulting task list."),
mcp.WithArray("task_ids",
mcp.Required(),
mcp.Description("Array of task IDs to delete"),
@@ -136,40 +161,57 @@ func (s *Server) registerTools(mcpServer *server.MCPServer) {
mcpServer.AddTool(deleteTasksTool, s.handleDeleteTasks)
}
-// handleUpdateGoal handles the update_goal tool call
-func (s *Server) handleUpdateGoal(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- s.logger.Info("Received update_goal tool call")
+// handleSetGoal handles the project_management__set_goal tool call
+func (s *Server) handleSetGoal(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ s.logger.Info("Received project_management__set_goal tool call")
// Extract parameters
arguments := request.GetArguments()
- goal, ok := arguments["goal"].(string)
- if !ok || goal == "" {
+ title, ok := arguments["title"].(string)
+ if !ok || title == "" {
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: "Error: title parameter is required and must be a string",
+ },
+ },
+ IsError: true,
+ }, nil
+ }
+
+ description, ok := arguments["description"].(string)
+ if !ok {
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
- Text: "Error: goal parameter is required and must be a string",
+ Text: "Error: description parameter is required and must be a string",
},
},
IsError: true,
}, nil
}
- // Update goal
- if err := s.planner.UpdateGoal(goal); err != nil {
- s.logger.Error("Failed to update goal", "error", err)
+ // Set goal
+ if err := s.planner.SetGoal(title, description); err != nil {
+ s.logger.Error("Failed to set goal", "error", err)
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
- Text: fmt.Sprintf("Error updating goal: %v", err),
+ Text: fmt.Sprintf("Error setting goal: %v", err),
},
},
IsError: true,
}, nil
}
- response := fmt.Sprintf("Goal \"%s\" saved! You probably want to add one or more tasks now.", goal)
+ goalText := title
+ if description != "" {
+ goalText = fmt.Sprintf("%s: %s", title, description)
+ }
+ response := fmt.Sprintf("Goal \"%s\" saved! You probably want to add one or more tasks now.", goalText)
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
@@ -180,9 +222,83 @@ func (s *Server) handleUpdateGoal(ctx context.Context, request mcp.CallToolReque
}, nil
}
-// handleAddTasks handles the add_tasks tool call
+// handleChangeGoal handles the project_management__change_goal tool call
+func (s *Server) handleChangeGoal(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ s.logger.Info("Received project_management__change_goal tool call")
+
+ // Extract parameters
+ arguments := request.GetArguments()
+ title, ok := arguments["title"].(string)
+ if !ok || title == "" {
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: "Error: title parameter is required and must be a string",
+ },
+ },
+ IsError: true,
+ }, nil
+ }
+
+ description, ok := arguments["description"].(string)
+ if !ok {
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: "Error: description parameter is required and must be a string",
+ },
+ },
+ IsError: true,
+ }, nil
+ }
+
+ reason, ok := arguments["reason"].(string)
+ if !ok || reason == "" {
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: "Error: reason parameter is required and must be a string",
+ },
+ },
+ IsError: true,
+ }, nil
+ }
+
+ // Change goal
+ if err := s.planner.ChangeGoal(title, description, reason); err != nil {
+ s.logger.Error("Failed to change goal", "error", err)
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: fmt.Sprintf("Error changing goal: %v", err),
+ },
+ },
+ IsError: true,
+ }, nil
+ }
+
+ goalText := title
+ if description != "" {
+ goalText = fmt.Sprintf("%s: %s", title, description)
+ }
+ response := fmt.Sprintf("Goal changed to \"%s\" (reason: %s). You probably want to add one or more tasks now.", goalText, reason)
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: response,
+ },
+ },
+ }, nil
+}
+
+// handleAddTasks handles the project_management__add_tasks tool call
func (s *Server) handleAddTasks(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- s.logger.Info("Received add_tasks tool call")
+ s.logger.Info("Received project_management__add_tasks tool call")
// Extract parameters
arguments := request.GetArguments()
@@ -276,7 +392,7 @@ func (s *Server) handleAddTasks(ctx context.Context, request mcp.CallToolRequest
if goal != nil {
goalText = fmt.Sprintf("\"%s\"", goal.Text)
}
- response = fmt.Sprintf("Tasks added successfully! Get started on your first one once you're ready, and call `get_tasks` frequently to remind yourself where you are in the process. Reminder that your overarching goal is %s.\n\n%s", goalText, taskList)
+ response = fmt.Sprintf("Tasks added successfully! Get started on your first one once you're ready, and call `project_management__get_tasks` frequently to remind yourself where you are in the process. Reminder that your overarching goal is %s.\n\n%s", goalText, taskList)
} else {
// Had existing tasks - just show the task list (like get_tasks)
response = taskList
@@ -291,11 +407,23 @@ func (s *Server) handleAddTasks(ctx context.Context, request mcp.CallToolRequest
}, nil
}
-// handleGetTasks handles the get_tasks tool call
+// handleGetTasks handles the project_management__get_tasks tool call
func (s *Server) handleGetTasks(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- s.logger.Info("Received get_tasks tool call")
+ s.logger.Info("Received project_management__get_tasks tool call")
- taskList := s.planner.GetTasks()
+ // Extract parameters
+ arguments := request.GetArguments()
+ statusFilter, _ := arguments["status"].(string)
+ if statusFilter == "" {
+ statusFilter = "all"
+ }
+
+ var taskList string
+ if statusFilter == "all" {
+ taskList = s.planner.GetTasks()
+ } else {
+ taskList = s.planner.GetTasksByStatus(statusFilter)
+ }
return &mcp.CallToolResult{
Content: []mcp.Content{
@@ -307,9 +435,9 @@ func (s *Server) handleGetTasks(ctx context.Context, request mcp.CallToolRequest
}, nil
}
-// 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")
+// handleUpdateTaskStatuses handles the project_management__update_task_statuses tool call
+func (s *Server) handleUpdateTaskStatuses(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ s.logger.Info("Received project_management__update_task_statuses tool call")
// Extract parameters
arguments := request.GetArguments()
@@ -429,9 +557,9 @@ func (s *Server) handleUpdateTasks(ctx context.Context, request mcp.CallToolRequ
}, nil
}
-// handleDeleteTasks handles the delete_tasks tool call
+// handleDeleteTasks handles the project_management__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")
+ s.logger.Info("Received project_management__delete_tasks tool call")
// Extract parameters
arguments := request.GetArguments()
@@ -34,7 +34,7 @@ func New(cfg *config.Config, logger *slog.Logger) *Manager {
}
}
-// UpdateGoal sets or updates the overarching goal
+// UpdateGoal sets or updates the overarching goal (kept for compatibility)
func (m *Manager) UpdateGoal(goalText string) error {
if len(goalText) > m.config.Planning.MaxGoalLength {
return fmt.Errorf("goal too long (max %d characters)", m.config.Planning.MaxGoalLength)
@@ -52,6 +52,70 @@ func (m *Manager) UpdateGoal(goalText string) error {
return nil
}
+// SetGoal sets the initial goal (returns error if already set)
+func (m *Manager) SetGoal(title, description string) error {
+ if len(title) > m.config.Planning.MaxGoalLength {
+ return fmt.Errorf("goal title too long (max %d characters)", m.config.Planning.MaxGoalLength)
+ }
+ if len(description) > m.config.Planning.MaxGoalLength {
+ return fmt.Errorf("goal description too long (max %d characters)", m.config.Planning.MaxGoalLength)
+ }
+
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ if m.goal != nil && m.goal.Text != "" {
+ return fmt.Errorf("goal already set; use change_goal to update it")
+ }
+
+ goalText := title
+ if description != "" {
+ goalText = fmt.Sprintf("%s: %s", title, description)
+ }
+
+ m.goal = &Goal{
+ Text: strings.TrimSpace(goalText),
+ UpdatedAt: time.Now(),
+ }
+
+ m.logger.Info("Goal set", "title", title, "description", description)
+ return nil
+}
+
+// ChangeGoal changes an existing goal (requires a reason)
+func (m *Manager) ChangeGoal(title, description, reason string) error {
+ if len(title) > m.config.Planning.MaxGoalLength {
+ return fmt.Errorf("goal title too long (max %d characters)", m.config.Planning.MaxGoalLength)
+ }
+ if len(description) > m.config.Planning.MaxGoalLength {
+ return fmt.Errorf("goal description too long (max %d characters)", m.config.Planning.MaxGoalLength)
+ }
+ if reason == "" {
+ return fmt.Errorf("reason is required when changing the goal")
+ }
+
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ if m.goal == nil || m.goal.Text == "" {
+ return fmt.Errorf("no goal set; use set_goal to create an initial goal")
+ }
+
+ goalText := title
+ if description != "" {
+ goalText = fmt.Sprintf("%s: %s", title, description)
+ }
+
+ oldGoal := m.goal.Text
+ m.goal = &Goal{
+ Text: strings.TrimSpace(goalText),
+ UpdatedAt: time.Now(),
+ }
+
+ m.logger.Info("Goal changed", "old", oldGoal, "new", goalText, "reason", reason)
+ return nil
+}
+
// AddTasksResult contains the result of adding tasks
type AddTasksResult struct {
AddedTasks []*Task
@@ -107,9 +171,36 @@ func (m *Manager) AddTasks(tasks []TaskInput) (*AddTasksResult, error) {
}, nil
}
-// formatTaskList returns a markdown-formatted list of tasks
+// formatTaskList returns a markdown-formatted list of all tasks
// This method assumes the mutex is already locked
func (m *Manager) formatTaskList() string {
+ return m.formatTaskListWithFilter(nil)
+}
+
+// GetTasks returns a markdown-formatted list of all tasks
+func (m *Manager) GetTasks() string {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+ return m.formatTaskList()
+}
+
+// GetTasksByStatus returns a markdown-formatted list of tasks filtered by status
+func (m *Manager) GetTasksByStatus(statusFilter string) string {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
+ // Parse the filter
+ var filterStatus *TaskStatus
+ if statusFilter != "" && statusFilter != "all" {
+ status := ParseStatus(statusFilter)
+ filterStatus = &status
+ }
+
+ return m.formatTaskListWithFilter(filterStatus)
+}
+
+// formatTaskListWithFilter returns a markdown-formatted list of tasks with optional status filter
+func (m *Manager) formatTaskListWithFilter(filterStatus *TaskStatus) string {
var lines []string
// Add goal if it exists
@@ -118,32 +209,60 @@ func (m *Manager) formatTaskList() string {
lines = append(lines, "")
}
- if len(m.tasks) == 0 {
- if len(lines) > 0 {
+ // Filter tasks if needed
+ filteredTasks := make(map[string]*Task)
+ for id, task := range m.tasks {
+ if filterStatus == nil || task.Status == *filterStatus {
+ filteredTasks[id] = task
+ }
+ }
+
+ if len(filteredTasks) == 0 {
+ if filterStatus != nil {
+ statusName := "pending"
+ switch *filterStatus {
+ case StatusInProgress:
+ statusName = "in progress"
+ case StatusCompleted:
+ statusName = "completed"
+ case StatusFailed:
+ statusName = "failed"
+ case StatusCancelled:
+ statusName = "cancelled"
+ }
+ lines = append(lines, fmt.Sprintf("No %s tasks.", statusName))
+ } else if len(lines) > 0 {
// We have a goal but no tasks
lines = append(lines, "No tasks defined yet.")
- return strings.Join(lines, "\n")
+ } else {
+ lines = append(lines, "No tasks defined yet.")
}
- return "No tasks defined yet."
+ return strings.Join(lines, "\n")
}
// Sort tasks by creation time for consistent output
- taskList := make([]*Task, 0, len(m.tasks))
- for _, task := range m.tasks {
+ taskList := make([]*Task, 0, len(filteredTasks))
+ for _, task := range filteredTasks {
taskList = append(taskList, task)
}
- // Check if there are any failed tasks
+ // Check if there are any failed or cancelled tasks
hasFailed := false
+ hasCancelled := false
for _, task := range taskList {
if task.Status == StatusFailed {
hasFailed = true
- break
+ }
+ if task.Status == StatusCancelled {
+ hasCancelled = true
}
}
// Add legend
legend := "Legend: ☐ pending ⟳ in progress ☑ completed"
+ if hasCancelled {
+ legend += " ⊗ cancelled"
+ }
if hasFailed {
legend += " ☒ failed"
}
@@ -169,13 +288,6 @@ func (m *Manager) formatTaskList() string {
return strings.Join(lines, "\n")
}
-// GetTasks returns a markdown-formatted list of tasks
-func (m *Manager) GetTasks() string {
- m.mu.RLock()
- defer m.mu.RUnlock()
- return m.formatTaskList()
-}
-
// UpdateTaskStatus updates the status of a specific task
func (m *Manager) UpdateTaskStatus(taskID string, status TaskStatus) error {
m.mu.Lock()