Skip to content

Commit 3e1fca0

Browse files
authored
Tommy/tool-specific-config-support (#1394)
* add enabledTools to StdioServerConfig * add EnabledTools to MCPServerConfig, and logic to bypass toolset config if present * add logic to register specific tools * update readme * Update to be consistent with: https://docs.google.com/document/d/1tOOBJ4y9xY61QVrO18ymuVt4SO9nV-z2B4ckaL2f9IU/edit?tab=t.0#heading=h.ffto4e5dwzlf specifically - allow for --tools and dynamic toolset mode together - allow for --tools and --toolsets together * go mod tidy * update * clean up comment * fix * fix * updte * update * clean up
1 parent 7cfb354 commit 3e1fca0

File tree

5 files changed

+181
-7
lines changed

5 files changed

+181
-7
lines changed

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,38 @@ To specify toolsets you want available to the LLM, you can pass an allow-list in
345345

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

348+
#### Specifying Individual Tools
349+
350+
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.
351+
352+
1. **Using Command Line Argument**:
353+
354+
```bash
355+
github-mcp-server --tools get_file_contents,issue_read,create_pull_request
356+
```
357+
358+
2. **Using Environment Variable**:
359+
```bash
360+
GITHUB_TOOLS="get_file_contents,issue_read,create_pull_request" ./github-mcp-server
361+
```
362+
363+
3. **Combining with Toolsets** (additive):
364+
```bash
365+
github-mcp-server --toolsets repos,issues --tools get_gist
366+
```
367+
This registers all tools from `repos` and `issues` toolsets, plus `get_gist`.
368+
369+
4. **Combining with Dynamic Toolsets** (additive):
370+
```bash
371+
github-mcp-server --tools get_file_contents --dynamic-toolsets
372+
```
373+
This registers `get_file_contents` plus the dynamic toolset tools (`enable_toolset`, `list_available_toolsets`, `get_toolset_tools`).
374+
375+
**Important Notes:**
376+
- Tools, toolsets, and dynamic toolsets can all be used together
377+
- Read-only mode takes priority: write tools are skipped if `--read-only` is set, even if explicitly requested via `--tools`
378+
- 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
379+
348380
### Using Toolsets With Docker
349381

350382
When using Docker, you can pass the toolsets as environment variables:
@@ -356,6 +388,25 @@ docker run -i --rm \
356388
ghcr.io/github/github-mcp-server
357389
```
358390

391+
### Using Tools With Docker
392+
393+
When using Docker, you can pass specific tools as environment variables. You can also combine tools with toolsets:
394+
395+
```bash
396+
# Tools only
397+
docker run -i --rm \
398+
-e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
399+
-e GITHUB_TOOLS="get_file_contents,issue_read,create_pull_request" \
400+
ghcr.io/github/github-mcp-server
401+
402+
# Tools combined with toolsets (additive)
403+
docker run -i --rm \
404+
-e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
405+
-e GITHUB_TOOLSETS="repos,issues" \
406+
-e GITHUB_TOOLS="get_gist" \
407+
ghcr.io/github/github-mcp-server
408+
```
409+
359410
### Special toolsets
360411

361412
#### "all" toolset

cmd/github-mcp-server/main.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,14 @@ var (
4646
return fmt.Errorf("failed to unmarshal toolsets: %w", err)
4747
}
4848

49-
// No passed toolsets configuration means we enable the default toolset
50-
if len(enabledToolsets) == 0 {
49+
// Parse tools (similar to toolsets)
50+
var enabledTools []string
51+
if err := viper.UnmarshalKey("tools", &enabledTools); err != nil {
52+
return fmt.Errorf("failed to unmarshal tools: %w", err)
53+
}
54+
55+
// If neither toolset config nor tools config is passed we enable the default toolset
56+
if len(enabledToolsets) == 0 && len(enabledTools) == 0 {
5157
enabledToolsets = []string{github.ToolsetMetadataDefault.ID}
5258
}
5359

@@ -57,6 +63,7 @@ var (
5763
Host: viper.GetString("host"),
5864
Token: token,
5965
EnabledToolsets: enabledToolsets,
66+
EnabledTools: enabledTools,
6067
DynamicToolsets: viper.GetBool("dynamic_toolsets"),
6168
ReadOnly: viper.GetBool("read-only"),
6269
ExportTranslations: viper.GetBool("export-translations"),
@@ -79,6 +86,7 @@ func init() {
7986

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

92100
// Bind flag to viper
93101
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
102+
_ = viper.BindPFlag("tools", rootCmd.PersistentFlags().Lookup("tools"))
94103
_ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets"))
95104
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
96105
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))

internal/ghmcp/server.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ type MCPServerConfig struct {
4040
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
4141
EnabledToolsets []string
4242

43+
// EnabledTools is a list of specific tools to enable (additive to toolsets)
44+
// When specified, these tools are registered in addition to any specified toolset tools
45+
EnabledTools []string
46+
4347
// Whether to enable dynamic toolsets
4448
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery
4549
DynamicToolsets bool
@@ -182,15 +186,32 @@ func NewMCPServer(cfg MCPServerConfig, logger *slog.Logger) (*server.MCPServer,
182186
github.FeatureFlags{LockdownMode: cfg.LockdownMode},
183187
repoAccessCache,
184188
)
185-
err = tsg.EnableToolsets(enabledToolsets, nil)
186189

187-
if err != nil {
188-
return nil, fmt.Errorf("failed to enable toolsets: %w", err)
190+
// Enable and register toolsets if configured
191+
// This always happens if toolsets are specified, regardless of whether tools are also specified
192+
if len(enabledToolsets) > 0 {
193+
err = tsg.EnableToolsets(enabledToolsets, nil)
194+
if err != nil {
195+
return nil, fmt.Errorf("failed to enable toolsets: %w", err)
196+
}
197+
198+
// Register all mcp functionality with the server
199+
tsg.RegisterAll(ghServer)
189200
}
190201

191-
// Register all mcp functionality with the server
192-
tsg.RegisterAll(ghServer)
202+
// Register specific tools if configured
203+
if len(cfg.EnabledTools) > 0 {
204+
// Clean and validate tool names
205+
enabledTools := github.CleanTools(cfg.EnabledTools)
193206

207+
// Register the specified tools (additive to any toolsets already enabled)
208+
err = tsg.RegisterSpecificTools(ghServer, enabledTools, cfg.ReadOnly)
209+
if err != nil {
210+
return nil, fmt.Errorf("failed to register tools: %w", err)
211+
}
212+
}
213+
214+
// Register dynamic toolsets if configured (additive to toolsets and tools)
194215
if cfg.DynamicToolsets {
195216
dynamic := github.InitDynamicToolset(ghServer, tsg, cfg.Translator)
196217
dynamic.RegisterTools(ghServer)
@@ -213,6 +234,10 @@ type StdioServerConfig struct {
213234
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
214235
EnabledToolsets []string
215236

237+
// EnabledTools is a list of specific tools to enable (additive to toolsets)
238+
// When specified, these tools are registered in addition to any specified toolset tools
239+
EnabledTools []string
240+
216241
// Whether to enable dynamic toolsets
217242
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery
218243
DynamicToolsets bool
@@ -270,6 +295,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
270295
Host: cfg.Host,
271296
Token: cfg.Token,
272297
EnabledToolsets: cfg.EnabledToolsets,
298+
EnabledTools: cfg.EnabledTools,
273299
DynamicToolsets: cfg.DynamicToolsets,
274300
ReadOnly: cfg.ReadOnly,
275301
Translator: t,

pkg/github/tools.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,3 +524,24 @@ func ContainsToolset(tools []string, toCheck string) bool {
524524
}
525525
return false
526526
}
527+
528+
// CleanTools cleans tool names by removing duplicates and trimming whitespace.
529+
// Validation of tool existence is done during registration.
530+
func CleanTools(toolNames []string) []string {
531+
seen := make(map[string]bool)
532+
result := make([]string, 0, len(toolNames))
533+
534+
// Remove duplicates and trim whitespace
535+
for _, tool := range toolNames {
536+
trimmed := strings.TrimSpace(tool)
537+
if trimmed == "" {
538+
continue
539+
}
540+
if !seen[trimmed] {
541+
seen[trimmed] = true
542+
result = append(result, trimmed)
543+
}
544+
}
545+
546+
return result
547+
}

pkg/toolsets/toolsets.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package toolsets
22

33
import (
44
"fmt"
5+
"os"
6+
"strings"
57

68
"github.com/mark3labs/mcp-go/mcp"
79
"github.com/mark3labs/mcp-go/server"
@@ -263,3 +265,68 @@ func (tg *ToolsetGroup) GetToolset(name string) (*Toolset, error) {
263265
}
264266
return toolset, nil
265267
}
268+
269+
type ToolDoesNotExistError struct {
270+
Name string
271+
}
272+
273+
func (e *ToolDoesNotExistError) Error() string {
274+
return fmt.Sprintf("tool %s does not exist", e.Name)
275+
}
276+
277+
func NewToolDoesNotExistError(name string) *ToolDoesNotExistError {
278+
return &ToolDoesNotExistError{Name: name}
279+
}
280+
281+
// FindToolByName searches all toolsets (enabled or disabled) for a tool by name.
282+
// Returns the tool, its parent toolset name, and an error if not found.
283+
func (tg *ToolsetGroup) FindToolByName(toolName string) (*server.ServerTool, string, error) {
284+
for toolsetName, toolset := range tg.Toolsets {
285+
// Check read tools
286+
for _, tool := range toolset.readTools {
287+
if tool.Tool.Name == toolName {
288+
return &tool, toolsetName, nil
289+
}
290+
}
291+
// Check write tools
292+
for _, tool := range toolset.writeTools {
293+
if tool.Tool.Name == toolName {
294+
return &tool, toolsetName, nil
295+
}
296+
}
297+
}
298+
return nil, "", NewToolDoesNotExistError(toolName)
299+
}
300+
301+
// RegisterSpecificTools registers only the specified tools.
302+
// Respects read-only mode (skips write tools if readOnly=true).
303+
// Returns error if any tool is not found.
304+
func (tg *ToolsetGroup) RegisterSpecificTools(s *server.MCPServer, toolNames []string, readOnly bool) error {
305+
var skippedTools []string
306+
for _, toolName := range toolNames {
307+
tool, _, err := tg.FindToolByName(toolName)
308+
if err != nil {
309+
return fmt.Errorf("tool %s not found: %w", toolName, err)
310+
}
311+
312+
// Check if it's a write tool and we're in read-only mode
313+
if tool.Tool.Annotations.ReadOnlyHint != nil {
314+
isWriteTool := !*tool.Tool.Annotations.ReadOnlyHint
315+
if isWriteTool && readOnly {
316+
// Skip write tools in read-only mode
317+
skippedTools = append(skippedTools, toolName)
318+
continue
319+
}
320+
}
321+
322+
// Register the tool
323+
s.AddTool(tool.Tool, tool.Handler)
324+
}
325+
326+
// Log skipped write tools if any
327+
if len(skippedTools) > 0 {
328+
fmt.Fprintf(os.Stderr, "Write tools skipped due to read-only mode: %s\n", strings.Join(skippedTools, ", "))
329+
}
330+
331+
return nil
332+
}

0 commit comments

Comments
 (0)