Skip to content

Commit f92b4e0

Browse files
committed
add additional tool to get team members
1 parent 4652d60 commit f92b4e0

File tree

5 files changed

+215
-0
lines changed

5 files changed

+215
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,10 @@ The following sets of tools are available (all are on by default):
421421
- **get_me** - Get my user profile
422422
- No parameters required
423423

424+
- **get_team_members** - Get team members
425+
- `org`: Organization login (owner) that contains the team. (string, required)
426+
- `team_slug`: Team slug (string, required)
427+
424428
- **get_teams** - Get teams
425429
- `user`: Username to get teams for. If not provided, uses the authenticated user. (string, optional)
426430

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"annotations": {
3+
"title": "Get team members",
4+
"readOnlyHint": true
5+
},
6+
"description": "Get member usernames of a specific team in an organization.",
7+
"inputSchema": {
8+
"properties": {
9+
"org": {
10+
"description": "Organization login (owner) that contains the team.",
11+
"type": "string"
12+
},
13+
"team_slug": {
14+
"description": "Team slug",
15+
"type": "string"
16+
}
17+
},
18+
"required": [
19+
"org",
20+
"team_slug"
21+
],
22+
"type": "object"
23+
},
24+
"name": "get_team_members"
25+
}

pkg/github/context_tools.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,60 @@ func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations
191191

192192
return tool, handler
193193
}
194+
195+
func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
196+
tool := mcp.NewTool("get_team_members",
197+
mcp.WithDescription(t("TOOL_GET_TEAM_MEMBERS_DESCRIPTION", "Get member usernames of a specific team in an organization.")),
198+
mcp.WithString("org",
199+
mcp.Description(t("TOOL_GET_TEAM_MEMBERS_ORG_DESCRIPTION", "Organization login (owner) that contains the team.")),
200+
mcp.Required(),
201+
),
202+
mcp.WithString("team_slug",
203+
mcp.Description(t("TOOL_GET_TEAM_MEMBERS_TEAM_SLUG_DESCRIPTION", "Team slug")),
204+
mcp.Required(),
205+
),
206+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
207+
Title: t("TOOL_GET_TEAM_MEMBERS_TITLE", "Get team members"),
208+
ReadOnlyHint: ToBoolPtr(true),
209+
}),
210+
)
211+
212+
type args struct {
213+
Org string `json:"org"`
214+
TeamSlug string `json:"team_slug"`
215+
}
216+
handler := mcp.NewTypedToolHandler(func(ctx context.Context, _ mcp.CallToolRequest, a args) (*mcp.CallToolResult, error) {
217+
gqlClient, err := getGQLClient(ctx)
218+
if err != nil {
219+
return mcp.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil
220+
}
221+
222+
var q struct {
223+
Organization struct {
224+
Team struct {
225+
Members struct {
226+
Nodes []struct {
227+
Login githubv4.String
228+
}
229+
} `graphql:"members(first: 100)"`
230+
} `graphql:"team(slug: $teamSlug)"`
231+
} `graphql:"organization(login: $org)"`
232+
}
233+
vars := map[string]interface{}{
234+
"org": githubv4.String(a.Org),
235+
"teamSlug": githubv4.String(a.TeamSlug),
236+
}
237+
if err := gqlClient.Query(ctx, &q, vars); err != nil {
238+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to get team members", err), nil
239+
}
240+
241+
var members []string
242+
for _, member := range q.Organization.Team.Members.Nodes {
243+
members = append(members, string(member.Login))
244+
}
245+
246+
return MarshalledTextResult(members), nil
247+
})
248+
249+
return tool, handler
250+
}

