Skip to content

Commit 70937d8

Browse files
authored
Merge branch 'main' into issues-196
2 parents b62fdb1 + 614f226 commit 70937d8

23 files changed

+947
-222
lines changed

README.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,24 @@ More about using MCP server tools in VS Code's [agent mode documentation](https:
147147

148148
### Build from source
149149

150-
If you don't have Docker, you can use `go` to build the binary in the
151-
`cmd/github-mcp-server` directory, and use the `github-mcp-server stdio`
152-
command with the `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable set to
153-
your token.
150+
If you don't have Docker, you can use `go build` to build the binary in the
151+
`cmd/github-mcp-server` directory, and use the `github-mcp-server stdio` command with the `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable set to your token. To specify the output location of the build, use the `-o` flag. You should configure your server to use the built executable as its `command`. For example:
152+
153+
```JSON
154+
{
155+
"mcp": {
156+
"servers": {
157+
"github": {
158+
"command": "/path/to/github-mcp-server",
159+
"args": ["stdio"],
160+
"env": {
161+
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
162+
}
163+
}
164+
}
165+
}
166+
}
167+
```
154168

155169
## GitHub Enterprise Server
156170

@@ -471,6 +485,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
471485
- `ref`: Git reference (string, optional)
472486
- `state`: Alert state (string, optional)
473487
- `severity`: Alert severity (string, optional)
488+
- `tool_name`: The name of the tool used for code scanning (string, optional)
474489

475490
## Resources
476491

cmd/github-mcp-server/main.go

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ var (
2929
Use: "server",
3030
Short: "GitHub MCP Server",
3131
Long: `A GitHub MCP server that handles various tools and resources.`,
32-
Version: fmt.Sprintf("%s (%s) %s", version, commit, date),
32+
Version: fmt.Sprintf("Version: %s\nCommit: %s\nBuild Date: %s", version, commit, date),
3333
}
3434

