From f2c7f30918ca1bce15418f95863120a6dbf4094f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:48:13 +0000 Subject: [PATCH 1/4] Initial plan From 833c7846d17a95ed6cf1aab14909206366f32594 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:59:41 +0000 Subject: [PATCH 2/4] fix: scale MCP logs timeout with count Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/logs_timeout_test.go | 64 +++++++++++++++++++++-------- pkg/cli/mcp_server_defaults_test.go | 15 +++---- pkg/cli/mcp_tools_privileged.go | 36 ++++++++++++---- 3 files changed, 82 insertions(+), 33 deletions(-) diff --git a/pkg/cli/logs_timeout_test.go b/pkg/cli/logs_timeout_test.go index f78fc358b1c..1c73c4c513f 100644 --- a/pkg/cli/logs_timeout_test.go +++ b/pkg/cli/logs_timeout_test.go @@ -109,25 +109,53 @@ func TestTimeoutLogic(t *testing.T) { } } -// TestMCPServerDefaultTimeout tests that the MCP server sets a default timeout -func TestMCPServerDefaultTimeout(t *testing.T) { - // Test that when no timeout is specified, MCP server uses 1 minute - timeoutValue := 0 - if timeoutValue == 0 { - timeoutValue = 1 - } - - if timeoutValue != 1 { - t.Errorf("Expected MCP server default timeout to be 1 but got %d", timeoutValue) - } - - // Test that explicit timeout overrides the default - timeoutValue = 5 - if timeoutValue == 0 { - timeoutValue = 1 +// TestEffectiveMCPLogsToolTimeoutMinutes verifies that the MCP logs tool +// scales its implicit timeout with larger fetch windows while preserving +// explicit user-provided timeouts. +func TestEffectiveMCPLogsToolTimeoutMinutes(t *testing.T) { + tests := []struct { + name string + requestedTimeout int + count int + want int + }{ + { + name: "explicit timeout is preserved", + requestedTimeout: 5, + count: 100, + want: 5, + }, + { + name: "small fetch window keeps one minute default", + requestedTimeout: 0, + count: 40, + want: 1, + }, + { + name: "fetch window above forty runs gets two minutes", + requestedTimeout: 0, + count: 41, + want: 2, + }, + { + name: "default hundred run window gets three minutes", + requestedTimeout: 0, + count: 100, + want: 3, + }, + { + name: "unspecified count falls back to default window size", + requestedTimeout: 0, + count: 0, + want: 3, + }, } - if timeoutValue != 5 { - t.Errorf("Expected explicit timeout to be preserved but got %d", timeoutValue) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := effectiveMCPLogsToolTimeoutMinutes(tt.requestedTimeout, tt.count); got != tt.want { + t.Errorf("effectiveMCPLogsToolTimeoutMinutes(%d, %d) = %d, want %d", tt.requestedTimeout, tt.count, got, tt.want) + } + }) } } diff --git a/pkg/cli/mcp_server_defaults_test.go b/pkg/cli/mcp_server_defaults_test.go index f5a055067d2..b2ab9f6ebca 100644 --- a/pkg/cli/mcp_server_defaults_test.go +++ b/pkg/cli/mcp_server_defaults_test.go @@ -66,11 +66,11 @@ func TestMCPToolElicitationDefaults(t *testing.T) { t.Fatalf("Failed to generate schema: %v", err) } - // Add defaults as done in createMCPServer - if err := AddSchemaDefault(schema, "count", 100); err != nil { + // Add defaults as done in registerLogsTool + if err := AddSchemaDefault(schema, "count", defaultMCPLogsToolCount); err != nil { t.Fatalf("Failed to add count default: %v", err) } - if err := AddSchemaDefault(schema, "timeout", 50); err != nil { + if err := AddSchemaDefault(schema, "timeout", defaultMCPLogsToolTimeoutMinutesForCount(defaultMCPLogsToolCount)); err != nil { t.Fatalf("Failed to add timeout default: %v", err) } if err := AddSchemaDefault(schema, "max_tokens", 12000); err != nil { @@ -92,8 +92,8 @@ func TestMCPToolElicitationDefaults(t *testing.T) { if err := json.Unmarshal(countProp.Default, &countDefault); err != nil { t.Fatalf("Failed to unmarshal count default: %v", err) } - if countDefault != 100 { - t.Errorf("Expected count default to be 100, got %v", countDefault) + if countDefault != defaultMCPLogsToolCount { + t.Errorf("Expected count default to be %d, got %v", defaultMCPLogsToolCount, countDefault) } // Verify timeout default @@ -108,8 +108,9 @@ func TestMCPToolElicitationDefaults(t *testing.T) { if err := json.Unmarshal(timeoutProp.Default, &timeoutDefault); err != nil { t.Fatalf("Failed to unmarshal timeout default: %v", err) } - if timeoutDefault != 50 { - t.Errorf("Expected timeout default to be 50, got %v", timeoutDefault) + expectedTimeoutDefault := defaultMCPLogsToolTimeoutMinutesForCount(defaultMCPLogsToolCount) + if timeoutDefault != expectedTimeoutDefault { + t.Errorf("Expected timeout default to be %d, got %v", expectedTimeoutDefault, timeoutDefault) } // Verify max_tokens default (backward-compat field) diff --git a/pkg/cli/mcp_tools_privileged.go b/pkg/cli/mcp_tools_privileged.go index e097d2a22ef..85e2eb106c8 100644 --- a/pkg/cli/mcp_tools_privileged.go +++ b/pkg/cli/mcp_tools_privileged.go @@ -15,6 +15,12 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) +const ( + defaultMCPLogsToolCount = 100 + defaultMCPLogsTimeoutMinutes = 1 + mcpLogsRunsPerDefaultTimeoutMinute = 40 +) + // appendRepoFlagFromEnv appends "--repo " to args when GITHUB_REPOSITORY // is set in the environment. This allows gh CLI subcommands to identify the repository // without falling back to git-based detection, which fails in sandboxed environments @@ -41,11 +47,27 @@ type logsArgs struct { Branch string `json:"branch,omitempty" jsonschema:"Filter runs by branch name"` AfterRunID int64 `json:"after_run_id,omitempty" jsonschema:"Filter runs with database ID after this value (exclusive)"` BeforeRunID int64 `json:"before_run_id,omitempty" jsonschema:"Filter runs with database ID before this value (exclusive)"` - Timeout int `json:"timeout,omitempty" jsonschema:"Maximum time in minutes to spend downloading logs (default: 1 for MCP server)"` + Timeout int `json:"timeout,omitempty" jsonschema:"Maximum time in minutes to spend downloading logs (default: auto-scales with count in the MCP server, from 1 minute for <=40 runs to 3 minutes for the default 100-run window)"` MaxTokens int `json:"max_tokens,omitempty" jsonschema:"Deprecated: accepted for backward compatibility but ignored. Output is always written to a file."` Artifacts []string `json:"artifacts,omitempty" jsonschema:"Artifact sets to download (default: usage). Valid sets: all, activation, agent, detection, experiment, firewall, github-api, mcp, usage"` } +func defaultMCPLogsToolTimeoutMinutesForCount(count int) int { + if count <= 0 { + count = defaultMCPLogsToolCount + } + + return max(defaultMCPLogsTimeoutMinutes, (count+mcpLogsRunsPerDefaultTimeoutMinute-1)/mcpLogsRunsPerDefaultTimeoutMinute) +} + +func effectiveMCPLogsToolTimeoutMinutes(requestedTimeout, count int) int { + if requestedTimeout > 0 { + return requestedTimeout + } + + return defaultMCPLogsToolTimeoutMinutesForCount(count) +} + // The logs tool requires write+ access and checks actor permissions. // Returns an error if schema generation fails. func registerLogsTool(server *mcp.Server, execCmd execCmdFunc, actor string, validateActor bool) error { @@ -56,10 +78,10 @@ func registerLogsTool(server *mcp.Server, execCmd execCmdFunc, actor string, val return err } // Add elicitation defaults for common parameters - if err := AddSchemaDefault(logsSchema, "count", 100); err != nil { + if err := AddSchemaDefault(logsSchema, "count", defaultMCPLogsToolCount); err != nil { mcpLog.Printf("Failed to add default for count: %v", err) } - if err := AddSchemaDefault(logsSchema, "timeout", 1); err != nil { + if err := AddSchemaDefault(logsSchema, "timeout", defaultMCPLogsToolTimeoutMinutesForCount(defaultMCPLogsToolCount)); err != nil { mcpLog.Printf("Failed to add default for timeout: %v", err) } if err := AddSchemaDefault(logsSchema, "max_tokens", 12000); err != nil { @@ -179,11 +201,9 @@ from where the previous request stopped due to timeout.`, cmdArgs = appendRepoFlagFromEnv(cmdArgs) - // Set timeout to 1 minute for MCP server if not explicitly specified - timeoutValue := args.Timeout - if timeoutValue == 0 { - timeoutValue = 1 - } + // Scale the implicit MCP timeout with the requested fetch window so + // larger fleet-wide requests do not hit the 60s server deadline by default. + timeoutValue := effectiveMCPLogsToolTimeoutMinutes(args.Timeout, args.Count) cmdArgs = append(cmdArgs, "--timeout", strconv.Itoa(timeoutValue)) // Always use --json mode in MCP server From 52cf62c39d992b631649d2b9f03149bd71ab1467 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:02:16 +0000 Subject: [PATCH 3/4] docs: clarify scaled MCP logs timeout tiers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/mcp_tools_privileged.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/cli/mcp_tools_privileged.go b/pkg/cli/mcp_tools_privileged.go index 85e2eb106c8..532384cd666 100644 --- a/pkg/cli/mcp_tools_privileged.go +++ b/pkg/cli/mcp_tools_privileged.go @@ -47,7 +47,7 @@ type logsArgs struct { Branch string `json:"branch,omitempty" jsonschema:"Filter runs by branch name"` AfterRunID int64 `json:"after_run_id,omitempty" jsonschema:"Filter runs with database ID after this value (exclusive)"` BeforeRunID int64 `json:"before_run_id,omitempty" jsonschema:"Filter runs with database ID before this value (exclusive)"` - Timeout int `json:"timeout,omitempty" jsonschema:"Maximum time in minutes to spend downloading logs (default: auto-scales with count in the MCP server, from 1 minute for <=40 runs to 3 minutes for the default 100-run window)"` + Timeout int `json:"timeout,omitempty" jsonschema:"Maximum time in minutes to spend downloading logs (default: auto-scales with count in the MCP server: 1 minute for up to 40 runs, 2 minutes for 41-80 runs, and 3 minutes for the default 100-run window)"` MaxTokens int `json:"max_tokens,omitempty" jsonschema:"Deprecated: accepted for backward compatibility but ignored. Output is always written to a file."` Artifacts []string `json:"artifacts,omitempty" jsonschema:"Artifact sets to download (default: usage). Valid sets: all, activation, agent, detection, experiment, firewall, github-api, mcp, usage"` } @@ -57,6 +57,8 @@ func defaultMCPLogsToolTimeoutMinutesForCount(count int) int { count = defaultMCPLogsToolCount } + // Round up in 40-run increments so requests slightly above the current + // 60-second threshold automatically get an extra minute of budget. return max(defaultMCPLogsTimeoutMinutes, (count+mcpLogsRunsPerDefaultTimeoutMinute-1)/mcpLogsRunsPerDefaultTimeoutMinute) } From 75db04afe771aad8a46f2b2cda90e11f1c9cbb00 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:53:03 +0000 Subject: [PATCH 4/4] fix logs tool effective count and timeout docs/tests Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/logs_timeout_test.go | 12 ++++++++++ pkg/cli/mcp_tools_privileged.go | 26 +++++++++++++--------- pkg/cli/mcp_tools_privileged_test.go | 33 ++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/pkg/cli/logs_timeout_test.go b/pkg/cli/logs_timeout_test.go index 1c73c4c513f..9a07d2543aa 100644 --- a/pkg/cli/logs_timeout_test.go +++ b/pkg/cli/logs_timeout_test.go @@ -137,6 +137,18 @@ func TestEffectiveMCPLogsToolTimeoutMinutes(t *testing.T) { count: 41, want: 2, }, + { + name: "eighty run fetch window stays in two minute tier", + requestedTimeout: 0, + count: 80, + want: 2, + }, + { + name: "eighty one run fetch window enters three minute tier", + requestedTimeout: 0, + count: 81, + want: 3, + }, { name: "default hundred run window gets three minutes", requestedTimeout: 0, diff --git a/pkg/cli/mcp_tools_privileged.go b/pkg/cli/mcp_tools_privileged.go index 532384cd666..abb0361fda0 100644 --- a/pkg/cli/mcp_tools_privileged.go +++ b/pkg/cli/mcp_tools_privileged.go @@ -47,21 +47,26 @@ type logsArgs struct { Branch string `json:"branch,omitempty" jsonschema:"Filter runs by branch name"` AfterRunID int64 `json:"after_run_id,omitempty" jsonschema:"Filter runs with database ID after this value (exclusive)"` BeforeRunID int64 `json:"before_run_id,omitempty" jsonschema:"Filter runs with database ID before this value (exclusive)"` - Timeout int `json:"timeout,omitempty" jsonschema:"Maximum time in minutes to spend downloading logs (default: auto-scales with count in the MCP server: 1 minute for up to 40 runs, 2 minutes for 41-80 runs, and 3 minutes for the default 100-run window)"` + Timeout int `json:"timeout,omitempty" jsonschema:"Maximum time in minutes to spend downloading logs (default: auto-scales with count in the MCP server, rounded up in 40-run increments; e.g. 1 minute up to 40, 2 minutes for 41-80, 3 minutes for 81-120, and so on)"` MaxTokens int `json:"max_tokens,omitempty" jsonschema:"Deprecated: accepted for backward compatibility but ignored. Output is always written to a file."` Artifacts []string `json:"artifacts,omitempty" jsonschema:"Artifact sets to download (default: usage). Valid sets: all, activation, agent, detection, experiment, firewall, github-api, mcp, usage"` } func defaultMCPLogsToolTimeoutMinutesForCount(count int) int { - if count <= 0 { - count = defaultMCPLogsToolCount - } + count = effectiveMCPLogsToolCount(count) // Round up in 40-run increments so requests slightly above the current // 60-second threshold automatically get an extra minute of budget. return max(defaultMCPLogsTimeoutMinutes, (count+mcpLogsRunsPerDefaultTimeoutMinute-1)/mcpLogsRunsPerDefaultTimeoutMinute) } +func effectiveMCPLogsToolCount(count int) int { + if count > 0 { + return count + } + return defaultMCPLogsToolCount +} + func effectiveMCPLogsToolTimeoutMinutes(requestedTimeout, count int) int { if requestedTimeout > 0 { return requestedTimeout @@ -83,6 +88,8 @@ func registerLogsTool(server *mcp.Server, execCmd execCmdFunc, actor string, val if err := AddSchemaDefault(logsSchema, "count", defaultMCPLogsToolCount); err != nil { mcpLog.Printf("Failed to add default for count: %v", err) } + // Schema default corresponds to defaultMCPLogsToolCount; runtime timeout + // scales with the effective count used for the request. if err := AddSchemaDefault(logsSchema, "timeout", defaultMCPLogsToolTimeoutMinutesForCount(defaultMCPLogsToolCount)); err != nil { mcpLog.Printf("Failed to add default for timeout: %v", err) } @@ -165,9 +172,8 @@ from where the previous request stopped due to timeout.`, if args.WorkflowName != "" { cmdArgs = append(cmdArgs, args.WorkflowName) } - if args.Count > 0 { - cmdArgs = append(cmdArgs, "-c", strconv.Itoa(args.Count)) - } + effectiveCount := effectiveMCPLogsToolCount(args.Count) + cmdArgs = append(cmdArgs, "-c", strconv.Itoa(effectiveCount)) if args.StartDate != "" { cmdArgs = append(cmdArgs, "--start-date", args.StartDate) } @@ -205,15 +211,15 @@ from where the previous request stopped due to timeout.`, // Scale the implicit MCP timeout with the requested fetch window so // larger fleet-wide requests do not hit the 60s server deadline by default. - timeoutValue := effectiveMCPLogsToolTimeoutMinutes(args.Timeout, args.Count) + timeoutValue := effectiveMCPLogsToolTimeoutMinutes(args.Timeout, effectiveCount) cmdArgs = append(cmdArgs, "--timeout", strconv.Itoa(timeoutValue)) // Always use --json mode in MCP server cmdArgs = append(cmdArgs, "--json") // Log the command being executed for debugging - mcpLog.Printf("Executing logs tool: workflow=%s, count=%d, firewall=%v, no_firewall=%v, filtered_integrity=%v, timeout=%d, command_args=%v", - args.WorkflowName, args.Count, args.Firewall, args.NoFirewall, args.FilteredIntegrity, timeoutValue, cmdArgs) + mcpLog.Printf("Executing logs tool: workflow=%s, requested_count=%d, effective_count=%d, firewall=%v, no_firewall=%v, filtered_integrity=%v, timeout=%d, command_args=%v", + args.WorkflowName, args.Count, effectiveCount, args.Firewall, args.NoFirewall, args.FilteredIntegrity, timeoutValue, cmdArgs) notifyProgress(ctx, req, 0, 100, "Downloading workflow logs...") diff --git a/pkg/cli/mcp_tools_privileged_test.go b/pkg/cli/mcp_tools_privileged_test.go index efe3343715a..0d3c2e2b858 100644 --- a/pkg/cli/mcp_tools_privileged_test.go +++ b/pkg/cli/mcp_tools_privileged_test.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "slices" + "strconv" "strings" "sync" "testing" @@ -261,6 +262,38 @@ func TestLogsToolPassesArtifactsArgument(t *testing.T) { t.Fatal("expected --artifacts flag in command args") } +func TestLogsToolUsesEffectiveCountForTimeoutScaling(t *testing.T) { + t.Run("omitted count uses MCP default for both -c and --timeout", func(t *testing.T) { + var capturedArgs []string + mockExecCmd := func(ctx context.Context, args ...string) *exec.Cmd { + capturedArgs = append([]string(nil), args...) + return exec.CommandContext(ctx, "sh", "-c", `printf '%s' "$1"`, "sh", `{"file_path":"/tmp/gh-aw/aw-mcp/logs/runs.json"}`) + } + + server := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "1.0"}, nil) + err := registerLogsTool(server, mockExecCmd, "", false) + require.NoError(t, err, "registerLogsTool should succeed") + + session := connectInMemory(t, server) + _, err = session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: "logs", + Arguments: map[string]any{}, + }) + require.NoError(t, err, "logs tool should succeed") + + countIndex := slices.Index(capturedArgs, "-c") + require.NotEqual(t, -1, countIndex, "logs tool should pass -c to keep MCP/CLI defaults aligned") + require.Less(t, countIndex+1, len(capturedArgs), "-c should have a value") + assert.Equal(t, strconv.Itoa(defaultMCPLogsToolCount), capturedArgs[countIndex+1]) + + timeoutIndex := slices.Index(capturedArgs, "--timeout") + require.NotEqual(t, -1, timeoutIndex, "logs tool should pass --timeout") + require.Less(t, timeoutIndex+1, len(capturedArgs), "--timeout should have a value") + assert.Equal(t, strconv.Itoa(defaultMCPLogsToolTimeoutMinutesForCount(defaultMCPLogsToolCount)), capturedArgs[timeoutIndex+1]) + }) + +} + // TestAuditToolPassesGithubRepositoryAsRepoFlag verifies that the audit MCP tool // appends --repo to the subprocess command when GITHUB_REPOSITORY // is set, allowing the audit command to resolve the repository without git.