Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions pkg/cli/mcp_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Comment on lines +175 to +176
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a debug-level log or removing this log statement. This validation is called for every logs tool invocation and will create noise in production logs.

Suggested change
mcpLog.Printf("Validating workflow name: %s", workflowName)

Copilot uses AI. Check for mistakes.
// 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)
Comment on lines +178 to +180
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolved workflow name is logged but not used. Either remove the log statement on line 180 or use the resolvedName for further validation if needed.

Suggested change
resolvedName, err := workflow.ResolveWorkflowName(workflowName)
if err == nil {
mcpLog.Printf("Workflow name resolved successfully: %s -> %s", workflowName, resolvedName)
if _, err := workflow.ResolveWorkflowName(workflowName); err == nil {
mcpLog.Printf("Workflow name resolved successfully: %s", workflowName)

Copilot uses AI. Check for mistakes.
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
Expand Down Expand Up @@ -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"}
Expand Down
57 changes: 57 additions & 0 deletions pkg/cli/mcp_server_workflow_validation_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
})
}
}
Loading