Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
305 changes: 159 additions & 146 deletions pkg/github/context_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/toolsets"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
"github.com/google/jsonschema-go/jsonschema"
Expand Down Expand Up @@ -36,8 +37,9 @@ type UserDetails struct {
}

// GetMe creates a tool to get details of the authenticated user.
func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
return mcp.Tool{
func GetMe(t translations.TranslationHelperFunc) toolsets.ServerTool {
return NewTool(
mcp.Tool{
Name: "get_me",
Description: t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request is about the user's own profile for GitHub. Or when information is missing to build other tool calls."),
Annotations: &mcp.ToolAnnotations{
Expand All @@ -48,50 +50,53 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Too
// OpenAI strict mode requires the properties field to be present.
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
},
mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) {
client, err := getClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
return func(ctx context.Context, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}

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

// Create minimal user representation instead of returning full user object
minimalUser := MinimalUser{
Login: user.GetLogin(),
ID: user.GetID(),
ProfileURL: user.GetHTMLURL(),
AvatarURL: user.GetAvatarURL(),
Details: &UserDetails{
Name: user.GetName(),
Company: user.GetCompany(),
Blog: user.GetBlog(),
Location: user.GetLocation(),
Email: user.GetEmail(),
Hireable: user.GetHireable(),
Bio: user.GetBio(),
TwitterUsername: user.GetTwitterUsername(),
PublicRepos: user.GetPublicRepos(),
PublicGists: user.GetPublicGists(),
Followers: user.GetFollowers(),
Following: user.GetFollowing(),
CreatedAt: user.GetCreatedAt().Time,
UpdatedAt: user.GetUpdatedAt().Time,
PrivateGists: user.GetPrivateGists(),
TotalPrivateRepos: user.GetTotalPrivateRepos(),
OwnedPrivateRepos: user.GetOwnedPrivateRepos(),
},
}
// Create minimal user representation instead of returning full user object
minimalUser := MinimalUser{
Login: user.GetLogin(),
ID: user.GetID(),
ProfileURL: user.GetHTMLURL(),
AvatarURL: user.GetAvatarURL(),
Details: &UserDetails{
Name: user.GetName(),
Company: user.GetCompany(),
Blog: user.GetBlog(),
Location: user.GetLocation(),
Email: user.GetEmail(),
Hireable: user.GetHireable(),
Bio: user.GetBio(),
TwitterUsername: user.GetTwitterUsername(),
PublicRepos: user.GetPublicRepos(),
PublicGists: user.GetPublicGists(),
Followers: user.GetFollowers(),
Following: user.GetFollowing(),
CreatedAt: user.GetCreatedAt().Time,
UpdatedAt: user.GetUpdatedAt().Time,
PrivateGists: user.GetPrivateGists(),
TotalPrivateRepos: user.GetTotalPrivateRepos(),
OwnedPrivateRepos: user.GetOwnedPrivateRepos(),
},
}

return MarshalledTextResult(minimalUser), nil, nil
})
return MarshalledTextResult(minimalUser), nil, nil
}
},
)
}

type TeamInfo struct {
Expand All @@ -105,8 +110,9 @@ type OrganizationTeams struct {
Teams []TeamInfo `json:"teams"`
}

func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
return mcp.Tool{
func GetTeams(t translations.TranslationHelperFunc) toolsets.ServerTool {
return NewTool(
mcp.Tool{
Name: "get_teams",
Description: t("TOOL_GET_TEAMS_DESCRIPTION", "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials"),
Annotations: &mcp.ToolAnnotations{
Expand All @@ -123,84 +129,88 @@ func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations
},
},
},
func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
user, err := OptionalParam[string](args, "user")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

var username string
if user != "" {
username = user
} else {
client, err := getClient(ctx)
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
user, err := OptionalParam[string](args, "user")
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
return utils.NewToolResultError(err.Error()), nil, nil
}

userResp, res, err := client.Users.Get(ctx, "")
var username string
if user != "" {
username = user
} else {
client, err := deps.GetClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}

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

gqlClient, err := deps.GetGQLClient(ctx)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to get user",
res,
err,
), nil, nil
return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil
}
username = userResp.GetLogin()
}

gqlClient, err := getGQLClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, 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, 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, nil
}
var organizations []OrganizationTeams
for _, org := range q.User.Organizations.Nodes {
orgTeams := OrganizationTeams{
Org: string(org.Login),
Teams: make([]TeamInfo, 0, len(org.Teams.Nodes)),
}

var organizations []OrganizationTeams
for _, org := range q.User.Organizations.Nodes {
orgTeams := OrganizationTeams{
Org: 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),
})
}

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)
}

organizations = append(organizations, orgTeams)
return MarshalledTextResult(organizations), nil, nil
}

return MarshalledTextResult(organizations), nil, nil
}
},
)
}

func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
return mcp.Tool{
func GetTeamMembers(t translations.TranslationHelperFunc) toolsets.ServerTool {
return NewTool(
mcp.Tool{
Name: "get_team_members",
Description: t("TOOL_GET_TEAM_MEMBERS_DESCRIPTION", "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials"),
Annotations: &mcp.ToolAnnotations{
Expand All @@ -222,46 +232,49 @@ func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelpe
Required: []string{"org", "team_slug"},
},
},
func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
org, err := RequiredParam[string](args, "org")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
org, err := RequiredParam[string](args, "org")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

teamSlug, err := RequiredParam[string](args, "team_slug")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
teamSlug, err := RequiredParam[string](args, "team_slug")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

gqlClient, err := getGQLClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil
}
gqlClient, err := deps.GetGQLClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, 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(org),
"teamSlug": githubv4.String(teamSlug),
}
if err := gqlClient.Query(ctx, &q, vars); err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to get team members", err), nil, 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(org),
"teamSlug": githubv4.String(teamSlug),
}
if err := gqlClient.Query(ctx, &q, vars); err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to get team members", err), nil, nil
}

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

return MarshalledTextResult(members), nil, nil
}
return MarshalledTextResult(members), nil, nil
}
},
)
}
Loading