Skip to content

Commit 45f55fc

Browse files
feat: partition tools by product/feature
1 parent bbba3bb commit 45f55fc

19 files changed

+1077
-255
lines changed

README.md

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,95 @@ 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 (all are on by default):
106+
107+
| Toolset | Description |
108+
| ----------------------- | ------------------------------------------------------------- |
109+
| `repos` | Repository-related tools (file operations, branches, commits) |
110+
| `issues` | Issue-related tools (create, read, update, comment) |
111+
| `users ` | Anything relating to GitHub Users |
112+
| `pull_requests` | Pull request operations (create, merge, review) |
113+
| `code_security` | Code scanning alerts and security features |
114+
| `experiments` | Experimental features (not considered stable) |
115+
116+
#### Specifying Toolsets
117+
118+
To reduce the available tools, you can pass an allow-list in two ways:
119+
120+
1. **Using Command Line Argument**:
121+
122+
```bash
123+
github-mcp-server --toolsets repos,issues,pull_requests,code_security
124+
```
125+
126+
2. **Using Environment Variable**:
127+
```bash
128+
GITHUB_TOOLSETS="repos,issues,pull_requests,code_security" ./github-mcp-server
129+
```
130+
131+
The environment variable `GITHUB_TOOLSETS` takes precedence over the command line argument if both are provided.
132+
133+
Any toolsets you specify will be enabled from the start, including when `--dynamic-toolsets` is on.
134+
135+
You might want to do this if the model is confused about which tools to call and you only require a subset.
136+
137+
138+
### Using Toolsets With Docker
139+
140+
When using Docker, you can pass the toolsets as environment variables:
141+
142+
```bash
143+
docker run -i --rm \
144+
-e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
145+
-e GITHUB_TOOLSETS="repos,issues,pull_requests,code_security,experiments" \
146+
ghcr.io/github/github-mcp-server
147+
```
148+
149+
### The "all" Toolset
150+
151+
The special toolset `all` can be provided to enable all available toolsets regardless of any other configuration:
152+
153+
```bash
154+
./github-mcp-server --toolsets all
155+
```
156+
157+
Or using the environment variable:
158+
159+
```bash
160+
GITHUB_TOOLSETS="all" ./github-mcp-server
161+
```
162+
163+
## Dynamic Tool Discovery
164+
165+
Instead of starting with all tools enabled, you can turn on Dynamic Toolset Discovery.
166+
This feature provides tools that help the MCP Host application to discover and enable sets of GitHub tools only when needed.
167+
This helps to avoid situations where models get confused by the shear number of tools available to them, which varies by model.
168+
169+
### Using Dynamic Tool Discovery
170+
171+
When using the binary, you can pass the `--dynamic-toolsets` flag.
172+
173+
```bash
174+
./github-mcp-server --dynamic-toolsets
175+
```
176+
177+
When using Docker, you can pass the toolsets as environment variables:
178+
179+
```bash
180+
docker run -i --rm \
181+
-e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
182+
-e GITHUB_DYNAMIC_TOOLSETS=1 \
183+
ghcr.io/github/github-mcp-server
184+
```
185+
186+
187+
99188
## GitHub Enterprise Server
100189

101190
The flag `--gh-host` and the environment variable `GH_HOST` can be used to set
@@ -317,7 +406,6 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
317406
### Repositories
318407

319408
- **create_or_update_file** - Create or update a single file in a repository
320-
321409
- `owner`: Repository owner (string, required)
322410
- `repo`: Repository name (string, required)
323411
- `path`: File path (string, required)
@@ -327,50 +415,43 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
327415
- `sha`: File SHA if updating (string, optional)
328416

329417
- **list_branches** - List branches in a GitHub repository
330-
331418
- `owner`: Repository owner (string, required)
332419
- `repo`: Repository name (string, required)
333420
- `page`: Page number (number, optional)
334421
- `perPage`: Results per page (number, optional)
335422

336423
- **push_files** - Push multiple files in a single commit
337-
338424
- `owner`: Repository owner (string, required)
339425
- `repo`: Repository name (string, required)
340426
- `branch`: Branch to push to (string, required)
341427
- `files`: Files to push, each with path and content (array, required)
342428
- `message`: Commit message (string, required)
343429

344430
- **search_repositories** - Search for GitHub repositories
345-
346431
- `query`: Search query (string, required)
347432
- `sort`: Sort field (string, optional)
348433
- `order`: Sort order (string, optional)
349434
- `page`: Page number (number, optional)
350435
- `perPage`: Results per page (number, optional)
351436

352437
- **create_repository** - Create a new GitHub repository
353-
354438
- `name`: Repository name (string, required)
355439
- `description`: Repository description (string, optional)
356440
- `private`: Whether the repository is private (boolean, optional)
357441
- `autoInit`: Auto-initialize with README (boolean, optional)
358442

359443
- **get_file_contents** - Get contents of a file or directory
360-
361444
- `owner`: Repository owner (string, required)
362445
- `repo`: Repository name (string, required)
363446
- `path`: File path (string, required)
364447
- `ref`: Git reference (string, optional)
365448

366449
- **fork_repository** - Fork a repository
367-
368450
- `owner`: Repository owner (string, required)
369451
- `repo`: Repository name (string, required)
370452
- `organization`: Target organization name (string, optional)
371453

372454
- **create_branch** - Create a new branch
373-
374455
- `owner`: Repository owner (string, required)
375456
- `repo`: Repository name (string, required)
376457
- `branch`: New branch name (string, required)
@@ -391,16 +472,15 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
391472
- `page`: Page number, for files in the commit (number, optional)
392473
- `perPage`: Results per page, for files in the commit (number, optional)
393474

394-
### Search
395-
396-
- **search_code** - Search for code across GitHub repositories
397-
475+
- **search_code** - Search for code across GitHub repositories
398476
- `query`: Search query (string, required)
399477
- `sort`: Sort field (string, optional)
400478
- `order`: Sort order (string, optional)
401479
- `page`: Page number (number, optional)
402480
- `perPage`: Results per page (number, optional)
403481

482+
### Users
483+
404484
- **search_users** - Search for GitHub users
405485
- `query`: Search query (string, required)
406486
- `sort`: Sort field (string, optional)

cmd/github-mcp-server/main.go

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,16 @@ var (
4444
if err != nil {
4545
stdlog.Fatal("Failed to initialize logger:", err)
4646
}
47+
48+
enabledToolsets := viper.GetStringSlice("toolsets")
49+
4750
logCommands := viper.GetBool("enable-command-logging")
4851
cfg := runConfig{
4952
readOnly: readOnly,
5053
logger: logger,
5154
logCommands: logCommands,
5255
exportTranslations: exportTranslations,
56+
enabledToolsets: enabledToolsets,
5357
}
5458
if err := runStdioServer(cfg); err != nil {
5559
stdlog.Fatal("failed to run stdio server:", err)
@@ -62,26 +66,30 @@ func init() {
6266
cobra.OnInitialize(initConfig)
6367

6468
// Add global flags that will be shared by all commands
69+
rootCmd.PersistentFlags().StringSlice("toolsets", github.DefaultTools, "An optional comma separated list of groups of tools to allow, defaults to enabling all")
70+
rootCmd.PersistentFlags().Bool("dynamic-toolsets", false, "Enable dynamic toolsets")
6571
rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations")
6672
rootCmd.PersistentFlags().String("log-file", "", "Path to log file")
6773
rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file")
6874
rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file")
6975
rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)")
7076

7177
// Bind flag to viper
78+
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
79+
_ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets"))
7280
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
7381
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
7482
_ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
7583
_ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations"))
76-
_ = viper.BindPFlag("gh-host", rootCmd.PersistentFlags().Lookup("gh-host"))
84+
_ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host"))
7785

7886
// Add subcommands
7987
rootCmd.AddCommand(stdioCmd)
8088
}
8189

8290
func initConfig() {
8391
// Initialize Viper configuration
84-
viper.SetEnvPrefix("APP")
92+
viper.SetEnvPrefix("github")
8593
viper.AutomaticEnv()
8694
}
8795

@@ -107,6 +115,7 @@ type runConfig struct {
107115
logger *log.Logger
108116
logCommands bool
109117
exportTranslations bool
118+
enabledToolsets []string
110119
}
111120

112121
func runStdioServer(cfg runConfig) error {
@@ -115,18 +124,14 @@ func runStdioServer(cfg runConfig) error {
115124
defer stop()
116125

117126
// Create GH client
118-
token := os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN")
127+
token := viper.GetString("personal_access_token")
119128
if token == "" {
120129
cfg.logger.Fatal("GITHUB_PERSONAL_ACCESS_TOKEN not set")
121130
}
122131
ghClient := gogithub.NewClient(nil).WithAuthToken(token)
123132
ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", version)
124133

125-
// Check GH_HOST env var first, then fall back to viper config
126-
host := os.Getenv("GH_HOST")
127-
if host == "" {
128-
host = viper.GetString("gh-host")
129-
}
134+
host := viper.GetString("host")
130135

131136
if host != "" {
132137
var err error
@@ -149,8 +154,40 @@ func runStdioServer(cfg runConfig) error {
149154
hooks := &server.Hooks{
150155
OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit},
151156
}
152-
// Create
153-
ghServer := github.NewServer(getClient, version, cfg.readOnly, t, server.WithHooks(hooks))
157+
// Create server
158+
ghServer := github.NewServer(version, server.WithHooks(hooks))
159+
160+
enabled := cfg.enabledToolsets
161+
dynamic := viper.GetBool("dynamic_toolsets")
162+
if dynamic {
163+
// filter "all" from the enabled toolsets
164+
enabled = make([]string, 0, len(cfg.enabledToolsets))
165+
for _, toolset := range cfg.enabledToolsets {
166+
if toolset != "all" {
167+
enabled = append(enabled, toolset)
168+
}
169+
}
170+
}
171+
172+
// Create default toolsets
173+
toolsets, err := github.InitToolsets(enabled, cfg.readOnly, getClient, t)
174+
context := github.InitContextToolset(ghServer, getClient, t)
175+
176+
if err != nil {
177+
stdlog.Fatal("Failed to initialize toolsets:", err)
178+
}
179+
180+
// Register resources with the server
181+
github.RegisterResources(ghServer, getClient, t)
182+
// Register the tools with the server
183+
toolsets.RegisterTools(ghServer)
184+
context.RegisterTools(ghServer)
185+
186+
if dynamic {
187+
dynamic := github.InitDynamicToolset(ghServer, toolsets, t)
188+
dynamic.RegisterTools(ghServer)
189+
}
190+
154191
stdioServer := server.NewStdioServer(ghServer)
155192

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

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/docker/docker v28.0.4+incompatible
77
github.com/google/go-cmp v0.7.0
88
github.com/google/go-github/v69 v69.2.0
9-
github.com/mark3labs/mcp-go v0.18.0
9+
github.com/mark3labs/mcp-go v0.20.1
1010
github.com/migueleliasweb/go-github-mock v1.1.0
1111
github.com/sirupsen/logrus v1.9.3
1212
github.com/spf13/cobra v1.9.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
5959
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
6060
github.com/mark3labs/mcp-go v0.18.0 h1:YuhgIVjNlTG2ZOwmrkORWyPTp0dz1opPEqvsPtySXao=
6161
github.com/mark3labs/mcp-go v0.18.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
62+
github.com/mark3labs/mcp-go v0.20.1 h1:E1Bbx9K8d8kQmDZ1QHblM38c7UU2evQ2LlkANk1U/zw=
63+
github.com/mark3labs/mcp-go v0.20.1/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
6264
github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE=
6365
github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc=
6466
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=

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+
}

0 commit comments

Comments
 (0)