99 "path/filepath"
1010 "strings"
1111 "sync"
12- "syscall"
1312 "time"
1413
1514 "github.com/githubnext/gh-aw/pkg/console"
@@ -135,38 +134,37 @@ func InspectWorkflowMCP(workflowFile string, serverFilter string, toolFilter str
135134 fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Inspecting MCP servers in: %s" , workflowPath )))
136135 }
137136
138- // Parse the workflow file
137+ // Parse the workflow file for MCP configurations
139138 content , err := os .ReadFile (workflowPath )
140139 if err != nil {
141140 return fmt .Errorf ("failed to read workflow file: %w" , err )
142141 }
143142
144- workflowData , err := parser .ExtractFrontmatterFromContent (string (content ))
143+ parsedData , err := parser .ExtractFrontmatterFromContent (string (content ))
145144 if err != nil {
146145 return fmt .Errorf ("failed to parse workflow file: %w" , err )
147146 }
148147
149148 // Validate frontmatter before analyzing MCPs
150- if err := parser .ValidateMainWorkflowFrontmatterWithSchemaAndLocation (workflowData .Frontmatter , workflowPath ); err != nil {
149+ if err := parser .ValidateMainWorkflowFrontmatterWithSchemaAndLocation (parsedData .Frontmatter , workflowPath ); err != nil {
151150 if verbose {
152151 fmt .Fprintln (os .Stderr , console .FormatWarningMessage (fmt .Sprintf ("Frontmatter validation failed: %v" , err )))
153152 fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Continuing with MCP inspection (validation errors may affect results)" ))
154- } else {
155- return fmt .Errorf ("frontmatter validation failed: %w" , err )
156153 }
154+ // Don't return error - continue with inspection even if validation fails
157155 } else if verbose {
158156 fmt .Fprintln (os .Stderr , console .FormatSuccessMessage ("Frontmatter validation passed" ))
159157 }
160158
161159 // Process imports from frontmatter to merge imported MCP servers
162160 markdownDir := filepath .Dir (workflowPath )
163- importsResult , err := parser .ProcessImportsFromFrontmatterWithManifest (workflowData .Frontmatter , markdownDir , nil )
161+ importsResult , err := parser .ProcessImportsFromFrontmatterWithManifest (parsedData .Frontmatter , markdownDir , nil )
164162 if err != nil {
165163 return fmt .Errorf ("failed to process imports from frontmatter: %w" , err )
166164 }
167165
168166 // Apply imported MCP servers to frontmatter
169- frontmatterWithImports , err := applyImportsToFrontmatter (workflowData .Frontmatter , importsResult )
167+ frontmatterWithImports , err := applyImportsToFrontmatter (parsedData .Frontmatter , importsResult )
170168 if err != nil {
171169 return fmt .Errorf ("failed to apply imports: %w" , err )
172170 }
@@ -196,6 +194,56 @@ func InspectWorkflowMCP(workflowFile string, serverFilter string, toolFilter str
196194 // Filter out safe-outputs MCP servers for inspection
197195 mcpConfigs = filterOutSafeOutputs (mcpConfigs )
198196
197+ // Check if safe-inputs are present in the workflow by parsing with the compiler
198+ // (the compiler resolves imports and merges safe-inputs)
199+ compiler := workflow .NewCompiler (verbose , "" , "" )
200+ workflowData , err := compiler .ParseWorkflowFile (workflowPath )
201+ if err != nil {
202+ if verbose {
203+ fmt .Fprintln (os .Stderr , console .FormatWarningMessage (fmt .Sprintf ("Failed to parse workflow for safe-inputs: %v" , err )))
204+ }
205+ }
206+
207+ // Start safe-inputs server if present
208+ var safeInputsServerCmd * exec.Cmd
209+ var safeInputsTmpDir string
210+ if workflowData != nil && workflowData .SafeInputs != nil && len (workflowData .SafeInputs .Tools ) > 0 {
211+ // Start safe-inputs server and add it to the list of MCP configs
212+ config , serverCmd , tmpDir , err := startSafeInputsServer (workflowData .SafeInputs , verbose )
213+ if err != nil {
214+ if verbose {
215+ fmt .Fprintln (os .Stderr , console .FormatWarningMessage (fmt .Sprintf ("Failed to start safe-inputs server: %v" , err )))
216+ }
217+ } else {
218+ safeInputsServerCmd = serverCmd
219+ safeInputsTmpDir = tmpDir
220+ // Add safe-inputs config to the list of MCP servers to inspect
221+ mcpConfigs = append (mcpConfigs , * config )
222+ }
223+ }
224+
225+ // Cleanup safe-inputs server when done
226+ if safeInputsServerCmd != nil {
227+ defer func () {
228+ if safeInputsServerCmd .Process != nil {
229+ // Try graceful shutdown first
230+ if err := safeInputsServerCmd .Process .Signal (os .Interrupt ); err != nil && verbose {
231+ fmt .Fprintln (os .Stderr , console .FormatWarningMessage (fmt .Sprintf ("Failed to send interrupt signal: %v" , err )))
232+ }
233+ // Wait a moment for graceful shutdown
234+ time .Sleep (500 * time .Millisecond )
235+ // Attempt force kill (may fail if process already exited gracefully, which is fine)
236+ _ = safeInputsServerCmd .Process .Kill ()
237+ }
238+ // Cleanup temporary directory
239+ if safeInputsTmpDir != "" {
240+ if err := os .RemoveAll (safeInputsTmpDir ); err != nil && verbose {
241+ fmt .Fprintln (os .Stderr , console .FormatWarningMessage (fmt .Sprintf ("Failed to cleanup temporary directory: %v" , err )))
242+ }
243+ }
244+ }()
245+ }
246+
199247 if len (mcpConfigs ) == 0 {
200248 if serverFilter != "" {
201249 fmt .Fprintln (os .Stderr , console .FormatWarningMessage (fmt .Sprintf ("No MCP servers matching filter '%s' found in workflow" , serverFilter )))
@@ -422,16 +470,83 @@ func waitForServerReady(port int, timeout time.Duration, verbose bool) bool {
422470 return false
423471}
424472
473+ // startSafeInputsServer starts the safe-inputs HTTP server and returns the MCP config
474+ func startSafeInputsServer (safeInputsConfig * workflow.SafeInputsConfig , verbose bool ) (* parser.MCPServerConfig , * exec.Cmd , string , error ) {
475+ mcpInspectLog .Printf ("Starting safe-inputs server with %d tools" , len (safeInputsConfig .Tools ))
476+
477+ // Check if node is available
478+ if _ , err := exec .LookPath ("node" ); err != nil {
479+ return nil , nil , "" , fmt .Errorf ("node not found. Please install Node.js to run the safe-inputs MCP server: %w" , err )
480+ }
481+
482+ if verbose {
483+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Found %d safe-input tool(s) to configure" , len (safeInputsConfig .Tools ))))
484+ }
485+
486+ // Create temporary directory for safe-inputs files
487+ tmpDir , err := os .MkdirTemp ("" , "gh-aw-safe-inputs-*" )
488+ if err != nil {
489+ return nil , nil , "" , fmt .Errorf ("failed to create temporary directory: %w" , err )
490+ }
491+
492+ if verbose {
493+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Created temporary directory: %s" , tmpDir )))
494+ }
495+
496+ // Write safe-inputs files to temporary directory
497+ if err := writeSafeInputsFiles (tmpDir , safeInputsConfig , verbose ); err != nil {
498+ os .RemoveAll (tmpDir )
499+ return nil , nil , "" , fmt .Errorf ("failed to write safe-inputs files: %w" , err )
500+ }
501+
502+ // Find an available port for the HTTP server
503+ port := findAvailablePort (safeInputsStartPort , verbose )
504+ if port == 0 {
505+ os .RemoveAll (tmpDir )
506+ return nil , nil , "" , fmt .Errorf ("failed to find an available port for the HTTP server" )
507+ }
508+
509+ if verbose {
510+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Using port %d for safe-inputs HTTP server" , port )))
511+ }
512+
513+ // Start the HTTP server
514+ serverCmd , err := startSafeInputsHTTPServer (tmpDir , port , verbose )
515+ if err != nil {
516+ os .RemoveAll (tmpDir )
517+ return nil , nil , "" , fmt .Errorf ("failed to start safe-inputs HTTP server: %w" , err )
518+ }
519+
520+ // Wait for the server to start up
521+ if ! waitForServerReady (port , 5 * time .Second , verbose ) {
522+ if serverCmd .Process != nil {
523+ _ = serverCmd .Process .Kill ()
524+ }
525+ os .RemoveAll (tmpDir )
526+ return nil , nil , "" , fmt .Errorf ("safe-inputs HTTP server failed to start within timeout" )
527+ }
528+
529+ if verbose {
530+ fmt .Fprintln (os .Stderr , console .FormatSuccessMessage ("Safe-inputs HTTP server started successfully" ))
531+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Server running on: http://localhost:%d" , port )))
532+ }
533+
534+ // Create MCP server config for the safe-inputs server
535+ config := & parser.MCPServerConfig {
536+ Name : "safeinputs" ,
537+ Type : "http" ,
538+ URL : fmt .Sprintf ("http://localhost:%d" , port ),
539+ Env : make (map [string ]string ),
540+ }
541+
542+ return config , serverCmd , tmpDir , nil
543+ }
544+
425545// spawnSafeInputsInspector generates safe-inputs MCP server files, starts the HTTP server,
426546// and launches the inspector to inspect it
427547func spawnSafeInputsInspector (workflowFile string , verbose bool ) error {
428548 mcpInspectLog .Printf ("Spawning safe-inputs inspector for workflow: %s" , workflowFile )
429549
430- // Check if npx is available
431- if _ , err := exec .LookPath ("npx" ); err != nil {
432- return fmt .Errorf ("npx not found. Please install Node.js and npm to use the MCP inspector: %w" , err )
433- }
434-
435550 // Check if node is available
436551 if _ , err := exec .LookPath ("node" ); err != nil {
437552 return fmt .Errorf ("node not found. Please install Node.js to run the safe-inputs MCP server: %w" , err )
@@ -456,19 +571,17 @@ func spawnSafeInputsInspector(workflowFile string, verbose bool) error {
456571 fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Inspecting safe-inputs from: %s" , workflowPath )))
457572 }
458573
459- // Parse the workflow file
460- content , err := os .ReadFile (workflowPath )
461- if err != nil {
462- return fmt .Errorf ("failed to read workflow file: %w" , err )
463- }
464-
465- workflowData , err := parser .ExtractFrontmatterFromContent (string (content ))
574+ // Use the workflow compiler to parse the file and resolve imports
575+ // This ensures that imported safe-inputs are properly merged
576+ compiler := workflow .NewCompiler (verbose , "" , "" )
577+ workflowData , err := compiler .ParseWorkflowFile (workflowPath )
466578 if err != nil {
467579 return fmt .Errorf ("failed to parse workflow file: %w" , err )
468580 }
469581
470- // Extract safe-inputs configuration
471- safeInputsConfig := workflow .ParseSafeInputs (workflowData .Frontmatter )
582+ // Get safe-inputs configuration from the parsed WorkflowData
583+ // This includes both direct and imported safe-inputs configurations
584+ safeInputsConfig := workflowData .SafeInputs
472585 if safeInputsConfig == nil || len (safeInputsConfig .Tools ) == 0 {
473586 return fmt .Errorf ("no safe-inputs configuration found in workflow" )
474587 }
@@ -518,14 +631,8 @@ func spawnSafeInputsInspector(workflowFile string, verbose bool) error {
518631 }
519632 // Wait a moment for graceful shutdown
520633 time .Sleep (500 * time .Millisecond )
521- // Check if process is still running before force kill
522- // On Unix, sending signal 0 checks if process exists without killing it
523- if err := serverCmd .Process .Signal (os .Signal (syscall .Signal (0 ))); err == nil {
524- // Process still running, force kill
525- if err := serverCmd .Process .Kill (); err != nil && verbose {
526- fmt .Fprintln (os .Stderr , console .FormatWarningMessage (fmt .Sprintf ("Failed to kill server process: %v" , err )))
527- }
528- }
634+ // Attempt force kill (may fail if process already exited gracefully, which is fine)
635+ _ = serverCmd .Process .Kill ()
529636 }
530637 }()
531638
@@ -537,21 +644,17 @@ func spawnSafeInputsInspector(workflowFile string, verbose bool) error {
537644 fmt .Fprintln (os .Stderr , console .FormatSuccessMessage ("Safe-inputs HTTP server started successfully" ))
538645 fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Server running on: http://localhost:%d" , port )))
539646 fmt .Println ()
540- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Configure the MCP inspector with the following settings:" ))
541- fmt .Fprintf (os .Stderr , " Type: HTTP\n " )
542- fmt .Fprintf (os .Stderr , " URL: http://localhost:%d\n " , port )
543- fmt .Println ()
544647
545- // Launch the inspector
546- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Launching @modelcontextprotocol/inspector..." ))
547- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Visit http://localhost:5173 after the inspector starts" ))
548-
549- inspectorCmd := exec .Command ("npx" , "@modelcontextprotocol/inspector" )
550- inspectorCmd .Stdout = os .Stdout
551- inspectorCmd .Stderr = os .Stderr
552- inspectorCmd .Stdin = os .Stdin
648+ // Create MCP server config for the safe-inputs server
649+ safeInputsMCPConfig := parser.MCPServerConfig {
650+ Name : "safeinputs" ,
651+ Type : "http" ,
652+ URL : fmt .Sprintf ("http://localhost:%d" , port ),
653+ Env : make (map [string ]string ),
654+ }
553655
554- return inspectorCmd .Run ()
656+ // Inspect the safe-inputs MCP server using the Go SDK (like other MCP servers)
657+ return inspectMCPServer (safeInputsMCPConfig , "" , verbose , false )
555658}
556659
557660// spawnMCPInspector launches the official @modelcontextprotocol/inspector tool
@@ -769,7 +872,6 @@ func NewMCPInspectSubcommand() *cobra.Command {
769872 var toolFilter string
770873 var spawnInspector bool
771874 var checkSecrets bool
772- var safeInputs bool
773875
774876 cmd := & cobra.Command {
775877 Use : "inspect [workflow-id-or-file]" ,
@@ -779,6 +881,8 @@ func NewMCPInspectSubcommand() *cobra.Command {
779881This command starts each MCP server configured in the workflow, queries its capabilities,
780882and displays the results in a formatted table. It supports stdio, Docker, and HTTP MCP servers.
781883
884+ Safe-inputs servers are automatically detected and inspected when present in the workflow.
885+
782886The workflow-id-or-file can be:
783887- A workflow ID (basename without .md extension, e.g., "weekly-research")
784888- A file path (e.g., "weekly-research.md" or ".github/workflows/weekly-research.md")
@@ -791,11 +895,11 @@ Examples:
791895 gh aw mcp inspect weekly-research -v # Verbose output with detailed connection info
792896 gh aw mcp inspect weekly-research --inspector # Launch @modelcontextprotocol/inspector
793897 gh aw mcp inspect weekly-research --check-secrets # Check GitHub Actions secrets
794- gh aw mcp inspect weekly-research --safe-inputs # Inspect safe-inputs MCP server with inspector
795898
796899The command will:
797900- Parse the workflow file to extract MCP server configurations
798901- Start each MCP server (stdio, docker, http)
902+ - Automatically start and inspect safe-inputs server if present
799903- Query available tools, resources, and roots
800904- Validate required secrets are available
801905- Display results in formatted tables with error details` ,
@@ -824,20 +928,6 @@ The command will:
824928 return fmt .Errorf ("--tool flag requires --server flag to be specified" )
825929 }
826930
827- // Validate that safe-inputs and inspector flags are mutually exclusive with other flags
828- if safeInputs {
829- if workflowFile == "" {
830- return fmt .Errorf ("--safe-inputs flag requires a workflow file to be specified" )
831- }
832- if spawnInspector {
833- return fmt .Errorf ("--safe-inputs already includes inspector functionality; do not use --inspector flag with --safe-inputs" )
834- }
835- if serverFilter != "" || toolFilter != "" {
836- return fmt .Errorf ("--safe-inputs cannot be used with --server or --tool flags" )
837- }
838- return spawnSafeInputsInspector (workflowFile , verbose )
839- }
840-
841931 // Handle spawn inspector flag
842932 if spawnInspector {
843933 return spawnMCPInspector (workflowFile , serverFilter , verbose )
@@ -851,7 +941,6 @@ The command will:
851941 cmd .Flags ().StringVar (& toolFilter , "tool" , "" , "Show detailed information about a specific tool (requires --server)" )
852942 cmd .Flags ().BoolVar (& spawnInspector , "inspector" , false , "Launch the official @modelcontextprotocol/inspector tool" )
853943 cmd .Flags ().BoolVar (& checkSecrets , "check-secrets" , false , "Check GitHub Actions repository secrets for missing secrets" )
854- cmd .Flags ().BoolVar (& safeInputs , "safe-inputs" , false , "Launch safe-inputs MCP server and inspect it" )
855944
856945 // Register completions for mcp inspect command
857946 cmd .ValidArgsFunction = CompleteWorkflowNames
0 commit comments