Skip to content

Commit 8d56fe2

Browse files
feat: partition tools by product/feature
1 parent 01aefd3 commit 8d56fe2

File tree

11 files changed

+1328
-227
lines changed

11 files changed

+1328
-227
lines changed

README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,79 @@ If you don't have Docker, you can use `go` to build the binary in the
9696
command with the `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable set to
9797
your token.
9898

99+
## Tool Configuration
100+
101+
The GitHub MCP Server supports enabling or disabling specific groups of functionalities via the `--toolsets` flag. This allows you to control which GitHub API capabilities are available to your AI tools.
102+
103+
### Available Toolsets
104+
105+
The following sets of tools are available:
106+
107+
| Toolset | Description | Default Status |
108+
| ----------------------- | ------------------------------------------------------------- | -------------- |
109+
| `repos` | Repository-related tools (file operations, branches, commits) | Enabled |
110+
| `issues` | Issue-related tools (create, read, update, comment) | Enabled |
111+
| `search` | Search functionality (code, repositories, users) | Enabled |
112+
| `pull_requests` | Pull request operations (create, merge, review) | Enabled |
113+
| `context` | Tools providing context about current user and GitHub context | Enabled |
114+
| `dynamic` | Tool discovery and dynamic enablement of GitHub MCP tools | Enabled |
115+
| `code_security` | Code scanning alerts and security features | Disabled |
116+
| `experiments` | Experimental features (not considered stable) | Disabled |
117+
| `all` | Special flag to enable all features | Disabled |
118+
119+
### Specifying Toolsets
120+
121+
You can enable specific features in two ways:
122+
123+
1. **Using Command Line Argument**:
124+
125+
```bash
126+
github-mcp-server --toolsets repos,issues,pull_requests,code_security
127+
```
128+
129+
2. **Using Environment Variable**:
130+
```bash
131+
GITHUB_TOOLSETS="repos,issues,pull_requests,code_security" ./github-mcp-server
132+
```
133+
134+
The environment variable `GITHUB_TOOLSETS` takes precedence over the command line argument if both are provided.
135+
136+
### Default Enabled Toolsets
137+
138+
By default, the following toolsets are enabled:
139+
140+
- `repos`
141+
- `issues`
142+
- `pull_requests`
143+
- `search`
144+
- `context-_ools`
145+
- `dynamic_tools`
146+
147+
### Using With Docker
148+
149+
When using Docker, you can pass the toolsets as environment variables:
150+
151+
```bash
152+
docker run -i --rm \
153+
-e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
154+
-e GITHUB_TOOLSETS="repos,issues,pull_requests,code_security,experiments" \
155+
ghcr.io/github/github-mcp-server
156+
```
157+
158+
### The "everything" Toolset
159+
160+
The special toolset `everything` can be provided to enable all available features regardless of any other toolsets passed:
161+
162+
```bash
163+
./github-mcp-server --toolsets everything
164+
```
165+
166+
Or using the environment variable:
167+
168+
```bash
169+
GITHUB_TOOLSETS="everything" ./github-mcp-server
170+
```
171+
99172
## GitHub Enterprise Server
100173

101174
The flag `--gh-host` and the environment variable `GH_HOST` can be used to set

cmd/github-mcp-server/main.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
stdlog "log"
88
"os"
99
"os/signal"
10+
"strings"
1011
"syscall"
1112

1213
"github.com/github/github-mcp-server/pkg/github"
@@ -43,12 +44,25 @@ var (
4344
if err != nil {
4445
stdlog.Fatal("Failed to initialize logger:", err)
4546
}
47+
48+
enabledToolsets := viper.GetStringSlice("toolsets")
49+
50+
// Env gets precedence over command line flags
51+
if envToolsets := os.Getenv("GITHUB_TOOLSETS"); envToolsets != "" {
52+
enabledToolsets = []string{}
53+
// Split envFeats by comma, trim whitespace, and add to the slice
54+
for _, toolset := range strings.Split(envToolsets, ",") {
55+
enabledToolsets = append(enabledToolsets, strings.TrimSpace(toolset))
56+
}
57+
}
58+
4659
logCommands := viper.GetBool("enable-command-logging")
4760
cfg := runConfig{
4861
readOnly: readOnly,
4962
logger: logger,
5063
logCommands: logCommands,
5164
exportTranslations: exportTranslations,
65+
enabledToolsets: enabledToolsets,
5266
}
5367
if err := runStdioServer(cfg); err != nil {
5468
stdlog.Fatal("failed to run stdio server:", err)
@@ -61,13 +75,15 @@ func init() {
6175
cobra.OnInitialize(initConfig)
6276

6377
// Add global flags that will be shared by all commands
78+
rootCmd.PersistentFlags().StringSlice("toolsets", github.DefaultTools, "A comma separated list of groups of tools to enable, defaults to issues/repos/search")
6479
rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations")
6580
rootCmd.PersistentFlags().String("log-file", "", "Path to log file")
6681
rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file")
6782
rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file")
6883
rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)")
6984

7085
// Bind flag to viper
86+
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
7187
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
7288
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
7389
_ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
@@ -106,6 +122,7 @@ type runConfig struct {
106122
logger *log.Logger
107123
logCommands bool
108124
exportTranslations bool
125+
enabledToolsets []string
109126
}
110127

111128
func runStdioServer(cfg runConfig) error {
@@ -140,8 +157,18 @@ func runStdioServer(cfg runConfig) error {
140157
getClient := func(_ context.Context) (*gogithub.Client, error) {
141158
return ghClient, nil // closing over client
142159
}
143-
// Create
144-
ghServer := github.NewServer(getClient, version, cfg.readOnly, t)
160+
161+
// Create server
162+
ghServer := github.NewServer(version)
163+
164+
// Create toolsets
165+
toolsets, err := github.InitToolsets(ghServer, cfg.enabledToolsets, cfg.readOnly, getClient, t)
166+
if err != nil {
167+
stdlog.Fatal("Failed to initialize toolsets:", err)
168+
}
169+
// Register the tools with the server
170+
toolsets.RegisterTools(ghServer)
171+
145172
stdioServer := server.NewStdioServer(ghServer)
146173

147174
stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0)

pkg/github/context_tools.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
10+
"github.com/github/github-mcp-server/pkg/translations"
11+
"github.com/mark3labs/mcp-go/mcp"
12+
"github.com/mark3labs/mcp-go/server"
13+
)
14+
15+
// GetMe creates a tool to get details of the authenticated user.
16+
func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
17+
return mcp.NewTool("get_me",
18+
mcp.WithDescription(t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request include \"me\", \"my\"...")),
19+
mcp.WithString("reason",
20+
mcp.Description("Optional: reason the session was created"),
21+
),
22+
),
23+
func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
24+
client, err := getClient(ctx)
25+
if err != nil {
26+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
27+
}
28+
user, resp, err := client.Users.Get(ctx, "")
29+
if err != nil {
30+
return nil, fmt.Errorf("failed to get user: %w", err)
31+
}
32+
defer func() { _ = resp.Body.Close() }()
33+
34+
if resp.StatusCode != http.StatusOK {
35+
body, err := io.ReadAll(resp.Body)
36+
if err != nil {
37+
return nil, fmt.Errorf("failed to read response body: %w", err)
38+
}
39+
return mcp.NewToolResultError(fmt.Sprintf("failed to get user: %s", string(body))), nil
40+
}
41+
42+
r, err := json.Marshal(user)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to marshal user: %w", err)
45+
}
46+
47+
return mcp.NewToolResultText(string(r)), nil
48+
}
49+
}

pkg/github/context_tools_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"testing"
8+
"time"
9+
10+
"github.com/github/github-mcp-server/pkg/translations"
11+
"github.com/google/go-github/v69/github"
12+
"github.com/migueleliasweb/go-github-mock/src/mock"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func Test_GetMe(t *testing.T) {
18+
// Verify tool definition
19+
mockClient := github.NewClient(nil)
20+
tool, _ := GetMe(stubGetClientFn(mockClient), translations.NullTranslationHelper)
21+
22+
assert.Equal(t, "get_me", tool.Name)
23+
assert.NotEmpty(t, tool.Description)
24+
assert.Contains(t, tool.InputSchema.Properties, "reason")
25+
assert.Empty(t, tool.InputSchema.Required) // No required parameters
26+
27+
// Setup mock user response
28+
mockUser := &github.User{
29+
Login: github.Ptr("testuser"),
30+
Name: github.Ptr("Test User"),
31+
Email: github.Ptr("test@example.com"),
32+
Bio: github.Ptr("GitHub user for testing"),
33+
Company: github.Ptr("Test Company"),
34+
Location: github.Ptr("Test Location"),
35+
HTMLURL: github.Ptr("https://github.com/testuser"),
36+
CreatedAt: &github.Timestamp{Time: time.Now().Add(-365 * 24 * time.Hour)},
37+
Type: github.Ptr("User"),
38+
Plan: &github.Plan{
39+
Name: github.Ptr("pro"),
40+
},
41+
}
42+
43+
tests := []struct {
44+
name string
45+
mockedClient *http.Client
46+
requestArgs map[string]interface{}
47+
expectError bool
48+
expectedUser *github.User
49+
expectedErrMsg string
50+
}{
51+
{
52+
name: "successful get user",
53+
mockedClient: mock.NewMockedHTTPClient(
54+
mock.WithRequestMatch(
55+
mock.GetUser,
56+
mockUser,
57+
),
58+
),
59+
requestArgs: map[string]interface{}{},
60+
expectError: false,
61+
expectedUser: mockUser,
62+
},
63+
{
64+
name: "successful get user with reason",
65+
mockedClient: mock.NewMockedHTTPClient(
66+
mock.WithRequestMatch(
67+
mock.GetUser,
68+
mockUser,
69+
),
70+
),
71+
requestArgs: map[string]interface{}{
72+
"reason": "Testing API",
73+
},
74+
expectError: false,
75+
expectedUser: mockUser,
76+
},
77+
{
78+
name: "get user fails",
79+
mockedClient: mock.NewMockedHTTPClient(
80+
mock.WithRequestMatchHandler(
81+
mock.GetUser,
82+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
83+
w.WriteHeader(http.StatusUnauthorized)
84+
_, _ = w.Write([]byte(`{"message": "Unauthorized"}`))
85+
}),
86+
),
87+
),
88+
requestArgs: map[string]interface{}{},
89+
expectError: true,
90+
expectedErrMsg: "failed to get user",
91+
},
92+
}
93+
94+
for _, tc := range tests {
95+
t.Run(tc.name, func(t *testing.T) {
96+
// Setup client with mock
97+
client := github.NewClient(tc.mockedClient)
98+
_, handler := GetMe(stubGetClientFn(client), translations.NullTranslationHelper)
99+
100+
// Create call request
101+
request := createMCPRequest(tc.requestArgs)
102+
103+
// Call handler
104+
result, err := handler(context.Background(), request)
105+
106+
// Verify results
107+
if tc.expectError {
108+
require.Error(t, err)
109+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
110+
return
111+
}
112+
113+
require.NoError(t, err)
114+
115+
// Parse result and get text content if no error
116+
textContent := getTextResult(t, result)
117+
118+
// Unmarshal and verify the result
119+
var returnedUser github.User
120+
err = json.Unmarshal([]byte(textContent.Text), &returnedUser)
121+
require.NoError(t, err)
122+
123+
// Verify user details
124+
assert.Equal(t, *tc.expectedUser.Login, *returnedUser.Login)
125+
assert.Equal(t, *tc.expectedUser.Name, *returnedUser.Name)
126+
assert.Equal(t, *tc.expectedUser.Email, *returnedUser.Email)
127+
assert.Equal(t, *tc.expectedUser.Bio, *returnedUser.Bio)
128+
assert.Equal(t, *tc.expectedUser.HTMLURL, *returnedUser.HTMLURL)
129+
assert.Equal(t, *tc.expectedUser.Type, *returnedUser.Type)
130+
})
131+
}
132+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy