Skip to content

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

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a70bcc5
add team tool with tests
mattdholloway Aug 7, 2025
c9289a2
add to tools
mattdholloway Aug 7, 2025
42fac05
add toolsnaps and docs
mattdholloway Aug 7, 2025
8d97b5e
Merge branch 'main' into teams-tool
mattdholloway Aug 7, 2025
c54f39d
Update pkg/github/context_tools.go
mattdholloway Aug 7, 2025
661daff
rewrite to allow providing user
mattdholloway Aug 8, 2025
8bc6489
Merge branch 'teams-tool' of https://github.com/github/github-mcp-ser…
mattdholloway Aug 8, 2025
8ebb834
rRename get_my_teams to get_teams and update documentation and tests
mattdholloway Aug 8, 2025
837af76
Merge branch 'main' into teams-tool
mattdholloway Aug 8, 2025
c04c6dc
remove old snap
mattdholloway Aug 8, 2025
896d34b
rm old comments
mattdholloway Aug 8, 2025
2b8c2c2
update test teams to numbered examples
mattdholloway Aug 8, 2025
b460280
Update descriptions for allow finding teams of other users
mattdholloway Aug 8, 2025
2d183e1
return empty result over custom empty error
mattdholloway Aug 8, 2025
8a332fc
fix test expectations for no teams found
mattdholloway Aug 8, 2025
b28d98b
flatten teams response to not include Nodes
mattdholloway Aug 11, 2025
3acaaeb
update description to include clarification about teams you are a mem…
mattdholloway Aug 11, 2025
165d38c
fix typo in tool desc
mattdholloway Aug 11, 2025
de994ca
updated description to be more generic for accecss note
mattdholloway Aug 11, 2025
4652d60
amended error handling
mattdholloway Aug 11, 2025
ce8880f
Update pkg/github/context_tools.go
mattdholloway Aug 11, 2025
f92b4e0
add additional tool to get team members
mattdholloway Aug 11, 2025
4429b1f
Merge branch 'teams-tool' of https://github.com/github/github-mcp-ser…
mattdholloway Aug 11, 2025
15e5e10
update tool desc for get_team_members to include warning about auth
mattdholloway Aug 11, 2025
8c59077
added new scope info
mattdholloway Aug 11, 2025
c590779
Merge branch 'main' into teams-tool
mattdholloway Aug 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ To keep your GitHub PAT secure and reusable across different MCP hosts:
- **Minimum scopes**: Only grant necessary permissions
- `repo` - Repository operations
- `read:packages` - Docker image access
- `read:org` - Organization team access
- **Separate tokens**: Use different PATs for different projects/environments
- **Regular rotation**: Update tokens periodically
- **Never commit**: Keep tokens out of version control
Expand Down Expand Up @@ -421,6 +422,13 @@ The following sets of tools are available (all are on by default):
- **get_me** - Get my user profile
- No parameters required

- **get_team_members** - Get team members
- `org`: Organization login (owner) that contains the team. (string, required)
- `team_slug`: Team slug (string, required)

- **get_teams** - Get teams
- `user`: Username to get teams for. If not provided, uses the authenticated user. (string, optional)

</details>

<details>
Expand Down
25 changes: 25 additions & 0 deletions pkg/github/__toolsnaps__/get_team_members.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"annotations": {
"title": "Get team members",
"readOnlyHint": true
},
"description": "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials",
"inputSchema": {
"properties": {
"org": {
"description": "Organization login (owner) that contains the team.",
"type": "string"
},
"team_slug": {
"description": "Team slug",
"type": "string"
}
},
"required": [
"org",
"team_slug"
],
"type": "object"
},
"name": "get_team_members"
}
17 changes: 17 additions & 0 deletions pkg/github/__toolsnaps__/get_teams.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"annotations": {
"title": "Get teams",
"readOnlyHint": true
},
"description": "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials",
"inputSchema": {
"properties": {
"user": {
"description": "Username to get teams for. If not provided, uses the authenticated user.",
"type": "string"
}
},
"type": "object"
},
"name": "get_teams"
}
157 changes: 157 additions & 0 deletions pkg/github/context_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/github/github-mcp-server/pkg/translations"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/shurcooL/githubv4"
)

// UserDetails contains additional fields about a GitHub user not already
Expand Down Expand Up @@ -90,3 +91,159 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Too

return tool, handler
}

func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
type TeamInfo struct {
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
}

type OrganizationTeams struct {
Login string `json:"login"`
Teams []TeamInfo `json:"teams"`
}

tool := mcp.NewTool("get_teams",
mcp.WithDescription(t("TOOL_GET_TEAMS_DESCRIPTION", "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials")),
mcp.WithString("user",
mcp.Description(t("TOOL_GET_TEAMS_USER_DESCRIPTION", "Username to get teams for. If not provided, uses the authenticated user.")),
),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_GET_TEAMS_TITLE", "Get teams"),
ReadOnlyHint: ToBoolPtr(true),
}),
)

type args struct {
User *string `json:"user,omitempty"`
}
handler := mcp.NewTypedToolHandler(func(ctx context.Context, _ mcp.CallToolRequest, a args) (*mcp.CallToolResult, error) {
var username string
if a.User != nil && *a.User != "" {
username = *a.User
} else {
client, err := getClient(ctx)
if err != nil {
return mcp.NewToolResultErrorFromErr("failed to get GitHub client", err), nil
}

user, res, err := client.Users.Get(ctx, "")
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to get user",
res,
err,
), nil
}
username = user.GetLogin()
}

gqlClient, err := getGQLClient(ctx)
if err != nil {
return mcp.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil
}

var q struct {
User struct {
Organizations struct {
Nodes []struct {
Login githubv4.String
Teams struct {
Nodes []struct {
Name githubv4.String
Slug githubv4.String
Description githubv4.String
}
} `graphql:"teams(first: 100, userLogins: [$login])"`
}
} `graphql:"organizations(first: 100)"`
} `graphql:"user(login: $login)"`
}
vars := map[string]interface{}{
"login": githubv4.String(username),
}
if err := gqlClient.Query(ctx, &q, vars); err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find teams", err), nil
}

var organizations []OrganizationTeams
for _, org := range q.User.Organizations.Nodes {
orgTeams := OrganizationTeams{
Login: string(org.Login),
Teams: make([]TeamInfo, 0, len(org.Teams.Nodes)),
}

for _, team := range org.Teams.Nodes {
orgTeams.Teams = append(orgTeams.Teams, TeamInfo{
Name: string(team.Name),
Slug: string(team.Slug),
Description: string(team.Description),
})
}

organizations = append(organizations, orgTeams)
}

return MarshalledTextResult(organizations), nil
})

return tool, handler
}

func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
tool := mcp.NewTool("get_team_members",
mcp.WithDescription(t("TOOL_GET_TEAM_MEMBERS_DESCRIPTION", "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials")),
mcp.WithString("org",
mcp.Description(t("TOOL_GET_TEAM_MEMBERS_ORG_DESCRIPTION", "Organization login (owner) that contains the team.")),
mcp.Required(),
),
mcp.WithString("team_slug",
mcp.Description(t("TOOL_GET_TEAM_MEMBERS_TEAM_SLUG_DESCRIPTION", "Team slug")),
mcp.Required(),
),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_GET_TEAM_MEMBERS_TITLE", "Get team members"),
ReadOnlyHint: ToBoolPtr(true),
}),
)

type args struct {
Org string `json:"org"`
TeamSlug string `json:"team_slug"`
}
handler := mcp.NewTypedToolHandler(func(ctx context.Context, _ mcp.CallToolRequest, a args) (*mcp.CallToolResult, error) {
gqlClient, err := getGQLClient(ctx)
if err != nil {
return mcp.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil
}

var q struct {
Organization struct {
Team struct {
Members struct {
Nodes []struct {
Login githubv4.String
}
} `graphql:"members(first: 100)"`
} `graphql:"team(slug: $teamSlug)"`
} `graphql:"organization(login: $org)"`
}
vars := map[string]interface{}{
"org": githubv4.String(a.Org),
"teamSlug": githubv4.String(a.TeamSlug),
}
if err := gqlClient.Query(ctx, &q, vars); err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to get team members", err), nil
}

var members []string
for _, member := range q.Organization.Team.Members.Nodes {
members = append(members, string(member.Login))
}

return MarshalledTextResult(members), nil
})

return tool, handler
}
Loading
Loading