Skip to content

Commit 5e47160

Browse files
feat: partition tools by product/feature
1 parent 651a3aa commit 5e47160

File tree

5 files changed

+538
-49
lines changed

5 files changed

+538
-49
lines changed

cmd/github-mcp-server/main.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import (
77
stdlog "log"
88
"os"
99
"os/signal"
10+
"strings"
1011
"syscall"
1112

1213
"github.com/github/github-mcp-server/pkg/github"
1314
iolog "github.com/github/github-mcp-server/pkg/log"
15+
"github.com/github/github-mcp-server/pkg/toolsets"
1416
"github.com/github/github-mcp-server/pkg/translations"
1517
gogithub "github.com/google/go-github/v69/github"
1618
"github.com/mark3labs/mcp-go/server"
@@ -43,12 +45,19 @@ var (
4345
if err != nil {
4446
stdlog.Fatal("Failed to initialize logger:", err)
4547
}
48+
enabledToolsets := viper.GetStringSlice("features")
49+
features, err := initToolsets(enabledToolsets)
50+
if err != nil {
51+
stdlog.Fatal("Failed to initialize features:", err)
52+
}
53+
4654
logCommands := viper.GetBool("enable-command-logging")
4755
cfg := runConfig{
4856
readOnly: readOnly,
4957
logger: logger,
5058
logCommands: logCommands,
5159
exportTranslations: exportTranslations,
60+
features: features,
5261
}
5362
if err := runStdioServer(cfg); err != nil {
5463
stdlog.Fatal("failed to run stdio server:", err)
@@ -57,17 +66,53 @@ var (
5766
}
5867
)
5968

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

63106
// Add global flags that will be shared by all commands
107+
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")
64108
rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations")
65109
rootCmd.PersistentFlags().String("log-file", "", "Path to log file")
66110
rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file")
67111
rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file")
68112
rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)")
69113

70114
// Bind flag to viper
115+
_ = viper.BindPFlag("features", rootCmd.PersistentFlags().Lookup("features"))
71116
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
72117
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
73118
_ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
@@ -106,6 +151,7 @@ type runConfig struct {
106151
logger *log.Logger
107152
logCommands bool
108153
exportTranslations bool
154+
features *toolsets.ToolsetGroup
109155
}
110156

111157
func runStdioServer(cfg runConfig) error {
@@ -141,7 +187,7 @@ func runStdioServer(cfg runConfig) error {
141187
return ghClient, nil // closing over client
142188
}
143189
// Create
144-
ghServer := github.NewServer(getClient, version, cfg.readOnly, t)
190+
ghServer := github.NewServer(getClient, cfg.features, version, cfg.readOnly, t)
145191
stdioServer := server.NewStdioServer(ghServer)
146192

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

pkg/github/server.go

Lines changed: 104 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"net/http"
1010

11+
"github.com/github/github-mcp-server/pkg/toolsets"
1112
"github.com/github/github-mcp-server/pkg/translations"
1213
"github.com/google/go-github/v69/github"
1314
"github.com/mark3labs/mcp-go/mcp"
@@ -17,69 +18,84 @@ import (
1718
type GetClientFn func(context.Context) (*github.Client, error)
1819

1920
// NewServer creates a new GitHub MCP server with the specified GH client and logger.
20-
func NewServer(getClient GetClientFn, version string, readOnly bool, t translations.TranslationHelperFunc) *server.MCPServer {
21+
func NewServer(getClient GetClientFn, toolsetGroup *toolsets.ToolsetGroup, version string, readOnly bool, t translations.TranslationHelperFunc) *server.MCPServer {
2122
// Create a new MCP server
2223
s := server.NewMCPServer(
2324
"github-mcp-server",
2425
version,
2526
server.WithResourceCapabilities(true, true),
2627
server.WithLogging())
2728

28-
// Add GitHub Resources
29-
s.AddResourceTemplate(GetRepositoryResourceContent(getClient, t))
30-
s.AddResourceTemplate(GetRepositoryResourceBranchContent(getClient, t))
31-
s.AddResourceTemplate(GetRepositoryResourceCommitContent(getClient, t))
32-
s.AddResourceTemplate(GetRepositoryResourceTagContent(getClient, t))
33-
s.AddResourceTemplate(GetRepositoryResourcePrContent(getClient, t))
34-
35-
// Add GitHub tools - Issues
36-
s.AddTool(GetIssue(getClient, t))
37-
s.AddTool(SearchIssues(getClient, t))
38-
s.AddTool(ListIssues(getClient, t))
39-
s.AddTool(GetIssueComments(getClient, t))
40-
if !readOnly {
41-
s.AddTool(CreateIssue(getClient, t))
42-
s.AddTool(AddIssueComment(getClient, t))
43-
s.AddTool(UpdateIssue(getClient, t))
29+
// Add GitHub tools - Users
30+
s.AddTool(GetMe(getClient, t)) // GetMe is always exposed and not part of configurable features
31+
32+
if toolsetGroup.IsEnabled("repos") {
33+
// Add GitHub Repository Resources
34+
s.AddResourceTemplate(GetRepositoryResourceContent(getClient, t))
35+
s.AddResourceTemplate(GetRepositoryResourceBranchContent(getClient, t))
36+
s.AddResourceTemplate(GetRepositoryResourceCommitContent(getClient, t))
37+
s.AddResourceTemplate(GetRepositoryResourceTagContent(getClient, t))
38+
s.AddResourceTemplate(GetRepositoryResourcePrContent(getClient, t))
39+
40+
// Add GitHub tools - Repositories
41+
s.AddTool(SearchRepositories(getClient, t))
42+
s.AddTool(GetFileContents(getClient, t))
43+
s.AddTool(ListCommits(getClient, t))
44+
if !readOnly {
45+
s.AddTool(CreateOrUpdateFile(getClient, t))
46+
s.AddTool(CreateRepository(getClient, t))
47+
s.AddTool(ForkRepository(getClient, t))
48+
s.AddTool(CreateBranch(getClient, t))
49+
s.AddTool(PushFiles(getClient, t))
50+
}
4451
}
4552

46-
// Add GitHub tools - Pull Requests
47-
s.AddTool(GetPullRequest(getClient, t))
48-
s.AddTool(ListPullRequests(getClient, t))
49-
s.AddTool(GetPullRequestFiles(getClient, t))
50-
s.AddTool(GetPullRequestStatus(getClient, t))
51-
s.AddTool(GetPullRequestComments(getClient, t))
52-
s.AddTool(GetPullRequestReviews(getClient, t))
53-
if !readOnly {
54-
s.AddTool(MergePullRequest(getClient, t))
55-
s.AddTool(UpdatePullRequestBranch(getClient, t))
56-
s.AddTool(CreatePullRequestReview(getClient, t))
57-
s.AddTool(CreatePullRequest(getClient, t))
58-
s.AddTool(UpdatePullRequest(getClient, t))
53+
if toolsetGroup.IsEnabled("issues") {
54+
// Add GitHub tools - Issues
55+
s.AddTool(GetIssue(getClient, t))
56+
s.AddTool(SearchIssues(getClient, t))
57+
s.AddTool(ListIssues(getClient, t))
58+
s.AddTool(GetIssueComments(getClient, t))
59+
if !readOnly {
60+
s.AddTool(CreateIssue(getClient, t))
61+
s.AddTool(AddIssueComment(getClient, t))
62+
s.AddTool(UpdateIssue(getClient, t))
63+
}
5964
}
6065

61-
// Add GitHub tools - Repositories
62-
s.AddTool(SearchRepositories(getClient, t))
63-
s.AddTool(GetFileContents(getClient, t))
64-
s.AddTool(ListCommits(getClient, t))
65-
if !readOnly {
66-
s.AddTool(CreateOrUpdateFile(getClient, t))
67-
s.AddTool(CreateRepository(getClient, t))
68-
s.AddTool(ForkRepository(getClient, t))
69-
s.AddTool(CreateBranch(getClient, t))
70-
s.AddTool(PushFiles(getClient, t))
66+
if toolsetGroup.IsEnabled("pull_requests") {
67+
// Add GitHub tools - Pull Requests
68+
s.AddTool(GetPullRequest(getClient, t))
69+
s.AddTool(ListPullRequests(getClient, t))
70+
s.AddTool(GetPullRequestFiles(getClient, t))
71+
s.AddTool(GetPullRequestStatus(getClient, t))
72+
s.AddTool(GetPullRequestComments(getClient, t))
73+
s.AddTool(GetPullRequestReviews(getClient, t))
74+
if !readOnly {
75+
s.AddTool(MergePullRequest(getClient, t))
76+
s.AddTool(UpdatePullRequestBranch(getClient, t))
77+
s.AddTool(CreatePullRequestReview(getClient, t))
78+
s.AddTool(CreatePullRequest(getClient, t))
79+
}
7180
}
7281

73-
// Add GitHub tools - Search
74-
s.AddTool(SearchCode(getClient, t))
75-
s.AddTool(SearchUsers(getClient, t))
82+
if toolsetGroup.IsEnabled("search") {
83+
// Add GitHub tools - Search
84+
s.AddTool(SearchCode(getClient, t))
85+
s.AddTool(SearchUsers(getClient, t))
86+
}
7687

77-
// Add GitHub tools - Users
78-
s.AddTool(GetMe(getClient, t))
88+
if toolsetGroup.IsEnabled("code_security") {
89+
// Add GitHub tools - Code Scanning
90+
s.AddTool(GetCodeScanningAlert(getClient, t))
91+
s.AddTool(ListCodeScanningAlerts(getClient, t))
92+
}
93+
94+
if toolsetGroup.IsEnabled("experiments") {
95+
s.AddTool(ListAvailableToolsets(toolsetGroup, t))
96+
s.AddTool(EnableToolset(toolsetGroup, t))
97+
}
7998

80-
// Add GitHub tools - Code Scanning
81-
s.AddTool(GetCodeScanningAlert(getClient, t))
82-
s.AddTool(ListCodeScanningAlerts(getClient, t))
8399
return s
84100
}
85101

@@ -143,6 +159,46 @@ func OptionalParamOK[T any](r mcp.CallToolRequest, p string) (value T, ok bool,
143159
return
144160
}
145161

162+
func EnableToolset(toolsets *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
163+
return mcp.NewTool("enable_toolset",
164+
mcp.WithDescription(t("TOOL_LIST_AVAILABLE_FEATURES_DESCRIPTION", "List all available features this MCP server can offer, providing the enabled status of each.")),
165+
),
166+
func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
167+
// We need to convert the toolsets back to a map for JSON serialization
168+
featureMap := make(map[string]bool)
169+
for name := range toolsets.Toolsets {
170+
featureMap[name] = toolsets.IsEnabled(name)
171+
}
172+
173+
r, err := json.Marshal(featureMap)
174+
if err != nil {
175+
return nil, fmt.Errorf("failed to marshal features: %w", err)
176+
}
177+
178+
return mcp.NewToolResultText(string(r)), nil
179+
}
180+
}
181+
182+
func ListAvailableToolsets(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
183+
return mcp.NewTool("list_available_toolsets",
184+
mcp.WithDescription(t("TOOL_LIST_AVAILABLE_FEATURES_DESCRIPTION", "List all available toolsets this MCP server can offer, providing the enabled status of each.")),
185+
),
186+
func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
187+
// We need to convert the toolsetGroup back to a map for JSON serialization
188+
featureMap := make(map[string]bool)
189+
for name := range toolsetGroup.Toolsets {
190+
featureMap[name] = toolsetGroup.IsEnabled(name)
191+
}
192+
193+
r, err := json.Marshal(featureMap)
194+
if err != nil {
195+
return nil, fmt.Errorf("failed to marshal features: %w", err)
196+
}
197+
198+
return mcp.NewToolResultText(string(r)), nil
199+
}
200+
}
201+
146202
// isAcceptedError checks if the error is an accepted error.
147203
func isAcceptedError(err error) bool {
148204
var acceptedError *github.AcceptedError

0 commit comments

Comments
 (0)