Skip to content

Commit 09622d4

Browse files
Merge branch 'main' into improve/license-automation
2 parents a6359ad + f9ef72f commit 09622d4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+9009
-6494
lines changed

.github/workflows/docker-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
# multi-platform images and export cache
5555
# https://github.com/docker/setup-buildx-action
5656
- name: Set up Docker Buildx
57-
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
57+
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
5858

5959
# Login against a Docker registry except on PR
6060
# https://github.com/docker/login-action

README.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ When using Docker, you can pass the toolsets as environment variables:
393393
```bash
394394
docker run -i --rm \
395395
-e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
396-
-e GITHUB_TOOLSETS="repos,issues,pull_requests,actions,code_security,experiments" \
396+
-e GITHUB_TOOLSETS="repos,issues,pull_requests,actions,code_security" \
397397
ghcr.io/github/github-mcp-server
398398
```
399399

@@ -490,6 +490,40 @@ The following sets of tools are available:
490490

491491
<summary><picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/workflow-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/workflow-light.png"><img src="pkg/octicons/icons/workflow-light.png" width="20" height="20" alt="workflow"></picture> Actions</summary>
492492

493+
- **actions_get** - Get details of GitHub Actions resources (workflows, workflow runs, jobs, and artifacts)
494+
- `method`: The method to execute (string, required)
495+
- `owner`: Repository owner (string, required)
496+
- `repo`: Repository name (string, required)
497+
- `resource_id`: The unique identifier of the resource. This will vary based on the "method" provided, so ensure you provide the correct ID:
498+
- Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'get_workflow' method.
499+
- Provide a workflow run ID for 'get_workflow_run', 'get_workflow_run_usage', and 'get_workflow_run_logs_url' methods.
500+
- Provide an artifact ID for 'download_workflow_run_artifact' method.
501+
- Provide a job ID for 'get_workflow_job' method.
502+
(string, required)
503+
504+
- **actions_list** - List GitHub Actions workflows in a repository
505+
- `method`: The action to perform (string, required)
506+
- `owner`: Repository owner (string, required)
507+
- `page`: Page number for pagination (default: 1) (number, optional)
508+
- `per_page`: Results per page for pagination (default: 30, max: 100) (number, optional)
509+
- `repo`: Repository name (string, required)
510+
- `resource_id`: The unique identifier of the resource. This will vary based on the "method" provided, so ensure you provide the correct ID:
511+
- Do not provide any resource ID for 'list_workflows' method.
512+
- Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'list_workflow_runs' method.
513+
- Provide a workflow run ID for 'list_workflow_jobs' and 'list_workflow_run_artifacts' methods.
514+
(string, optional)
515+
- `workflow_jobs_filter`: Filters for workflow jobs. **ONLY** used when method is 'list_workflow_jobs' (object, optional)
516+
- `workflow_runs_filter`: Filters for workflow runs. **ONLY** used when method is 'list_workflow_runs' (object, optional)
517+
518+
- **actions_run_trigger** - Trigger GitHub Actions workflow actions
519+
- `inputs`: Inputs the workflow accepts. Only used for 'run_workflow' method. (object, optional)
520+
- `method`: The method to execute (string, required)
521+
- `owner`: Repository owner (string, required)
522+
- `ref`: The git reference for the workflow. The reference can be a branch or tag name. Required for 'run_workflow' method. (string, optional)
523+
- `repo`: Repository name (string, required)
524+
- `run_id`: The ID of the workflow run. Required for all methods except 'run_workflow'. (number, optional)
525+
- `workflow_id`: The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml). Required for 'run_workflow' method. (string, optional)
526+
493527
- **cancel_workflow_run** - Cancel workflow run
494528
- `owner`: Repository owner (string, required)
495529
- `repo`: Repository name (string, required)
@@ -514,6 +548,15 @@ The following sets of tools are available:
514548
- `run_id`: Workflow run ID (required when using failed_only) (number, optional)
515549
- `tail_lines`: Number of lines to return from the end of the log (number, optional)
516550

