Skip to content

Commit 8aba085

Browse files
feat: partition tools by product/feature
1 parent 62eed34 commit 8aba085

17 files changed

+969
-257
lines changed

Diff for: README.md

+92-12
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,95 @@ If you don't have Docker, you can use `go build` to build the binary in the
110110
}
111111
```
112112

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

115204
The flag `--gh-host` and the environment variable `GH_HOST` can be used to set
@@ -331,7 +420,6 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
331420
### Repositories
332421

333422
- **create_or_update_file** - Create or update a single file in a repository
334-
335423
- `owner`: Repository owner (string, required)
336424
- `repo`: Repository name (string, required)
337425
- `path`: File path (string, required)
@@ -341,50 +429,43 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
341429
- `sha`: File SHA if updating (string, optional)
342430

343431
- **list_branches** - List branches in a GitHub repository
344-
345432
- `owner`: Repository owner (string, required)
346433
- `repo`: Repository name (string, required)
347434
- `page`: Page number (number, optional)
348435
- `perPage`: Results per page (number, optional)
349436

350437
- **push_files** - Push multiple files in a single commit
351-
352438
- `owner`: Repository owner (string, required)
353439
- `repo`: Repository name (string, required)
354440
- `branch`: Branch to push to (string, required)
355441
- `files`: Files to push, each with path and content (array, required)
356442
- `message`: Commit message (string, required)
357443

358444
- **search_repositories** - Search for GitHub repositories
359-
360445
- `query`: Search query (string, required)
361446
- `sort`: Sort field (string, optional)
362447
- `order`: Sort order (string, optional)
363448
- `page`: Page number (number, optional)
364449
- `perPage`: Results per page (number, optional)
365450

366451
- **create_repository** - Create a new GitHub repository
367-
368452
- `name`: Repository name (string, required)
369453
- `description`: Repository description (string, optional)
370454
- `private`: Whether the repository is private (boolean, optional)
371455
- `autoInit`: Auto-initialize with README (boolean, optional)
372456

373457
- **get_file_contents** - Get contents of a file or directory
374-
375458
- `owner`: Repository owner (string, required)
376459
- `repo`: Repository name (string, required)
377460
- `path`: File path (string, required)
378461
- `ref`: Git reference (string, optional)
379462

380463
- **fork_repository** - Fork a repository
381-
382464
- `owner`: Repository owner (string, required)
383465
- `repo`: Repository name (string, required)
384466
- `organization`: Target organization name (string, optional)
385467

386468
- **create_branch** - Create a new branch
387-
388469
- `owner`: Repository owner (string, required)
389470
- `repo`: Repository name (string, required)
390471
- `branch`: New branch name (string, required)
@@ -405,16 +486,15 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
405486
- `page`: Page number, for files in the commit (number, optional)
406487
- `perPage`: Results per page, for files in the commit (number, optional)
407488

408-
### Search
409-
410-
- **search_code** - Search for code across GitHub repositories
411-
489+
- **search_code** - Search for code across GitHub repositories
412490
- `query`: Search query (string, required)
413491
- `sort`: Sort field (string, optional)
414492
- `order`: Sort order (string, optional)
415493
- `page`: Page number (number, optional)
416494
- `perPage`: Results per page (number, optional)
417495

496+
### Users
497+
418498
- **search_users** - Search for GitHub users
419499
- `query`: Search query (string, required)
420500
- `sort`: Sort field (string, optional)

Diff for: cmd/github-mcp-server/main.go

+47-10
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(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)

Diff for: go.mod

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

Diff for: go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
5757
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
5858
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
5959
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
60-
github.com/mark3labs/mcp-go v0.18.0 h1:YuhgIVjNlTG2ZOwmrkORWyPTp0dz1opPEqvsPtySXao=
61-
github.com/mark3labs/mcp-go v0.18.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
60+
github.com/mark3labs/mcp-go v0.20.1 h1:E1Bbx9K8d8kQmDZ1QHblM38c7UU2evQ2LlkANk1U/zw=
61+
github.com/mark3labs/mcp-go v0.20.1/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
6262
github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE=
6363
github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc=
6464
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=

Diff for: pkg/github/context_tools.go

+49
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)