From dbf471f890a914f05a700c77be01a17f37f6567d Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sat, 5 Jul 2025 14:42:26 -0700 Subject: [PATCH] Add MCP prompt methods and placeholder support to tasks --- mcp_test.go | 2 +- taskcp.go | 58 +++++++++++++++++++++++++++++---------- taskcp_test.go | 73 ++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 104 insertions(+), 29 deletions(-) diff --git a/mcp_test.go b/mcp_test.go index 50e78b8..80b419a 100644 --- a/mcp_test.go +++ b/mcp_test.go @@ -7,7 +7,7 @@ import ( ) func TestRegisterMCPTools(t *testing.T) { - service := New() + service := New("test_service") s := server.NewMCPServer("Test Server", "1.0.0") diff --git a/taskcp.go b/taskcp.go index 386f5e7..c55ec00 100644 --- a/taskcp.go +++ b/taskcp.go @@ -3,18 +3,21 @@ package taskcp import ( "fmt" "iter" + "strings" "github.com/google/uuid" ) type Service struct { - Projects map[string]*Project + Projects map[string]*Project + mcpService string } type Project struct { ID string Tasks map[string]*Task - NextTaskID string + nextTaskID string + mcpService string } type TaskState string @@ -34,13 +37,16 @@ type Task struct { Error string `json:"-"` Notes string `json:"-"` + projectID string + mcpService string nextTaskID string completionCallback func(task *Task) } -func New() *Service { +func New(mcpService string) *Service { return &Service{ - Projects: map[string]*Project{}, + Projects: map[string]*Project{}, + mcpService: mcpService, } } @@ -48,7 +54,8 @@ func (s *Service) AddProject() *Project { project := &Project{ ID: uuid.New().String(), Tasks: map[string]*Task{}, - NextTaskID: "", + nextTaskID: "", + mcpService: s.mcpService, } s.Projects[project.ID] = project return project @@ -66,10 +73,14 @@ func (s *Service) GetProject(id string) (*Project, error) { func (p *Project) InsertTaskBefore(beforeID string, instructions string, completionCallback func(task *Task)) *Task { task := p.newTask(instructions, completionCallback, beforeID) - for t := range p.tasks() { - if t.nextTaskID == beforeID { - t.nextTaskID = task.ID - break + if p.nextTaskID == "" && beforeID == "" { + p.nextTaskID = task.ID + } else { + for t := range p.tasks() { + if t.nextTaskID == beforeID { + t.nextTaskID = task.ID + break + } } } @@ -77,11 +88,11 @@ func (p *Project) InsertTaskBefore(beforeID string, instructions string, complet } func (p *Project) GetNextTask() *Task { - if p.NextTaskID == "" { + if p.nextTaskID == "" { return nil } - task := p.Tasks[p.NextTaskID] + task := p.Tasks[p.nextTaskID] task.State = TaskStateRunning return task } @@ -92,7 +103,7 @@ func (p *Project) SetTaskSuccess(id string, result string, notes string) *Task { task.Result = result task.Notes = notes task.completionCallback(task) - p.NextTaskID = task.nextTaskID + p.nextTaskID = task.nextTaskID return p.GetNextTask() } @@ -103,7 +114,7 @@ func (p *Project) SetTaskFailure(id string, error string, notes string) *Task { task.Error = error task.Notes = notes task.completionCallback(task) - p.NextTaskID = task.nextTaskID + p.nextTaskID = task.nextTaskID return p.GetNextTask() } @@ -115,14 +126,20 @@ func (p *Project) newTask(instructions string, completionCallback func(task *Tas nextTaskID: nextTaskID, Instructions: instructions, completionCallback: completionCallback, + projectID: p.ID, + mcpService: p.mcpService, } + + task.Instructions = strings.ReplaceAll(task.Instructions, "{SUCCESS_PROMPT}", task.SuccessPrompt()) + task.Instructions = strings.ReplaceAll(task.Instructions, "{FAILURE_PROMPT}", task.FailurePrompt()) + p.Tasks[task.ID] = task return task } func (p *Project) tasks() iter.Seq[*Task] { return func(yield func(*Task) bool) { - for tid := p.NextTaskID; tid != ""; tid = p.Tasks[tid].nextTaskID { + for tid := p.nextTaskID; tid != ""; tid = p.Tasks[tid].nextTaskID { t := p.Tasks[tid] if !yield(t) { return @@ -130,3 +147,16 @@ func (p *Project) tasks() iter.Seq[*Task] { } } } + +func (t *Task) SuccessPrompt() string { + return fmt.Sprintf(`To mark this task as successful, use the MCP tool: +%s.set_task_success(project_id="%s", task_id="%s", result="", notes="")`, + t.mcpService, t.projectID, t.ID) +} + +func (t *Task) FailurePrompt() string { + return fmt.Sprintf(`To mark this task as failed, use the MCP tool: +%s.set_task_failure(project_id="%s", task_id="%s", error="", notes="")`, + t.mcpService, t.projectID, t.ID) +} + diff --git a/taskcp_test.go b/taskcp_test.go index 8e8257e..2814466 100644 --- a/taskcp_test.go +++ b/taskcp_test.go @@ -7,19 +7,64 @@ import ( "github.com/stretchr/testify/require" ) -func TestTaskCP(t *testing.T) { - tcp := taskcp.New() - p := tcp.AddProject() - require.NotNil(t, p) - - tk := p.InsertTaskBefore(p.NextTaskID, "Hello, world!", func(task *taskcp.Task) { - t.Logf("Task %s changed: %+v", task.ID, task) - }) - require.NotNil(t, tk) - - p.SetTaskSuccess(tk.ID, "Hello, world!", "Notes") - require.Equal(t, taskcp.TaskStateSuccess, tk.State) - require.Equal(t, "Hello, world!", tk.Result) - require.Equal(t, "Notes", tk.Notes) +func TestTaskPrompts(t *testing.T) { + service := taskcp.New("my_service") + project := service.AddProject() + + task := project.InsertTaskBefore("", "Write unit tests", func(task *taskcp.Task) {}) + + successPrompt := task.SuccessPrompt() + require.Contains(t, successPrompt, "my_service.set_task_success") + require.Contains(t, successPrompt, `project_id="`+project.ID+`"`) + require.Contains(t, successPrompt, `task_id="`+task.ID+`"`) + + failurePrompt := task.FailurePrompt() + require.Contains(t, failurePrompt, "my_service.set_task_failure") + require.Contains(t, failurePrompt, `project_id="`+project.ID+`"`) + require.Contains(t, failurePrompt, `task_id="`+task.ID+`"`) } + +func TestPlaceholderExpansion(t *testing.T) { + service := taskcp.New("my_service") + project := service.AddProject() + + task1 := project.InsertTaskBefore("", "Please complete this task. {SUCCESS_PROMPT}", func(task *taskcp.Task) {}) + require.Contains(t, task1.Instructions, "my_service.set_task_success") + require.NotContains(t, task1.Instructions, "{SUCCESS_PROMPT}") + + task2 := project.InsertTaskBefore("", "Try this risky operation. {FAILURE_PROMPT}", func(task *taskcp.Task) {}) + require.Contains(t, task2.Instructions, "my_service.set_task_failure") + require.NotContains(t, task2.Instructions, "{FAILURE_PROMPT}") +} + +func TestTaskFlow(t *testing.T) { + service := taskcp.New("test_service") + project := service.AddProject() + + var completed []string + + task1 := project.InsertTaskBefore("", "First task", func(task *taskcp.Task) { + completed = append(completed, task.ID) + }) + + task2 := project.InsertTaskBefore("", "Second task", func(task *taskcp.Task) { + completed = append(completed, task.ID) + }) + + current := project.GetNextTask() + require.NotNil(t, current) + require.Equal(t, task1.ID, current.ID) + + next := project.SetTaskSuccess(current.ID, "Task 1 done", "") + require.NotNil(t, next) + require.Equal(t, task2.ID, next.ID) + require.Equal(t, taskcp.TaskStateRunning, next.State) + + next2 := project.SetTaskFailure(next.ID, "Task 2 failed", "Error details") + require.Nil(t, next2) + + require.Equal(t, []string{task1.ID, task2.ID}, completed) + require.Equal(t, taskcp.TaskStateSuccess, project.Tasks[task1.ID].State) + require.Equal(t, taskcp.TaskStateFailure, project.Tasks[task2.ID].State) +} \ No newline at end of file