-
Notifications
You must be signed in to change notification settings - Fork 3.2k
refactor: immutable ToolsetGroup with self-describing tools and feature flags #1602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: SamMorrowDrums/server-tool-refactor-projects
Are you sure you want to change the base?
refactor: immutable ToolsetGroup with self-describing tools and feature flags #1602
Conversation
Add CLI flag and config support for feature flags in the local server: - Add --features flag to main.go (StringSlice, comma-separated) - Add EnabledFeatures field to StdioServerConfig and MCPServerConfig - Create createFeatureChecker() that builds a set from enabled features - Wire WithFeatureChecker() into the toolset group filter chain This enables tools/resources/prompts that have FeatureFlagEnable set to a flag name that is passed via --features. The checker uses a simple set membership test for O(1) lookup. Usage: github-mcp-server stdio --features=my_feature,another_feature GITHUB_FEATURES=my_feature github-mcp-server stdio
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds feature flag support to the local GitHub MCP server, enabling runtime control over which tools, resources, and prompts are available based on enabled features. The implementation includes a new --features CLI flag, feature flag fields on ServerTool/ServerResource/ServerPrompt, and a filtering mechanism integrated into the toolset group's filter chain.
Key Changes:
- Added
--featuresCLI flag andGITHUB_FEATURESenvironment variable support for comma-separated feature flags - Implemented
FeatureFlagCheckerinterface andcreateFeatureChecker()for O(1) flag lookup - Added
FeatureFlagEnableandFeatureFlagDisablefields to ServerTool, ServerResourceTemplate, and ServerPrompt - Integrated feature flag filtering into
WithFeatureChecker()method with immutable filter chaining
Reviewed changes
Copilot reviewed 35 out of 35 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
pkg/toolsets/toolsets.go |
Core feature flag filtering logic: FeatureFlagChecker type, WithFeatureChecker() method, isFeatureFlagAllowed() logic integrated into Available* methods |
pkg/toolsets/toolsets_test.go |
Comprehensive test coverage for feature flags (enable/disable/both/errors) across tools, resources, and prompts |
pkg/toolsets/server_tool.go |
Added FeatureFlagEnable and FeatureFlagDisable string fields to ServerTool, ServerResourceTemplate, and ServerPrompt |
pkg/github/toolset_group.go |
New factory function for creating ToolsetGroup with all tools/resources/prompts |
internal/ghmcp/server.go |
Added EnabledFeatures config field, createFeatureChecker() implementation, wired into filter chain via WithFeatureChecker() |
cmd/github-mcp-server/main.go |
Added --features persistent flag with viper binding and GITHUB_FEATURES env var support |
Multiple pkg/github/*.go files |
Updated all tool/resource/prompt constructors to include toolset metadata parameter (structural refactor supporting feature flags) |
This commit adds comprehensive validation tests to ensure all MCP items have required metadata: - TestAllToolsHaveRequiredMetadata: Validates Toolset.ID and Annotations - TestAllToolsHaveValidToolsetID: Ensures toolsets are in AvailableToolsets() - TestAllResourcesHaveRequiredMetadata: Validates resource metadata - TestAllPromptsHaveRequiredMetadata: Validates prompt metadata - TestToolReadOnlyHintConsistency: Validates IsReadOnly() matches annotation - TestNoDuplicate*Names: Ensures unique names across tools/resources/prompts - TestAllToolsHaveHandlerFunc: Ensures all tools have handlers - TestDefaultToolsetsAreValid: Validates default toolset IDs - TestToolsetMetadataConsistency: Ensures consistent descriptions per toolset Also fixes a bug discovered by these tests: ToolsetMetadataGit was defined but not added to AvailableToolsets(), causing get_repository_tree to have an invalid toolset ID.
When no toolsets are specified and dynamic mode is disabled, the server should use the default toolsets. The bug was introduced when adding dynamic toolsets support: 1. CleanToolsets(nil) was converting nil to empty slice 2. Empty slice passed to WithToolsets means 'no toolsets' 3. This resulted in zero tools being registered Fix: Preserve nil for non-dynamic mode (nil = use defaults in WithToolsets) and only set empty slice when dynamic mode is enabled without explicit toolsets.
78c89a3 to
689a040
Compare
internal/ghmcp/server.go
Outdated
| } | ||
| // Add deprecated tool aliases for backward compatibility | ||
| // See docs/deprecated-tool-aliases.md for the full list of renames | ||
| tsg.AddDeprecatedToolAliases(github.DeprecatedToolAliases) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, isn't this returning a new tsg? Should be immutable...
So I think this is incorrect usage.
| } | ||
|
|
||
| // mockGetRawClient returns a mock raw client for documentation generation | ||
| func mockGetRawClient(_ context.Context) (*raw.Client, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am sure this can be removed too...
- Rename AddDeprecatedToolAliases to WithDeprecatedToolAliases for immutable filter chain consistency (returns new ToolsetGroup) - Remove unused mockGetRawClient from generate_docs.go (use nil instead) - Remove legacy ServerTool functions (NewServerToolLegacy and NewServerToolFromHandlerLegacy) - no usages - Add panic in Handler()/RegisterFunc() when HandlerFunc is nil - Add HasHandler() method for checking if tool has a handler - Add tests for HasHandler and nil handler panic behavior - Update all tests to use new WithDeprecatedToolAliases pattern
| repoAccessCache := lockdown.GetInstance(nil) | ||
| tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000, github.FeatureFlags{}, repoAccessCache) | ||
| // Create toolset group with mock clients (no deps needed for doc generation) | ||
| tsg := github.NewToolsetGroup(t, mockGetClient, nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure the interface is right yet. Do we need deps here...
…lsetGroup This change applies the same HandlerFunc pattern used by tools to resources, allowing NewToolsetGroup to be fully stateless (only requiring translations). Key changes: - Add ResourceHandlerFunc type to toolsets package - Update ServerResourceTemplate to use HandlerFunc instead of direct Handler - Add HasHandler() and Handler(deps) methods to ServerResourceTemplate - Update RegisterResourceTemplates to take deps parameter - Refactor repository resource definitions to use HandlerFunc pattern - Make AllResources(t) stateless (only takes translations) - Make NewToolsetGroup(t) stateless (only takes translations) - Update generate_docs.go - no longer needs mock clients - Update tests to use new patterns This resolves the concern about mixed concerns in doc generation - the toolset metadata and resource templates can now be created without any runtime dependencies, while handlers are generated on-demand when deps are provided during registration.
- Replace slice joining with strings.Builder for all doc generation - Iterate AllTools() directly instead of ToolsetIDs()/ToolsForToolset() - Removes need for special 'dynamic' toolset handling (no tools = no output) - Context toolset still explicitly handled for custom description - Consistent pattern across generateToolsetsDoc, generateToolsDoc, generateRemoteToolsetsDoc, and generateDeprecatedAliasesTable
- Add AvailableToolsets() method that returns toolsets with actual tools - Support variadic exclude parameter for filtering out specific toolsets - Simplifies doc generation by removing manual skip logic - Naturally excludes empty toolsets (like 'dynamic') without special cases
9ef4da2 to
a536c15
Compare
- Add Default field to ToolsetMetadata and derive defaults from metadata - Move toolset validation into WithToolsets (trims whitespace, dedupes, tracks unrecognized) - Add UnrecognizedToolsets() method for warning about typos - Add DefaultToolsetIDs() method to derive defaults from metadata - Remove redundant functions: CleanToolsets, GetValidToolsetIDs, AvailableToolsets, GetDefaultToolsetIDs - Update DynamicTools to take ToolsetGroup for schema enum generation - Add stubTranslator for cases needing ToolsetGroup without translations This eliminates hardcoded toolset lists - everything is now derived from the actual registered tools and their metadata.
a536c15 to
6b6a874
Compare
Summary
Major refactor of the toolsets package to create an elegant, immutable filtering system with self-describing tools.
Key Changes
Self-Describing Tools:
ToolsetMetadata(ID and description)Tool.Annotations.ReadOnlyHintAddReadTools()/AddWriteTools()Simplified ToolsetGroup:
NewToolsetGroup(tools, resources, prompts)- takes lists directlyToolsetstruct withSetDependencies()RegisterAll(ctx, server, deps)Immutable Filter Chain:
Feature Flags:
FeatureFlagEnable- tool only available when flag is onFeatureFlagDisable- tool excluded when flag is on--featuresCLI flag for local serverDeterministic Ordering:
Available*()methods return items sorted by toolset ID, then nameNew Files:
pkg/github/toolset_group.go-NewToolsetGroup()factorypkg/github/prompts.go-AllPrompts()pkg/github/resources.go-AllResources()docs/deprecated-tool-aliases.md- alias documentationUsage
# Enable specific features github-mcp-server stdio --features=my_feature,another_feature GITHUB_FEATURES=my_feature github-mcp-server stdioStack: