Skip to content

Commit b863da9

Browse files
Migrate context_tools to new ServerTool pattern
Convert GetMe, GetTeams, and GetTeamMembers to use the new typed dependency injection pattern: - Functions now take only translations helper, return toolsets.ServerTool - Handler is generated lazily via deps.GetClient/deps.GetGQLClient - Tests updated to use serverTool.Handler(deps) pattern - Fixed error return pattern to return nil for Go error (via result.IsError) Co-authored-by: Adam Holt <omgitsads@users.noreply.github.com>
1 parent b29546e commit b863da9

File tree

3 files changed

+204
-168
lines changed

3 files changed

+204
-168
lines changed

pkg/github/context_tools.go

Lines changed: 159 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
ghErrors "github.com/github/github-mcp-server/pkg/errors"
9+
"github.com/github/github-mcp-server/pkg/toolsets"
910
"github.com/github/github-mcp-server/pkg/translations"
1011
"github.com/github/github-mcp-server/pkg/utils"
1112
"github.com/google/jsonschema-go/jsonschema"
@@ -36,8 +37,9 @@ type UserDetails struct {
3637
}
3738

3839
// GetMe creates a tool to get details of the authenticated user.
39-
func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
40-
return mcp.Tool{
40+
func GetMe(t translations.TranslationHelperFunc) toolsets.ServerTool {
41+
return NewTool(
42+
mcp.Tool{
4143
Name: "get_me",
4244
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."),
4345
Annotations: &mcp.ToolAnnotations{
@@ -48,50 +50,53 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Too
4850
// OpenAI strict mode requires the properties field to be present.
4951
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
5052
},
51-
mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) {
52-
client, err := getClient(ctx)
53-
if err != nil {
54-
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
55-
}
53+
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
54+
return func(ctx context.Context, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) {
55+
client, err := deps.GetClient(ctx)
56+
if err != nil {
57+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
58+
}
5659

57-
user, res, err := client.Users.Get(ctx, "")
58-
if err != nil {
59-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
60-
"failed to get user",
61-
res,
62-
err,
63-
), nil, err
64-
}
60+
user, res, err := client.Users.Get(ctx, "")
61+
if err != nil {
62+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
63+
"failed to get user",
64+
res,
65+
err,
66+
), nil, nil
67+
}
6568

66-
// Create minimal user representation instead of returning full user object
67-
minimalUser := MinimalUser{
68-
Login: user.GetLogin(),
69-
ID: user.GetID(),
70-
ProfileURL: user.GetHTMLURL(),
71-
AvatarURL: user.GetAvatarURL(),
72-
Details: &UserDetails{
73-
Name: user.GetName(),
74-
Company: user.GetCompany(),
75-
Blog: user.GetBlog(),
76-
Location: user.GetLocation(),
77-
Email: user.GetEmail(),
78-
Hireable: user.GetHireable(),
79-
Bio: user.GetBio(),
80-
TwitterUsername: user.GetTwitterUsername(),
81-
PublicRepos: user.GetPublicRepos(),
82-
PublicGists: user.GetPublicGists(),
83-
Followers: user.GetFollowers(),
84-
Following: user.GetFollowing(),
85-
CreatedAt: user.GetCreatedAt().Time,
86-
UpdatedAt: user.GetUpdatedAt().Time,
87-
PrivateGists: user.GetPrivateGists(),
88-
TotalPrivateRepos: user.GetTotalPrivateRepos(),
89-
OwnedPrivateRepos: user.GetOwnedPrivateRepos(),
90-
},
91-
}
69+
// Create minimal user representation instead of returning full user object
70+
minimalUser := MinimalUser{
71+
Login: user.GetLogin(),
72+
ID: user.GetID(),
73+
ProfileURL: user.GetHTMLURL(),
74+
AvatarURL: user.GetAvatarURL(),
75+
Details: &UserDetails{
76+
Name: user.GetName(),
77+
Company: user.GetCompany(),
78+
Blog: user.GetBlog(),
79+
Location: user.GetLocation(),
80+
Email: user.GetEmail(),
81+
Hireable: user.GetHireable(),
82+
Bio: user.GetBio(),
83+
TwitterUsername: user.GetTwitterUsername(),
84+
PublicRepos: user.GetPublicRepos(),
85+
PublicGists: user.GetPublicGists(),
86+
Followers: user.GetFollowers(),
87+
Following: user.GetFollowing(),
88+
CreatedAt: user.GetCreatedAt().Time,
89+
UpdatedAt: user.GetUpdatedAt().Time,
90+
PrivateGists: user.GetPrivateGists(),
91+
TotalPrivateRepos: user.GetTotalPrivateRepos(),
92+
OwnedPrivateRepos: user.GetOwnedPrivateRepos(),
93+
},
94+
}
9295

93-
return MarshalledTextResult(minimalUser), nil, nil
94-
})
96+
return MarshalledTextResult(minimalUser), nil, nil
97+
}
98+
},
99+
)
95100
}
96101

