Skip to content

Commit 9d8224a

Browse files
committed
toolsets: fail closed on unknown toolsets in strict mode (#2117)
1 parent bf64678 commit 9d8224a

File tree

5 files changed

+82
-0
lines changed

5 files changed

+82
-0
lines changed

cmd/github-mcp-server/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ var (
8383
Host: viper.GetString("host"),
8484
Token: token,
8585
EnabledToolsets: enabledToolsets,
86+
StrictToolsets: viper.GetBool("strict_toolsets"),
8687
EnabledTools: enabledTools,
8788
EnabledFeatures: enabledFeatures,
8889
DynamicToolsets: viper.GetBool("dynamic_toolsets"),
@@ -134,6 +135,7 @@ func init() {
134135

135136
// Add global flags that will be shared by all commands
136137
rootCmd.PersistentFlags().StringSlice("toolsets", nil, github.GenerateToolsetsHelp())
138+
rootCmd.PersistentFlags().Bool("strict-toolsets", false, "Fail startup if any configured toolset is unrecognized")
137139
rootCmd.PersistentFlags().StringSlice("tools", nil, "Comma-separated list of specific tools to enable")
138140
rootCmd.PersistentFlags().StringSlice("exclude-tools", nil, "Comma-separated list of tool names to disable regardless of other settings")
139141
rootCmd.PersistentFlags().StringSlice("features", nil, "Comma-separated list of feature flags to enable")
@@ -156,6 +158,7 @@ func init() {
156158

157159
// Bind flag to viper
158160
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
161+
_ = viper.BindPFlag("strict_toolsets", rootCmd.PersistentFlags().Lookup("strict-toolsets"))
159162
_ = viper.BindPFlag("tools", rootCmd.PersistentFlags().Lookup("tools"))
160163
_ = viper.BindPFlag("exclude_tools", rootCmd.PersistentFlags().Lookup("exclude-tools"))
161164
_ = viper.BindPFlag("features", rootCmd.PersistentFlags().Lookup("features"))

docs/toolsets-and-icons.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ ToolsetMetadataRepos = inventory.ToolsetMetadata{
2424
| `Default` | `bool` | Whether this toolset is enabled by default |
2525
| `Icon` | `string` | Octicon name for visual representation in MCP clients |
2626

27+
## Strict Toolset Validation
28+
29+
By default, unknown toolset names are ignored and a warning is logged. To fail closed on typos or stale config, enable strict mode:
30+
31+
```bash
32+
github-mcp-server stdio --toolsets=repos,isssues --strict-toolsets
33+
```
34+
35+
Strict mode exits startup with a validation error when any configured toolset is unrecognized.
36+
37+
### Migration Path
38+
39+
1. Run once without strict mode and check logs for `unrecognized toolsets ignored`.
40+
2. Fix typos or remove unsupported toolset names.
41+
3. Re-run with `--strict-toolsets` (or `GITHUB_STRICT_TOOLSETS=true`) in CI/production.
42+
2743
## Adding Icons to Toolsets
2844

2945
Icons help users quickly identify toolsets in MCP-compatible clients. We use [Primer Octicons](https://primer.style/foundations/icons) for all icons.

internal/ghmcp/server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ type StdioServerConfig struct {
181181
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
182182
EnabledToolsets []string
183183

184+
// StrictToolsets fails startup when configured toolsets are unrecognized.
185+
StrictToolsets bool
186+
184187
// EnabledTools is a list of specific tools to enable (additive to toolsets)
185188
// When specified, these tools are registered in addition to any specified toolset tools
186189
EnabledTools []string
@@ -269,6 +272,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
269272
Host: cfg.Host,
270273
Token: cfg.Token,
271274
EnabledToolsets: cfg.EnabledToolsets,
275+
StrictToolsets: cfg.StrictToolsets,
272276
EnabledTools: cfg.EnabledTools,
273277
EnabledFeatures: cfg.EnabledFeatures,
274278
DynamicToolsets: cfg.DynamicToolsets,

pkg/github/server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ type MCPServerConfig struct {
3030
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
3131
EnabledToolsets []string
3232

33+
// StrictToolsets fails startup when configured toolsets are unrecognized.
34+
StrictToolsets bool
35+
3336
// EnabledTools is a list of specific tools to enable (additive to toolsets)
3437
// When specified, these tools are registered in addition to any specified toolset tools
3538
EnabledTools []string
@@ -110,6 +113,9 @@ func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependenci
110113
ghServer.AddReceivingMiddleware(addGitHubAPIErrorToContext)
111114

112115
if unrecognized := inv.UnrecognizedToolsets(); len(unrecognized) > 0 {
116+
if cfg.StrictToolsets {
117+
return nil, fmt.Errorf("strict toolset validation failed: unrecognized toolsets: %s", strings.Join(unrecognized, ", "))
118+
}
113119
cfg.Logger.Warn("Warning: unrecognized toolsets ignored", "toolsets", strings.Join(unrecognized, ", "))
114120
}
115121

pkg/github/server_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"io"
9+
"log/slog"
810
"net/http"
911
"testing"
1012
"time"
@@ -219,3 +221,54 @@ func TestResolveEnabledToolsets(t *testing.T) {
219221
})
220222
}
221223
}
224+
225+
func TestNewMCPServer_StrictToolsetsFailsOnUnknownToolset(t *testing.T) {
226+
t.Parallel()
227+
228+
cfg := MCPServerConfig{
229+
Version: "test",
230+
Token: "test-token",
231+
EnabledToolsets: []string{"unknown-toolset"},
232+
StrictToolsets: true,
233+
Translator: translations.NullTranslationHelper,
234+
ContentWindowSize: 5000,
235+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
236+
}
237+
238+
inv, err := NewInventory(cfg.Translator).
239+
WithDeprecatedAliases(DeprecatedToolAliases).
240+
WithToolsets(cfg.EnabledToolsets).
241+
Build()
242+
require.NoError(t, err)
243+
require.NotEmpty(t, inv.UnrecognizedToolsets())
244+
245+
_, err = NewMCPServer(context.Background(), &cfg, stubDeps{}, inv)
246+
require.Error(t, err)
247+
assert.Contains(t, err.Error(), "strict toolset validation failed")
248+
assert.Contains(t, err.Error(), "unknown-toolset")
249+
}
250+
251+
func TestNewMCPServer_NonStrictToolsetsAllowsUnknownToolset(t *testing.T) {
252+
t.Parallel()
253+
254+
cfg := MCPServerConfig{
255+
Version: "test",
256+
Token: "test-token",
257+
EnabledToolsets: []string{"unknown-toolset"},
258+
StrictToolsets: false,
259+
Translator: translations.NullTranslationHelper,
260+
ContentWindowSize: 5000,
261+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
262+
}
263+
264+
inv, err := NewInventory(cfg.Translator).
265+
WithDeprecatedAliases(DeprecatedToolAliases).
266+
WithToolsets(cfg.EnabledToolsets).
267+
Build()
268+
require.NoError(t, err)
269+
require.NotEmpty(t, inv.UnrecognizedToolsets())
270+
271+
server, err := NewMCPServer(context.Background(), &cfg, stubDeps{}, inv)
272+
require.NoError(t, err)
273+
require.NotNil(t, server)
274+
}

0 commit comments

Comments
 (0)