Skip to content

Commit d69b616

Browse files
committed
Move some utilities to go SDK
1 parent b466148 commit d69b616

7 files changed

Lines changed: 436 additions & 355 deletions

File tree

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/go-openapi/jsonpointer v0.19.5 // indirect
2121
github.com/go-openapi/swag v0.21.1 // indirect
2222
github.com/google/go-github/v71 v71.0.0 // indirect
23+
github.com/google/jsonschema-go v0.3.0 // indirect
2324
github.com/gorilla/css v1.0.1 // indirect
2425
github.com/gorilla/mux v1.8.0 // indirect
2526
github.com/invopop/jsonschema v0.13.0 // indirect
@@ -40,6 +41,7 @@ require (
4041
github.com/google/go-querystring v1.1.0
4142
github.com/google/uuid v1.6.0 // indirect
4243
github.com/inconshreveable/mousetrap v1.1.0 // indirect
44+
github.com/modelcontextprotocol/go-sdk v1.1.0
4345
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
4446
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
4547
github.com/rogpeppe/go-internal v1.13.1 // indirect
@@ -52,7 +54,7 @@ require (
5254
github.com/spf13/pflag v1.0.10
5355
github.com/subosito/gotenv v1.6.0 // indirect
5456
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
55-
golang.org/x/oauth2 v0.29.0 // indirect
57+
golang.org/x/oauth2 v0.30.0 // indirect
5658
golang.org/x/sys v0.31.0 // indirect
5759
golang.org/x/text v0.28.0 // indirect
5860
golang.org/x/time v0.5.0 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ github.com/google/go-github/v77 v77.0.0 h1:9DsKKbZqil5y/4Z9mNpZDQnpli6PJbqipSuuN
3030
github.com/google/go-github/v77 v77.0.0/go.mod h1:c8VmGXRUmaZUqbctUcGEDWYnMrtzZfJhDSylEf1wfmA=
3131
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
3232
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
33+
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
34+
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
3335
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
3436
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
3537
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
@@ -63,6 +65,8 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX
6365
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
6466
github.com/migueleliasweb/go-github-mock v1.3.0 h1:2sVP9JEMB2ubQw1IKto3/fzF51oFC6eVWOOFDgQoq88=
6567
github.com/migueleliasweb/go-github-mock v1.3.0/go.mod h1:ipQhV8fTcj/G6m7BKzin08GaJ/3B5/SonRAkgrk0zCY=
68+
github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
69+
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
6670
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
6771
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
6872
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
@@ -112,6 +116,8 @@ golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
112116
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
113117
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
114118
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
119+
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
120+
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
115121
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
116122
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
117123
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=

pkg/errors/error.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/github/github-mcp-server/pkg/utils"
78
"github.com/google/go-github/v77/github"
8-
"github.com/mark3labs/mcp-go/mcp"
9+
"github.com/modelcontextprotocol/go-sdk/mcp"
910
)
1011

1112
type GitHubAPIError struct {
@@ -112,7 +113,7 @@ func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github
112113
if ctx != nil {
113114
_, _ = addGitHubAPIErrorToContext(ctx, apiErr) // Explicitly ignore error for graceful handling
114115
}
115-
return mcp.NewToolResultErrorFromErr(message, err)
116+
return utils.NewToolResultErrorFromErr(message, err)
116117
}
117118

118119
// NewGitHubGraphQLErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware
@@ -121,5 +122,5 @@ func NewGitHubGraphQLErrorResponse(ctx context.Context, message string, err erro
121122
if ctx != nil {
122123
_, _ = addGitHubGraphQLErrorToContext(ctx, graphQLErr) // Explicitly ignore error for graceful handling
123124
}
124-
return mcp.NewToolResultErrorFromErr(message, err)
125+
return utils.NewToolResultErrorFromErr(message, err)
125126
}

pkg/github/context_tools.go

Lines changed: 70 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import (
66

77
ghErrors "github.com/github/github-mcp-server/pkg/errors"
88
"github.com/github/github-mcp-server/pkg/translations"
9-
"github.com/mark3labs/mcp-go/mcp"
10-
"github.com/mark3labs/mcp-go/server"
9+
"github.com/github/github-mcp-server/pkg/utils"
10+
"github.com/google/jsonschema-go/jsonschema"
11+
"github.com/modelcontextprotocol/go-sdk/mcp"
1112
"github.com/shurcooL/githubv4"
1213
)
1314

@@ -34,20 +35,20 @@ type UserDetails struct {
3435
}
3536

3637
// GetMe creates a tool to get details of the authenticated user.
37-
func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
38-
tool := mcp.NewTool("get_me",
39-
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.")),
40-
mcp.WithToolAnnotation(mcp.ToolAnnotation{
38+
func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandler) {
39+
tool := mcp.Tool{
40+
Name: "get_me",
41+
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."),
42+
Annotations: &mcp.ToolAnnotations{
4143
Title: t("TOOL_GET_ME_USER_TITLE", "Get my user profile"),
42-
ReadOnlyHint: ToBoolPtr(true),
43-
}),
44-
)
44+
ReadOnlyHint: true,
45+
},
46+
}
4547

46-
type args struct{}
47-
handler := mcp.NewTypedToolHandler(func(ctx context.Context, _ mcp.CallToolRequest, _ args) (*mcp.CallToolResult, error) {
48+
handler := mcp.ToolHandler(func(ctx context.Context, _ *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
4849
client, err := getClient(ctx)
4950
if err != nil {
50-
return mcp.NewToolResultErrorFromErr("failed to get GitHub client", err), nil
51+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), err
5152
}
5253

5354
user, res, err := client.Users.Get(ctx, "")
@@ -56,7 +57,7 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Too
5657
"failed to get user",
5758
res,
5859
err,
59-
), nil
60+
), err
6061
}
6162

6263
// Create minimal user representation instead of returning full user object
@@ -103,21 +104,30 @@ type OrganizationTeams struct {
103104
Teams []TeamInfo `json:"teams"`
104105
}
105106

106-
func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
107-
return mcp.NewTool("get_teams",
108-
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")),
109-
mcp.WithString("user",
110-
mcp.Description(t("TOOL_GET_TEAMS_USER_DESCRIPTION", "Username to get teams for. If not provided, uses the authenticated user.")),
111-
),
112-
mcp.WithToolAnnotation(mcp.ToolAnnotation{
107+
func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
108+
// return mcp.NewTool("get_teams",
109+
// 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")),
110+
// mcp.WithString("user",
111+
// mcp.Description(t("TOOL_GET_TEAMS_USER_DESCRIPTION", "Username to get teams for. If not provided, uses the authenticated user.")),
112+
// ),
113+
// mcp.WithToolAnnotation(mcp.ToolAnnotation{
114+
// Title: t("TOOL_GET_TEAMS_TITLE", "Get teams"),
115+
// ReadOnlyHint: ToBoolPtr(true),
116+
// }),
117+
// ),
118+
return mcp.Tool{
119+
Name: "get_teams",
120+
Description: t("TOOL_GET_TEAMS_DESCRIPTION", "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials"),
121+
Annotations: &mcp.ToolAnnotations{
113122
Title: t("TOOL_GET_TEAMS_TITLE", "Get teams"),
114-
ReadOnlyHint: ToBoolPtr(true),
115-
}),
116-
),
117-
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
118-
user, err := OptionalParam[string](request, "user")
123+
ReadOnlyHint: true,
124+
},
125+
InputSchema: &jsonschema.Schema{},
126+
},
127+
func(ctx context.Context, request *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
128+
user, err := OptionalParam[string](args, "user")
119129
if err != nil {
120-
return mcp.NewToolResultError(err.Error()), nil
130+
return utils.NewToolResultError(err.Error()), nil, nil
121131
}
122132

123133
var username string
@@ -126,7 +136,7 @@ func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations
126136
} else {
127137
client, err := getClient(ctx)
128138
if err != nil {
129-
return mcp.NewToolResultErrorFromErr("failed to get GitHub client", err), nil
139+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
130140
}
131141

132142
userResp, res, err := client.Users.Get(ctx, "")
@@ -135,14 +145,14 @@ func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations
135145
"failed to get user",
136146
res,
137147
err,
138-
), nil
148+
), nil, nil
139149
}
140150
username = userResp.GetLogin()
141151
}
142152