pkg/github/context_tools_test.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,131 @@ func Test_GetTeams(t *testing.T) {
376376
})
377377
}
378378
}
379+
380+
func Test_GetTeamMembers(t *testing.T) {
381+
t.Parallel()
382+
383+
tool, _ := GetTeamMembers(nil, translations.NullTranslationHelper)
384+
require.NoError(t, toolsnaps.Test(tool.Name, tool))
385+
386+
assert.Equal(t, "get_team_members", tool.Name)
387+
assert.True(t, *tool.Annotations.ReadOnlyHint, "get_team_members tool should be read-only")
388+
389+
mockTeamMembersResponse := githubv4mock.DataResponse(map[string]any{
390+
"organization": map[string]any{
391+
"team": map[string]any{
392+
"members": map[string]any{
393+
"nodes": []map[string]any{
394+
{
395+
"login": "user1",
396+
},
397+
{
398+
"login": "user2",
399+
},
400+
},
401+
},
402+
},
403+
},
404+
})
405+
406+
mockNoMembersResponse := githubv4mock.DataResponse(map[string]any{
407+
"organization": map[string]any{
408+
"team": map[string]any{
409+
"members": map[string]any{
410+
"nodes": []map[string]any{},
411+
},
412+
},
413+
},
414+
})
415+
416+
tests := []struct {
417+
name string
418+
stubbedGetGQLClientFn GetGQLClientFn
419+
requestArgs map[string]any
420+
expectToolError bool
421+
expectedToolErrMsg string
422+
expectedMembersCount int
423+
}{
424+
{
425+
name: "successful get team members",
426+
stubbedGetGQLClientFn: func(_ context.Context) (*githubv4.Client, error) {
427+
queryStr := "query($org:String!$teamSlug:String!){organization(login: $org){team(slug: $teamSlug){members(first: 100){nodes{login}}}}}"
428+
vars := map[string]interface{}{
429+
"org": "testorg",
430+
"teamSlug": "testteam",
431+
}
432+
matcher := githubv4mock.NewQueryMatcher(queryStr, vars, mockTeamMembersResponse)
433+
httpClient := githubv4mock.NewMockedHTTPClient(matcher)
434+
return githubv4.NewClient(httpClient), nil
435+
},
436+
requestArgs: map[string]any{
437+
"org": "testorg",
438+
"team_slug": "testteam",
439+
},
440+
expectToolError: false,
441+
expectedMembersCount: 2,
442+
},
443+
{
444+
name: "team with no members",
445+
stubbedGetGQLClientFn: func(_ context.Context) (*githubv4.Client, error) {
446+
queryStr := "query($org:String!$teamSlug:String!){organization(login: $org){team(slug: $teamSlug){members(first: 100){nodes{login}}}}}"
447+
vars := map[string]interface{}{
448+
"org": "testorg",
449+
"teamSlug": "emptyteam",
450+
}
451+
matcher := githubv4mock.NewQueryMatcher(queryStr, vars, mockNoMembersResponse)
452+
httpClient := githubv4mock.NewMockedHTTPClient(matcher)
453+
return githubv4.NewClient(httpClient), nil
454+
},
455+
requestArgs: map[string]any{
456+
"org": "testorg",
457+
"team_slug": "emptyteam",
458+
},
459+
expectToolError: false,
460+
expectedMembersCount: 0,
461+
},
462+
{
463+
name: "getting GraphQL client fails",
464+
stubbedGetGQLClientFn: func(_ context.Context) (*githubv4.Client, error) {
465+
return nil, fmt.Errorf("GraphQL client error")
466+
},
467+
requestArgs: map[string]any{
468+
"org": "testorg",
469+
"team_slug": "testteam",
470+
},
471+
expectToolError: true,
472+
expectedToolErrMsg: "failed to get GitHub GQL client: GraphQL client error",
473+
},
474+
}
475+
476+
for _, tc := range tests {
477+
t.Run(tc.name, func(t *testing.T) {
478+
_, handler := GetTeamMembers(tc.stubbedGetGQLClientFn, translations.NullTranslationHelper)
479+
480+
request := createMCPRequest(tc.requestArgs)
481+
result, err := handler(context.Background(), request)
482+
require.NoError(t, err)
483+
textContent := getTextResult(t, result)
484+
485+
if tc.expectToolError {
486+
assert.True(t, result.IsError, "expected tool call result to be an error")
487+
assert.Contains(t, textContent.Text, tc.expectedToolErrMsg)
488+
return
489+
}
490+
491+
var members []string
492+
err = json.Unmarshal([]byte(textContent.Text), &members)
493+
require.NoError(t, err)
494+
495+
assert.Len(t, members, tc.expectedMembersCount)
496+
497+
if tc.expectedMembersCount > 0 {
498+
assert.Equal(t, "user1", members[0])
499+
500+
if tc.expectedMembersCount > 1 {
501+
assert.Equal(t, "user2", members[1])
502+
}
503+
}
504+
})
505+
}
506+
}

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
163163
AddReadTools(
164164
toolsets.NewServerTool(GetMe(getClient, t)),
165165
toolsets.NewServerTool(GetTeams(getClient, getGQLClient, t)),
166+
toolsets.NewServerTool(GetTeamMembers(getGQLClient, t)),
166167
)
167168

168169
gists := toolsets.NewToolset("gists", "GitHub Gist related tools").

0 commit comments

Comments
 (0)