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
10 changes: 10 additions & 0 deletions pkg/cli/gateway_logs_mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,24 @@ import (

// extractMCPToolUsageData creates detailed MCP tool usage data from gateway metrics
func extractMCPToolUsageData(logDir string, verbose bool) (*MCPToolUsageData, error) {
gatewayLogsLog.Printf("Extracting MCP tool usage data from: %s", logDir)

// Parse gateway logs (falls back to rpc-messages.jsonl automatically)
gatewayMetrics, err := parseGatewayLogs(logDir, verbose)
if err != nil {
// Return nil if no log file exists (not an error for workflows without MCP)
if strings.Contains(err.Error(), "not found") {
gatewayLogsLog.Print("No gateway log file found, skipping MCP tool usage extraction")
return nil, nil
}
return nil, fmt.Errorf("failed to parse gateway logs: %w", err)
}

if gatewayMetrics == nil || len(gatewayMetrics.Servers) == 0 {
gatewayLogsLog.Print("No gateway metrics or servers found")
return nil, nil
}
gatewayLogsLog.Printf("Found gateway metrics: %d servers", len(gatewayMetrics.Servers))

mcpData := &MCPToolUsageData{
Summary: []MCPToolSummary{},
Expand Down Expand Up @@ -66,20 +71,25 @@ func extractMCPToolUsageData(logDir string, verbose bool) (*MCPToolUsageData, er
}

if usingRPCMessages {
gatewayLogsLog.Printf("Reading tool calls from rpc-messages.jsonl: %s", gatewayLogPath)
// Build tool call records from rpc-messages.jsonl
toolCalls, err := buildToolCallsFromRPCMessages(gatewayLogPath)
if err != nil {
return nil, fmt.Errorf("failed to read rpc-messages.jsonl: %w", err)
}
mcpData.ToolCalls = toolCalls
gatewayLogsLog.Printf("Loaded %d tool calls from rpc-messages.jsonl", len(toolCalls))
} else {
gatewayLogsLog.Printf("Reading tool calls from gateway.jsonl: %s", gatewayLogPath)
if err := extractToolCallsFromGatewayLog(gatewayLogPath, mcpData); err != nil {
return nil, err
}
gatewayLogsLog.Printf("Loaded %d tool calls from gateway.jsonl", len(mcpData.ToolCalls))
}

// Build summary statistics from aggregated metrics
buildMCPSummaryStats(gatewayMetrics, mcpData)
gatewayLogsLog.Printf("Built MCP summary: %d tool summaries, %d server stats", len(mcpData.Summary), len(mcpData.Servers))

return mcpData, nil
}
Expand Down
7 changes: 7 additions & 0 deletions pkg/cli/logs_report_firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package cli

import (
"slices"

"github.com/github/gh-aw/pkg/logger"
)

var firewallReportLog = logger.New("cli:logs_report_firewall")

// AccessLogSummary contains aggregated access log analysis
type AccessLogSummary struct {
TotalRequests int `json:"total_requests" console:"header:Total Requests"`
Expand Down Expand Up @@ -37,6 +41,7 @@ type domainAggregation struct {
// aggregateDomainStats aggregates domain statistics across runs
// This is a shared helper for both access log and firewall log summaries
func aggregateDomainStats(processedRuns []ProcessedRun, getAnalysis func(*ProcessedRun) (allowedDomains, blockedDomains []string, totalRequests, allowedCount, blockedCount int, exists bool)) *domainAggregation {
firewallReportLog.Printf("Aggregating domain stats across %d runs", len(processedRuns))
agg := &domainAggregation{
allAllowedDomains: make(map[string]bool),
allBlockedDomains: make(map[string]bool),
Expand All @@ -60,6 +65,8 @@ func aggregateDomainStats(processedRuns []ProcessedRun, getAnalysis func(*Proces
}
}

firewallReportLog.Printf("Domain aggregation complete: %d allowed, %d blocked, %d total requests",
len(agg.allAllowedDomains), len(agg.allBlockedDomains), agg.totalRequests)
return agg
}

Expand Down
8 changes: 8 additions & 0 deletions pkg/workflow/build_input_schema.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package workflow

import "github.com/github/gh-aw/pkg/logger"

var buildInputSchemaLog = logger.New("workflow:build_input_schema")

// buildInputSchema converts GitHub Actions input definitions (workflow_dispatch,
// workflow_call, or dispatch_repository inputs) into JSON Schema properties and
// a required field list suitable for MCP tool inputSchema.
Expand All @@ -11,12 +15,14 @@ package workflow
// Choice inputs with options are mapped to a string enum. Unknown types default
// to string.
func buildInputSchema(inputs map[string]any, descriptionFn func(inputName string) string) (properties map[string]any, required []string) {
buildInputSchemaLog.Printf("Building input schema for %d inputs", len(inputs))
properties = make(map[string]any)
required = []string{}

for inputName, inputDef := range inputs {
inputDefMap, ok := inputDef.(map[string]any)
if !ok {
buildInputSchemaLog.Printf("Skipping input %q: expected map, got %T", inputName, inputDef)
continue
}

Expand Down Expand Up @@ -68,12 +74,14 @@ func buildInputSchema(inputs map[string]any, descriptionFn func(inputName string
if defaultVal, ok := inputDefMap["default"]; ok {
prop["default"] = defaultVal
}
buildInputSchemaLog.Printf("Input %q: type=%s, required=%v", inputName, inputType, inputRequired)
properties[inputName] = prop

if inputRequired {
required = append(required, inputName)
}
}

buildInputSchemaLog.Printf("Built input schema: %d properties, %d required", len(properties), len(required))
return properties, required
}
8 changes: 8 additions & 0 deletions pkg/workflow/mcp_cli_mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import (
"strings"

"github.com/github/gh-aw/pkg/constants"
"github.com/github/gh-aw/pkg/logger"
)

var mcpCLIMountLog = logger.New("workflow:mcp_cli_mount")

// mcp_cli_mount.go generates a workflow step that mounts MCP servers as local CLI tools
// and produces the prompt section that informs the agent about these tools.
//
Expand Down Expand Up @@ -54,8 +57,10 @@ func getMCPCLIServerNames(data *WorkflowData) []string {
// Without the feature flag, code generation remains unchanged regardless of
// the mount-as-clis setting.
if !isFeatureEnabled(constants.MCPCLIFeatureFlag, data) {
mcpCLIMountLog.Print("mcp-cli feature flag not enabled, skipping CLI mount generation")
return nil
}
mcpCLIMountLog.Print("mcp-cli feature flag enabled, collecting CLI server names")

var servers []string

Expand Down Expand Up @@ -109,10 +114,12 @@ func getMCPCLIServerNames(data *WorkflowData) []string {
}

if len(servers) == 0 {
mcpCLIMountLog.Print("No MCP CLI servers configured")
return nil
}

sort.Strings(servers)
mcpCLIMountLog.Printf("MCP CLI servers selected: %v", servers)
return servers
}

Expand Down Expand Up @@ -150,6 +157,7 @@ func (c *Compiler) generateMCPCLIMountStep(yaml *strings.Builder, data *Workflow
if len(servers) == 0 {
return
}
mcpCLIMountLog.Printf("Generating MCP CLI mount step for %d servers: %v", len(servers), servers)

yaml.WriteString(" - name: Mount MCP servers as CLIs\n")
yaml.WriteString(" id: mount-mcp-clis\n")
Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/mcp_property_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import (
"fmt"

"github.com/github/gh-aw/pkg/constants"
"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/parser"
)

var mcpPropertyValidationLog = logger.New("workflow:mcp_property_validation")

// validateStringProperty validates that a property is a string and returns appropriate error message
func validateStringProperty(toolName, propertyName string, value any, exists bool) error {
if !exists {
Expand All @@ -31,6 +34,8 @@ func validateStringProperty(toolName, propertyName string, value any, exists boo

// validateMCPRequirements validates the specific requirements for MCP configuration
func validateMCPRequirements(toolName string, mcpConfig map[string]any, toolConfig map[string]any) error {
mcpPropertyValidationLog.Printf("Validating MCP requirements for tool: %s", toolName)

// Validate 'type' property - allow inference from other fields
mcpType, hasType := mcpConfig["type"]
var typeStr string
Expand All @@ -41,12 +46,14 @@ func validateMCPRequirements(toolName string, mcpConfig map[string]any, toolConf
return fmt.Errorf("tool '%s' mcp configuration 'type' must be a string, got %T. Valid types per MCP Gateway Specification: stdio, http. Note: 'local' is accepted for backward compatibility and treated as 'stdio'.\n\nExample:\ntools:\n %s:\n type: \"stdio\"\n command: \"node server.js\"\n\nSee: %s", toolName, mcpType, toolName, constants.DocsToolsURL)
}
typeStr = mcpType.(string)
mcpPropertyValidationLog.Printf("Tool %s: explicit MCP type=%s", toolName, typeStr)
} else {
// Infer type from presence of fields
typeStr = inferMCPType(mcpConfig)
if typeStr == "" {
return fmt.Errorf("tool '%s' unable to determine MCP type: missing type, url, command, or container.\n\nExample:\ntools:\n %s:\n command: \"node server.js\"\n args: [\"--port\", \"3000\"]\n\nSee: %s", toolName, toolName, constants.DocsToolsURL)
}
mcpPropertyValidationLog.Printf("Tool %s: inferred MCP type=%s", toolName, typeStr)
}

// Normalize "local" to "stdio" for validation
Expand Down