551+
- **get_job_logs** - Get GitHub Actions workflow job logs
552+
- `failed_only`: When true, gets logs for all failed jobs in the workflow run specified by run_id. Requires run_id to be provided. (boolean, optional)
553+
- `job_id`: The unique identifier of the workflow job. Required when getting logs for a single job. (number, optional)
554+
- `owner`: Repository owner (string, required)
555+
- `repo`: Repository name (string, required)
556+
- `return_content`: Returns actual log content instead of URLs (boolean, optional)
557+
- `run_id`: The unique identifier of the workflow run. Required when failed_only is true to get logs for all failed jobs in the run. (number, optional)
558+
- `tail_lines`: Number of lines to return from the end of the log (number, optional)
559+
517560
- **get_workflow_run** - Get workflow run
518561
- `owner`: Repository owner (string, required)
519562
- `repo`: Repository name (string, required)

cmd/github-mcp-server/main.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,18 @@ var (
5454

5555
// Parse tools (similar to toolsets)
5656
var enabledTools []string
57-
if err := viper.UnmarshalKey("tools", &enabledTools); err != nil {
58-
return fmt.Errorf("failed to unmarshal tools: %w", err)
57+
if viper.IsSet("tools") {
58+
if err := viper.UnmarshalKey("tools", &enabledTools); err != nil {
59+
return fmt.Errorf("failed to unmarshal tools: %w", err)
60+
}
5961
}
6062

6163
// Parse enabled features (similar to toolsets)
6264
var enabledFeatures []string
63-
if err := viper.UnmarshalKey("features", &enabledFeatures); err != nil {
64-
return fmt.Errorf("failed to unmarshal features: %w", err)
65+
if viper.IsSet("features") {
66+
if err := viper.UnmarshalKey("features", &enabledFeatures); err != nil {
67+
return fmt.Errorf("failed to unmarshal features: %w", err)
68+
}
6569
}
6670

6771
ttl := viper.GetDuration("repo-access-cache-ttl")

docs/tool-renaming.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,20 @@ Will get `issue_read` and `get_file_contents` tools registered, with no errors.
4646
<!-- START AUTOMATED ALIASES -->
4747
| Old Name | New Name |
4848
|----------|----------|
49-
| *(none currently)* | |
49+
| `cancel_workflow_run` | `actions_run_trigger` |
50+
| `delete_workflow_run_logs` | `actions_run_trigger` |
51+
| `download_workflow_run_artifact` | `actions_get` |
52+
| `get_workflow` | `actions_get` |
53+
| `get_workflow_job` | `actions_get` |
54+
| `get_workflow_job_logs` | `actions_get` |
55+
| `get_workflow_run` | `actions_get` |
56+
| `get_workflow_run_logs` | `actions_get` |
57+
| `get_workflow_run_usage` | `actions_get` |
58+
| `list_workflow_jobs` | `actions_list` |
59+
| `list_workflow_run_artifacts` | `actions_list` |
60+
| `list_workflow_runs` | `actions_list` |
61+
| `list_workflows` | `actions_list` |
62+
| `rerun_failed_jobs` | `actions_run_trigger` |
63+
| `rerun_workflow_run` | `actions_run_trigger` |
64+
| `run_workflow` | `actions_run_trigger` |
5065
<!-- END AUTOMATED ALIASES -->

internal/ghmcp/server.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,13 @@ func NewMCPServer(cfg MCPServerConfig) (*mcp.Server, error) {
203203
cfg.ContentWindowSize,
204204
)
205205

206+
// Inject dependencies into context for all tool handlers
207+
ghServer.AddReceivingMiddleware(func(next mcp.MethodHandler) mcp.MethodHandler {
208+
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
209+
return next(github.ContextWithDeps(ctx, deps), method, req)
210+
}
211+
})
212+
206213
// Build and register the tool/resource/prompt inventory
207214
inventory := github.NewInventory(cfg.Translator).
208215
WithDeprecatedAliases(github.DeprecatedToolAliases).

internal/ghmcp/server_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package ghmcp
2+
3+
import (
4+
"testing"
5+
6+
"github.com/github/github-mcp-server/pkg/translations"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// TestNewMCPServer_CreatesSuccessfully verifies that the server can be created
12+
// with the deps injection middleware properly configured.
13+
func TestNewMCPServer_CreatesSuccessfully(t *testing.T) {
14+
t.Parallel()
15+
16+
// Create a minimal server configuration
17+
cfg := MCPServerConfig{
18+
Version: "test",
19+
Host: "", // defaults to github.com
20+
Token: "test-token",
21+
EnabledToolsets: []string{"context"},
22+
ReadOnly: false,
23+
Translator: translations.NullTranslationHelper,
24+
ContentWindowSize: 5000,
25+
LockdownMode: false,
26+
}
27+
28+
// Create the server
29+
server, err := NewMCPServer(cfg)
30+
require.NoError(t, err, "expected server creation to succeed")
31+
require.NotNil(t, server, "expected server to be non-nil")
32+
33+
// The fact that the server was created successfully indicates that:
34+
// 1. The deps injection middleware is properly added
35+
// 2. Tools can be registered without panicking
36+
//
37+
// If the middleware wasn't properly added, tool calls would panic with
38+
// "ToolDependencies not found in context" when executed.
39+
//
40+
// The actual middleware functionality and tool execution with ContextWithDeps
41+
// is already tested in pkg/github/*_test.go.
42+
}
43+
44+
// TestResolveEnabledToolsets verifies the toolset resolution logic.
45+
func TestResolveEnabledToolsets(t *testing.T) {
46+
t.Parallel()
47+
48+
tests := []struct {
49+
name string
50+
cfg MCPServerConfig
51+
expectedResult []string
52+
}{
53+
{
54+
name: "nil toolsets without dynamic mode and no tools - use defaults",
55+
cfg: MCPServerConfig{
56+
EnabledToolsets: nil,
57+
DynamicToolsets: false,
58+
EnabledTools: nil,
59+
},
60+
expectedResult: nil, // nil means "use defaults"
61+
},
62+
{
63+
name: "nil toolsets with dynamic mode - start empty",
64+
cfg: MCPServerConfig{
65+
EnabledToolsets: nil,
66+
DynamicToolsets: true,
67+
EnabledTools: nil,
68+
},
69+
expectedResult: []string{}, // empty slice means no toolsets
70+
},
71+
{
72+
name: "explicit toolsets",
73+
cfg: MCPServerConfig{
74+
EnabledToolsets: []string{"repos", "issues"},
75+
DynamicToolsets: false,
76+
},
77+
expectedResult: []string{"repos", "issues"},
78+
},
79+
{
80+
name: "empty toolsets - disable all",
81+
cfg: MCPServerConfig{
82+
EnabledToolsets: []string{},
83+
DynamicToolsets: false,
84+
},
85+
expectedResult: []string{}, // empty slice means no toolsets
86+
},
87+
{
88+
name: "specific tools without toolsets - no default toolsets",
89+
cfg: MCPServerConfig{
90+
EnabledToolsets: nil,
91+
DynamicToolsets: false,
92+
EnabledTools: []string{"get_me"},
93+
},
94+
expectedResult: []string{}, // empty slice when tools specified but no toolsets
95+
},
96+
{
97+
name: "dynamic mode with explicit toolsets removes all and default",
98+
cfg: MCPServerConfig{
99+
EnabledToolsets: []string{"all", "repos"},
100+
DynamicToolsets: true,
101+
},
102+
expectedResult: []string{"repos"}, // "all" is removed in dynamic mode
103+
},
104+
}
105+
106+
for _, tc := range tests {
107+
t.Run(tc.name, func(t *testing.T) {
108+
result := resolveEnabledToolsets(tc.cfg)
109+
assert.Equal(t, tc.expectedResult, result)
110+
})
111+
}
112+
}

pkg/buffer/buffer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import (
2626
// The function uses a ring buffer to efficiently store only the last maxJobLogLines lines.
2727
// If the response contains more lines than maxJobLogLines, only the most recent lines are kept.
2828
func ProcessResponseAsRingBufferToEnd(httpResp *http.Response, maxJobLogLines int) (string, int, *http.Response, error) {
29+
if maxJobLogLines > 100000 {
30+
maxJobLogLines = 100000
31+
}
32+
2933
lines := make([]string, maxJobLogLines)
3034
validLines := make([]bool, maxJobLogLines)
3135
totalLines := 0

pkg/errors/error.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package errors
33
import (
44
"context"
55
"fmt"
6+
"net/http"
67

78
"github.com/github/github-mcp-server/pkg/utils"
89
"github.com/google/go-github/v79/github"
@@ -44,10 +45,29 @@ func (e *GitHubGraphQLError) Error() string {
4445
return fmt.Errorf("%s: %w", e.Message, e.Err).Error()
4546
}
4647

48+
type GitHubRawAPIError struct {
49+
Message string `json:"message"`
50+
Response *http.Response `json:"-"`
51+
Err error `json:"-"`
52+
}
53+
54+
func newGitHubRawAPIError(message string, resp *http.Response, err error) *GitHubRawAPIError {
55+
return &GitHubRawAPIError{
56+
Message: message,
57+
Response: resp,
58+
Err: err,
59+
}
60+
}
61+
62+
func (e *GitHubRawAPIError) Error() string {
63+
return fmt.Errorf("%s: %w", e.Message, e.Err).Error()
64+
}
65+
4766
type GitHubErrorKey struct{}
4867
type GitHubCtxErrors struct {
4968
api []*GitHubAPIError
5069
graphQL []*GitHubGraphQLError
70+
raw []*GitHubRawAPIError
5171
}
5272

5373
// ContextWithGitHubErrors updates or creates a context with a pointer to GitHub error information (to be used by middleware).
@@ -59,6 +79,7 @@ func ContextWithGitHubErrors(ctx context.Context) context.Context {
5979
// If the context already has GitHubCtxErrors, we just empty the slices to start fresh
6080
val.api = []*GitHubAPIError{}
6181
val.graphQL = []*GitHubGraphQLError{}
82+
val.raw = []*GitHubRawAPIError{}
6283
} else {
6384
// If not, we create a new GitHubCtxErrors and set it in the context
6485
ctx = context.WithValue(ctx, GitHubErrorKey{}, &GitHubCtxErrors{})
@@ -83,6 +104,14 @@ func GetGitHubGraphQLErrors(ctx context.Context) ([]*GitHubGraphQLError, error)
83104
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
84105
}
85106

107+
// GetGitHubRawAPIErrors retrieves the slice of GitHubRawAPIErrors from the context.
108+
func GetGitHubRawAPIErrors(ctx context.Context) ([]*GitHubRawAPIError, error) {
109+
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
110+
return val.raw, nil // return the slice of raw API errors from the context
111+
}
112+
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
113+
}
114+
86115
func NewGitHubAPIErrorToCtx(ctx context.Context, message string, resp *github.Response, err error) (context.Context, error) {
87116
apiErr := newGitHubAPIError(message, resp, err)
88117
if ctx != nil {
@@ -107,6 +136,15 @@ func addGitHubGraphQLErrorToContext(ctx context.Context, err *GitHubGraphQLError
107136
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
108137
}
109138

139+
func addRawAPIErrorToContext(ctx context.Context, err *GitHubRawAPIError) (context.Context, error) {
140+
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
141+
val.raw = append(val.raw, err)
142+
return ctx, nil
143+
}
144+
145+
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
146+
}
147+
110148
// NewGitHubAPIErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware
111149
func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github.Response, err error) *mcp.CallToolResult {
112150
apiErr := newGitHubAPIError(message, resp, err)
@@ -125,6 +163,15 @@ func NewGitHubGraphQLErrorResponse(ctx context.Context, message string, err erro
125163
return utils.NewToolResultErrorFromErr(message, err)
126164
}
127165

166+
// NewGitHubRawAPIErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware
167+
func NewGitHubRawAPIErrorResponse(ctx context.Context, message string, resp *http.Response, err error) *mcp.CallToolResult {
168+
rawErr := newGitHubRawAPIError(message, resp, err)
169+
if ctx != nil {
170+
_, _ = addRawAPIErrorToContext(ctx, rawErr) // Explicitly ignore error for graceful handling
171+
}
172+
return utils.NewToolResultErrorFromErr(message, err)
173+
}
174+
128175
// NewGitHubAPIStatusErrorResponse handles cases where the API call succeeds (err == nil)
129176
// but returns an unexpected HTTP status code. It creates a synthetic error from the
130177
// status code and response body, then records it in context for observability tracking.

0 commit comments

Comments
 (0)