Skip to content

Commit 5a75247

Browse files
authored
Merge branch 'main' into issue-461
2 parents 165786d + c17ebfe commit 5a75247

File tree

7 files changed

+117
-45
lines changed

7 files changed

+117
-45
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,12 +278,13 @@ docker run -i --rm \
278278
ghcr.io/github/github-mcp-server
279279
```
280280

281-
## GitHub Enterprise Server
281+
## GitHub Enterprise Server and Enterprise Cloud with data residency (ghe.com)
282282

283283
The flag `--gh-host` and the environment variable `GITHUB_HOST` can be used to set
284-
the GitHub Enterprise Server hostname.
285-
Prefix the hostname with the `https://` URI scheme, as it otherwise defaults to `http://` which GitHub Enterprise Server does not support.
284+
the hostname for GitHub Enterprise Server or GitHub Enterprise Cloud with data residency.
286285

286+
- For GitHub Enterprise Server, prefix the hostname with the `https://` URI scheme, as it otherwise defaults to `http://`, which GitHub Enterprise Server does not support.
287+
- For GitHub Enterprise Cloud with data residency, use `https://YOURSUBDOMAIN.ghe.com` as the hostname.
287288
``` json
288289
"github": {
289290
"command": "docker",
@@ -299,7 +300,7 @@ Prefix the hostname with the `https://` URI scheme, as it otherwise defaults to
299300
],
300301
"env": {
301302
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}",
302-
"GITHUB_HOST": "https://<your GHES domain name>"
303+
"GITHUB_HOST": "https://<your GHES or ghe.com domain name>"
303304
}
304305
}
305306
```

internal/ghmcp/server.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,26 +113,22 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
113113
}
114114

115115
// Create default toolsets
116-
toolsets, err := github.InitToolsets(
117-
enabledToolsets,
118-
cfg.ReadOnly,
119-
getClient,
120-
getGQLClient,
121-
cfg.Translator,
122-
)
116+
tsg := github.DefaultToolsetGroup(cfg.ReadOnly, getClient, getGQLClient, cfg.Translator)
117+
err = tsg.EnableToolsets(enabledToolsets)
118+
123119
if err != nil {
124-
return nil, fmt.Errorf("failed to initialize toolsets: %w", err)
120+
return nil, fmt.Errorf("failed to enable toolsets: %w", err)
125121
}
126122

127123
context := github.InitContextToolset(getClient, cfg.Translator)
128124
github.RegisterResources(ghServer, getClient, cfg.Translator)
129125

130126
// Register the tools with the server
131-
toolsets.RegisterTools(ghServer)
127+
tsg.RegisterTools(ghServer)
132128
context.RegisterTools(ghServer)
133129

134130
if cfg.DynamicToolsets {
135-
dynamic := github.InitDynamicToolset(ghServer, toolsets, cfg.Translator)
131+
dynamic := github.InitDynamicToolset(ghServer, tsg, cfg.Translator)
136132
dynamic.RegisterTools(ghServer)
137133
}
138134

pkg/github/search.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,19 @@ func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (to
146146
}
147147
}
148148

149+
type MinimalUser struct {
150+
Login string `json:"login"`
151+
ID int64 `json:"id,omitempty"`
152+
ProfileURL string `json:"profile_url,omitempty"`
153+
AvatarURL string `json:"avatar_url,omitempty"`
154+
}
155+
156+
type MinimalSearchUsersResult struct {
157+
TotalCount int `json:"total_count"`
158+
IncompleteResults bool `json:"incomplete_results"`
159+
Items []MinimalUser `json:"items"`
160+
}
161+
149162
// SearchUsers creates a tool to search for GitHub users.
150163
func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
151164
return mcp.NewTool("search_users",
@@ -200,7 +213,7 @@ func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (t
200213
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
201214
}
202215

203-
result, resp, err := client.Search.Users(ctx, query, opts)
216+
result, resp, err := client.Search.Users(ctx, "type:user "+query, opts)
204217
if err != nil {
205218
return nil, fmt.Errorf("failed to search users: %w", err)
206219
}
@@ -214,11 +227,28 @@ func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (t
214227
return mcp.NewToolResultError(fmt.Sprintf("failed to search users: %s", string(body))), nil
215228
}
216229

217-
r, err := json.Marshal(result)
230+
minimalUsers := make([]MinimalUser, 0, len(result.Users))
231+
for _, user := range result.Users {
232+
mu := MinimalUser{
233+
Login: user.GetLogin(),
234+
ID: user.GetID(),
235+
ProfileURL: user.GetHTMLURL(),
236+
AvatarURL: user.GetAvatarURL(),
237+
}
238+
239+
minimalUsers = append(minimalUsers, mu)
240+
}
241+
242+
minimalResp := MinimalSearchUsersResult{
243+
TotalCount: result.GetTotal(),
244+
IncompleteResults: result.GetIncompleteResults(),
245+
Items: minimalUsers,
246+
}
247+
248+
r, err := json.Marshal(minimalResp)
218249
if err != nil {
219250
return nil, fmt.Errorf("failed to marshal response: %w", err)
220251
}
221-
222252
return mcp.NewToolResultText(string(r)), nil
223253
}
224254
}

pkg/github/search_test.go

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -335,18 +335,13 @@ func Test_SearchUsers(t *testing.T) {
335335
ID: github.Ptr(int64(1001)),
336336
HTMLURL: github.Ptr("https://github.com/user1"),
337337
AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/1001"),
338-
Type: github.Ptr("User"),
339-
Followers: github.Ptr(100),
340-
Following: github.Ptr(50),
341338
},
342339
{
343340
Login: github.Ptr("user2"),
344341
ID: github.Ptr(int64(1002)),
345342
HTMLURL: github.Ptr("https://github.com/user2"),
346343
AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/1002"),
347344
Type: github.Ptr("User"),
348-
Followers: github.Ptr(200),
349-
Following: github.Ptr(75),
350345
},
351346
},
352347
}
@@ -365,7 +360,7 @@ func Test_SearchUsers(t *testing.T) {
365360
mock.WithRequestMatchHandler(
366361
mock.GetSearchUsers,
367362
expectQueryParams(t, map[string]string{
368-
"q": "location:finland language:go",
363+
"q": "type:user location:finland language:go",
369364
"sort": "followers",
370365
"order": "desc",
371366
"page": "1",
@@ -391,7 +386,7 @@ func Test_SearchUsers(t *testing.T) {
391386
mock.WithRequestMatchHandler(
392387
mock.GetSearchUsers,
393388
expectQueryParams(t, map[string]string{
394-
"q": "location:finland language:go",
389+
"q": "type:user location:finland language:go",
395390
"page": "1",
396391
"per_page": "30",
397392
}).andThen(
@@ -451,19 +446,17 @@ func Test_SearchUsers(t *testing.T) {
451446
textContent := getTextResult(t, result)
452447

453448
// Unmarshal and verify the result
454-
var returnedResult github.UsersSearchResult
449+
var returnedResult MinimalSearchUsersResult
455450
err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
456451
require.NoError(t, err)
457-
assert.Equal(t, *tc.expectedResult.Total, *returnedResult.Total)
458-
assert.Equal(t, *tc.expectedResult.IncompleteResults, *returnedResult.IncompleteResults)
459-
assert.Len(t, returnedResult.Users, len(tc.expectedResult.Users))
460-
for i, user := range returnedResult.Users {
461-
assert.Equal(t, *tc.expectedResult.Users[i].Login, *user.Login)
462-
assert.Equal(t, *tc.expectedResult.Users[i].ID, *user.ID)
463-
assert.Equal(t, *tc.expectedResult.Users[i].HTMLURL, *user.HTMLURL)
464-
assert.Equal(t, *tc.expectedResult.Users[i].AvatarURL, *user.AvatarURL)
465-
assert.Equal(t, *tc.expectedResult.Users[i].Type, *user.Type)
466-
assert.Equal(t, *tc.expectedResult.Users[i].Followers, *user.Followers)
452+
assert.Equal(t, *tc.expectedResult.Total, returnedResult.TotalCount)
453+
assert.Equal(t, *tc.expectedResult.IncompleteResults, returnedResult.IncompleteResults)
454+
assert.Len(t, returnedResult.Items, len(tc.expectedResult.Users))
455+
for i, user := range returnedResult.Items {
456+
assert.Equal(t, *tc.expectedResult.Users[i].Login, user.Login)
457+
assert.Equal(t, *tc.expectedResult.Users[i].ID, user.ID)
458+
assert.Equal(t, *tc.expectedResult.Users[i].HTMLURL, user.ProfileURL)
459+
assert.Equal(t, *tc.expectedResult.Users[i].AvatarURL, user.AvatarURL)
467460
}
468461
})
469462
}

pkg/github/tools.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ type GetGQLClientFn func(context.Context) (*githubv4.Client, error)
1515

1616
var DefaultTools = []string{"all"}
1717

18-
func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (*toolsets.ToolsetGroup, error) {
19-
// Create a new toolset group
18+
func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) *toolsets.ToolsetGroup {
2019
tsg := toolsets.NewToolsetGroup(readOnly)
2120

2221
// Define all available features with their default state (disabled)
@@ -118,13 +117,8 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
118117
tsg.AddToolset(secretProtection)
119118
tsg.AddToolset(notifications)
120119
tsg.AddToolset(experiments)
121-
// Enable the requested features
122120

123-
if err := tsg.EnableToolsets(passedToolsets); err != nil {
124-
return nil, err
125-
}
126-
127-
return tsg, nil
121+
return tsg
128122
}
129123

130124
func InitContextToolset(getClient GetClientFn, t translations.TranslationHelperFunc) *toolsets.Toolset {

pkg/toolsets/toolsets.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ import (
77
"github.com/mark3labs/mcp-go/server"
88
)
99

10+
type ToolsetDoesNotExistError struct {
11+
Name string
12+
}
13+
14+
func (e *ToolsetDoesNotExistError) Error() string {
15+
return fmt.Sprintf("toolset %s does not exist", e.Name)
16+
}
17+
18+
func (e *ToolsetDoesNotExistError) Is(target error) bool {
19+
if target == nil {
20+
return false
21+
}
22+
if _, ok := target.(*ToolsetDoesNotExistError); ok {
23+
return true
24+
}
25+
return false
26+
}
27+
28+
func NewToolsetDoesNotExistError(name string) *ToolsetDoesNotExistError {
29+
return &ToolsetDoesNotExistError{Name: name}
30+
}
31+
1032
func NewServerTool(tool mcp.Tool, handler server.ToolHandlerFunc) server.ServerTool {
1133
return server.ServerTool{Tool: tool, Handler: handler}
1234
}
@@ -150,7 +172,7 @@ func (tg *ToolsetGroup) EnableToolsets(names []string) error {
150172
func (tg *ToolsetGroup) EnableToolset(name string) error {
151173
toolset, exists := tg.Toolsets[name]
152174
if !exists {
153-
return fmt.Errorf("toolset %s does not exist", name)
175+
return NewToolsetDoesNotExistError(name)
154176
}
155177
toolset.Enabled = true
156178
tg.Toolsets[name] = toolset
@@ -162,3 +184,11 @@ func (tg *ToolsetGroup) RegisterTools(s *server.MCPServer) {
162184
toolset.RegisterTools(s)
163185
}
164186
}
187+
188+
func (tg *ToolsetGroup) GetToolset(name string) (*Toolset, error) {
189+
toolset, exists := tg.Toolsets[name]
190+
if !exists {
191+
return nil, NewToolsetDoesNotExistError(name)
192+
}
193+
return toolset, nil
194+
}

pkg/toolsets/toolsets_test.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package toolsets
22

33
import (
4+
"errors"
45
"testing"
56
)
67

@@ -151,6 +152,9 @@ func TestEnableToolsets(t *testing.T) {
151152
if err == nil {
152153
t.Error("Expected error when enabling list with non-existent toolset")
153154
}
155+
if !errors.Is(err, NewToolsetDoesNotExistError("non-existent")) {
156+
t.Errorf("Expected ToolsetDoesNotExistError when enabling non-existent toolset, got: %v", err)
157+
}
154158

155159
// Test with empty list
156160
err = tsg.EnableToolsets([]string{})
@@ -207,7 +211,7 @@ func TestEnableEverything(t *testing.T) {
207211
func TestIsEnabledWithEverythingOn(t *testing.T) {
208212
tsg := NewToolsetGroup(false)
209213

210-
// Enable "everything"
214+
// Enable "all"
211215
err := tsg.EnableToolsets([]string{"all"})
212216
if err != nil {
213217
t.Errorf("Expected no error when enabling 'all', got: %v", err)
@@ -222,3 +226,27 @@ func TestIsEnabledWithEverythingOn(t *testing.T) {
222226
t.Error("Expected IsEnabled to return true for any toolset when everythingOn is true")
223227
}
224228
}
229+
230+
func TestToolsetGroup_GetToolset(t *testing.T) {
231+
tsg := NewToolsetGroup(false)
232+
toolset := NewToolset("my-toolset", "desc")
233+
tsg.AddToolset(toolset)
234+
235+
// Should find the toolset
236+
got, err := tsg.GetToolset("my-toolset")
237+
if err != nil {
238+
t.Fatalf("expected no error, got %v", err)
239+
}
240+
if got != toolset {
241+
t.Errorf("expected to get the same toolset instance")
242+
}
243+
244+
// Should not find a non-existent toolset
245+
_, err = tsg.GetToolset("does-not-exist")
246+
if err == nil {
247+
t.Error("expected error for missing toolset, got nil")
248+
}
249+
if !errors.Is(err, NewToolsetDoesNotExistError("does-not-exist")) {
250+
t.Errorf("expected error to be ToolsetDoesNotExistError, got %v", err)
251+
}
252+
}

0 commit comments

Comments
 (0)