-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Expand file tree
/
Copy pathcontext_tools.go
More file actions
193 lines (172 loc) · 6.3 KB
/
context_tools.go
File metadata and controls
193 lines (172 loc) · 6.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package github
import (
"context"
"fmt"
"time"
ghErrors "github.com/github/github-mcp-server/pkg/errors"
"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
// 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(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
tool := mcp.NewTool("get_me",
mcp.WithDescription(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.")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_GET_ME_USER_TITLE", "Get my user profile"),
ReadOnlyHint: ToBoolPtr(true),
}),
)
type args struct{}
handler := mcp.NewTypedToolHandler(func(ctx context.Context, _ mcp.CallToolRequest, _ args) (*mcp.CallToolResult, error) {
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
}
// 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
})
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.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", 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 mcp.NewToolResultError(err.Error()), 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
}