143153
gqlClient, err := getGQLClient(ctx)
144154
if err != nil {
145-
return mcp.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil
155+
return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil
146156
}
147157

148158
var q struct {
@@ -165,7 +175,7 @@ func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations
165175
"login": githubv4.String(username),
166176
}
167177
if err := gqlClient.Query(ctx, &q, vars); err != nil {
168-
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find teams", err), nil
178+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find teams", err), nil, nil
169179
}
170180

171181
var organizations []OrganizationTeams
@@ -186,40 +196,46 @@ func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations
186196
organizations = append(organizations, orgTeams)
187197
}
188198

189-
return MarshalledTextResult(organizations), nil
199+
return MarshalledTextResult(organizations), nil, nil
190200
}
191201
}
192202

193-
func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
194-
return mcp.NewTool("get_team_members",
195-
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")),
196-
mcp.WithString("org",
197-
mcp.Description(t("TOOL_GET_TEAM_MEMBERS_ORG_DESCRIPTION", "Organization login (owner) that contains the team.")),
198-
mcp.Required(),
199-
),
200-
mcp.WithString("team_slug",
201-
mcp.Description(t("TOOL_GET_TEAM_MEMBERS_TEAM_SLUG_DESCRIPTION", "Team slug")),
202-
mcp.Required(),
203-
),
204-
mcp.WithToolAnnotation(mcp.ToolAnnotation{
203+
func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
204+
return mcp.Tool{
205+
Name: "get_team_members",
206+
Description: t("TOOL_GET_TEAM_MEMBERS_DESCRIPTION", "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials"),
207+
Annotations: &mcp.ToolAnnotations{
205208
Title: t("TOOL_GET_TEAM_MEMBERS_TITLE", "Get team members"),
206-
ReadOnlyHint: ToBoolPtr(true),
207-
}),
208-
),
209-
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
210-
org, err := RequiredParam[string](request, "org")
209+
ReadOnlyHint: true,
210+
},
211+
InputSchema: &jsonschema.Schema{
212+
Properties: map[string]*jsonschema.Schema{
213+
"org": &jsonschema.Schema{
214+
Type: "string",
215+
Description: t("TOOL_GET_TEAM_MEMBERS_ORG_DESCRIPTION", "Organization login (owner) that contains the team."),
216+
},
217+
"team_slug": &jsonschema.Schema{
218+
Type: "string",
219+
Description: t("TOOL_GET_TEAM_MEMBERS_TEAM_SLUG_DESCRIPTION", "Team slug"),
220+
},
221+
},
222+
Required: []string{"org", "team_slug"},
223+
},
224+
},
225+
func(ctx context.Context, request *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
226+
org, err := RequiredParam[string](args, "org")
211227
if err != nil {
212-
return mcp.NewToolResultError(err.Error()), nil
228+
return utils.NewToolResultError(err.Error()), nil, nil
213229
}
214230

215-
teamSlug, err := RequiredParam[string](request, "team_slug")
231+
teamSlug, err := RequiredParam[string](args, "team_slug")
216232
if err != nil {
217-
return mcp.NewToolResultError(err.Error()), nil
233+
return utils.NewToolResultError(err.Error()), nil, nil
218234
}
219235

220236
gqlClient, err := getGQLClient(ctx)
221237
if err != nil {
222-
return mcp.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil
238+
return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil
223239
}
224240

225241
var q struct {
@@ -238,14 +254,14 @@ func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelpe
238254
"teamSlug": githubv4.String(teamSlug),
239255
}
240256
if err := gqlClient.Query(ctx, &q, vars); err != nil {
241-
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to get team members", err), nil
257+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to get team members", err), nil, nil
242258
}
243259

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

249-
return MarshalledTextResult(members), nil
265+
return MarshalledTextResult(members), nil, nil
250266
}
251267
}

0 commit comments

Comments
 (0)