diff --git a/pkg/cli/mcp_server.go b/pkg/cli/mcp_server.go index d1760f49e9..19371793ec 100644 --- a/pkg/cli/mcp_server.go +++ b/pkg/cli/mcp_server.go @@ -14,6 +14,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/workflow" "github.com/modelcontextprotocol/go-sdk/jsonrpc" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -162,6 +163,49 @@ func hasWriteAccess(permission string) bool { } } +// validateWorkflowName validates that a workflow name exists. +// Returns nil if the workflow exists, or an error with suggestions if not. +// Empty workflow names are considered valid (means all workflows). +func validateWorkflowName(workflowName string) error { + // Empty workflow name means "all workflows" - this is valid + if workflowName == "" { + return nil + } + + mcpLog.Printf("Validating workflow name: %s", workflowName) + + // Try to resolve as workflow ID first + resolvedName, err := workflow.ResolveWorkflowName(workflowName) + if err == nil { + mcpLog.Printf("Workflow name resolved successfully: %s -> %s", workflowName, resolvedName) + return nil + } + + // Check if it's a valid GitHub Actions workflow name + agenticWorkflowNames, nameErr := getAgenticWorkflowNames(false) + if nameErr == nil && sliceutil.Contains(agenticWorkflowNames, workflowName) { + mcpLog.Printf("Workflow name is valid GitHub Actions workflow name: %s", workflowName) + return nil + } + + // Workflow not found - build error with suggestions + mcpLog.Printf("Workflow name not found: %s", workflowName) + + suggestions := []string{ + "Use the 'status' tool to see all available workflows", + "Check for typos in the workflow name", + "Use the workflow ID (e.g., 'test-claude') or GitHub Actions workflow name (e.g., 'Test Claude')", + } + + // Add fuzzy match suggestions + similarNames := suggestWorkflowNames(workflowName) + if len(similarNames) > 0 { + suggestions = append([]string{fmt.Sprintf("Did you mean: %s?", strings.Join(similarNames, ", "))}, suggestions...) + } + + return fmt.Errorf("workflow '%s' not found. %s", workflowName, strings.Join(suggestions, " ")) +} + // NewMCPServerCommand creates the mcp-server command func NewMCPServerCommand() *cobra.Command { var port int @@ -718,6 +762,19 @@ return a schema description instead of the full output. Adjust the 'max_tokens' } } + // Validate workflow name before executing command + if err := validateWorkflowName(args.WorkflowName); err != nil { + mcpLog.Printf("Workflow name validation failed: %v", err) + return nil, nil, &jsonrpc.Error{ + Code: jsonrpc.CodeInvalidParams, + Message: err.Error(), + Data: mcpErrorData(map[string]any{ + "workflow_name": args.WorkflowName, + "error_type": "workflow_not_found", + }), + } + } + // Build command arguments // Force output directory to /tmp/gh-aw/aw-mcp/logs for MCP server cmdArgs := []string{"logs", "-o", "/tmp/gh-aw/aw-mcp/logs"} diff --git a/pkg/cli/mcp_server_workflow_validation_test.go b/pkg/cli/mcp_server_workflow_validation_test.go new file mode 100644 index 0000000000..c26051f132 --- /dev/null +++ b/pkg/cli/mcp_server_workflow_validation_test.go @@ -0,0 +1,57 @@ +//go:build !integration + +package cli + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMCPValidateWorkflowName(t *testing.T) { + tests := []struct { + name string + workflowName string + shouldSucceed bool + errorContains string + }{ + { + name: "empty workflow name is valid (all workflows)", + workflowName: "", + shouldSucceed: true, + }, + { + name: "non-existent workflow returns error", + workflowName: "nonexistent-workflow-xyz-12345", + shouldSucceed: false, + errorContains: "workflow 'nonexistent-workflow-xyz-12345' not found", + }, + { + name: "error includes suggestions", + workflowName: "invalid-name", + shouldSucceed: false, + errorContains: "Use the 'status' tool to see all available workflows", + }, + { + name: "error includes fuzzy matched suggestions for similar names", + workflowName: "brave-test", // Similar to "brave" workflow + shouldSucceed: false, + errorContains: "workflow 'brave-test' not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateWorkflowName(tt.workflowName) + + if tt.shouldSucceed { + assert.NoError(t, err, "Validation should succeed for workflow: %s", tt.workflowName) + } else { + assert.Error(t, err, "Validation should fail for workflow: %s", tt.workflowName) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains, "Error message should contain expected text") + } + } + }) + } +}