Skip to content

Commit 0dff8b8

Browse files
feat: partition tools by product/feature
1 parent 4cf96ab commit 0dff8b8

File tree

5 files changed

+517
-48
lines changed

5 files changed

+517
-48
lines changed

cmd/github-mcp-server/main.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import (
99
stdlog "log"
1010
"os"
1111
"os/signal"
12+
"strings"
1213
"syscall"
1314

15+
"github.com/github/github-mcp-server/pkg/features"
1416
"github.com/github/github-mcp-server/pkg/github"
1517
iolog "github.com/github/github-mcp-server/pkg/log"
1618
"github.com/github/github-mcp-server/pkg/translations"
@@ -46,13 +48,20 @@ var (
4648
if err != nil {
4749
stdlog.Fatal("Failed to initialize logger:", err)
4850
}
51+
enabledFeatures := viper.GetStringSlice("features")
52+
features, err := initFeatures(enabledFeatures)
53+
if err != nil {
54+
stdlog.Fatal("Failed to initialize features:", err)
55+
}
56+
4957
logCommands := viper.GetBool("enable-command-logging")
5058
cfg := runConfig{
5159
readOnly: readOnly,
5260
logger: logger,
5361
logCommands: logCommands,
5462
exportTranslations: exportTranslations,
5563
prettyPrintJSON: prettyPrintJSON,
64+
features: features,
5665
}
5766
if err := runStdioServer(cfg); err != nil {
5867
stdlog.Fatal("failed to run stdio server:", err)
@@ -61,10 +70,45 @@ var (
6170
}
6271
)
6372

73+
func initFeatures(passedFeatures []string) (*features.FeatureSet, error) {
74+
// Create a new feature set
75+
fs := features.NewFeatureSet()
76+
77+
// Define all available features with their default state (disabled)
78+
fs.AddFeature("repos", "Repository related tools", false)
79+
fs.AddFeature("issues", "Issues related tools", false)
80+
fs.AddFeature("search", "Search related tools", false)
81+
fs.AddFeature("pull_requests", "Pull request related tools", false)
82+
fs.AddFeature("code_security", "Code security related tools", false)
83+
fs.AddFeature("experiments", "Experimental features that are not considered stable yet", false)
84+
85+
// fs.AddFeature("actions", "GitHub Actions related tools", false)
86+
// fs.AddFeature("projects", "GitHub Projects related tools", false)
87+
// fs.AddFeature("secret_protection", "Secret protection related tools", false)
88+
// fs.AddFeature("gists", "Gist related tools", false)
89+
90+
// Env gets precedence over command line flags
91+
if envFeats := os.Getenv("GITHUB_FEATURES"); envFeats != "" {
92+
passedFeatures = []string{}
93+
// Split envFeats by comma, trim whitespace, and add to the slice
94+
for _, feature := range strings.Split(envFeats, ",") {
95+
passedFeatures = append(passedFeatures, strings.TrimSpace(feature))
96+
}
97+
}
98+
99+
// Enable the requested features
100+
if err := fs.EnableFeatures(passedFeatures); err != nil {
101+
return nil, err
102+
}
103+
104+
return fs, nil
105+
}
106+
64107
func init() {
65108
cobra.OnInitialize(initConfig)
66109

67110
// Add global flags that will be shared by all commands
111+
rootCmd.PersistentFlags().StringSlice("features", []string{"repos", "issues", "pull_requests", "search"}, "A comma separated list of groups of tools to enable, defaults to issues/repos/search")
68112
rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations")
69113
rootCmd.PersistentFlags().String("log-file", "", "Path to log file")
70114
rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file")
@@ -73,6 +117,7 @@ func init() {
73117
rootCmd.PersistentFlags().Bool("pretty-print-json", false, "Pretty print JSON output")
74118

75119
// Bind flag to viper
120+
_ = viper.BindPFlag("features", rootCmd.PersistentFlags().Lookup("features"))
76121
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
77122
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
78123
_ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
@@ -113,6 +158,7 @@ type runConfig struct {
113158
logCommands bool
114159
exportTranslations bool
115160
prettyPrintJSON bool
161+
features *features.FeatureSet
116162
}
117163

118164
// JSONPrettyPrintWriter is a Writer that pretty prints input to indented JSON
@@ -158,7 +204,7 @@ func runStdioServer(cfg runConfig) error {
158204
t, dumpTranslations := translations.TranslationHelper()
159205

160206
// Create
161-
ghServer := github.NewServer(ghClient, version, cfg.readOnly, t)
207+
ghServer := github.NewServer(ghClient, cfg.features, version, cfg.readOnly, t)
162208
stdioServer := server.NewStdioServer(ghServer)
163209

164210
stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0)

pkg/features/features.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package features
2+
3+
import "fmt"
4+
5+
type Feature struct {
6+
Name string
7+
Description string
8+
Enabled bool
9+
}
10+
11+
type FeatureSet struct {
12+
Features map[string]Feature
13+
everythingOn bool
14+
}
15+
16+
func NewFeatureSet() *FeatureSet {
17+
return &FeatureSet{
18+
Features: make(map[string]Feature),
19+
everythingOn: false,
20+
}
21+
}
22+
23+
func (fs *FeatureSet) AddFeature(name string, description string, enabled bool) {
24+
fs.Features[name] = Feature{
25+
Name: name,
26+
Description: description,
27+
Enabled: enabled,
28+
}
29+
}
30+
31+
func (fs *FeatureSet) IsEnabled(name string) bool {
32+
// If everythingOn is true, all features are enabled
33+
if fs.everythingOn {
34+
return true
35+
}
36+
37+
feature, exists := fs.Features[name]
38+
if !exists {
39+
return false
40+
}
41+
return feature.Enabled
42+
}
43+
44+
func (fs *FeatureSet) EnableFeatures(names []string) error {
45+
for _, name := range names {
46+
err := fs.EnableFeature(name)
47+
if err != nil {
48+
return err
49+
}
50+
}
51+
return nil
52+
}
53+
54+
func (fs *FeatureSet) EnableFeature(name string) error {
55+
// Special case for "everything"
56+
if name == "everything" {
57+
fs.everythingOn = true
58+
return nil
59+
}
60+
61+
feature, exists := fs.Features[name]
62+
if !exists {
63+
return fmt.Errorf("feature %s does not exist", name)
64+
}
65+
feature.Enabled = true
66+
fs.Features[name] = feature
67+
return nil
68+
}

pkg/features/features_test.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package features
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestNewFeatureSet(t *testing.T) {
8+
fs := NewFeatureSet()
9+
if fs == nil {
10+
t.Fatal("Expected NewFeatureSet to return a non-nil pointer")
11+
}
12+
if fs.Features == nil {
13+
t.Fatal("Expected Features map to be initialized")
14+
}
15+
if len(fs.Features) != 0 {
16+
t.Fatalf("Expected Features map to be empty, got %d items", len(fs.Features))
17+
}
18+
if fs.everythingOn {
19+
t.Fatal("Expected everythingOn to be initialized as false")
20+
}
21+
}
22+
23+
func TestAddFeature(t *testing.T) {
24+
fs := NewFeatureSet()
25+
26+
// Test adding a feature
27+
fs.AddFeature("test-feature", "A test feature", true)
28+
29+
// Verify feature was added correctly
30+
if len(fs.Features) != 1 {
31+
t.Errorf("Expected 1 feature, got %d", len(fs.Features))
32+
}
33+
34+
feature, exists := fs.Features["test-feature"]
35+
if !exists {
36+
t.Fatal("Feature was not added to the map")
37+
}
38+
39+
if feature.Name != "test-feature" {
40+
t.Errorf("Expected feature name to be 'test-feature', got '%s'", feature.Name)
41+
}
42+
43+
if feature.Description != "A test feature" {
44+
t.Errorf("Expected feature description to be 'A test feature', got '%s'", feature.Description)
45+
}
46+
47+
if !feature.Enabled {
48+
t.Error("Expected feature to be enabled")
49+
}
50+
51+
// Test adding another feature
52+
fs.AddFeature("another-feature", "Another test feature", false)
53+
54+
if len(fs.Features) != 2 {
55+
t.Errorf("Expected 2 features, got %d", len(fs.Features))
56+
}
57+
58+
// Test overriding existing feature
59+
fs.AddFeature("test-feature", "Updated description", false)
60+
61+
feature = fs.Features["test-feature"]
62+
if feature.Description != "Updated description" {
63+
t.Errorf("Expected feature description to be updated to 'Updated description', got '%s'", feature.Description)
64+
}
65+
66+
if feature.Enabled {
67+
t.Error("Expected feature to be disabled after update")
68+
}
69+
}
70+
71+
func TestIsEnabled(t *testing.T) {
72+
fs := NewFeatureSet()
73+
74+
// Test with non-existent feature
75+
if fs.IsEnabled("non-existent") {
76+
t.Error("Expected IsEnabled to return false for non-existent feature")
77+
}
78+
79+
// Test with disabled feature
80+
fs.AddFeature("disabled-feature", "A disabled feature", false)
81+
if fs.IsEnabled("disabled-feature") {
82+
t.Error("Expected IsEnabled to return false for disabled feature")
83+
}
84+
85+
// Test with enabled feature
86+
fs.AddFeature("enabled-feature", "An enabled feature", true)
87+
if !fs.IsEnabled("enabled-feature") {
88+
t.Error("Expected IsEnabled to return true for enabled feature")
89+
}
90+
}
91+
92+
func TestEnableFeature(t *testing.T) {
93+
fs := NewFeatureSet()
94+
95+
// Test enabling non-existent feature
96+
err := fs.EnableFeature("non-existent")
97+
if err == nil {
98+
t.Error("Expected error when enabling non-existent feature")
99+
}
100+
101+
// Test enabling feature
102+
fs.AddFeature("test-feature", "A test feature", false)
103+
104+
if fs.IsEnabled("test-feature") {
105+
t.Error("Expected feature to be disabled initially")
106+
}
107+
108+
err = fs.EnableFeature("test-feature")
109+
if err != nil {
110+
t.Errorf("Expected no error when enabling feature, got: %v", err)
111+
}
112+
113+
if !fs.IsEnabled("test-feature") {
114+
t.Error("Expected feature to be enabled after EnableFeature call")
115+
}
116+
117+
// Test enabling already enabled feature
118+
err = fs.EnableFeature("test-feature")
119+
if err != nil {
120+
t.Errorf("Expected no error when enabling already enabled feature, got: %v", err)
121+
}
122+
}
123+
124+
func TestEnableFeatures(t *testing.T) {
125+
fs := NewFeatureSet()
126+
127+
// Prepare features
128+
fs.AddFeature("feature1", "Feature 1", false)
129+
fs.AddFeature("feature2", "Feature 2", false)
130+
131+
// Test enabling multiple features
132+
err := fs.EnableFeatures([]string{"feature1", "feature2"})
133+
if err != nil {
134+
t.Errorf("Expected no error when enabling features, got: %v", err)
135+
}
136+
137+
if !fs.IsEnabled("feature1") {
138+
t.Error("Expected feature1 to be enabled")
139+
}
140+
141+
if !fs.IsEnabled("feature2") {
142+
t.Error("Expected feature2 to be enabled")
143+
}
144+
145+
// Test with non-existent feature in the list
146+
err = fs.EnableFeatures([]string{"feature1", "non-existent"})
147+
if err == nil {
148+
t.Error("Expected error when enabling list with non-existent feature")
149+
}
150+
151+
// Test with empty list
152+
err = fs.EnableFeatures([]string{})
153+
if err != nil {
154+
t.Errorf("Expected no error with empty feature list, got: %v", err)
155+
}
156+
157+
// Test enabling everything through EnableFeatures
158+
fs = NewFeatureSet()
159+
err = fs.EnableFeatures([]string{"everything"})
160+
if err != nil {
161+
t.Errorf("Expected no error when enabling 'everything', got: %v", err)
162+
}
163+
164+
if !fs.everythingOn {
165+
t.Error("Expected everythingOn to be true after enabling 'everything' via EnableFeatures")
166+
}
167+
}
168+
169+
func TestEnableEverything(t *testing.T) {
170+
fs := NewFeatureSet()
171+
172+
// Add a disabled feature
173+
fs.AddFeature("test-feature", "A test feature", false)
174+
175+
// Verify it's disabled
176+
if fs.IsEnabled("test-feature") {
177+
t.Error("Expected feature to be disabled initially")
178+
}
179+
180+
// Enable "everything"
181+
err := fs.EnableFeature("everything")
182+
if err != nil {
183+
t.Errorf("Expected no error when enabling 'everything', got: %v", err)
184+
}
185+
186+
// Verify everythingOn was set
187+
if !fs.everythingOn {
188+
t.Error("Expected everythingOn to be true after enabling 'everything'")
189+
}
190+
191+
// Verify the previously disabled feature is now enabled
192+
if !fs.IsEnabled("test-feature") {
193+
t.Error("Expected feature to be enabled when everythingOn is true")
194+
}
195+
196+
// Verify a non-existent feature is also enabled
197+
if !fs.IsEnabled("non-existent") {
198+
t.Error("Expected non-existent feature to be enabled when everythingOn is true")
199+
}
200+
}
201+
202+
func TestIsEnabledWithEverythingOn(t *testing.T) {
203+
fs := NewFeatureSet()
204+
205+
// Enable "everything"
206+
err := fs.EnableFeature("everything")
207+
if err != nil {
208+
t.Errorf("Expected no error when enabling 'everything', got: %v", err)
209+
}
210+
211+
// Test that any feature name returns true with IsEnabled
212+
if !fs.IsEnabled("some-feature") {
213+
t.Error("Expected IsEnabled to return true for any feature when everythingOn is true")
214+
}
215+
216+
if !fs.IsEnabled("another-feature") {
217+
t.Error("Expected IsEnabled to return true for any feature when everythingOn is true")
218+
}
219+
}

0 commit comments

Comments
 (0)