Skip to content

Commit 0a19bf4

Browse files
authored
Migrate Repository Resources to the Go SDK (#1457)
* Migrate repo resources to Go SDK * Enable resources for repos * Properly handle encoding and closing of the buffer * Remove outdated comment * Switch to StdEncoding, as it was originally * fix casing for linter * Update licenses
1 parent 1b769a5 commit 0a19bf4

File tree

15 files changed

+250
-615
lines changed

15 files changed

+250
-615
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ require (
5353
github.com/spf13/cast v1.10.0 // indirect
5454
github.com/spf13/pflag v1.0.10
5555
github.com/subosito/gotenv v1.6.0 // indirect
56-
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
56+
github.com/yosida95/uritemplate/v3 v3.0.2
5757
golang.org/x/oauth2 v0.30.0 // indirect
5858
golang.org/x/sys v0.31.0 // indirect
5959
golang.org/x/text v0.28.0 // indirect

pkg/github/repository_resource.go

Lines changed: 101 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package github
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/base64"
67
"errors"
@@ -15,107 +16,120 @@ import (
1516
"github.com/github/github-mcp-server/pkg/raw"
1617
"github.com/github/github-mcp-server/pkg/translations"
1718
"github.com/google/go-github/v79/github"
18-
"github.com/mark3labs/mcp-go/mcp"
19-
"github.com/mark3labs/mcp-go/server"
19+
"github.com/modelcontextprotocol/go-sdk/mcp"
20+
"github.com/yosida95/uritemplate/v3"
21+
)
22+
23+
var (
24+
repositoryResourceContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/contents{/path*}")
25+
repositoryResourceBranchContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}")
26+
repositoryResourceCommitContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/sha/{sha}/contents{/path*}")
27+
repositoryResourceTagContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}")
28+
repositoryResourcePrContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}")
2029
)
2130

2231
// GetRepositoryResourceContent defines the resource template and handler for getting repository content.
23-
func GetRepositoryResourceContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
24-
return mcp.NewResourceTemplate(
25-
"repo://{owner}/{repo}/contents{/path*}", // Resource template
26-
t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"),
27-
),
28-
RepositoryResourceContentsHandler(getClient, getRawClient)
32+
func GetRepositoryResourceContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) {
33+
return mcp.ResourceTemplate{
34+
Name: "repository_content",
35+
URITemplate: repositoryResourceContentURITemplate.Raw(), // Resource template
36+
Description: t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"),
37+
},
38+
RepositoryResourceContentsHandler(getClient, getRawClient, repositoryResourceContentURITemplate)
2939
}
3040

3141
// GetRepositoryResourceBranchContent defines the resource template and handler for getting repository content for a branch.
32-
func GetRepositoryResourceBranchContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
33-
return mcp.NewResourceTemplate(
34-
"repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", // Resource template
35-
t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"),
36-
),
37-
RepositoryResourceContentsHandler(getClient, getRawClient)
42+
func GetRepositoryResourceBranchContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) {
43+
return mcp.ResourceTemplate{
44+
Name: "repository_content_branch",
45+
URITemplate: repositoryResourceBranchContentURITemplate.Raw(), // Resource template
46+
Description: t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"),
47+
},
48+
RepositoryResourceContentsHandler(getClient, getRawClient, repositoryResourceBranchContentURITemplate)
3849
}
3950

4051
// GetRepositoryResourceCommitContent defines the resource template and handler for getting repository content for a commit.
41-
func GetRepositoryResourceCommitContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
42-
return mcp.NewResourceTemplate(
43-
"repo://{owner}/{repo}/sha/{sha}/contents{/path*}", // Resource template
44-
t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"),
45-
),
46-
RepositoryResourceContentsHandler(getClient, getRawClient)
52+
func GetRepositoryResourceCommitContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) {
53+
return mcp.ResourceTemplate{
54+
Name: "repository_content_commit",
55+
URITemplate: repositoryResourceCommitContentURITemplate.Raw(), // Resource template
56+
Description: t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"),
57+
},
58+
RepositoryResourceContentsHandler(getClient, getRawClient, repositoryResourceCommitContentURITemplate)
4759
}
4860

4961
// GetRepositoryResourceTagContent defines the resource template and handler for getting repository content for a tag.
50-
func GetRepositoryResourceTagContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
51-
return mcp.NewResourceTemplate(
52-
"repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", // Resource template
53-
t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"),
54-
),
55-
RepositoryResourceContentsHandler(getClient, getRawClient)
62+
func GetRepositoryResourceTagContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) {
63+
return mcp.ResourceTemplate{
64+
Name: "repository_content_tag",
65+
URITemplate: repositoryResourceTagContentURITemplate.Raw(), // Resource template
66+
Description: t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"),
67+
},
68+
RepositoryResourceContentsHandler(getClient, getRawClient, repositoryResourceTagContentURITemplate)
5669
}
5770

5871
// GetRepositoryResourcePrContent defines the resource template and handler for getting repository content for a pull request.
59-
func GetRepositoryResourcePrContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
60-
return mcp.NewResourceTemplate(
61-
"repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}", // Resource template
62-
t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"),
63-
),
64-
RepositoryResourceContentsHandler(getClient, getRawClient)
72+
func GetRepositoryResourcePrContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) {
73+
return mcp.ResourceTemplate{
74+
Name: "repository_content_pr",
75+
URITemplate: repositoryResourcePrContentURITemplate.Raw(), // Resource template
76+
Description: t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"),
77+
},
78+
RepositoryResourceContentsHandler(getClient, getRawClient, repositoryResourcePrContentURITemplate)
6579
}
6680

6781
// RepositoryResourceContentsHandler returns a handler function for repository content requests.
68-
func RepositoryResourceContentsHandler(getClient GetClientFn, getRawClient raw.GetRawClientFn) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
69-
return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
70-
// the matcher will give []string with one element
71-
// https://github.com/mark3labs/mcp-go/pull/54
72-
o, ok := request.Params.Arguments["owner"].([]string)
73-
if !ok || len(o) == 0 {
82+
func RepositoryResourceContentsHandler(getClient GetClientFn, getRawClient raw.GetRawClientFn, resourceURITemplate *uritemplate.Template) mcp.ResourceHandler {
83+
return func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
84+
// Match the URI to extract parameters
85+
uriValues := resourceURITemplate.Match(request.Params.URI)
86+
if uriValues == nil {
87+
return nil, fmt.Errorf("failed to match URI: %s", request.Params.URI)
88+
}
89+
90+
// Extract required vars
91+
owner := uriValues.Get("owner").String()
92+
repo := uriValues.Get("repo").String()
93+
94+
if owner == "" {
7495
return nil, errors.New("owner is required")
7596
}
76-
owner := o[0]
7797

78-
r, ok := request.Params.Arguments["repo"].([]string)
79-
if !ok || len(r) == 0 {
98+
if repo == "" {
8099
return nil, errors.New("repo is required")
81100
}
82-
repo := r[0]
83101

84-
// path should be a joined list of the path parts
85-
path := ""
86-
p, ok := request.Params.Arguments["path"].([]string)
87-
if ok {
88-
path = strings.Join(p, "/")
89-
}
102+
path := uriValues.Get("path").String()
90103

91104
opts := &github.RepositoryContentGetOptions{}
92105
rawOpts := &raw.ContentOpts{}
93106

94-
sha, ok := request.Params.Arguments["sha"].([]string)
95-
if ok && len(sha) > 0 {
96-
opts.Ref = sha[0]
97-
rawOpts.SHA = sha[0]
107+
sha := uriValues.Get("sha").String()
108+
if sha != "" {
109+
opts.Ref = sha
110+
rawOpts.SHA = sha
98111
}
99112

100-
branch, ok := request.Params.Arguments["branch"].([]string)
101-
if ok && len(branch) > 0 {
102-
opts.Ref = "refs/heads/" + branch[0]
103-
rawOpts.Ref = "refs/heads/" + branch[0]
113+
branch := uriValues.Get("branch").String()
114+
if branch != "" {
115+
opts.Ref = "refs/heads/" + branch
116+
rawOpts.Ref = "refs/heads/" + branch
104117
}
105118

106-
tag, ok := request.Params.Arguments["tag"].([]string)
107-
if ok && len(tag) > 0 {
108-
opts.Ref = "refs/tags/" + tag[0]
109-
rawOpts.Ref = "refs/tags/" + tag[0]
119+
tag := uriValues.Get("tag").String()
120+
if tag != "" {
121+
opts.Ref = "refs/tags/" + tag
122+
rawOpts.Ref = "refs/tags/" + tag
110123
}
111-
prNumber, ok := request.Params.Arguments["prNumber"].([]string)
112-
if ok && len(prNumber) > 0 {
124+
125+
prNumber := uriValues.Get("prNumber").String()
126+
if prNumber != "" {
113127
// fetch the PR from the API to get the latest commit and use SHA
114128
githubClient, err := getClient(ctx)
115129
if err != nil {
116130
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
117131
}
118-
prNum, err := strconv.Atoi(prNumber[0])
132+
prNum, err := strconv.Atoi(prNumber)
119133
if err != nil {
120134
return nil, fmt.Errorf("invalid pull request number: %w", err)
121135
}
@@ -161,19 +175,33 @@ func RepositoryResourceContentsHandler(getClient GetClientFn, getRawClient raw.G
161175

162176
switch {
163177
case strings.HasPrefix(mimeType, "text"), strings.HasPrefix(mimeType, "application"):
164-
return []mcp.ResourceContents{
165-
mcp.TextResourceContents{
166-
URI: request.Params.URI,
167-
MIMEType: mimeType,
168-
Text: string(content),
178+
return &mcp.ReadResourceResult{
179+
Contents: []*mcp.ResourceContents{
180+
{
181+
URI: request.Params.URI,
182+
MIMEType: mimeType,
183+
Text: string(content),
184+
},
169185
},
170186
}, nil
171187
default:
172-
return []mcp.ResourceContents{
173-
mcp.BlobResourceContents{
174-
URI: request.Params.URI,
175-
MIMEType: mimeType,
176-
Blob: base64.StdEncoding.EncodeToString(content),
188+
var buf bytes.Buffer
189+
base64Encoder := base64.NewEncoder(base64.StdEncoding, &buf)
190+
_, err := base64Encoder.Write(content)
191+
if err != nil {
192+
return nil, fmt.Errorf("failed to base64 encode content: %w", err)
193+
}
194+
if err := base64Encoder.Close(); err != nil {
195+
return nil, fmt.Errorf("failed to close base64 encoder: %w", err)
196+
}
197+
198+
return &mcp.ReadResourceResult{
199+
Contents: []*mcp.ResourceContents{
200+
{
201+
URI: request.Params.URI,
202+
MIMEType: mimeType,
203+
Blob: buf.Bytes(),
204+
},
177205
},
178206
}, nil
179207
}

0 commit comments

Comments
 (0)