diff --git a/apps/backend/internal/mcp/handlers/config_executor_handlers.go b/apps/backend/internal/mcp/handlers/config_executor_handlers.go index 1b782dbc88..e5774b3b06 100644 --- a/apps/backend/internal/mcp/handlers/config_executor_handlers.go +++ b/apps/backend/internal/mcp/handlers/config_executor_handlers.go @@ -11,6 +11,33 @@ import ( "go.uber.org/zap" ) +func (h *Handlers) handleListExecutors(ctx context.Context, msg *ws.Message) (*ws.Message, error) { + executors, err := h.taskSvc.ListExecutors(ctx) + if err != nil { + h.logger.Error("failed to list executors", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to list executors", nil) + } + + profiles, err := h.taskSvc.ListAllExecutorProfiles(ctx) + if err != nil { + h.logger.Error("failed to list executor profiles for executor listing", zap.Error(err)) + return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to list executor profiles", nil) + } + profilesByExecutor := make(map[string][]dto.ExecutorProfileDTO, len(executors)) + for _, profile := range profiles { + profilesByExecutor[profile.ExecutorID] = append(profilesByExecutor[profile.ExecutorID], dto.FromExecutorProfile(profile)) + } + + dtos := make([]dto.ExecutorDTO, 0, len(executors)) + for _, executor := range executors { + executorDTO := dto.FromExecutor(executor) + executorDTO.Profiles = profilesByExecutor[executor.ID] + dtos = append(dtos, executorDTO) + } + + return ws.NewResponse(msg.ID, msg.Action, dto.ListExecutorsResponse{Executors: dtos, Total: len(dtos)}) +} + func (h *Handlers) handleListExecutorProfiles(ctx context.Context, msg *ws.Message) (*ws.Message, error) { return h.handleListByField(ctx, msg, "executor_id", "failed to list executor profiles", "Failed to list executor profiles", func(ctx context.Context, executorID string) (any, error) { diff --git a/apps/backend/internal/mcp/handlers/config_handlers_test.go b/apps/backend/internal/mcp/handlers/config_handlers_test.go index 4313cd8e31..1f3d088be0 100644 --- a/apps/backend/internal/mcp/handlers/config_handlers_test.go +++ b/apps/backend/internal/mcp/handlers/config_handlers_test.go @@ -8,6 +8,7 @@ import ( "github.com/kandev/kandev/internal/common/logger" "github.com/kandev/kandev/internal/orchestrator/messagequeue" + "github.com/kandev/kandev/internal/task/dto" "github.com/kandev/kandev/internal/task/models" "github.com/kandev/kandev/internal/task/service" v1 "github.com/kandev/kandev/pkg/api/v1" @@ -618,6 +619,34 @@ func TestRegisterHandlers_NilDeps_DoesNotPanic(t *testing.T) { assert.NotPanics(t, func() { h.RegisterHandlers(d) }) } +func TestRegisterHandlers_ListExecutorsActionDispatches(t *testing.T) { + svc, _ := newTestTaskService(t) + h := &Handlers{taskSvc: svc, logger: testLogger(t).WithFields()} + d := ws.NewDispatcher() + h.RegisterHandlers(d) + + msg := makeWSMessage(t, ws.ActionMCPListExecutors, map[string]interface{}{}) + resp, err := d.Dispatch(context.Background(), msg) + + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, ws.MessageTypeResponse, resp.Type) + assert.Equal(t, ws.ActionMCPListExecutors, resp.Action) + + var payload dto.ListExecutorsResponse + require.NoError(t, json.Unmarshal(resp.Payload, &payload)) + assert.Equal(t, len(payload.Executors), payload.Total) + assert.Contains(t, executorIDs(payload.Executors), models.ExecutorIDLocal) +} + +func executorIDs(executors []dto.ExecutorDTO) []string { + ids := make([]string, 0, len(executors)) + for _, executor := range executors { + ids = append(ids, executor.ID) + } + return ids +} + // --- Helper function tests --- func TestUnmarshalStringField(t *testing.T) { diff --git a/apps/backend/internal/mcp/handlers/handlers.go b/apps/backend/internal/mcp/handlers/handlers.go index 17a9c4e0f6..9b8e618dea 100644 --- a/apps/backend/internal/mcp/handlers/handlers.go +++ b/apps/backend/internal/mcp/handlers/handlers.go @@ -223,10 +223,11 @@ func (h *Handlers) RegisterHandlers(d *ws.Dispatcher) { d.RegisterFunc(ws.ActionMCPDeleteAgentProfile, h.handleDeleteAgentProfile) count += 6 } - // list_executor_profiles is always available (read-only, used in task mode for create_task) + // Executor discovery/profile listing is always available (read-only, used in task mode for create_task) if h.taskSvc != nil { + d.RegisterFunc(ws.ActionMCPListExecutors, h.handleListExecutors) d.RegisterFunc(ws.ActionMCPListExecutorProfiles, h.handleListExecutorProfiles) - count++ + count += 2 } if h.mcpConfigSvc != nil { d.RegisterFunc(ws.ActionMCPGetMcpConfig, h.handleGetMcpConfig)