97102
type TeamInfo struct {
@@ -105,8 +110,9 @@ type OrganizationTeams struct {
105110
Teams []TeamInfo `json:"teams"`
106111
}
107112

108-
func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
109-
return mcp.Tool{
113+
func GetTeams(t translations.TranslationHelperFunc) toolsets.ServerTool {
114+
return NewTool(
115+
mcp.Tool{
110116
Name: "get_teams",
111117
Description: t("TOOL_GET_TEAMS_DESCRIPTION", "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials"),
112118
Annotations: &mcp.ToolAnnotations{
@@ -123,84 +129,88 @@ func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations
123129
},
124130
},
125131
},
126-
func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
127-
user, err := OptionalParam[string](args, "user")
128-
if err != nil {
129-
return utils.NewToolResultError(err.Error()), nil, nil
130-
}
131-
132-
var username string
133-
if user != "" {
134-
username = user
135-
} else {
136-
client, err := getClient(ctx)
132+
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
133+
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
134+
user, err := OptionalParam[string](args, "user")
137135
if err != nil {
138-
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
136+
return utils.NewToolResultError(err.Error()), nil, nil
139137
}
140138

141-
userResp, res, err := client.Users.Get(ctx, "")
139+
var username string
140+
if user != "" {
141+
username = user
142+
} else {
143+
client, err := deps.GetClient(ctx)
144+
if err != nil {
145+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
146+
}
147+
148+
userResp, res, err := client.Users.Get(ctx, "")
149+
if err != nil {
150+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
151+
"failed to get user",
152+
res,
153+
err,
154+
), nil, nil
155+
}
156+
username = userResp.GetLogin()
157+
}
158+
159+
gqlClient, err := deps.GetGQLClient(ctx)
142160
if err != nil {
143-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
144-
"failed to get user",
145-
res,
146-
err,
147-
), nil, nil
161+
return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil
148162
}
149-
username = userResp.GetLogin()
150-
}
151163

152-
gqlClient, err := getGQLClient(ctx)
153-
if err != nil {
154-
return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil
155-
}
164+
var q struct {
165+
User struct {
166+
Organizations struct {
167+
Nodes []struct {
168+
Login githubv4.String
169+
Teams struct {
170+
Nodes []struct {
171+
Name githubv4.String
172+
Slug githubv4.String
173+
Description githubv4.String
174+
}
175+
} `graphql:"teams(first: 100, userLogins: [$login])"`
176+
}
177+
} `graphql:"organizations(first: 100)"`
178+
} `graphql:"user(login: $login)"`
179+
}
180+
vars := map[string]interface{}{
181+
"login": githubv4.String(username),
182+
}
183+
if err := gqlClient.Query(ctx, &q, vars); err != nil {
184+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find teams", err), nil, nil
185+
}
156186

157-
var q struct {
158-
User struct {
159-
Organizations struct {
160-
Nodes []struct {
161-
Login githubv4.String
162-
Teams struct {
163-
Nodes []struct {
164-
Name githubv4.String
165-
Slug githubv4.String
166-
Description githubv4.String
167-
}
168-
} `graphql:"teams(first: 100, userLogins: [$login])"`
169-
}
170-
} `graphql:"organizations(first: 100)"`
171-
} `graphql:"user(login: $login)"`
172-
}
173-
vars := map[string]interface{}{
174-
"login": githubv4.String(username),
175-
}
176-
if err := gqlClient.Query(ctx, &q, vars); err != nil {
177-
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find teams", err), nil, nil
178-
}
187+
var organizations []OrganizationTeams
188+
for _, org := range q.User.Organizations.Nodes {
189+
orgTeams := OrganizationTeams{
190+
Org: string(org.Login),
191+
Teams: make([]TeamInfo, 0, len(org.Teams.Nodes)),
192+
}
179193

180-
var organizations []OrganizationTeams
181-
for _, org := range q.User.Organizations.Nodes {
182-
orgTeams := OrganizationTeams{
183-
Org: string(org.Login),
184-
Teams: make([]TeamInfo, 0, len(org.Teams.Nodes)),
185-
}
194+
for _, team := range org.Teams.Nodes {
195+
orgTeams.Teams = append(orgTeams.Teams, TeamInfo{
196+
Name: string(team.Name),
197+
Slug: string(team.Slug),
198+
Description: string(team.Description),
199+
})
200+
}
186201

187-
for _, team := range org.Teams.Nodes {
188-
orgTeams.Teams = append(orgTeams.Teams, TeamInfo{
189-
Name: string(team.Name),
190-
Slug: string(team.Slug),
191-
Description: string(team.Description),
192-
})
202+
organizations = append(organizations, orgTeams)
193203
}
194204

195-
organizations = append(organizations, orgTeams)
205+
return MarshalledTextResult(organizations), nil, nil
196206
}
197-
198-
return MarshalledTextResult(organizations), nil, nil
199-
}
207+
},
208+
)
200209
}
201210

202-
func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
203-
return mcp.Tool{
211+
func GetTeamMembers(t translations.TranslationHelperFunc) toolsets.ServerTool {
212+
return NewTool(
213+
mcp.Tool{
204214
Name: "get_team_members",
205215
Description: t("TOOL_GET_TEAM_MEMBERS_DESCRIPTION", "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials"),
206216
Annotations: &mcp.ToolAnnotations{
@@ -222,46 +232,49 @@ func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelpe
222232
Required: []string{"org", "team_slug"},
223233
},
224234
},
225-
func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
226-
org, err := RequiredParam[string](args, "org")
227-
if err != nil {
228-
return utils.NewToolResultError(err.Error()), nil, nil
229-
}
235+
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
236+
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
237+
org, err := RequiredParam[string](args, "org")
238+
if err != nil {
239+
return utils.NewToolResultError(err.Error()), nil, nil
240+
}
230241

231-
teamSlug, err := RequiredParam[string](args, "team_slug")
232-
if err != nil {
233-
return utils.NewToolResultError(err.Error()), nil, nil
234-
}
242+
teamSlug, err := RequiredParam[string](args, "team_slug")
243+
if err != nil {
244+
return utils.NewToolResultError(err.Error()), nil, nil
245+
}
235246

236-
gqlClient, err := getGQLClient(ctx)
237-
if err != nil {
238-
return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil
239-
}
247+
gqlClient, err := deps.GetGQLClient(ctx)
248+
if err != nil {
249+
return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil
250+
}
240251

241-
var q struct {
242-
Organization struct {
243-
Team struct {
244-
Members struct {
245-
Nodes []struct {
246-
Login githubv4.String
247-
}
248-
} `graphql:"members(first: 100)"`
249-
} `graphql:"team(slug: $teamSlug)"`
250-
} `graphql:"organization(login: $org)"`
251-
}
252-
vars := map[string]interface{}{
253-
"org": githubv4.String(org),
254-
"teamSlug": githubv4.String(teamSlug),
255-
}
256-
if err := gqlClient.Query(ctx, &q, vars); err != nil {
257-
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to get team members", err), nil, nil
258-
}
252+
var q struct {
253+
Organization struct {
254+
Team struct {
255+
Members struct {
256+
Nodes []struct {
257+
Login githubv4.String
258+
}
259+
} `graphql:"members(first: 100)"`
260+
} `graphql:"team(slug: $teamSlug)"`
261+
} `graphql:"organization(login: $org)"`
262+
}
263+
vars := map[string]interface{}{
264+
"org": githubv4.String(org),
265+
"teamSlug": githubv4.String(teamSlug),
266+
}
267+
if err := gqlClient.Query(ctx, &q, vars); err != nil {
268+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to get team members", err), nil, nil
269+
}
259270

260-
var members []string
261-
for _, member := range q.Organization.Team.Members.Nodes {
262-
members = append(members, string(member.Login))
263-
}
271+
var members []string
272+
for _, member := range q.Organization.Team.Members.Nodes {
273+
members = append(members, string(member.Login))
274+
}
264275

265-
return MarshalledTextResult(members), nil, nil
266-
}
276+
return MarshalledTextResult(members), nil, nil
277+
}
278+
},
279+
)
267280
}

0 commit comments

Comments
 (0)