3535
stdioCmd = &cobra.Command{
@@ -44,12 +44,16 @@ var (
4444
if err != nil {
4545
stdlog.Fatal("Failed to initialize logger:", err)
4646
}
47+
48+
enabledToolsets := viper.GetStringSlice("toolsets")
49+
4750
logCommands := viper.GetBool("enable-command-logging")
4851
cfg := runConfig{
4952
readOnly: readOnly,
5053
logger: logger,
5154
logCommands: logCommands,
5255
exportTranslations: exportTranslations,
56+
enabledToolsets: enabledToolsets,
5357
}
5458
if err := runStdioServer(cfg); err != nil {
5559
stdlog.Fatal("failed to run stdio server:", err)
@@ -61,27 +65,33 @@ var (
6165
func init() {
6266
cobra.OnInitialize(initConfig)
6367

68+
rootCmd.SetVersionTemplate("{{.Short}}\n{{.Version}}\n")
69+
6470
// Add global flags that will be shared by all commands
71+
rootCmd.PersistentFlags().StringSlice("toolsets", github.DefaultTools, "An optional comma separated list of groups of tools to allow, defaults to enabling all")
72+
rootCmd.PersistentFlags().Bool("dynamic-toolsets", false, "Enable dynamic toolsets")
6573
rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations")
6674
rootCmd.PersistentFlags().String("log-file", "", "Path to log file")
6775
rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file")
6876
rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file")
6977
rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)")
7078

7179
// Bind flag to viper
80+
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
81+
_ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets"))
7282
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
7383
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
7484
_ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
7585
_ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations"))
76-
_ = viper.BindPFlag("gh-host", rootCmd.PersistentFlags().Lookup("gh-host"))
86+
_ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host"))
7787

7888
// Add subcommands
7989
rootCmd.AddCommand(stdioCmd)
8090
}
8191

8292
func initConfig() {
8393
// Initialize Viper configuration
84-
viper.SetEnvPrefix("APP")
94+
viper.SetEnvPrefix("github")
8595
viper.AutomaticEnv()
8696
}
8797

@@ -107,6 +117,7 @@ type runConfig struct {
107117
logger *log.Logger
108118
logCommands bool
109119
exportTranslations bool
120+
enabledToolsets []string
110121
}
111122

112123
func runStdioServer(cfg runConfig) error {
@@ -115,18 +126,14 @@ func runStdioServer(cfg runConfig) error {
115126
defer stop()
116127

117128
// Create GH client
118-
token := os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN")
129+
token := viper.GetString("personal_access_token")
119130
if token == "" {
120131
cfg.logger.Fatal("GITHUB_PERSONAL_ACCESS_TOKEN not set")
121132
}
122133
ghClient := gogithub.NewClient(nil).WithAuthToken(token)
123134
ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", version)
124135

125-
// Check GH_HOST env var first, then fall back to viper config
126-
host := os.Getenv("GH_HOST")
127-
if host == "" {
128-
host = viper.GetString("gh-host")
129-
}
136+
host := viper.GetString("host")
130137

131138
if host != "" {
132139
var err error
@@ -149,8 +156,40 @@ func runStdioServer(cfg runConfig) error {
149156
hooks := &server.Hooks{
150157
OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit},
151158
}
152-
// Create
153-
ghServer := github.NewServer(getClient, version, cfg.readOnly, t, server.WithHooks(hooks))
159+
// Create server
160+
ghServer := github.NewServer(version, server.WithHooks(hooks))
161+
162+
enabled := cfg.enabledToolsets
163+
dynamic := viper.GetBool("dynamic_toolsets")
164+
if dynamic {
165+
// filter "all" from the enabled toolsets
166+
enabled = make([]string, 0, len(cfg.enabledToolsets))
167+
for _, toolset := range cfg.enabledToolsets {
168+
if toolset != "all" {
169+
enabled = append(enabled, toolset)
170+
}
171+
}
172+
}
173+
174+
// Create default toolsets
175+
toolsets, err := github.InitToolsets(enabled, cfg.readOnly, getClient, t)
176+
context := github.InitContextToolset(getClient, t)
177+
178+
if err != nil {
179+
stdlog.Fatal("Failed to initialize toolsets:", err)
180+
}
181+
182+
// Register resources with the server
183+
github.RegisterResources(ghServer, getClient, t)
184+
// Register the tools with the server
185+
toolsets.RegisterTools(ghServer)
186+
context.RegisterTools(ghServer)
187+
188+
if dynamic {
189+
dynamic := github.InitDynamicToolset(ghServer, toolsets, t)
190+
dynamic.RegisterTools(ghServer)
191+
}
192+
154193
stdioServer := server.NewStdioServer(ghServer)
155194

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

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/docker/docker v28.0.4+incompatible
77
github.com/google/go-cmp v0.7.0
88
github.com/google/go-github/v69 v69.2.0
9-
github.com/mark3labs/mcp-go v0.18.0
9+
github.com/mark3labs/mcp-go v0.20.1
1010
github.com/migueleliasweb/go-github-mock v1.1.0
1111
github.com/sirupsen/logrus v1.9.3
1212
github.com/spf13/cobra v1.9.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
5757
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
5858
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
5959
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
60-
github.com/mark3labs/mcp-go v0.18.0 h1:YuhgIVjNlTG2ZOwmrkORWyPTp0dz1opPEqvsPtySXao=
61-
github.com/mark3labs/mcp-go v0.18.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
60+
github.com/mark3labs/mcp-go v0.20.1 h1:E1Bbx9K8d8kQmDZ1QHblM38c7UU2evQ2LlkANk1U/zw=
61+
github.com/mark3labs/mcp-go v0.20.1/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
6262
github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE=
6363
github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc=
6464
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=

pkg/github/code_scanning.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,16 @@ func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHel
8686
mcp.Description("The Git reference for the results you want to list."),
8787
),
8888
mcp.WithString("state",
89-
mcp.Description("State of the code scanning alerts to list. Set to closed to list only closed code scanning alerts. Default: open"),
89+
mcp.Description("Filter code scanning alerts by state. Defaults to open"),
9090
mcp.DefaultString("open"),
91+
mcp.Enum("open", "closed", "dismissed", "fixed"),
9192
),
9293
mcp.WithString("severity",
93-
mcp.Description("Only code scanning alerts with this severity will be returned. Possible values are: critical, high, medium, low, warning, note, error."),
94+
mcp.Description("Filter code scanning alerts by severity"),
95+
mcp.Enum("critical", "high", "medium", "low", "warning", "note", "error"),
96+
),
97+
mcp.WithString("tool_name",
98+
mcp.Description("The name of the tool used for code scanning."),
9499
),
95100
),
96101
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
@@ -114,12 +119,16 @@ func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHel
114119
if err != nil {
115120
return mcp.NewToolResultError(err.Error()), nil
116121
}
122+
toolName, err := OptionalParam[string](request, "tool_name")
123+
if err != nil {
124+
return mcp.NewToolResultError(err.Error()), nil
125+
}
117126

118127
client, err := getClient(ctx)
119128
if err != nil {
120129
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
121130
}
122-
alerts, resp, err := client.CodeScanning.ListAlertsForRepo(ctx, owner, repo, &github.AlertListOptions{Ref: ref, State: state, Severity: severity})
131+
alerts, resp, err := client.CodeScanning.ListAlertsForRepo(ctx, owner, repo, &github.AlertListOptions{Ref: ref, State: state, Severity: severity, ToolName: toolName})
123132
if err != nil {
124133
return nil, fmt.Errorf("failed to list alerts: %w", err)
125134
}

pkg/github/code_scanning_test.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ func Test_ListCodeScanningAlerts(t *testing.T) {
127127
assert.Contains(t, tool.InputSchema.Properties, "ref")
128128
assert.Contains(t, tool.InputSchema.Properties, "state")
129129
assert.Contains(t, tool.InputSchema.Properties, "severity")
130+
assert.Contains(t, tool.InputSchema.Properties, "tool_name")
130131
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
131132

132133
// Setup mock alerts for success case
@@ -159,20 +160,22 @@ func Test_ListCodeScanningAlerts(t *testing.T) {
159160
mock.WithRequestMatchHandler(
160161
mock.GetReposCodeScanningAlertsByOwnerByRepo,
161162
expectQueryParams(t, map[string]string{
162-
"ref": "main",
163-
"state": "open",
164-
"severity": "high",
163+
"ref": "main",
164+
"state": "open",
165+
"severity": "high",
166+
"tool_name": "codeql",
165167
}).andThen(
166168
mockResponse(t, http.StatusOK, mockAlerts),
167169
),
168170
),
169171
),
170172
requestArgs: map[string]interface{}{
171-
"owner": "owner",
172-
"repo": "repo",
173-
"ref": "main",
174-
"state": "open",
175-
"severity": "high",
173+
"owner": "owner",
174+
"repo": "repo",
175+
"ref": "main",
176+
"state": "open",
177+
"severity": "high",
178+
"tool_name": "codeql",
176179
},
177180
expectError: false,
178181
expectedAlerts: mockAlerts,

pkg/github/context_tools.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
10+
"github.com/github/github-mcp-server/pkg/translations"
11+
"github.com/mark3labs/mcp-go/mcp"
12+
"github.com/mark3labs/mcp-go/server"
13+
)
14+
15+
// GetMe creates a tool to get details of the authenticated user.
16+
func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
17+
return mcp.NewTool("get_me",
18+
mcp.WithDescription(t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request include \"me\", \"my\"...")),
19+
mcp.WithString("reason",
20+
mcp.Description("Optional: reason the session was created"),
21+
),
22+
),
23+
func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
24+
client, err := getClient(ctx)
25+
if err != nil {
26+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
27+
}
28+
user, resp, err := client.Users.Get(ctx, "")
29+
if err != nil {
30+
return nil, fmt.Errorf("failed to get user: %w", err)
31+
}
32+
defer func() { _ = resp.Body.Close() }()
33+
34+
if resp.StatusCode != http.StatusOK {
35+
body, err := io.ReadAll(resp.Body)
36+
if err != nil {
37+
return nil, fmt.Errorf("failed to read response body: %w", err)
38+
}
39+
return mcp.NewToolResultError(fmt.Sprintf("failed to get user: %s", string(body))), nil
40+
}
41+
42+
r, err := json.Marshal(user)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to marshal user: %w", err)
45+
}
46+
47+
return mcp.NewToolResultText(string(r)), nil
48+
}
49+
}

0 commit comments

Comments
 (0)