diff --git a/cmd/gh-aw/main.go b/cmd/gh-aw/main.go index 9f9f6e5710..f46a5323d6 100644 --- a/cmd/gh-aw/main.go +++ b/cmd/gh-aw/main.go @@ -373,7 +373,7 @@ func init() { rootCmd.AddCommand(enableCmd) rootCmd.AddCommand(disableCmd) rootCmd.AddCommand(cli.NewLogsCommand()) - rootCmd.AddCommand(cli.NewMCPInspectCommand()) + rootCmd.AddCommand(cli.NewMCPCommand()) rootCmd.AddCommand(versionCmd) } diff --git a/docs/src/content/docs/guides/mcps.md b/docs/src/content/docs/guides/mcps.md index c9904e2be5..ef9e547d0f 100644 --- a/docs/src/content/docs/guides/mcps.md +++ b/docs/src/content/docs/guides/mcps.md @@ -41,7 +41,7 @@ tools: > [!TIP] > You can inspect test your MCP configuration by running
-> `gh aw mcp-inspect ` +> `gh aw mcp inspect ` ### Engine Compatibility @@ -168,7 +168,7 @@ When using an agentic engine that allows tool whitelisting (e.g. Claude), this g > [!TIP] > You can inspect the tools available for an Agentic Workflow by running
-> `gh aw mcp-inspect ` +> `gh aw mcp inspect ` ### Wildcard Access @@ -236,23 +236,23 @@ The compiler enforces these network permission rules: ### MCP Server Inspection -Use the `mcp-inspect` command to analyze and troubleshoot MCP configurations: +Use the `mcp inspect` command to analyze and troubleshoot MCP configurations: ```bash # List all workflows with MCP servers configured -gh aw mcp-inspect +gh aw mcp inspect # Inspect all MCP servers in a specific workflow -gh aw mcp-inspect my-workflow +gh aw mcp inspect my-workflow # Inspect a specific MCP server in a workflow -gh aw mcp-inspect my-workflow --server trello-server +gh aw mcp inspect my-workflow --server trello-server # Enable verbose output for debugging connection issues -gh aw mcp-inspect my-workflow --verbose +gh aw mcp inspect my-workflow --verbose +``` -# Launch official MCP inspector web interface -gh aw mcp-inspect my-workflow --inspector +The `mcp inspect` command automatically generates MCP server configurations using the Claude agentic engine and displays them along with the inspection results. ### Common Issues and Solutions @@ -278,13 +278,13 @@ Error: Tool 'my_tool' not found **Solutions**: 1. Add tool to `allowed` list -2. Check tool name spelling (use `gh aw mcp-inspect` to see available tools) +2. Check tool name spelling (use `gh aw mcp inspect` to see available tools) 3. Verify MCP server is running correctly ## Related Documentation - [Tools Configuration](../reference/tools/) - Complete tools reference -- [CLI Commands](../tools/cli/) - CLI commands including `mcp-inspect` +- [CLI Commands](../tools/cli/) - CLI commands including `mcp inspect` - [Include Directives](../reference/include-directives/) - Modularizing workflows with includes - [Frontmatter Options](../reference/frontmatter/) - All configuration options - [Workflow Structure](../reference/workflow-structure/) - Directory organization diff --git a/docs/src/content/docs/reference/tools.md b/docs/src/content/docs/reference/tools.md index 7f1383ad34..185435ed81 100644 --- a/docs/src/content/docs/reference/tools.md +++ b/docs/src/content/docs/reference/tools.md @@ -25,7 +25,7 @@ All tools declared in included components are merged into the final workflow. > [!TIP] > You can inspect the tools available for an Agentic Workflow by running
-> `gh aw mcp-inspect ` +> `gh aw mcp inspect ` ## GitHub Tools (`github:`) diff --git a/docs/src/content/docs/tools/cli.md b/docs/src/content/docs/tools/cli.md index 67f6403c10..6ec1364db4 100644 --- a/docs/src/content/docs/tools/cli.md +++ b/docs/src/content/docs/tools/cli.md @@ -189,28 +189,25 @@ gh aw logs --format json -o ./exports/ ## šŸ” MCP Server Inspection -The `mcp-inspect` command allows you to analyze and troubleshoot Model Context Protocol (MCP) servers configured in your workflows. +The `mcp inspect` command allows you to analyze and troubleshoot Model Context Protocol (MCP) servers configured in your workflows. > **šŸ“˜ Complete MCP Guide**: For comprehensive MCP setup, configuration examples, and troubleshooting, see the [MCPs](../guides/mcps/). ```bash # List all workflows that contain MCP server configurations -gh aw mcp-inspect +gh aw mcp inspect # Inspect all MCP servers in a specific workflow -gh aw mcp-inspect workflow-name +gh aw mcp inspect workflow-name # Filter inspection to specific servers by name -gh aw mcp-inspect workflow-name --server server-name +gh aw mcp inspect workflow-name --server server-name # Show detailed information about a specific tool (requires --server) -gh aw mcp-inspect workflow-name --server server-name --tool tool-name +gh aw mcp inspect workflow-name --server server-name --tool tool-name # Enable verbose output with connection details -gh aw mcp-inspect workflow-name --verbose - -# Launch the official @modelcontextprotocol/inspector web interface -gh aw mcp-inspect workflow-name --inspector +gh aw mcp inspect workflow-name --verbose ``` **Key Features:** diff --git a/pkg/cli/mcp.go b/pkg/cli/mcp.go new file mode 100644 index 0000000000..7c3ffe4fe7 --- /dev/null +++ b/pkg/cli/mcp.go @@ -0,0 +1,24 @@ +package cli + +import ( + "github.com/spf13/cobra" +) + +// NewMCPCommand creates the mcp command with subcommands +func NewMCPCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "mcp", + Short: "Model Context Protocol (MCP) server management", + Long: `Manage Model Context Protocol (MCP) servers used by agentic workflows. + +This command provides subcommands for inspecting, configuring, and launching MCP servers.`, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, + } + + // Add subcommands + cmd.AddCommand(NewMCPInspectSubCommand()) + + return cmd +} diff --git a/pkg/cli/mcp_inspect.go b/pkg/cli/mcp_inspect.go index 0471795e92..43c689a8ab 100644 --- a/pkg/cli/mcp_inspect.go +++ b/pkg/cli/mcp_inspect.go @@ -1,10 +1,13 @@ package cli import ( + "encoding/json" "fmt" "os" "os/exec" + "os/signal" "path/filepath" + "regexp" "strings" "sync" "time" @@ -121,9 +124,314 @@ func InspectWorkflowMCP(workflowFile string, serverFilter string, toolFilter str } } + // Always generate MCP configuration in memory using Claude engine + if verbose { + fmt.Println(console.FormatInfoMessage("Generating MCP configuration using Claude agentic engine...")) + } + + if err := generateAndDisplayMCPConfig(workflowData, verbose); err != nil { + if verbose { + fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Failed to generate MCP configuration: %v", err))) + } + } + + return nil +} + +// generateAndDisplayMCPConfig generates MCP configuration using Claude engine and displays it +func generateAndDisplayMCPConfig(workflowData *parser.FrontmatterResult, verbose bool) error { + // Create Claude engine to generate MCP configuration + claudeEngine := workflow.NewClaudeEngine() + + // Extract tools from frontmatter + tools := make(map[string]any) + if toolsSection, hasTools := workflowData.Frontmatter["tools"]; hasTools { + if toolsMap, ok := toolsSection.(map[string]any); ok { + tools = toolsMap + } + } + + // Extract MCP tool names from existing configurations + mcpConfigs, err := parser.ExtractMCPConfigurations(workflowData.Frontmatter, "") + if err != nil { + return fmt.Errorf("failed to extract MCP configurations: %w", err) + } + + // Build list of MCP servers to include in config + mcpTools := []string{} + + // Add existing MCP server configurations + for _, config := range mcpConfigs { + mcpTools = append(mcpTools, config.Name) + } + + // Add standard servers if configured (avoid duplicates) + if _, hasGithub := tools["github"]; hasGithub { + found := false + for _, existing := range mcpTools { + if existing == "github" { + found = true + break + } + } + if !found { + mcpTools = append(mcpTools, "github") + } + } + + if _, hasPlaywright := tools["playwright"]; hasPlaywright { + found := false + for _, existing := range mcpTools { + if existing == "playwright" { + found = true + break + } + } + if !found { + mcpTools = append(mcpTools, "playwright") + } + } + + if _, hasSafeOutputs := workflowData.Frontmatter["safe-outputs"]; hasSafeOutputs { + found := false + for _, existing := range mcpTools { + if existing == "safe-outputs" { + found = true + break + } + } + if !found { + mcpTools = append(mcpTools, "safe-outputs") + } + } + + if len(mcpTools) == 0 { + if verbose { + fmt.Println(console.FormatInfoMessage("No MCP tools found for configuration generation")) + } + return nil + } + + // Create a minimal WorkflowData for MCP config generation + workflowDataForMCP := &workflow.WorkflowData{ + Tools: tools, + NetworkPermissions: nil, // Will be populated if needed + } + + // Generate the MCP configuration + var mcpConfigBuilder strings.Builder + claudeEngine.RenderMCPConfig(&mcpConfigBuilder, tools, mcpTools, workflowDataForMCP) + + fmt.Println() + fmt.Println(console.FormatSuccessMessage(fmt.Sprintf("Generated MCP configuration for %d server(s)", len(mcpTools)))) + fmt.Println(console.FormatInfoMessage("Claude Engine MCP Configuration:")) + fmt.Println() + fmt.Println(mcpConfigBuilder.String()) + + // Parse and spawn MCP servers from the generated configuration + if err := spawnMCPServersFromConfig(mcpConfigBuilder.String(), verbose); err != nil { + if verbose { + fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Failed to spawn MCP servers: %v", err))) + } + } + + return nil +} + +// MCPServerConfig represents a single MCP server configuration +type MCPServerConfig struct { + Command string `json:"command"` + Args []string `json:"args"` + Env map[string]string `json:"env"` +} + +// MCPConfig represents the complete MCP configuration +type MCPConfig struct { + MCPServers map[string]MCPServerConfig `json:"mcpServers"` +} + +// spawnMCPServersFromConfig parses the generated MCP configuration and spawns servers +func spawnMCPServersFromConfig(configScript string, verbose bool) error { + // Extract JSON from the generated shell script + jsonConfig, err := extractJSONFromScript(configScript) + if err != nil { + return fmt.Errorf("failed to extract JSON from configuration script: %w", err) + } + + if verbose { + fmt.Println(console.FormatInfoMessage("Extracted MCP JSON configuration:")) + fmt.Println(jsonConfig) + fmt.Println() + } + + // Replace GitHub Actions template variables with actual environment values + resolvedConfig := resolveTemplateVariables(jsonConfig, verbose) + + if verbose { + fmt.Println(console.FormatInfoMessage("Resolved MCP JSON configuration:")) + fmt.Println(resolvedConfig) + fmt.Println() + } + + // Parse the JSON configuration + var config MCPConfig + if err := json.Unmarshal([]byte(resolvedConfig), &config); err != nil { + return fmt.Errorf("failed to parse MCP configuration JSON: %w", err) + } + + if len(config.MCPServers) == 0 { + if verbose { + fmt.Println(console.FormatInfoMessage("No MCP servers found in configuration to spawn")) + } + return nil + } + + fmt.Println() + fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Spawning %d MCP server(s) from generated configuration...", len(config.MCPServers)))) + + var wg sync.WaitGroup + var serverProcesses []*exec.Cmd + + // Start each server + for serverName, serverConfig := range config.MCPServers { + if verbose { + fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Starting MCP server: %s", serverName))) + } + + // Create the command + cmd := exec.Command(serverConfig.Command, serverConfig.Args...) + + // Set environment variables + cmd.Env = os.Environ() + for key, value := range serverConfig.Env { + // Resolve environment variable references (simple implementation) + resolvedValue := os.ExpandEnv(value) + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, resolvedValue)) + } + + // Start the server process + if err := cmd.Start(); err != nil { + fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Failed to start server %s: %v", serverName, err))) + continue + } + + serverProcesses = append(serverProcesses, cmd) + + // Monitor the process in the background + wg.Add(1) + go func(serverCmd *exec.Cmd, name string) { + defer wg.Done() + if err := serverCmd.Wait(); err != nil && verbose { + fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Server %s exited with error: %v", name, err))) + } + }(cmd, serverName) + + if verbose { + fmt.Println(console.FormatSuccessMessage(fmt.Sprintf("Started server: %s (PID: %d)", serverName, cmd.Process.Pid))) + } + } + + if len(serverProcesses) > 0 { + fmt.Println(console.FormatSuccessMessage(fmt.Sprintf("Successfully started %d MCP server(s)", len(serverProcesses)))) + fmt.Println(console.FormatInfoMessage("Servers are running in the background")) + fmt.Println(console.FormatInfoMessage("Press Ctrl+C to stop the inspection and cleanup servers")) + + // Set up cleanup on interrupt + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + go func() { + <-c + fmt.Println() + fmt.Println(console.FormatInfoMessage("Cleaning up MCP servers...")) + for i, cmd := range serverProcesses { + if cmd.Process != nil { + if err := cmd.Process.Kill(); err != nil && verbose { + fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Failed to kill server process %d: %v", cmd.Process.Pid, err))) + } + } + // Give each process a chance to clean up + if i < len(serverProcesses)-1 { + time.Sleep(100 * time.Millisecond) + } + } + + // Wait for all background goroutines to finish (with timeout) + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + // All finished + case <-time.After(5 * time.Second): + // Timeout waiting for cleanup + if verbose { + fmt.Println(console.FormatWarningMessage("Timeout waiting for server cleanup")) + } + } + + os.Exit(0) + }() + + // Keep the main process alive to maintain servers + select {} + } + return nil } +// resolveTemplateVariables replaces GitHub Actions template variables with local environment values +func resolveTemplateVariables(jsonConfig string, verbose bool) string { + // Replace common GitHub Actions template variables with environment values or defaults + resolved := jsonConfig + + // Replace ${{ env.GITHUB_AW_SAFE_OUTPUTS }} with environment value or default + if safeOutputs := os.Getenv("GITHUB_AW_SAFE_OUTPUTS"); safeOutputs != "" { + resolved = strings.ReplaceAll(resolved, `"${{ env.GITHUB_AW_SAFE_OUTPUTS }}"`, fmt.Sprintf(`"%s"`, safeOutputs)) + } else { + // Default to a temporary file for local testing + resolved = strings.ReplaceAll(resolved, `"${{ env.GITHUB_AW_SAFE_OUTPUTS }}"`, `"/tmp/safe-outputs.jsonl"`) + } + + // Replace ${{ toJSON(env.GITHUB_AW_SAFE_OUTPUTS_CONFIG) }} with environment value or default + if safeOutputsConfig := os.Getenv("GITHUB_AW_SAFE_OUTPUTS_CONFIG"); safeOutputsConfig != "" { + resolved = strings.ReplaceAll(resolved, `${{ toJSON(env.GITHUB_AW_SAFE_OUTPUTS_CONFIG) }}`, safeOutputsConfig) + } else { + // Default to empty config for local testing + resolved = strings.ReplaceAll(resolved, `${{ toJSON(env.GITHUB_AW_SAFE_OUTPUTS_CONFIG) }}`, `"{}"`) + } + + // Replace ${{ secrets.GITHUB_TOKEN }} with environment value or default + if ghToken := os.Getenv("GITHUB_TOKEN"); ghToken != "" { + resolved = strings.ReplaceAll(resolved, `"${{ secrets.GITHUB_TOKEN }}"`, fmt.Sprintf(`"%s"`, ghToken)) + } else if ghToken := os.Getenv("GH_TOKEN"); ghToken != "" { + resolved = strings.ReplaceAll(resolved, `"${{ secrets.GITHUB_TOKEN }}"`, fmt.Sprintf(`"%s"`, ghToken)) + } else { + if verbose { + fmt.Println(console.FormatWarningMessage("GitHub token not found in environment (set GITHUB_TOKEN or GH_TOKEN)")) + } + resolved = strings.ReplaceAll(resolved, `"${{ secrets.GITHUB_TOKEN }}"`, `"your-github-token"`) + } + + return resolved +} + +// extractJSONFromScript extracts the JSON configuration from the generated shell script +func extractJSONFromScript(script string) (string, error) { + // Look for the JSON content between << 'EOF' and EOF (multiline with DOTALL flag) + re := regexp.MustCompile(`(?s)cat > [^<]+ << 'EOF'\s*\n(.*?)\n\s*EOF`) + matches := re.FindStringSubmatch(script) + + if len(matches) < 2 { + return "", fmt.Errorf("could not find JSON configuration in script") + } + + return strings.TrimSpace(matches[1]), nil +} + // listWorkflowsWithMCP shows available workflow files that contain MCP configurations func listWorkflowsWithMCP(workflowsDir string, verbose bool) error { if _, err := os.Stat(workflowsDir); os.IsNotExist(err) { @@ -184,39 +492,40 @@ func listWorkflowsWithMCP(workflowsDir string, verbose bool) error { for _, workflow := range workflowsWithMCP { fmt.Printf(" • %s\n", workflow) } - fmt.Printf("\nRun 'gh aw mcp-inspect ' to inspect MCP servers in a specific workflow.\n") + fmt.Printf("\nRun 'gh aw mcp inspect ' to inspect MCP servers in a specific workflow.\n") return nil } -// NewMCPInspectCommand creates the mcp-inspect command -func NewMCPInspectCommand() *cobra.Command { + +// NewMCPInspectSubCommand creates the mcp inspect subcommand +func NewMCPInspectSubCommand() *cobra.Command { var serverFilter string var toolFilter string - var spawnInspector bool cmd := &cobra.Command{ - Use: "mcp-inspect [workflow-file]", + Use: "inspect [workflow-file]", Short: "Inspect MCP servers and list available tools, resources, and roots", Long: `Inspect MCP servers used by a workflow and display available tools, resources, and roots. -This command starts each MCP server configured in the workflow, queries its capabilities, -and displays the results in a formatted table. It supports stdio, Docker, and HTTP MCP servers. +This command generates MCP configurations using the Claude agentic engine and automatically +spawns configured servers including github, playwright, and safe-outputs. Examples: - gh aw mcp-inspect # List workflows with MCP servers - gh aw mcp-inspect weekly-research # Inspect MCP servers in weekly-research.md - gh aw mcp-inspect repomind --server repo-mind # Inspect only the repo-mind server - gh aw mcp-inspect weekly-research --server github --tool create_issue # Show details for a specific tool - gh aw mcp-inspect weekly-research -v # Verbose output with detailed connection info - gh aw mcp-inspect weekly-research --inspector # Launch @modelcontextprotocol/inspector + gh aw mcp inspect # List workflows with MCP servers + gh aw mcp inspect weekly-research # Inspect MCP servers in weekly-research.md + gh aw mcp inspect repomind --server repo-mind # Inspect only the repo-mind server + gh aw mcp inspect weekly-research --server github --tool create_issue # Show details for a specific tool + gh aw mcp inspect weekly-research -v # Verbose output with detailed connection info The command will: - Parse the workflow file to extract MCP server configurations -- Start each MCP server (stdio, docker, http) +- Generate MCP configuration using the Claude agentic engine +- Spawn MCP servers from the generated configuration - Query available tools, resources, and roots - Validate required secrets are available -- Display results in formatted tables with error details`, +- Display results in formatted tables with error details +- Keep servers running until interrupted (Ctrl+C)`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var workflowFile string @@ -225,8 +534,8 @@ The command will: } verbose, _ := cmd.Flags().GetBool("verbose") - if cmd.Parent() != nil { - parentVerbose, _ := cmd.Parent().PersistentFlags().GetBool("verbose") + if cmd.Parent() != nil && cmd.Parent().Parent() != nil { + parentVerbose, _ := cmd.Parent().Parent().PersistentFlags().GetBool("verbose") verbose = verbose || parentVerbose } @@ -235,10 +544,7 @@ The command will: return fmt.Errorf("--tool flag requires --server flag to be specified") } - // Handle spawn inspector flag - if spawnInspector { - return spawnMCPInspector(workflowFile, serverFilter, verbose) - } + return InspectWorkflowMCP(workflowFile, serverFilter, toolFilter, verbose) }, @@ -247,208 +553,7 @@ The command will: cmd.Flags().StringVar(&serverFilter, "server", "", "Filter to inspect only the specified MCP server") cmd.Flags().StringVar(&toolFilter, "tool", "", "Show detailed information about a specific tool (requires --server)") cmd.Flags().BoolP("verbose", "v", false, "Enable verbose output with detailed connection information") - cmd.Flags().BoolVar(&spawnInspector, "inspector", false, "Launch the official @modelcontextprotocol/inspector tool") return cmd } -// spawnMCPInspector launches the official @modelcontextprotocol/inspector tool -// and spawns any stdio MCP servers beforehand -func spawnMCPInspector(workflowFile string, serverFilter string, verbose bool) error { - // Check if npx is available - if _, err := exec.LookPath("npx"); err != nil { - return fmt.Errorf("npx not found. Please install Node.js and npm to use the MCP inspector: %w", err) - } - - var mcpConfigs []parser.MCPServerConfig - var serverProcesses []*exec.Cmd - var wg sync.WaitGroup - - // If workflow file is specified, extract MCP configurations and start servers - if workflowFile != "" { - workflowsDir := workflow.GetWorkflowDir() - - // Normalize the workflow file path - if !strings.HasSuffix(workflowFile, ".md") { - workflowFile += ".md" - } - - workflowPath := filepath.Join(workflowsDir, workflowFile) - if !filepath.IsAbs(workflowPath) { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current directory: %w", err) - } - workflowPath = filepath.Join(cwd, workflowPath) - } - - // Check if file exists - if _, err := os.Stat(workflowPath); os.IsNotExist(err) { - return fmt.Errorf("workflow file not found: %s", workflowPath) - } - - // Parse the workflow file to extract MCP configurations - content, err := os.ReadFile(workflowPath) - if err != nil { - return err - } - - workflowData, err := parser.ExtractFrontmatterFromContent(string(content)) - if err != nil { - return err - } - - // Extract MCP configurations - mcpConfigs, err = parser.ExtractMCPConfigurations(workflowData.Frontmatter, serverFilter) - if err != nil { - return err - } - - if len(mcpConfigs) > 0 { - fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Found %d MCP server(s) in workflow:", len(mcpConfigs)))) - for _, config := range mcpConfigs { - fmt.Printf(" • %s (%s)\n", config.Name, config.Type) - } - fmt.Println() - - // Start stdio MCP servers in the background - stdioServers := []parser.MCPServerConfig{} - for _, config := range mcpConfigs { - if config.Type == "stdio" { - stdioServers = append(stdioServers, config) - } - } - - if len(stdioServers) > 0 { - fmt.Println(console.FormatInfoMessage("Starting stdio MCP servers...")) - - for _, config := range stdioServers { - if verbose { - fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Starting server: %s", config.Name))) - } - - // Create the command for the MCP server - var cmd *exec.Cmd - if config.Container != "" { - // Docker container mode - args := append([]string{"run", "--rm", "-i"}, config.Args...) - cmd = exec.Command("docker", args...) - } else { - // Direct command mode - if config.Command == "" { - fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Skipping server %s: no command specified", config.Name))) - continue - } - cmd = exec.Command(config.Command, config.Args...) - } - - // Set environment variables - cmd.Env = os.Environ() - for key, value := range config.Env { - // Resolve environment variable references - resolvedValue := os.ExpandEnv(value) - cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, resolvedValue)) - } - - // Start the server process - if err := cmd.Start(); err != nil { - fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Failed to start server %s: %v", config.Name, err))) - continue - } - - serverProcesses = append(serverProcesses, cmd) - - // Monitor the process in the background - wg.Add(1) - go func(serverCmd *exec.Cmd, serverName string) { - defer wg.Done() - if err := serverCmd.Wait(); err != nil && verbose { - fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Server %s exited with error: %v", serverName, err))) - } - }(cmd, config.Name) - - if verbose { - fmt.Println(console.FormatSuccessMessage(fmt.Sprintf("Started server: %s (PID: %d)", config.Name, cmd.Process.Pid))) - } - } - - // Give servers a moment to start up - time.Sleep(2 * time.Second) - fmt.Println(console.FormatSuccessMessage("All stdio servers started successfully")) - } - - fmt.Println(console.FormatInfoMessage("Configuration details for MCP inspector:")) - for _, config := range mcpConfigs { - fmt.Printf("\nšŸ“” %s (%s):\n", config.Name, config.Type) - switch config.Type { - case "stdio": - if config.Container != "" { - fmt.Printf(" Container: %s\n", config.Container) - } else { - fmt.Printf(" Command: %s\n", config.Command) - if len(config.Args) > 0 { - fmt.Printf(" Args: %s\n", strings.Join(config.Args, " ")) - } - } - case "http": - fmt.Printf(" URL: %s\n", config.URL) - } - if len(config.Env) > 0 { - fmt.Printf(" Environment Variables: %v\n", config.Env) - } - } - fmt.Println() - } else { - fmt.Println(console.FormatWarningMessage("No MCP servers found in workflow")) - return nil - } - } - - // Set up cleanup function for stdio servers - defer func() { - if len(serverProcesses) > 0 { - fmt.Println(console.FormatInfoMessage("Cleaning up MCP servers...")) - for i, cmd := range serverProcesses { - if cmd.Process != nil { - if err := cmd.Process.Kill(); err != nil && verbose { - fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Failed to kill server process %d: %v", cmd.Process.Pid, err))) - } - } - // Give each process a chance to clean up - if i < len(serverProcesses)-1 { - time.Sleep(100 * time.Millisecond) - } - } - // Wait for all background goroutines to finish (with timeout) - done := make(chan struct{}) - go func() { - wg.Wait() - close(done) - }() - - select { - case <-done: - // All finished - case <-time.After(5 * time.Second): - // Timeout waiting for cleanup - if verbose { - fmt.Println(console.FormatWarningMessage("Timeout waiting for server cleanup")) - } - } - } - }() - - fmt.Println(console.FormatInfoMessage("Launching @modelcontextprotocol/inspector...")) - fmt.Println(console.FormatInfoMessage("Visit http://localhost:5173 after the inspector starts")) - if len(serverProcesses) > 0 { - fmt.Println(console.FormatInfoMessage(fmt.Sprintf("%d stdio MCP server(s) are running in the background", len(serverProcesses)))) - fmt.Println(console.FormatInfoMessage("Configure them in the inspector using the details shown above")) - } - - cmd := exec.Command("npx", "@modelcontextprotocol/inspector") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - return cmd.Run() -} diff --git a/pkg/cli/mcp_test.go b/pkg/cli/mcp_test.go new file mode 100644 index 0000000000..92ea22de56 --- /dev/null +++ b/pkg/cli/mcp_test.go @@ -0,0 +1,121 @@ +package cli + +import ( + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func TestNewMCPCommand(t *testing.T) { + tests := []struct { + name string + test func(t *testing.T) + }{ + { + name: "mcp command structure", + test: func(t *testing.T) { + cmd := NewMCPCommand() + + if cmd.Use != "mcp" { + t.Errorf("Expected Use to be 'mcp', got '%s'", cmd.Use) + } + + if cmd.Short == "" { + t.Error("Expected Short description to be set") + } + + if !strings.Contains(cmd.Long, "Model Context Protocol") { + t.Error("Expected Long description to mention Model Context Protocol") + } + }, + }, + { + name: "mcp command has inspect subcommand", + test: func(t *testing.T) { + cmd := NewMCPCommand() + + var inspectCmd *cobra.Command + for _, subCmd := range cmd.Commands() { + if subCmd.Use == "inspect [workflow-file]" { + inspectCmd = subCmd + break + } + } + + if inspectCmd == nil { + t.Error("Expected 'inspect' subcommand to be present") + } + + if inspectCmd != nil && inspectCmd.Short == "" { + t.Error("Expected inspect subcommand to have Short description") + } + }, + }, + { + name: "mcp inspect has required flags", + test: func(t *testing.T) { + cmd := NewMCPInspectSubCommand() + + expectedFlags := []string{"server", "tool", "verbose"} + + for _, flagName := range expectedFlags { + flag := cmd.Flags().Lookup(flagName) + if flag == nil { + t.Errorf("Expected flag '%s' to be present", flagName) + } + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + +func TestMCPInspectSubCommand(t *testing.T) { + tests := []struct { + name string + test func(t *testing.T) + }{ + { + name: "inspect subcommand structure", + test: func(t *testing.T) { + cmd := NewMCPInspectSubCommand() + + if cmd.Use != "inspect [workflow-file]" { + t.Errorf("Expected Use to be 'inspect [workflow-file]', got '%s'", cmd.Use) + } + + if cmd.Short == "" { + t.Error("Expected Short description to be set") + } + + expectedFeatures := []string{"generates MCP configurations", "Claude agentic engine", "github, playwright, and safe-outputs"} + for _, feature := range expectedFeatures { + if !strings.Contains(cmd.Long, feature) { + t.Errorf("Expected Long description to mention '%s'", feature) + } + } + }, + }, + { + name: "inspect subcommand examples", + test: func(t *testing.T) { + cmd := NewMCPInspectSubCommand() + + expectedExamples := []string{"--server", "--tool", "-v"} + for _, example := range expectedExamples { + if !strings.Contains(cmd.Long, example) { + t.Errorf("Expected Long description to include example with '%s'", example) + } + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +}