diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index 05c2c6e0b..6ca0fb23b 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -83,6 +83,7 @@ var ( Host: viper.GetString("host"), Token: token, EnabledToolsets: enabledToolsets, + StrictToolsets: viper.GetBool("strict_toolsets"), EnabledTools: enabledTools, EnabledFeatures: enabledFeatures, DynamicToolsets: viper.GetBool("dynamic_toolsets"), @@ -134,6 +135,7 @@ func init() { // Add global flags that will be shared by all commands rootCmd.PersistentFlags().StringSlice("toolsets", nil, github.GenerateToolsetsHelp()) + rootCmd.PersistentFlags().Bool("strict-toolsets", false, "Fail startup if any configured toolset is unrecognized") rootCmd.PersistentFlags().StringSlice("tools", nil, "Comma-separated list of specific tools to enable") rootCmd.PersistentFlags().StringSlice("exclude-tools", nil, "Comma-separated list of tool names to disable regardless of other settings") rootCmd.PersistentFlags().StringSlice("features", nil, "Comma-separated list of feature flags to enable") @@ -156,6 +158,7 @@ func init() { // Bind flag to viper _ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets")) + _ = viper.BindPFlag("strict_toolsets", rootCmd.PersistentFlags().Lookup("strict-toolsets")) _ = viper.BindPFlag("tools", rootCmd.PersistentFlags().Lookup("tools")) _ = viper.BindPFlag("exclude_tools", rootCmd.PersistentFlags().Lookup("exclude-tools")) _ = viper.BindPFlag("features", rootCmd.PersistentFlags().Lookup("features")) diff --git a/docs/toolsets-and-icons.md b/docs/toolsets-and-icons.md index 9c26b4aa1..866383935 100644 --- a/docs/toolsets-and-icons.md +++ b/docs/toolsets-and-icons.md @@ -24,6 +24,22 @@ ToolsetMetadataRepos = inventory.ToolsetMetadata{ | `Default` | `bool` | Whether this toolset is enabled by default | | `Icon` | `string` | Octicon name for visual representation in MCP clients | +## Strict Toolset Validation + +By default, unknown toolset names are ignored and a warning is logged. To fail closed on typos or stale config, enable strict mode: + +```bash +github-mcp-server stdio --toolsets=repos,isssues --strict-toolsets +``` + +Strict mode exits startup with a validation error when any configured toolset is unrecognized. + +### Migration Path + +1. Run once without strict mode and check logs for `unrecognized toolsets ignored`. +2. Fix typos or remove unsupported toolset names. +3. Re-run with `--strict-toolsets` (or `GITHUB_STRICT_TOOLSETS=true`) in CI/production. + ## Adding Icons to Toolsets Icons help users quickly identify toolsets in MCP-compatible clients. We use [Primer Octicons](https://primer.style/foundations/icons) for all icons. diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index 5c4e7f6f1..967f7b9f7 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -181,6 +181,9 @@ type StdioServerConfig struct { // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration EnabledToolsets []string + // StrictToolsets fails startup when configured toolsets are unrecognized. + StrictToolsets bool + // 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 @@ -269,6 +272,7 @@ func RunStdioServer(cfg StdioServerConfig) error { Host: cfg.Host, Token: cfg.Token, EnabledToolsets: cfg.EnabledToolsets, + StrictToolsets: cfg.StrictToolsets, EnabledTools: cfg.EnabledTools, EnabledFeatures: cfg.EnabledFeatures, DynamicToolsets: cfg.DynamicToolsets, diff --git a/pkg/github/server.go b/pkg/github/server.go index 06c12575d..351b6d293 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -30,6 +30,9 @@ type MCPServerConfig struct { // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration EnabledToolsets []string + // StrictToolsets fails startup when configured toolsets are unrecognized. + StrictToolsets bool + // 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 @@ -110,6 +113,9 @@ func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependenci ghServer.AddReceivingMiddleware(addGitHubAPIErrorToContext) if unrecognized := inv.UnrecognizedToolsets(); len(unrecognized) > 0 { + if cfg.StrictToolsets { + return nil, fmt.Errorf("strict toolset validation failed: unrecognized toolsets: %s", strings.Join(unrecognized, ", ")) + } cfg.Logger.Warn("Warning: unrecognized toolsets ignored", "toolsets", strings.Join(unrecognized, ", ")) } diff --git a/pkg/github/server_test.go b/pkg/github/server_test.go index 2b99cab12..4efb62c4b 100644 --- a/pkg/github/server_test.go +++ b/pkg/github/server_test.go @@ -5,6 +5,8 @@ import ( "encoding/json" "errors" "fmt" + "io" + "log/slog" "net/http" "testing" "time" @@ -219,3 +221,54 @@ func TestResolveEnabledToolsets(t *testing.T) { }) } } + +func TestNewMCPServer_StrictToolsetsFailsOnUnknownToolset(t *testing.T) { + t.Parallel() + + cfg := MCPServerConfig{ + Version: "test", + Token: "test-token", + EnabledToolsets: []string{"unknown-toolset"}, + StrictToolsets: true, + Translator: translations.NullTranslationHelper, + ContentWindowSize: 5000, + Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), + } + + inv, err := NewInventory(cfg.Translator). + WithDeprecatedAliases(DeprecatedToolAliases). + WithToolsets(cfg.EnabledToolsets). + Build() + require.NoError(t, err) + require.NotEmpty(t, inv.UnrecognizedToolsets()) + + _, err = NewMCPServer(context.Background(), &cfg, stubDeps{}, inv) + require.Error(t, err) + assert.Contains(t, err.Error(), "strict toolset validation failed") + assert.Contains(t, err.Error(), "unknown-toolset") +} + +func TestNewMCPServer_NonStrictToolsetsAllowsUnknownToolset(t *testing.T) { + t.Parallel() + + cfg := MCPServerConfig{ + Version: "test", + Token: "test-token", + EnabledToolsets: []string{"unknown-toolset"}, + StrictToolsets: false, + Translator: translations.NullTranslationHelper, + ContentWindowSize: 5000, + Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), + } + + inv, err := NewInventory(cfg.Translator). + WithDeprecatedAliases(DeprecatedToolAliases). + WithToolsets(cfg.EnabledToolsets). + Build() + require.NoError(t, err) + require.NotEmpty(t, inv.UnrecognizedToolsets()) + + server, err := NewMCPServer(context.Background(), &cfg, stubDeps{}, inv) + require.NoError(t, err) + require.NotNil(t, server) +}