From e545257be8a4b7dbafad2798a420df56e6f387a5 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 20 Aug 2026 05:59:23 +0000 Subject: [PATCH 1/3] feat(codersdk/toolsdk): remove task management MCP tools --- codersdk/toolsdk/toolsdk.go | 308 -------------- codersdk/toolsdk/toolsdk_test.go | 662 ------------------------------- 2 files changed, 970 deletions(-) diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go index c808bc1b107..6b33b7a6ad4 100644 --- a/codersdk/toolsdk/toolsdk.go +++ b/codersdk/toolsdk/toolsdk.go @@ -52,12 +52,6 @@ const ( ToolNameWorkspaceEditFiles = "coder_workspace_edit_files" ToolNameWorkspacePortForward = "coder_workspace_port_forward" ToolNameWorkspaceListApps = "coder_workspace_list_apps" - ToolNameCreateTask = "coder_create_task" - ToolNameDeleteTask = "coder_delete_task" - ToolNameListTasks = "coder_list_tasks" - ToolNameGetTaskStatus = "coder_get_task_status" - ToolNameSendTaskInput = "coder_send_task_input" - ToolNameGetTaskLogs = "coder_get_task_logs" ToolNameCreateChat = "coder_create_chat" ToolNameGetChat = "coder_get_chat" ToolNameDownloadChatFile = "coder_download_chat_file" @@ -342,12 +336,6 @@ var All = []GenericTool{ WorkspaceEditFiles.Generic(), WorkspacePortForward.Generic(), WorkspaceListApps.Generic(), - CreateTask.Generic(), - DeleteTask.Generic(), - ListTasks.Generic(), - GetTaskStatus.Generic(), - SendTaskInput.Generic(), - GetTaskLogs.Generic(), CreateChat.Generic(), GetChat.Generic(), DownloadChatFile.Generic(), @@ -2203,298 +2191,6 @@ var WorkspaceListApps = Tool[WorkspaceListAppsArgs, WorkspaceListAppsResponse]{ }, } -type CreateTaskArgs struct { - Input string `json:"input"` - TemplateVersionID string `json:"template_version_id"` - TemplateVersionPresetID string `json:"template_version_preset_id"` - User string `json:"user"` -} - -var CreateTask = Tool[CreateTaskArgs, codersdk.Task]{ - Tool: aisdk.Tool{ - Name: ToolNameCreateTask, - Description: `Create a task.`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "input": map[string]any{ - "type": "string", - "description": "Input/prompt for the task.", - }, - "template_version_id": map[string]any{ - "type": "string", - "description": "ID of the template version to create the task from.", - }, - "template_version_preset_id": map[string]any{ - "type": "string", - "description": "Optional ID of the template version preset to create the task from.", - }, - "user": map[string]any{ - "type": "string", - "description": userDescription("create a task"), - }, - }, - Required: []string{"input", "template_version_id"}, - }, - }, - MCPAnnotations: mcpMutationAnnotations, - UserClientOptional: true, - Handler: func(ctx context.Context, deps Deps, args CreateTaskArgs) (codersdk.Task, error) { - if args.Input == "" { - return codersdk.Task{}, xerrors.New("input is required") - } - - tvID, err := uuid.Parse(args.TemplateVersionID) - if err != nil { - return codersdk.Task{}, xerrors.New("template_version_id must be a valid UUID") - } - - var tvPresetID uuid.UUID - if args.TemplateVersionPresetID != "" { - tvPresetID, err = uuid.Parse(args.TemplateVersionPresetID) - if err != nil { - return codersdk.Task{}, xerrors.New("template_version_preset_id must be a valid UUID") - } - } - - if args.User == "" { - args.User = codersdk.Me - } - - task, err := deps.coderClient.CreateTask(ctx, args.User, codersdk.CreateTaskRequest{ - Input: args.Input, - TemplateVersionID: tvID, - TemplateVersionPresetID: tvPresetID, - }) - if err != nil { - return codersdk.Task{}, xerrors.Errorf("create task: %w", err) - } - - return task, nil - }, -} - -type DeleteTaskArgs struct { - TaskID string `json:"task_id"` -} - -var DeleteTask = Tool[DeleteTaskArgs, codersdk.Response]{ - Tool: aisdk.Tool{ - Name: ToolNameDeleteTask, - Description: `Delete a task.`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "task_id": map[string]any{ - "type": "string", - "description": taskIDDescription("delete"), - }, - }, - Required: []string{"task_id"}, - }, - }, - MCPAnnotations: mcpDestructiveAnnotations, - UserClientOptional: true, - Handler: func(ctx context.Context, deps Deps, args DeleteTaskArgs) (codersdk.Response, error) { - if args.TaskID == "" { - return codersdk.Response{}, xerrors.New("task_id is required") - } - - task, err := deps.coderClient.TaskByIdentifier(ctx, args.TaskID) - if err != nil { - return codersdk.Response{}, xerrors.Errorf("resolve task: %w", err) - } - - err = deps.coderClient.DeleteTask(ctx, task.OwnerName, task.ID) - if err != nil { - return codersdk.Response{}, xerrors.Errorf("delete task: %w", err) - } - - return codersdk.Response{ - Message: "Task deleted successfully", - }, nil - }, -} - -type ListTasksArgs struct { - Status codersdk.TaskStatus `json:"status"` - User string `json:"user"` -} - -type ListTasksResponse struct { - Tasks []codersdk.Task `json:"tasks"` -} - -var ListTasks = Tool[ListTasksArgs, ListTasksResponse]{ - Tool: aisdk.Tool{ - Name: ToolNameListTasks, - Description: `List tasks.`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "status": map[string]any{ - "type": "string", - "description": "Optional filter by task status.", - }, - "user": map[string]any{ - "type": "string", - "description": userDescription("list tasks"), - }, - }, - Required: []string{}, - }, - }, - MCPAnnotations: mcpReadOnlyAnnotations, - UserClientOptional: true, - Handler: func(ctx context.Context, deps Deps, args ListTasksArgs) (ListTasksResponse, error) { - if args.User == "" { - args.User = codersdk.Me - } - - tasks, err := deps.coderClient.Tasks(ctx, &codersdk.TasksFilter{ - Owner: args.User, - Status: args.Status, - }) - if err != nil { - return ListTasksResponse{}, xerrors.Errorf("list tasks: %w", err) - } - - return ListTasksResponse{ - Tasks: tasks, - }, nil - }, -} - -type GetTaskStatusArgs struct { - TaskID string `json:"task_id"` -} - -type GetTaskStatusResponse struct { - Status codersdk.TaskStatus `json:"status"` - State *codersdk.TaskStateEntry `json:"state"` -} - -var GetTaskStatus = Tool[GetTaskStatusArgs, GetTaskStatusResponse]{ - Tool: aisdk.Tool{ - Name: ToolNameGetTaskStatus, - Description: `Get the status of a task.`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "task_id": map[string]any{ - "type": "string", - "description": taskIDDescription("get"), - }, - }, - Required: []string{"task_id"}, - }, - }, - MCPAnnotations: mcpReadOnlyAnnotations, - UserClientOptional: true, - Handler: func(ctx context.Context, deps Deps, args GetTaskStatusArgs) (GetTaskStatusResponse, error) { - if args.TaskID == "" { - return GetTaskStatusResponse{}, xerrors.New("task_id is required") - } - - task, err := deps.coderClient.TaskByIdentifier(ctx, args.TaskID) - if err != nil { - return GetTaskStatusResponse{}, xerrors.Errorf("resolve task %q: %w", args.TaskID, err) - } - - return GetTaskStatusResponse{ - Status: task.Status, - State: task.CurrentState, - }, nil - }, -} - -type SendTaskInputArgs struct { - TaskID string `json:"task_id"` - Input string `json:"input"` -} - -var SendTaskInput = Tool[SendTaskInputArgs, codersdk.Response]{ - Tool: aisdk.Tool{ - Name: ToolNameSendTaskInput, - Description: `Send input to a running task.`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "task_id": map[string]any{ - "type": "string", - "description": taskIDDescription("prompt"), - }, - "input": map[string]any{ - "type": "string", - "description": "The input to send to the task.", - }, - }, - Required: []string{"task_id", "input"}, - }, - }, - MCPAnnotations: mcpMutationAnnotations, - UserClientOptional: true, - Handler: func(ctx context.Context, deps Deps, args SendTaskInputArgs) (codersdk.Response, error) { - if args.TaskID == "" { - return codersdk.Response{}, xerrors.New("task_id is required") - } - - if args.Input == "" { - return codersdk.Response{}, xerrors.New("input is required") - } - - task, err := deps.coderClient.TaskByIdentifier(ctx, args.TaskID) - if err != nil { - return codersdk.Response{}, xerrors.Errorf("resolve task %q: %w", args.TaskID, err) - } - - err = deps.coderClient.TaskSend(ctx, task.OwnerName, task.ID, codersdk.TaskSendRequest{ - Input: args.Input, - }) - if err != nil { - return codersdk.Response{}, xerrors.Errorf("send task input %q: %w", args.TaskID, err) - } - - return codersdk.Response{ - Message: "Input sent to task successfully.", - }, nil - }, -} - -type GetTaskLogsArgs struct { - TaskID string `json:"task_id"` -} - -var GetTaskLogs = Tool[GetTaskLogsArgs, codersdk.TaskLogsResponse]{ - Tool: aisdk.Tool{ - Name: ToolNameGetTaskLogs, - Description: `Get the logs of a task.`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "task_id": map[string]any{ - "type": "string", - "description": taskIDDescription("query"), - }, - }, - Required: []string{"task_id"}, - }, - }, - MCPAnnotations: mcpReadOnlyAnnotations, - UserClientOptional: true, - Handler: func(ctx context.Context, deps Deps, args GetTaskLogsArgs) (codersdk.TaskLogsResponse, error) { - if args.TaskID == "" { - return codersdk.TaskLogsResponse{}, xerrors.New("task_id is required") - } - - task, err := deps.coderClient.TaskByIdentifier(ctx, args.TaskID) - if err != nil { - return codersdk.TaskLogsResponse{}, err - } - - logs, err := deps.coderClient.TaskLogs(ctx, task.OwnerName, task.ID) - if err != nil { - return codersdk.TaskLogsResponse{}, xerrors.Errorf("get task logs %q: %w", args.TaskID, err) - } - - return logs, nil - }, -} - // NormalizeWorkspaceInput converts workspace name input to standard format. // Handles the following input formats: // - workspace → workspace @@ -2525,10 +2221,6 @@ const workspaceDescription = "The workspace ID or name in the format [owner/]wor const workspaceAgentDescription = "The workspace name in the format [owner/]workspace[.agent]. If an owner is not specified, the authenticated user is used." -func taskIDDescription(action string) string { - return fmt.Sprintf("ID or workspace identifier in the format [owner/]workspace[.agent] for the task to %s. If an owner is not specified, the authenticated user is used.", action) -} - func userDescription(action string) string { return fmt.Sprintf("Username or ID of the user for which to %s. Omit or use the `me` keyword to %s for the authenticated user.", action, action) } diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go index 44414d85fec..55911f5290f 100644 --- a/codersdk/toolsdk/toolsdk_test.go +++ b/codersdk/toolsdk/toolsdk_test.go @@ -6,8 +6,6 @@ import ( "encoding/json" "flag" "fmt" - "net/http" - "net/http/httptest" "os" "path/filepath" "sort" @@ -22,7 +20,6 @@ import ( "go.uber.org/goleak" "golang.org/x/xerrors" - agentapi "github.com/coder/agentapi-sdk-go" "github.com/coder/aisdk-go" "github.com/coder/coder/v2/agent" "github.com/coder/coder/v2/agent/agenttest" @@ -31,12 +28,10 @@ import ( "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbgen" - "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/toolsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" - "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" ) @@ -1587,384 +1582,6 @@ func TestTools(t *testing.T) { } }) - t.Run("CreateTask", func(t *testing.T) { - t.Parallel() - - presetID := uuid.New() - // nolint:gocritic // This is in a test package and does not end up in the build - aiTV := dbfake.TemplateVersion(t, store).Seed(database.TemplateVersion{ - OrganizationID: owner.OrganizationID, - CreatedBy: member.ID, - HasAITask: sql.NullBool{ - Bool: true, - Valid: true, - }, - }).Preset(database.TemplateVersionPreset{ - ID: presetID, - DesiredInstances: sql.NullInt32{ - Int32: 1, - Valid: true, - }, - }).Do() - - tests := []struct { - name string - args toolsdk.CreateTaskArgs - error string - }{ - { - name: "OK", - args: toolsdk.CreateTaskArgs{ - TemplateVersionID: aiTV.TemplateVersion.ID.String(), - Input: "do a barrel roll", - User: "me", - }, - }, - { - name: "NoUser", - args: toolsdk.CreateTaskArgs{ - TemplateVersionID: aiTV.TemplateVersion.ID.String(), - Input: "do another barrel roll", - }, - }, - { - name: "NoInput", - args: toolsdk.CreateTaskArgs{ - TemplateVersionID: aiTV.TemplateVersion.ID.String(), - }, - error: "input is required", - }, - { - name: "NotTaskTemplate", - args: toolsdk.CreateTaskArgs{ - TemplateVersionID: r.TemplateVersion.ID.String(), - Input: "do yet another barrel roll", - }, - error: "Template does not have a valid \"coder_ai_task\" resource.", - }, - { - name: "WithPreset", - args: toolsdk.CreateTaskArgs{ - TemplateVersionID: aiTV.TemplateVersion.ID.String(), - TemplateVersionPresetID: presetID.String(), - Input: "not enough barrel rolls", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - - _, err = testTool(t, toolsdk.CreateTask, tb, tt.args) - if tt.error != "" { - require.Error(t, err) - require.ErrorContains(t, err, tt.error) - } else { - require.NoError(t, err) - } - }) - } - }) - - t.Run("DeleteTask", func(t *testing.T) { - t.Parallel() - - // nolint:gocritic // This is in a test package and does not end up in the build - aiTV := dbfake.TemplateVersion(t, store).Seed(database.TemplateVersion{ - OrganizationID: owner.OrganizationID, - CreatedBy: member.ID, - HasAITask: sql.NullBool{ - Bool: true, - Valid: true, - }, - }).Do() - - build1 := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{ - Name: "delete-task-workspace-1", - OrganizationID: owner.OrganizationID, - OwnerID: member.ID, - TemplateID: aiTV.Template.ID, - }).WithTask(database.TaskTable{ - Name: "delete-task-1", - Prompt: "delete task 1", - }, nil).Do() - task1 := build1.Task - - build2 := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{ - Name: "delete-task-workspace-2", - OrganizationID: owner.OrganizationID, - OwnerID: member.ID, - TemplateID: aiTV.Template.ID, - }).WithTask(database.TaskTable{ - Name: "delete-task-2", - Prompt: "delete task 2", - }, nil).Do() - task2 := build2.Task - - tests := []struct { - name string - args toolsdk.DeleteTaskArgs - error string - }{ - { - name: "ByUUID", - args: toolsdk.DeleteTaskArgs{ - TaskID: task1.ID.String(), - }, - }, - { - name: "ByIdentifier", - args: toolsdk.DeleteTaskArgs{ - TaskID: task2.Name, - }, - }, - { - name: "NoID", - args: toolsdk.DeleteTaskArgs{}, - error: "task_id is required", - }, - { - name: "NoTaskByID", - args: toolsdk.DeleteTaskArgs{ - TaskID: uuid.New().String(), - }, - error: "Resource not found", - }, - { - name: "NoTaskByWorkspaceIdentifier", - args: toolsdk.DeleteTaskArgs{ - TaskID: "non-existent", - }, - error: "Resource not found", - }, - { - name: "ExistsButNotATask", - args: toolsdk.DeleteTaskArgs{ - TaskID: r.Workspace.ID.String(), - }, - error: "Resource not found", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - - _, err = testTool(t, toolsdk.DeleteTask, tb, tt.args) - if tt.error != "" { - require.Error(t, err) - require.ErrorContains(t, err, tt.error) - } else { - require.NoError(t, err) - } - }) - } - }) - - t.Run("ListTasks", func(t *testing.T) { - t.Parallel() - - ctx := testutil.Context(t, testutil.WaitLong) - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - owner := coderdtest.CreateFirstUser(t, client) - _, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - taskClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - - // Create a template with AI task support using the proper flow. - version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: echo.ApplyComplete, - ProvisionGraph: []*proto.Response{ - {Type: &proto.Response_Graph{Graph: &proto.GraphComplete{ - HasAiTasks: true, - }}}, - }, - }) - coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) - template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) - - // This task should not show up since listing is user-scoped. - _, err := client.CreateTask(ctx, member.Username, codersdk.CreateTaskRequest{ - TemplateVersionID: template.ActiveVersionID, - Input: "task for member", - Name: "list-task-workspace-member", - }) - require.NoError(t, err) - - // Create tasks for taskUser. These should show up in the list. - for i := range 5 { - taskName := fmt.Sprintf("list-task-workspace-%d", i) - task, err := taskClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{ - TemplateVersionID: template.ActiveVersionID, - Input: fmt.Sprintf("task %d", i), - Name: taskName, - }) - require.NoError(t, err) - require.True(t, task.WorkspaceID.Valid, "task should have workspace ID") - - // For the first task, stop the workspace to make it paused. - if i == 0 { - ws, err := taskClient.Workspace(ctx, task.WorkspaceID.UUID) - require.NoError(t, err) - coderdtest.AwaitWorkspaceBuildJobCompleted(t, taskClient, ws.LatestBuild.ID) - - // Stop the workspace to set task status to paused. - build, err := taskClient.CreateWorkspaceBuild(ctx, task.WorkspaceID.UUID, codersdk.CreateWorkspaceBuildRequest{ - Transition: codersdk.WorkspaceTransitionStop, - }) - require.NoError(t, err) - coderdtest.AwaitWorkspaceBuildJobCompleted(t, taskClient, build.ID) - } - } - - tests := []struct { - name string - args toolsdk.ListTasksArgs - expected []string - error string - }{ - { - name: "ListAllOwned", - args: toolsdk.ListTasksArgs{}, - expected: []string{ - "list-task-workspace-0", - "list-task-workspace-1", - "list-task-workspace-2", - "list-task-workspace-3", - "list-task-workspace-4", - }, - }, - { - name: "ListFiltered", - args: toolsdk.ListTasksArgs{ - Status: codersdk.TaskStatusPaused, - }, - expected: []string{ - "list-task-workspace-0", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - tb, err := toolsdk.NewDeps(taskClient) - require.NoError(t, err) - - res, err := testTool(t, toolsdk.ListTasks, tb, tt.args) - if tt.error != "" { - require.Error(t, err) - require.ErrorContains(t, err, tt.error) - } else { - require.NoError(t, err) - require.Len(t, res.Tasks, len(tt.expected)) - for _, task := range res.Tasks { - require.Contains(t, tt.expected, task.Name) - } - } - }) - } - }) - - t.Run("GetTask", func(t *testing.T) { - t.Parallel() - - // nolint:gocritic // This is in a test package and does not end up in the build - aiTV := dbfake.TemplateVersion(t, store).Seed(database.TemplateVersion{ - OrganizationID: owner.OrganizationID, - CreatedBy: member.ID, - HasAITask: sql.NullBool{ - Bool: true, - Valid: true, - }, - }).Do() - - build := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{ - Name: "get-task-workspace-1", - OrganizationID: owner.OrganizationID, - OwnerID: member.ID, - TemplateID: aiTV.Template.ID, - }).WithTask(database.TaskTable{ - Name: "get-task-1", - Prompt: "get task", - }, nil).Do() - task := build.Task - - tests := []struct { - name string - args toolsdk.GetTaskStatusArgs - expected codersdk.TaskStatus - error string - }{ - { - name: "ByUUID", - args: toolsdk.GetTaskStatusArgs{ - TaskID: task.ID.String(), - }, - expected: codersdk.TaskStatusInitializing, - }, - { - name: "ByIdentifier", - args: toolsdk.GetTaskStatusArgs{ - TaskID: task.Name, - }, - expected: codersdk.TaskStatusInitializing, - }, - { - name: "NoID", - args: toolsdk.GetTaskStatusArgs{}, - error: "task_id is required", - }, - { - name: "NoTaskByID", - args: toolsdk.GetTaskStatusArgs{ - TaskID: uuid.New().String(), - }, - error: "Resource not found", - }, - { - name: "NoTaskByWorkspaceIdentifier", - args: toolsdk.GetTaskStatusArgs{ - TaskID: "non-existent", - }, - error: "Resource not found", - }, - { - name: "ExistsButNotATask", - args: toolsdk.GetTaskStatusArgs{ - TaskID: r.Workspace.ID.String(), - }, - error: "Resource not found", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - - res, err := testTool(t, toolsdk.GetTaskStatus, tb, tt.args) - if tt.error != "" { - require.Error(t, err) - require.ErrorContains(t, err, tt.error) - } else { - require.NoError(t, err) - require.Equal(t, tt.expected, res.Status) - } - }) - } - }) - t.Run("WorkspaceListApps", func(t *testing.T) { t.Parallel() @@ -2104,285 +1721,6 @@ func TestTools(t *testing.T) { }) } }) - - t.Run("SendTaskInput", func(t *testing.T) { - t.Parallel() - - // Start a fake AgentAPI that accepts GET /status and POST /message. - srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet && r.URL.Path == "/status" { - httpapi.Write(r.Context(), rw, http.StatusOK, agentapi.GetStatusResponse{ - Status: agentapi.StatusStable, - }) - return - } - if r.Method == http.MethodPost && r.URL.Path == "/message" { - rw.Header().Set("Content-Type", "application/json") - - var req agentapi.PostMessageParams - ok := httpapi.Read(r.Context(), rw, r, &req) - assert.True(t, ok, "failed to read request") - - assert.Equal(t, req.Content, "frob the baz") - assert.Equal(t, req.Type, agentapi.MessageTypeUser) - - httpapi.Write(r.Context(), rw, http.StatusOK, agentapi.PostMessageResponse{ - Ok: true, - }) - return - } - rw.WriteHeader(http.StatusInternalServerError) - })) - t.Cleanup(srv.Close) - - // nolint:gocritic // This is in a test package and does not end up in the build - aiTV := dbfake.TemplateVersion(t, store).Seed(database.TemplateVersion{ - OrganizationID: owner.OrganizationID, - CreatedBy: member.ID, - HasAITask: sql.NullBool{ - Bool: true, - Valid: true, - }, - }).Do() - - ws := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{ - Name: "send-task-input-ws", - OrganizationID: owner.OrganizationID, - OwnerID: member.ID, - TemplateID: aiTV.Template.ID, - }).WithTask(database.TaskTable{ - Name: "send-task-input", - Prompt: "send task input", - }, &proto.App{Url: srv.URL}).Do() - task := ws.Task - - _ = agenttest.New(t, client.URL, ws.AgentToken) - coderdtest.NewWorkspaceAgentWaiter(t, client, ws.Workspace.ID). - WaitFor(coderdtest.AgentsReady) - - ctx := testutil.Context(t, testutil.WaitShort) - - // Ensure the app is healthy (required to send task input). - err := store.UpdateWorkspaceAppHealthByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceAppHealthByIDParams{ - ID: task.WorkspaceAppID.UUID, - Health: database.WorkspaceAppHealthHealthy, - }) - require.NoError(t, err) - - tests := []struct { - name string - args toolsdk.SendTaskInputArgs - error string - }{ - { - name: "ByUUID", - args: toolsdk.SendTaskInputArgs{ - TaskID: task.ID.String(), - Input: "frob the baz", - }, - }, - { - name: "ByIdentifier", - args: toolsdk.SendTaskInputArgs{ - TaskID: task.Name, - Input: "frob the baz", - }, - }, - { - name: "NoID", - args: toolsdk.SendTaskInputArgs{}, - error: "task_id is required", - }, - { - name: "NoInput", - args: toolsdk.SendTaskInputArgs{ - TaskID: "send-task-input", - }, - error: "input is required", - }, - { - name: "NoTaskByID", - args: toolsdk.SendTaskInputArgs{ - TaskID: uuid.New().String(), - Input: "this is ignored", - }, - error: "Resource not found", - }, - { - name: "NoTaskByWorkspaceIdentifier", - args: toolsdk.SendTaskInputArgs{ - TaskID: "non-existent", - Input: "this is ignored", - }, - error: "Resource not found", - }, - { - name: "ExistsButNotATask", - args: toolsdk.SendTaskInputArgs{ - TaskID: r.Workspace.ID.String(), - Input: "this is ignored", - }, - error: "Resource not found", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - - _, err = testTool(t, toolsdk.SendTaskInput, tb, tt.args) - if tt.error != "" { - require.Error(t, err) - require.ErrorContains(t, err, tt.error) - } else { - require.NoError(t, err) - } - }) - } - }) - - t.Run("GetTaskLogs", func(t *testing.T) { - t.Parallel() - - messages := []agentapi.Message{ - { - Id: 0, - Content: "welcome", - Role: agentapi.RoleAgent, - }, - { - Id: 1, - Content: "frob the dazzle", - Role: agentapi.RoleUser, - }, - { - Id: 2, - Content: "frob dazzled", - Role: agentapi.RoleAgent, - }, - } - - // Start a fake AgentAPI that returns some messages. - srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet && r.URL.Path == "/messages" { - httpapi.Write(r.Context(), rw, http.StatusOK, agentapi.GetMessagesResponse{ - Messages: messages, - }) - return - } - rw.WriteHeader(http.StatusInternalServerError) - })) - t.Cleanup(srv.Close) - - // nolint:gocritic // This is in a test package and does not end up in the build - aiTV := dbfake.TemplateVersion(t, store).Seed(database.TemplateVersion{ - OrganizationID: owner.OrganizationID, - CreatedBy: member.ID, - HasAITask: sql.NullBool{ - Bool: true, - Valid: true, - }, - }).Do() - - ws := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{ - Name: "get-task-logs-ws", - OrganizationID: owner.OrganizationID, - OwnerID: member.ID, - TemplateID: aiTV.Template.ID, - }).WithTask(database.TaskTable{ - Name: "get-task-logs", - Prompt: "get task logs", - }, &proto.App{Url: srv.URL}).Do() - task := ws.Task - - _ = agenttest.New(t, client.URL, ws.AgentToken) - coderdtest.NewWorkspaceAgentWaiter(t, client, ws.Workspace.ID). - WaitFor(coderdtest.AgentsReady) - - ctx := testutil.Context(t, testutil.WaitShort) - - // Ensure the app is healthy (required to read task logs). - err := store.UpdateWorkspaceAppHealthByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceAppHealthByIDParams{ - ID: task.WorkspaceAppID.UUID, - Health: database.WorkspaceAppHealthHealthy, - }) - require.NoError(t, err) - - tests := []struct { - name string - args toolsdk.GetTaskLogsArgs - expected []agentapi.Message - error string - }{ - { - name: "ByUUID", - args: toolsdk.GetTaskLogsArgs{ - TaskID: task.ID.String(), - }, - expected: messages, - }, - { - name: "ByIdentifier", - args: toolsdk.GetTaskLogsArgs{ - TaskID: task.Name, - }, - expected: messages, - }, - { - name: "NoID", - args: toolsdk.GetTaskLogsArgs{}, - error: "task_id is required", - }, - { - name: "NoTaskByID", - args: toolsdk.GetTaskLogsArgs{ - TaskID: uuid.New().String(), - }, - error: "Resource not found", - }, - { - name: "NoTaskByWorkspaceIdentifier", - args: toolsdk.GetTaskLogsArgs{ - TaskID: "non-existent", - }, - error: "Resource not found", - }, - { - name: "ExistsButNotATask", - args: toolsdk.GetTaskLogsArgs{ - TaskID: r.Workspace.ID.String(), - }, - error: "Resource not found", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - - res, err := testTool(t, toolsdk.GetTaskLogs, tb, tt.args) - if tt.error != "" { - require.Error(t, err) - require.ErrorContains(t, err, tt.error) - } else { - require.NoError(t, err) - require.Len(t, res.Logs, len(tt.expected)) - for i, msg := range tt.expected { - require.Equal(t, msg.Id, int64(res.Logs[i].ID)) - require.Equal(t, msg.Content, res.Logs[i].Content) - if msg.Role == agentapi.RoleUser { - require.Equal(t, codersdk.TaskLogTypeInput, res.Logs[i].Type) - } else { - require.Equal(t, codersdk.TaskLogTypeOutput, res.Logs[i].Type) - } - require.Equal(t, msg.Time, res.Logs[i].Time) - } - } - }) - } - }) } // TestedTools keeps track of which tools have been tested. From e52a41404d330584bb8a3bc05e02fbf5dc78757e Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:27:57 +0000 Subject: [PATCH 2/3] docs: remove task management tools from MCP server tool list --- docs/ai-coder/mcp-server.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/ai-coder/mcp-server.md b/docs/ai-coder/mcp-server.md index fb5b61fa61c..f16b4089814 100644 --- a/docs/ai-coder/mcp-server.md +++ b/docs/ai-coder/mcp-server.md @@ -95,7 +95,7 @@ through `2026-07-28`, does not issue `Mcp-Session-Id` headers, and answers server-event stream or explicit session termination, both permitted by the specification). The server exposes tools only; MCP resources, prompts, structured tool output, elicitation, and the MCP Tasks extension (which is -unrelated to Coder's task tools) are not implemented. +unrelated to Coder's task reporting tool) are not implemented. ### Prerequisites @@ -198,7 +198,6 @@ The MCP server exposes tools across several areas: - **Template operations**: list, inspect, create, and manage templates and versions - **File operations**: read, write, and edit files in a workspace - **Workspace interaction**: run commands, forward ports, list apps, and read logs -- **Task management**: create, list, inspect, and control tasks - **Coder Agents chats**: create chats, send messages, read transcripts and status, interrupt, archive, and list available models - **User and system**: authenticated user details, tar uploads, and task reporting From e2183d4434d19856b0c9aed844b1a1cfef77e0f3 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:50:00 +0000 Subject: [PATCH 3/3] docs: drop MCP Tasks extension disambiguation parenthetical --- docs/ai-coder/mcp-server.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ai-coder/mcp-server.md b/docs/ai-coder/mcp-server.md index f16b4089814..8905f97538d 100644 --- a/docs/ai-coder/mcp-server.md +++ b/docs/ai-coder/mcp-server.md @@ -94,8 +94,8 @@ through `2026-07-28`, does not issue `Mcp-Session-Id` headers, and answers `GET` and `DELETE` with `405 Method Not Allowed` (there is no standalone server-event stream or explicit session termination, both permitted by the specification). The server exposes tools only; MCP resources, prompts, -structured tool output, elicitation, and the MCP Tasks extension (which is -unrelated to Coder's task reporting tool) are not implemented. +structured tool output, elicitation, and the MCP Tasks extension are not +implemented. ### Prerequisites