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)
+ }
+}