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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy