diff --git a/apps/backend/internal/mcp/server/handoff_handlers.go b/apps/backend/internal/mcp/server/handoff_handlers.go index 3b12baf53b..528cade664 100644 --- a/apps/backend/internal/mcp/server/handoff_handlers.go +++ b/apps/backend/internal/mcp/server/handoff_handlers.go @@ -9,24 +9,31 @@ import ( "github.com/mark3labs/mcp-go/server" ) -// registerHandoffTools registers the office task-handoff MCP tools: -// list_related_tasks_kandev, list_task_documents_kandev, -// get_task_document_kandev, write_task_document_kandev. These are -// available in ModeOffice and ModeTask so both office coordinators and -// task assignees can read parent / sibling documents and write -// child→parent coordination docs. -func (s *Server) registerHandoffTools() { +// registerRelatedTasksTool registers list_related_tasks_kandev, which lets an +// agent discover parent / child / sibling / blocker task IDs. Useful in both +// kanban (ModeTask) and office (ModeOffice) modes — kanban agents commonly use +// it to find a sibling task to send a follow-up to via message_task_kandev. +func (s *Server) registerRelatedTasksTool() { s.mcpServer.AddTool( mcp.NewTool("list_related_tasks_kandev", mcp.WithDescription( `List parent, children, siblings, blockers, and blocked tasks for the current task. -Use this to discover task IDs you can read documents from. Document keys are included so you can pick -the document to fetch with get_task_document_kandev. Pass task_id to inspect a different task in the same workspace.`, +Use this to discover task IDs you can reach via message_task_kandev. In office mode, document keys are +also included so you can fetch documents with get_task_document_kandev. +Pass task_id to inspect a different task in the same workspace.`, ), mcp.WithString("task_id", mcp.Description("Defaults to the current task.")), ), s.wrapHandler("list_related_tasks_kandev", s.listRelatedTasksHandler()), ) +} + +// registerTaskDocumentTools registers the cross-task document tools used for +// office parent/child coordination: list_task_documents_kandev, +// get_task_document_kandev, write_task_document_kandev. These are office-only +// — kanban tasks don't use the document handoff pattern, so the surface is +// kept lean. +func (s *Server) registerTaskDocumentTools() { s.mcpServer.AddTool( mcp.NewTool("list_task_documents_kandev", mcp.WithDescription( diff --git a/apps/backend/internal/mcp/server/server.go b/apps/backend/internal/mcp/server/server.go index 1db879659d..8fe3b8f1d2 100644 --- a/apps/backend/internal/mcp/server/server.go +++ b/apps/backend/internal/mcp/server/server.go @@ -314,7 +314,8 @@ func (s *Server) registerTools() { // surface for office mode only keeps: // - ask_user_question — interactive prompt path // - plan tools — structured plan capture - // - handoff tools — workflow handoff between steps + // - related-tasks — discover parent/child/sibling IDs + // - task-document tools — parent/child coordination docs // delegate_task was removed in favour of // `agentctl kandev tasks create --parent $KANDEV_TASK_ID …`. if !s.disableAskQuestion { @@ -323,9 +324,14 @@ func (s *Server) registerTools() { } s.registerPlanTools() count += 4 - s.registerHandoffTools() - count += 4 + s.registerRelatedTasksTool() + count++ + s.registerTaskDocumentTools() + count += 3 default: // ModeTask + // Kanban tasks get list_related_tasks_kandev (useful for finding + // a sibling to message_task_kandev) but NOT the task-document + // tools — those are office coordination plumbing. s.registerKanbanTools() count += 11 if !s.disableAskQuestion { @@ -334,8 +340,8 @@ func (s *Server) registerTools() { } s.registerPlanTools() count += 4 - s.registerHandoffTools() - count += 4 + s.registerRelatedTasksTool() + count++ } s.logger.Info("registered MCP tools", zap.String("mode", s.mode), diff --git a/apps/backend/internal/mcp/server/server_test.go b/apps/backend/internal/mcp/server/server_test.go index 63f3d411b1..48ae27f493 100644 --- a/apps/backend/internal/mcp/server/server_test.go +++ b/apps/backend/internal/mcp/server/server_test.go @@ -59,6 +59,13 @@ func TestServerModeTask_RegistersCorrectTools(t *testing.T) { assert.Contains(t, tools, "list_agents_kandev") assert.Contains(t, tools, "list_executor_profiles_kandev") + // Task mode keeps list_related_tasks_kandev (sibling discovery) but + // drops the task-document tools — those are office-only. + assert.Contains(t, tools, "list_related_tasks_kandev") + assert.NotContains(t, tools, "list_task_documents_kandev") + assert.NotContains(t, tools, "get_task_document_kandev") + assert.NotContains(t, tools, "write_task_document_kandev") + // Task mode should NOT have config/mutation tools assert.NotContains(t, tools, "create_workflow_kandev") assert.NotContains(t, tools, "update_workflow_kandev") @@ -197,8 +204,9 @@ func TestServerModeTask_ToolCount(t *testing.T) { s := New(backend, "test-session", "test-task", 10005, log, "", false, ModeTask) tools := getRegisteredToolNames(s) - // 11 kanban + 1 interaction + 4 plan + 4 handoff = 20 - assert.Equal(t, 20, len(tools)) + // 11 kanban + 1 interaction + 4 plan + 1 related-tasks = 17. + // Task-document tools (list/get/write) are office-only. + assert.Equal(t, 17, len(tools)) } func TestServerModeConfig_ToolCount(t *testing.T) { @@ -279,8 +287,8 @@ func TestServerModeOffice_ToolCount(t *testing.T) { s := New(backend, "test-session", "test-task", 10005, log, "", false, ModeOffice) tools := getRegisteredToolNames(s) - // 4 plan + 1 interaction + 4 handoff = 9 (delegate_task_kandev - // retired in favour of `agentctl kandev task create …`). + // 4 plan + 1 interaction + 1 related-tasks + 3 task-documents = 9 + // (delegate_task_kandev retired in favour of `agentctl kandev task create …`). assert.Equal(t, 9, len(tools)) } @@ -298,7 +306,7 @@ func TestServerModeOffice_DisableAskQuestion(t *testing.T) { // delegate_task_kandev was retired from ModeOffice (now lives in // the agentctl CLI as `agentctl kandev task create --parent …`). assert.NotContains(t, tools, "delegate_task_kandev") - // 4 plan + 4 handoff = 8 (no ask_user_question, no delegate) + // 4 plan + 1 related-tasks + 3 task-documents = 8 (no ask_user_question, no delegate) assert.Equal(t, 8, len(tools)) }