-
Notifications
You must be signed in to change notification settings - Fork 2k
Add get_teams
and get_team_members
tools
#834
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+569
−0
Merged
Changes from 12 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
a70bcc5
add team tool with tests
mattdholloway c9289a2
add to tools
mattdholloway 42fac05
add toolsnaps and docs
mattdholloway 8d97b5e
Merge branch 'main' into teams-tool
mattdholloway c54f39d
Update pkg/github/context_tools.go
mattdholloway 661daff
rewrite to allow providing user
mattdholloway 8bc6489
Merge branch 'teams-tool' of https://github.com/github/github-mcp-ser…
mattdholloway 8ebb834
rRename get_my_teams to get_teams and update documentation and tests
mattdholloway 837af76
Merge branch 'main' into teams-tool
mattdholloway c04c6dc
remove old snap
mattdholloway 896d34b
rm old comments
mattdholloway 2b8c2c2
update test teams to numbered examples
mattdholloway b460280
Update descriptions for allow finding teams of other users
mattdholloway 2d183e1
return empty result over custom empty error
mattdholloway 8a332fc
fix test expectations for no teams found
mattdholloway b28d98b
flatten teams response to not include Nodes
mattdholloway 3acaaeb
update description to include clarification about teams you are a mem…
mattdholloway 165d38c
fix typo in tool desc
mattdholloway de994ca
updated description to be more generic for accecss note
mattdholloway 4652d60
amended error handling
mattdholloway ce8880f
Update pkg/github/context_tools.go
mattdholloway f92b4e0
add additional tool to get team members
mattdholloway 4429b1f
Merge branch 'teams-tool' of https://github.com/github/github-mcp-ser…
mattdholloway 15e5e10
update tool desc for get_team_members to include warning about auth
mattdholloway 8c59077
added new scope info
mattdholloway c590779
Merge branch 'main' into teams-tool
mattdholloway 2b7fccf
refactor to parse params individually
LuluBeatson 5672632
GetTeams - rename "login" field to "org"
LuluBeatson c192a47
Merge branch 'main' into teams-tool
LuluBeatson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"annotations": { | ||
"title": "Get my teams", | ||
"readOnlyHint": true | ||
}, | ||
"description": "Get details of the teams the authenticated user is a member of.", | ||
"inputSchema": { | ||
"properties": { | ||
"user": { | ||
"description": "Username to get teams for. If not provided, uses the authenticated user.", | ||
"type": "string" | ||
} | ||
}, | ||
"type": "object" | ||
}, | ||
"name": "get_teams" | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,13 +3,16 @@ package github | |
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"testing" | ||
"time" | ||
|
||
"github.com/github/github-mcp-server/internal/githubv4mock" | ||
"github.com/github/github-mcp-server/internal/toolsnaps" | ||
"github.com/github/github-mcp-server/pkg/translations" | ||
"github.com/google/go-github/v74/github" | ||
"github.com/migueleliasweb/go-github-mock/src/mock" | ||
"github.com/shurcooL/githubv4" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
@@ -139,3 +142,234 @@ func Test_GetMe(t *testing.T) { | |
}) | ||
} | ||
} | ||
|
||
func Test_GetTeams(t *testing.T) { | ||
t.Parallel() | ||
|
||
tool, _ := GetTeams(nil, nil, translations.NullTranslationHelper) | ||
require.NoError(t, toolsnaps.Test(tool.Name, tool)) | ||
|
||
assert.Equal(t, "get_teams", tool.Name) | ||
assert.True(t, *tool.Annotations.ReadOnlyHint, "get_teams tool should be read-only") | ||
|
||
mockUser := &github.User{ | ||
Login: github.Ptr("testuser"), | ||
Name: github.Ptr("Test User"), | ||
Email: github.Ptr("[email protected]"), | ||
Bio: github.Ptr("GitHub user for testing"), | ||
Company: github.Ptr("Test Company"), | ||
Location: github.Ptr("Test Location"), | ||
HTMLURL: github.Ptr("https://github.com/testuser"), | ||
CreatedAt: &github.Timestamp{Time: time.Now().Add(-365 * 24 * time.Hour)}, | ||
Type: github.Ptr("User"), | ||
Hireable: github.Ptr(true), | ||
TwitterUsername: github.Ptr("testuser_twitter"), | ||
Plan: &github.Plan{ | ||
Name: github.Ptr("pro"), | ||
}, | ||
} | ||
|
||
mockTeamsResponse := githubv4mock.DataResponse(map[string]any{ | ||
"user": map[string]any{ | ||
"organizations": map[string]any{ | ||
"nodes": []map[string]any{ | ||
{ | ||
"login": "testorg1", | ||
"teams": map[string]any{ | ||
"nodes": []map[string]any{ | ||
{ | ||
"name": "team1", | ||
"slug": "team1", | ||
"description": "Team 1", | ||
}, | ||
{ | ||
"name": "team2", | ||
"slug": "team2", | ||
"description": "Team 2", | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
"login": "testorg2", | ||
"teams": map[string]any{ | ||
"nodes": []map[string]any{ | ||
{ | ||
"name": "team3", | ||
"slug": "team3", | ||
"description": "Team 3", | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}) | ||
|
||
mockNoTeamsResponse := githubv4mock.DataResponse(map[string]any{ | ||
"user": map[string]any{ | ||
"organizations": map[string]any{ | ||
"nodes": []map[string]any{}, | ||
}, | ||
}, | ||
}) | ||
|
||
tests := []struct { | ||
name string | ||
stubbedGetClientFn GetClientFn | ||
stubbedGetGQLClientFn GetGQLClientFn | ||
requestArgs map[string]any | ||
expectToolError bool | ||
expectedToolErrMsg string | ||
expectedTeamsCount int | ||
}{ | ||
{ | ||
name: "successful get teams", | ||
stubbedGetClientFn: stubGetClientFromHTTPFn( | ||
mock.NewMockedHTTPClient( | ||
mock.WithRequestMatch( | ||
mock.GetUser, | ||
mockUser, | ||
), | ||
), | ||
), | ||
stubbedGetGQLClientFn: func(_ context.Context) (*githubv4.Client, error) { | ||
queryStr := "query($login:String!){user(login: $login){organizations(first: 100){nodes{login,teams(first: 100, userLogins: [$login]){nodes{name,slug,description}}}}}}" | ||
vars := map[string]interface{}{ | ||
"login": "testuser", | ||
} | ||
matcher := githubv4mock.NewQueryMatcher(queryStr, vars, mockTeamsResponse) | ||
httpClient := githubv4mock.NewMockedHTTPClient(matcher) | ||
return githubv4.NewClient(httpClient), nil | ||
}, | ||
requestArgs: map[string]any{}, | ||
expectToolError: false, | ||
expectedTeamsCount: 2, | ||
}, | ||
{ | ||
name: "successful get teams for specific user", | ||
stubbedGetClientFn: nil, | ||
stubbedGetGQLClientFn: func(_ context.Context) (*githubv4.Client, error) { | ||
queryStr := "query($login:String!){user(login: $login){organizations(first: 100){nodes{login,teams(first: 100, userLogins: [$login]){nodes{name,slug,description}}}}}}" | ||
vars := map[string]interface{}{ | ||
"login": "specificuser", | ||
} | ||
matcher := githubv4mock.NewQueryMatcher(queryStr, vars, mockTeamsResponse) | ||
httpClient := githubv4mock.NewMockedHTTPClient(matcher) | ||
return githubv4.NewClient(httpClient), nil | ||
}, | ||
requestArgs: map[string]any{ | ||
"user": "specificuser", | ||
}, | ||
expectToolError: false, | ||
expectedTeamsCount: 2, | ||
}, | ||
{ | ||
name: "no teams found", | ||
stubbedGetClientFn: stubGetClientFromHTTPFn( | ||
mock.NewMockedHTTPClient( | ||
mock.WithRequestMatch( | ||
mock.GetUser, | ||
mockUser, | ||
), | ||
), | ||
), | ||
stubbedGetGQLClientFn: func(_ context.Context) (*githubv4.Client, error) { | ||
queryStr := "query($login:String!){user(login: $login){organizations(first: 100){nodes{login,teams(first: 100, userLogins: [$login]){nodes{name,slug,description}}}}}}" | ||
vars := map[string]interface{}{ | ||
"login": "testuser", | ||
} | ||
matcher := githubv4mock.NewQueryMatcher(queryStr, vars, mockNoTeamsResponse) | ||
httpClient := githubv4mock.NewMockedHTTPClient(matcher) | ||
return githubv4.NewClient(httpClient), nil | ||
}, | ||
requestArgs: map[string]any{}, | ||
expectToolError: true, | ||
expectedToolErrMsg: "no teams found for user", | ||
}, | ||
{ | ||
name: "getting client fails", | ||
stubbedGetClientFn: stubGetClientFnErr("expected test error"), | ||
stubbedGetGQLClientFn: nil, | ||
requestArgs: map[string]any{}, | ||
expectToolError: true, | ||
expectedToolErrMsg: "failed to get GitHub client: expected test error", | ||
}, | ||
{ | ||
name: "get user fails", | ||
stubbedGetClientFn: stubGetClientFromHTTPFn( | ||
mock.NewMockedHTTPClient( | ||
mock.WithRequestMatchHandler( | ||
mock.GetUser, | ||
badRequestHandler("expected test failure"), | ||
), | ||
), | ||
), | ||
stubbedGetGQLClientFn: nil, | ||
requestArgs: map[string]any{}, | ||
expectToolError: true, | ||
expectedToolErrMsg: "expected test failure", | ||
}, | ||
{ | ||
name: "getting GraphQL client fails", | ||
stubbedGetClientFn: stubGetClientFromHTTPFn( | ||
mock.NewMockedHTTPClient( | ||
mock.WithRequestMatch( | ||
mock.GetUser, | ||
mockUser, | ||
), | ||
), | ||
), | ||
stubbedGetGQLClientFn: func(_ context.Context) (*githubv4.Client, error) { | ||
return nil, fmt.Errorf("GraphQL client error") | ||
}, | ||
requestArgs: map[string]any{}, | ||
expectToolError: true, | ||
expectedToolErrMsg: "failed to get GitHub GQL client: GraphQL client error", | ||
}, | ||
} | ||
|
||
for _, tc := range tests { | ||
t.Run(tc.name, func(t *testing.T) { | ||
_, handler := GetTeams(tc.stubbedGetClientFn, tc.stubbedGetGQLClientFn, translations.NullTranslationHelper) | ||
|
||
request := createMCPRequest(tc.requestArgs) | ||
result, err := handler(context.Background(), request) | ||
require.NoError(t, err) | ||
textContent := getTextResult(t, result) | ||
|
||
if tc.expectToolError { | ||
assert.True(t, result.IsError, "expected tool call result to be an error") | ||
assert.Contains(t, textContent.Text, tc.expectedToolErrMsg) | ||
return | ||
} | ||
|
||
var organizations []struct { | ||
Login string `json:"login"` | ||
Teams struct { | ||
Nodes []struct { | ||
Name string `json:"name"` | ||
Slug string `json:"slug"` | ||
Description string `json:"description"` | ||
} `json:"nodes"` | ||
} `json:"teams"` | ||
} | ||
err = json.Unmarshal([]byte(textContent.Text), &organizations) | ||
require.NoError(t, err) | ||
|
||
assert.Len(t, organizations, tc.expectedTeamsCount) | ||
|
||
if tc.expectedTeamsCount > 0 { | ||
assert.Equal(t, "testorg1", organizations[0].Login) | ||
assert.Len(t, organizations[0].Teams.Nodes, 2) | ||
assert.Equal(t, "team1", organizations[0].Teams.Nodes[0].Name) | ||
assert.Equal(t, "team1", organizations[0].Teams.Nodes[0].Slug) | ||
|
||
assert.Equal(t, "testorg2", organizations[1].Login) | ||
assert.Len(t, organizations[1].Teams.Nodes, 1) | ||
assert.Equal(t, "team3", organizations[1].Teams.Nodes[0].Name) | ||
} | ||
}) | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.