Files
github--github-mcp-server/pkg/github/context_tools.go
copilot-swe-agent[bot] 7381ad4f2d
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
Add update_user_profile MCP tool
Agent-Logs-Url: https://github.com/github/github-mcp-server/sessions/8180b3b5-6e5d-44bb-bff0-72b375d242ab

Co-authored-by: RossTarrant <14926097+RossTarrant@users.noreply.github.com>
2026-05-01 17:18:17 +00:00

444 lines
15 KiB
Go

package github
import (
"context"
"encoding/json"
"time"
ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
gogithub "github.com/google/go-github/v82/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
)
// GetMeUIResourceURI is the URI for the get_me tool's MCP App UI resource.
const GetMeUIResourceURI = "ui://github-mcp-server/get-me"
// UserDetails contains additional fields about a GitHub user not already
// present in MinimalUser. Used by get_me context tool but omitted from search_users.
type UserDetails struct {
Name string `json:"name,omitempty"`
Company string `json:"company,omitempty"`
Blog string `json:"blog,omitempty"`
Location string `json:"location,omitempty"`
Email string `json:"email,omitempty"`
Hireable bool `json:"hireable,omitempty"`
Bio string `json:"bio,omitempty"`
TwitterUsername string `json:"twitter_username,omitempty"`
PublicRepos int `json:"public_repos"`
PublicGists int `json:"public_gists"`
Followers int `json:"followers"`
Following int `json:"following"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PrivateGists int `json:"private_gists,omitempty"`
TotalPrivateRepos int64 `json:"total_private_repos,omitempty"`
OwnedPrivateRepos int64 `json:"owned_private_repos,omitempty"`
}
// GetMe creates a tool to get details of the authenticated user.
func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
return NewTool(
ToolsetMetadataContext,
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{
Title: t("TOOL_GET_ME_USER_TITLE", "Get my user profile"),
ReadOnlyHint: true,
},
// Use json.RawMessage to ensure "properties" is included even when empty.
// OpenAI strict mode requires the properties field to be present.
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
Meta: mcp.Meta{
"ui": map[string]any{
"resourceUri": GetMeUIResourceURI,
},
},
},
nil,
func(ctx context.Context, deps ToolDependencies, _ *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, 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(),
},
}
return MarshalledTextResult(minimalUser), nil, nil
},
)
}
type TeamInfo struct {
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
}
type OrganizationTeams struct {
Org string `json:"org"`
Teams []TeamInfo `json:"teams"`
}
func GetTeams(t translations.TranslationHelperFunc) inventory.ServerTool {
return NewTool(
ToolsetMetadataContext,
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{
Title: t("TOOL_GET_TEAMS_TITLE", "Get teams"),
ReadOnlyHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"user": {
Type: "string",
Description: t("TOOL_GET_TEAMS_USER_DESCRIPTION", "Username to get teams for. If not provided, uses the authenticated user."),
},
},
},
},
[]scopes.Scope{scopes.ReadOrg},
func(ctx context.Context, deps ToolDependencies, _ *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 := 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 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]any{
"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)),
}
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, nil
},
)
}
// UpdateUserProfile creates a tool to update the authenticated user's profile.
func UpdateUserProfile(t translations.TranslationHelperFunc) inventory.ServerTool {
return NewTool(
ToolsetMetadataContext,
mcp.Tool{
Name: "update_user_profile",
Description: t("TOOL_UPDATE_USER_PROFILE_DESCRIPTION", "Update the authenticated GitHub user's profile information. At least one field to update must be provided."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_UPDATE_USER_PROFILE_USER_TITLE", "Update my user profile"),
ReadOnlyHint: false,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"name": {
Type: "string",
Description: t("TOOL_UPDATE_USER_PROFILE_NAME_DESCRIPTION", "The new name of the user"),
},
"email": {
Type: "string",
Description: t("TOOL_UPDATE_USER_PROFILE_EMAIL_DESCRIPTION", "The publicly visible email address of the user"),
},
"blog": {
Type: "string",
Description: t("TOOL_UPDATE_USER_PROFILE_BLOG_DESCRIPTION", "The new blog URL of the user"),
},
"twitter_username": {
Type: "string",
Description: t("TOOL_UPDATE_USER_PROFILE_TWITTER_USERNAME_DESCRIPTION", "The new Twitter username of the user"),
},
"company": {
Type: "string",
Description: t("TOOL_UPDATE_USER_PROFILE_COMPANY_DESCRIPTION", "The new company of the user"),
},
"location": {
Type: "string",
Description: t("TOOL_UPDATE_USER_PROFILE_LOCATION_DESCRIPTION", "The new location of the user"),
},
"hireable": {
Type: "boolean",
Description: t("TOOL_UPDATE_USER_PROFILE_HIREABLE_DESCRIPTION", "The new hireable value of the user"),
},
"bio": {
Type: "string",
Description: t("TOOL_UPDATE_USER_PROFILE_BIO_DESCRIPTION", "The new short biography of the user"),
},
},
},
},
[]scopes.Scope{scopes.User},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
name, err := OptionalParam[string](args, "name")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
email, err := OptionalParam[string](args, "email")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
blog, err := OptionalParam[string](args, "blog")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
twitterUsername, err := OptionalParam[string](args, "twitter_username")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
company, err := OptionalParam[string](args, "company")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
location, err := OptionalParam[string](args, "location")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
hireable, err := OptionalParam[bool](args, "hireable")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
bio, err := OptionalParam[string](args, "bio")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
// Require at least one field to be set
_, hasHireable := args["hireable"]
if name == "" && email == "" && blog == "" && twitterUsername == "" &&
company == "" && location == "" && !hasHireable && bio == "" {
return utils.NewToolResultError("at least one field to update must be provided"), nil, nil
}
userReq := &gogithub.User{}
if name != "" {
userReq.Name = gogithub.Ptr(name)
}
if email != "" {
userReq.Email = gogithub.Ptr(email)
}
if blog != "" {
userReq.Blog = gogithub.Ptr(blog)
}
if twitterUsername != "" {
userReq.TwitterUsername = gogithub.Ptr(twitterUsername)
}
if company != "" {
userReq.Company = gogithub.Ptr(company)
}
if location != "" {
userReq.Location = gogithub.Ptr(location)
}
if hasHireable {
userReq.Hireable = gogithub.Ptr(hireable)
}
if bio != "" {
userReq.Bio = gogithub.Ptr(bio)
}
client, err := deps.GetClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}
user, res, err := client.Users.Edit(ctx, userReq)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to update user profile",
res,
err,
), nil, nil
}
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(),
},
}
return MarshalledTextResult(minimalUser), nil, nil
},
)
}
func GetTeamMembers(t translations.TranslationHelperFunc) inventory.ServerTool {
return NewTool(
ToolsetMetadataContext,
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{
Title: t("TOOL_GET_TEAM_MEMBERS_TITLE", "Get team members"),
ReadOnlyHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"org": {
Type: "string",
Description: t("TOOL_GET_TEAM_MEMBERS_ORG_DESCRIPTION", "Organization login (owner) that contains the team."),
},
"team_slug": {
Type: "string",
Description: t("TOOL_GET_TEAM_MEMBERS_TEAM_SLUG_DESCRIPTION", "Team slug"),
},
},
Required: []string{"org", "team_slug"},
},
},
[]scopes.Scope{scopes.ReadOrg},
func(ctx context.Context, deps ToolDependencies, _ *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
}
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]any{
"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))
}
return MarshalledTextResult(members), nil, nil
},
)
}