Skip to content
Closed
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
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,35 @@ For a complete overview of all installation options, see our **[Installation Gui

> **Note:** Any host application that supports local MCP servers should be able to access the local GitHub MCP server. However, the specific configuration process, syntax and stability of the integration will vary by host application. While many may follow a similar format to the examples above, this is not guaranteed. Please refer to your host application's documentation for the correct MCP configuration syntax and setup process.

### Install with Go

If you have Go installed, you can install the server with a single command:

```bash
go install github.com/github/github-mcp-server/cmd/github-mcp-server@latest
```

Then configure your MCP client to use the local binary:

```JSON
{
"mcp": {
"servers": {
"github": {
"command": "github-mcp-server",
"args": ["stdio"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
}
}
}
}
}
```

### Build from source

If you don't have Docker, you can use `go build` to build the binary in the
If you don't have Docker or Go, you can use `go build` to build the binary in the
`cmd/github-mcp-server` directory, and use the `github-mcp-server stdio` command with the `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable set to your token. To specify the output location of the build, use the `-o` flag. You should configure your server to use the built executable as its `command`. For example:

```JSON
Expand Down
21 changes: 21 additions & 0 deletions pkg/errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}

Expand Down
70 changes: 63 additions & 7 deletions pkg/github/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkg/github/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down