8000 feat: partition tools by product/feature · github/github-mcp-server@30ce29b · GitHub
[go: up one dir, main page]

Skip to content

Commit 30ce29b

Browse files
feat: partition tools by product/feature
1 parent 651a3aa commit 30ce29b

File tree

5 files changed

+540
-49
lines changed

5 files changed

+540
-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 comman 9E88 ds
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: 106 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(s, 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

@@ -119,6 +135,48 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mc
119135
}
120136
}
121137

138+
func EnableToolset(s *server.MCPServer, toolsets *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
139+
return mcp.NewTool("enable_toolset",
140+
mcp.WithDescription(t("TOOL_ENABLE_TOOLSET_DESCRIPTION", "Enable one of the sets of tools this MCP server provides.")),
141+
mcp.WithString("toolset",
142+
mcp.Required(),
143+
mcp.Description("The name of the toolset to enable"),
144+
mcp.Enum("repos", "issues", "search", "pull_requests", "code_security", "experiments"),
145+
),
146+
),
147+
func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
148+
// We need to convert the toolsets back to a map for JSON serialization
149+
toolsetName, err := requiredParam[string](request, "toolset")
150+
if err != nil {
151+
return mcp.NewToolResultError(err.Error()), nil
152+
}
153+
toolsets.EnableFeature(toolsetName)
154+
// TODO s.AddTool()
155+
// TODO SEND TOOL UPDATE TO CLIENT
156+
return mcp.NewToolResultText(fmt.Sprintf("Toolset %s enabled", toolsetName)), nil
157+
}
158+
}
159+
160+
func ListAvailableToolsets(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
161+
return mcp.NewTool("list_available_toolsets",
162+
mcp.WithDescription(t("TOOL_LIST_AVAILABLE_FEATURES_DESCRIPTION", "List all available toolsets this MCP server can offer, providing the enabled status of each.")),
163+
),
164+
func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
165+
// We need to convert the toolsetGroup back to a map for JSON serialization
166+
featureMap := make(map[string]bool)
167+
for name := range toolsetGroup.Toolsets {
168+
featureMap[name] = toolsetGroup.IsEnabled(name)
169+
}
170+
171+
r, err := json.Marshal(featureMap)
172+
if err != nil {
173+
return nil, fmt.Errorf("failed to marshal features: %w", err)
174+
}
175+
176+
return mcp.NewToolResultText(string(r)), nil
177+
}
178+
}
179+
122180
// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request.
123181
// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong.
124182
func OptionalParamOK[T any](r mcp.CallToolRequest, p string) (value T, ok bool, err error) {

0 commit comments

Comments
 (0)
0