Skip to content
Draft
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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,38 @@ To specify toolsets you want available to the LLM, you can pass an allow-list in

The environment variable `GITHUB_TOOLSETS` takes precedence over the command line argument if both are provided.

#### Specifying Individual Tools

You can also configure specific tools using the `--tools` flag. Tools can be used independently or combined with toolsets and dynamic toolsets discovery for fine-grained control.

1. **Using Command Line Argument**:

```bash
github-mcp-server --tools get_file_contents,issue_read,create_pull_request
```

2. **Using Environment Variable**:
```bash
GITHUB_TOOLS="get_file_contents,issue_read,create_pull_request" ./github-mcp-server
```

3. **Combining with Toolsets** (additive):
```bash
github-mcp-server --toolsets repos,issues --tools get_gist
```
This registers all tools from `repos` and `issues` toolsets, plus `get_gist`.

4. **Combining with Dynamic Toolsets** (additive):
```bash
github-mcp-server --tools get_file_contents --dynamic-toolsets
```
This registers `get_file_contents` plus the dynamic toolset tools (`enable_toolset`, `list_available_toolsets`, `get_toolset_tools`).

**Important Notes:**
- Tools, toolsets, and dynamic toolsets can all be used together
- Read-only mode takes priority: write tools are skipped if `--read-only` is set, even if explicitly requested via `--tools`
- Tool names must match exactly (e.g., `get_file_contents`, not `getFileContents`). Invalid tool names will cause the server to fail at startup with an error message

### Using Toolsets With Docker

When using Docker, you can pass the toolsets as environment variables:
Expand All @@ -354,6 +386,25 @@ docker run -i --rm \
ghcr.io/github/github-mcp-server
```

### Using Tools With Docker

When using Docker, you can pass specific tools as environment variables. You can also combine tools with toolsets:

```bash
# Tools only
docker run -i --rm \
-e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
-e GITHUB_TOOLS="get_file_contents,issue_read,create_pull_request" \
ghcr.io/github/github-mcp-server

# Tools combined with toolsets (additive)
docker run -i --rm \
-e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
-e GITHUB_TOOLSETS="repos,issues" \
-e GITHUB_TOOLS="get_gist" \
ghcr.io/github/github-mcp-server
```

### Special toolsets

#### "all" toolset
Expand Down
13 changes: 11 additions & 2 deletions cmd/github-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,14 @@ var (
return fmt.Errorf("failed to unmarshal toolsets: %w", err)
}

// No passed toolsets configuration means we enable the default toolset
if len(enabledToolsets) == 0 {
// Parse tools (similar to toolsets)
var enabledTools []string
if err := viper.UnmarshalKey("tools", &enabledTools); err != nil {
return fmt.Errorf("failed to unmarshal tools: %w", err)
}

// If neither toolset config nor tools config is passed we enable the default toolset
if len(enabledToolsets) == 0 && len(enabledTools) == 0 {
enabledToolsets = []string{github.ToolsetMetadataDefault.ID}
}

Expand All @@ -55,6 +61,7 @@ var (
Host: viper.GetString("host"),
Token: token,
EnabledToolsets: enabledToolsets,
EnabledTools: enabledTools,
DynamicToolsets: viper.GetBool("dynamic_toolsets"),
ReadOnly: viper.GetBool("read-only"),
ExportTranslations: viper.GetBool("export-translations"),
Expand All @@ -76,6 +83,7 @@ func init() {

// Add global flags that will be shared by all commands
rootCmd.PersistentFlags().StringSlice("toolsets", nil, github.GenerateToolsetsHelp())
rootCmd.PersistentFlags().StringSlice("tools", nil, "Comma-separated list of specific tools to enable")
rootCmd.PersistentFlags().Bool("dynamic-toolsets", false, "Enable dynamic toolsets")
rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations")
rootCmd.PersistentFlags().String("log-file", "", "Path to log file")
Expand All @@ -87,6 +95,7 @@ func init() {

// Bind flag to viper
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
_ = viper.BindPFlag("tools", rootCmd.PersistentFlags().Lookup("tools"))
_ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets"))
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/google/go-querystring v1.1.0
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
Expand Down
36 changes: 31 additions & 5 deletions internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ type MCPServerConfig struct {
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
EnabledToolsets []string

// EnabledTools is a list of specific tools to enable (additive to toolsets)
// When specified, these tools are registered in addition to any specified toolset tools
EnabledTools []string

// Whether to enable dynamic toolsets
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery
DynamicToolsets bool
Expand Down Expand Up @@ -166,15 +170,32 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
cfg.ContentWindowSize,
github.FeatureFlags{LockdownMode: cfg.LockdownMode},
)
err = tsg.EnableToolsets(enabledToolsets, nil)

if err != nil {
return nil, fmt.Errorf("failed to enable toolsets: %w", err)
// Enable and register toolsets if configured
// This always happens if toolsets are specified, regardless of whether tools are also specified
if len(enabledToolsets) > 0 {
err = tsg.EnableToolsets(enabledToolsets, nil)
if err != nil {
return nil, fmt.Errorf("failed to enable toolsets: %w", err)
}

// Register all mcp functionality with the server
tsg.RegisterAll(ghServer)
}

// Register all mcp functionality with the server
tsg.RegisterAll(ghServer)
// Register specific tools if configured
if len(cfg.EnabledTools) > 0 {
// Clean and validate tool names
enabledTools := github.CleanTools(cfg.EnabledTools)

// Register the specified tools (additive to any toolsets already enabled)
err = tsg.RegisterSpecificTools(ghServer, enabledTools, cfg.ReadOnly)
if err != nil {
return nil, fmt.Errorf("failed to register tools: %w", err)
}
}

// Register dynamic toolsets if configured (additive to toolsets and tools)
if cfg.DynamicToolsets {
dynamic := github.InitDynamicToolset(ghServer, tsg, cfg.Translator)
dynamic.RegisterTools(ghServer)
Expand All @@ -197,6 +218,10 @@ type StdioServerConfig struct {
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
EnabledToolsets []string

// EnabledTools is a list of specific tools to enable (additive to toolsets)
// When specified, these tools are registered in addition to any specified toolset tools
EnabledTools []string

// Whether to enable dynamic toolsets
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery
DynamicToolsets bool
Expand Down Expand Up @@ -234,6 +259,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
Host: cfg.Host,
Token: cfg.Token,
EnabledToolsets: cfg.EnabledToolsets,
EnabledTools: cfg.EnabledTools,
DynamicToolsets: cfg.DynamicToolsets,
ReadOnly: cfg.ReadOnly,
Translator: t,
Expand Down
21 changes: 21 additions & 0 deletions pkg/github/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,3 +523,24 @@ func ContainsToolset(tools []string, toCheck string) bool {
}
return false
}

// CleanTools cleans tool names by removing duplicates and trimming whitespace.
// Validation of tool existence is done during registration.
func CleanTools(toolNames []string) []string {
seen := make(map[string]bool)
result := make([]string, 0, len(toolNames))

// Remove duplicates and trim whitespace
for _, tool := range toolNames {
trimmed := strings.TrimSpace(tool)
if trimmed == "" {
continue
}
if !seen[trimmed] {
seen[trimmed] = true
result = append(result, trimmed)
}
}

return result
}
67 changes: 67 additions & 0 deletions pkg/toolsets/toolsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package toolsets

import (
"fmt"
"os"
"strings"

"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
Expand Down Expand Up @@ -263,3 +265,68 @@ func (tg *ToolsetGroup) GetToolset(name string) (*Toolset, error) {
}
return toolset, nil
}

type ToolDoesNotExistError struct {
Name string
}

func (e *ToolDoesNotExistError) Error() string {
return fmt.Sprintf("tool %s does not exist", e.Name)
}

func NewToolDoesNotExistError(name string) *ToolDoesNotExistError {
return &ToolDoesNotExistError{Name: name}
}

// FindToolByName searches all toolsets (enabled or disabled) for a tool by name.
// Returns the tool, its parent toolset name, and an error if not found.
func (tg *ToolsetGroup) FindToolByName(toolName string) (*server.ServerTool, string, error) {
for toolsetName, toolset := range tg.Toolsets {
// Check read tools
for _, tool := range toolset.readTools {
if tool.Tool.Name == toolName {
return &tool, toolsetName, nil
}
}
// Check write tools
for _, tool := range toolset.writeTools {
if tool.Tool.Name == toolName {
return &tool, toolsetName, nil
}
}
}
return nil, "", NewToolDoesNotExistError(toolName)
}

// RegisterSpecificTools registers only the specified tools.
// Respects read-only mode (skips write tools if readOnly=true).
// Returns error if any tool is not found.
func (tg *ToolsetGroup) RegisterSpecificTools(s *server.MCPServer, toolNames []string, readOnly bool) error {
var skippedTools []string
for _, toolName := range toolNames {
tool, _, err := tg.FindToolByName(toolName)
if err != nil {
return fmt.Errorf("tool %s not found: %w", toolName, err)
}

// Check if it's a write tool and we're in read-only mode
if tool.Tool.Annotations.ReadOnlyHint != nil {
isWriteTool := !*tool.Tool.Annotations.ReadOnlyHint
if isWriteTool && readOnly {
// Skip write tools in read-only mode
skippedTools = append(skippedTools, toolName)
continue
}
}

// Register the tool
s.AddTool(tool.Tool, tool.Handler)
}

// Log skipped write tools if any
if len(skippedTools) > 0 {
fmt.Fprintf(os.Stderr, "Write tools skipped due to read-only mode: %s\n", strings.Join(skippedTools, ", "))
}

return nil
}
Loading