From 50dc9b9367c97601c2239ed4733dc7c6a91545b7 Mon Sep 17 00:00:00 2001 From: Will-hxw Date: Wed, 29 Apr 2026 07:31:27 +0800 Subject: [PATCH 1/3] docs(search): improve search_code query description to prevent 422 errors The previous description gave AI models insufficient guidance on GitHub code search syntax, leading to common mistakes like: - Using \| instead of /...|.../ for regex OR - Confusing filename: with path: qualifiers - Misunderstanding boolean operator precedence The new description explicitly documents: - Qualifiers: repo:, org:, language:, path:, path:/regex/, symbol:, content:, is: - Boolean operators: AND, OR, NOT, parentheses - Regex syntax: surround with slashes e.g. /foo|bar/ - Glob patterns in path: path:*.ts, path:/src/**/*.js Fixes #2390 --- pkg/github/search.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/github/search.go b/pkg/github/search.go index d5ddb4a72a..d3b954b715 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -173,7 +173,7 @@ func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool { Properties: map[string]*jsonschema.Schema{ "query": { Type: "string", - Description: "Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more.", + Description: "Search query using GitHub code search syntax. Qualifiers: repo:owner/repo, org:name, language:Go, path:src/*.js, path:/regex/, symbol:Name, content:term, is:archived|fork. Boolean: AND, OR, NOT, parentheses. Regex: surround with slashes e.g. /sparse.*index/. Glob in path: path:*.ts, path:/src/**/*.js. Examples: 'MyFunc language:go repo:owner/repo', 'symbol:WithContext language:go', '/GetAttributes|SetAttributes/ repo:owner/repo', '(Foo OR Bar) path:src repo:owner/repo'.", }, "sort": { Type: "string", From 7b962fdab2cd22fd400dfabe989cc9608859bd3b Mon Sep 17 00:00:00 2001 From: Will-hxw Date: Wed, 29 Apr 2026 10:19:28 +0800 Subject: [PATCH 2/3] fix(actions): allow run_id alone to get all job logs Allow get_job_logs tool to return logs for all jobs in a workflow run when only run_id is provided (without failed_only parameter). This matches the documented behavior where: - run_id + failed_only=true returns only failed job logs - run_id alone (or with failed_only=false) returns all job logs Fixes #2389 --- pkg/github/actions.go | 70 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/pkg/github/actions.go b/pkg/github/actions.go index c3b5bb8c71..aafefb6ca5 100644 --- a/pkg/github/actions.go +++ b/pkg/github/actions.go @@ -125,6 +125,61 @@ func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo return utils.NewToolResultText(string(r)), nil, nil } +// handleAllJobLogs gets logs for all jobs in a workflow run +func handleAllJobLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) { + // First, get all jobs for the workflow run + jobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, &github.ListWorkflowJobsOptions{ + Filter: "latest", + }) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow jobs", resp, err), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if len(jobs.Jobs) == 0 { + result := map[string]any{ + "message": "No jobs found in this workflow run", + "run_id": runID, + "total_jobs": 0, + } + r, _ := json.Marshal(result) + return utils.NewToolResultText(string(r)), nil, nil + } + + // Collect logs for all jobs + var logResults []map[string]any + for _, job := range jobs.Jobs { + jobResult, resp, err := getJobLogData(ctx, client, owner, repo, job.GetID(), job.GetName(), returnContent, tailLines, contentWindowSize) + if err != nil { + // Continue with other jobs even if one fails + jobResult = map[string]any{ + "job_id": job.GetID(), + "job_name": job.GetName(), + "error": err.Error(), + } + // Enable reporting of status codes and error causes + _, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get job logs", resp, err) // Explicitly ignore error for graceful handling + } + + logResults = append(logResults, jobResult) + } + + result := map[string]any{ + "message": fmt.Sprintf("Retrieved logs for %d jobs", len(jobs.Jobs)), + "run_id": runID, + "total_jobs": len(jobs.Jobs), + "logs": logResults, + "return_format": map[string]bool{"content": returnContent, "urls": !returnContent}, + } + + r, err := json.Marshal(result) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil, nil +} + // getJobLogData retrieves log data for a single job, either as URL or content func getJobLogData(ctx context.Context, client *github.Client, owner, repo string, jobID int64, jobName string, returnContent bool, tailLines int, contentWindowSize int) (map[string]any, *github.Response, error) { // Get the download URL for the job logs @@ -716,19 +771,20 @@ For single job logs, provide job_id. For all failed jobs in a run, provide run_i if failedOnly && runID == 0 { return utils.NewToolResultError("run_id is required when failed_only is true"), nil, nil } - if !failedOnly && jobID == 0 { - return utils.NewToolResultError("job_id is required when failed_only is false"), nil, nil - } - if failedOnly && runID > 0 { - // Handle failed-only mode: get logs for all failed jobs in the workflow run - return handleFailedJobLogs(ctx, client, owner, repo, int64(runID), returnContent, tailLines, deps.GetContentWindowSize()) + if runID > 0 { + if failedOnly { + // Handle failed-only mode: get logs for all failed jobs in the workflow run + return handleFailedJobLogs(ctx, client, owner, repo, int64(runID), returnContent, tailLines, deps.GetContentWindowSize()) + } + // Handle all jobs mode: get logs for all jobs in the workflow run + return handleAllJobLogs(ctx, client, owner, repo, int64(runID), returnContent, tailLines, deps.GetContentWindowSize()) } else if jobID > 0 { // Handle single job mode return handleSingleJobLogs(ctx, client, owner, repo, int64(jobID), returnContent, tailLines, deps.GetContentWindowSize()) } - return utils.NewToolResultError("Either job_id must be provided for single job logs, or run_id with failed_only=true for failed job logs"), nil, nil + return utils.NewToolResultError("Either job_id or run_id must be provided"), nil, nil }, ) return tool From dd5d5a8e293e99ddbcc70aac7c0f9a456ecdf012 Mon Sep 17 00:00:00 2001 From: Will-hxw Date: Wed, 29 Apr 2026 10:28:05 +0800 Subject: [PATCH 3/3] fix(errors): improve rate limit error messages for agents Detect RateLimitError and AbuseRateLimitError in NewGitHubAPIErrorResponse and return clear, actionable messages with retry duration instead of burying the information in raw HTTP strings. Fixes #2385 --- pkg/errors/error.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pkg/errors/error.go b/pkg/errors/error.go index d757651592..ce3bc6bcf8 100644 --- a/pkg/errors/error.go +++ b/pkg/errors/error.go @@ -2,8 +2,10 @@ package errors import ( "context" + "errors" "fmt" "net/http" + "time" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v82/github" @@ -159,6 +161,25 @@ func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github if ctx != nil { _, _ = addGitHubAPIErrorToContext(ctx, apiErr) // Explicitly ignore error for graceful handling } + + // Handle rate limit errors with clear, actionable messages + var rateLimitErr *github.RateLimitError + if errors.As(err, &rateLimitErr) { + retryIn := time.Until(rateLimitErr.Rate.Reset.Time).Round(time.Second) + return utils.NewToolResultError(fmt.Sprintf( + "%s: GitHub API rate limit exceeded. Retry after %v.", message, retryIn)) + } + + var abuseErr *github.AbuseRateLimitError + if errors.As(err, &abuseErr) { + if abuseErr.RetryAfter != nil { + return utils.NewToolResultError(fmt.Sprintf( + "%s: GitHub secondary rate limit exceeded. Retry after %v.", message, abuseErr.RetryAfter.Round(time.Second))) + } + return utils.NewToolResultError(fmt.Sprintf( + "%s: GitHub secondary rate limit exceeded. Wait before retrying.", message)) + } + return utils.NewToolResultErrorFromErr(message, err) }