Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b2503f3

Browse files
mntltySamMorrowDrumsCopilot
authored
chore: repository resource tests (github#69)
* refactor to make testing easier * not needed in handler func * small cleanup * create repository_resource_test * remove chatty comments * comment cleanup, function rename and some more tests * fix test for ubuntu runner * remove it for now * make required args explicit instead of panic * more tests and cleanup * chore: use raw repo resources (github#70) * use raw repo URIs for resources * fetch repository content from raw urls * ensure no error in test write * Update pkg/github/repository_resource.go Co-authored-by: Copilot <[email protected]> * use appropriate file name for text file test --------- Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Sam Morrow <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent b3a3d15 commit b2503f3

File tree

3 files changed

+437
-77
lines changed

3 files changed

+437
-77
lines changed

pkg/github/repository_resource.go

Lines changed: 149 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ package github
33
import (
44
"context"
55
"encoding/base64"
6+
"errors"
7+
"fmt"
8+
"io"
69
"mime"
10+
"net/http"
711
"path/filepath"
812
"strings"
913

@@ -13,110 +17,185 @@ import (
1317
"github.com/mark3labs/mcp-go/server"
1418
)
1519

16-
// getRepositoryContent defines the resource template and handler for the Repository Content API.
17-
func getRepositoryContent(client *github.Client, t translations.TranslationHelperFunc) (mainTemplate mcp.ResourceTemplate, reftemplate mcp.ResourceTemplate, shaTemplate mcp.ResourceTemplate, tagTemplate mcp.ResourceTemplate, prTemplate mcp.ResourceTemplate, handler server.ResourceTemplateHandlerFunc) {
18-
20+
// getRepositoryResourceContent defines the resource template and handler for getting repository content.
21+
func getRepositoryResourceContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
1922
return mcp.NewResourceTemplate(
2023
"repo://{owner}/{repo}/contents{/path*}", // Resource template
2124
t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"),
22-
), mcp.NewResourceTemplate(
25+
),
26+
repositoryResourceContentsHandler(client)
27+
}
28+
29+
// getRepositoryContent defines the resource template and handler for getting repository content for a branch.
30+
func getRepositoryResourceBranchContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
31+
return mcp.NewResourceTemplate(
2332
"repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", // Resource template
2433
t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"),
25-
), mcp.NewResourceTemplate(
34+
),
35+
repositoryResourceContentsHandler(client)
36+
}
37+
38+
// getRepositoryResourceCommitContent defines the resource template and handler for getting repository content for a commit.
39+
func getRepositoryResourceCommitContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
40+
return mcp.NewResourceTemplate(
2641
"repo://{owner}/{repo}/sha/{sha}/contents{/path*}", // Resource template
2742
t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"),
28-
), mcp.NewResourceTemplate(
43+
),
44+
repositoryResourceContentsHandler(client)
45+
}
46+
47+
// getRepositoryResourceTagContent defines the resource template and handler for getting repository content for a tag.
48+
func getRepositoryResourceTagContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
49+
return mcp.NewResourceTemplate(
2950
"repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", // Resource template
3051
t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"),
31-
), mcp.NewResourceTemplate(
52+
),
53+
repositoryResourceContentsHandler(client)
54+
}
55+
56+
// getRepositoryResourcePrContent defines the resource template and handler for getting repository content for a pull request.
57+
func getRepositoryResourcePrContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
58+
return mcp.NewResourceTemplate(
3259
"repo://{owner}/{repo}/refs/pull/{pr_number}/head/contents{/path*}", // Resource template
3360
t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"),
34-
), func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
35-
// Extract parameters from request.Params.URI
61+
),
62+
repositoryResourceContentsHandler(client)
63+
}
3664

37-
owner := request.Params.Arguments["owner"].([]string)[0]
38-
repo := request.Params.Arguments["repo"].([]string)[0]
39-
// path should be a joined list of the path parts
40-
path := strings.Join(request.Params.Arguments["path"].([]string), "/")
65+
func repositoryResourceContentsHandler(client *github.Client) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
66+
return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
67+
// the matcher will give []string with one elemenent
68+
// https://github.com/mark3labs/mcp-go/pull/54
69+
o, ok := request.Params.Arguments["owner"].([]string)
70+
if !ok || len(o) == 0 {
71+
return nil, errors.New("owner is required")
72+
}
73+
owner := o[0]
4174

42-
opts := &github.RepositoryContentGetOptions{}
75+
r, ok := request.Params.Arguments["repo"].([]string)
76+
if !ok || len(r) == 0 {
77+
return nil, errors.New("repo is required")
78+
}
79+
repo := r[0]
4380

44-
sha, ok := request.Params.Arguments["sha"].([]string)
45-
if ok {
46-
opts.Ref = sha[0]
47-
}
81+
// path should be a joined list of the path parts
82+
path := ""
83+
p, ok := request.Params.Arguments["path"].([]string)
84+
if ok {
85+
path = strings.Join(p, "/")
86+
}
4887

49-
branch, ok := request.Params.Arguments["branch"].([]string)
50-
if ok {
51-
opts.Ref = "refs/heads/" + branch[0]
52-
}
88+
opts := &github.RepositoryContentGetOptions{}
5389

54-
tag, ok := request.Params.Arguments["tag"].([]string)
55-
if ok {
56-
opts.Ref = "refs/tags/" + tag[0]
57-
}
58-
prNumber, ok := request.Params.Arguments["pr_number"].([]string)
59-
if ok {
60-
opts.Ref = "refs/pull/" + prNumber[0] + "/head"
61-
}
90+
sha, ok := request.Params.Arguments["sha"].([]string)
91+
if ok && len(sha) > 0 {
92+
opts.Ref = sha[0]
93+
}
6294

63-
// Use the GitHub client to fetch repository content
64-
fileContent, directoryContent, _, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
65-
if err != nil {
66-
return nil, err
67-
}
95+
branch, ok := request.Params.Arguments["branch"].([]string)
96+
if ok && len(branch) > 0 {
97+
opts.Ref = "refs/heads/" + branch[0]
98+
}
99+
100+
tag, ok := request.Params.Arguments["tag"].([]string)
101+
if ok && len(tag) > 0 {
102+
opts.Ref = "refs/tags/" + tag[0]
103+
}
104+
prNumber, ok := request.Params.Arguments["pr_number"].([]string)
105+
if ok && len(prNumber) > 0 {
106+
opts.Ref = "refs/pull/" + prNumber[0] + "/head"
107+
}
68108

69-
if directoryContent != nil {
70-
// Process the directory content and return it as resource contents
71-
var resources []mcp.ResourceContents
72-
for _, entry := range directoryContent {
73-
mimeType := "text/directory"
74-
if entry.GetType() == "file" {
75-
mimeType = mime.TypeByExtension(filepath.Ext(entry.GetName()))
109+
fileContent, directoryContent, _, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
110+
if err != nil {
111+
return nil, err
112+
}
113+
114+
if directoryContent != nil {
115+
var resources []mcp.ResourceContents
116+
for _, entry := range directoryContent {
117+
mimeType := "text/directory"
118+
if entry.GetType() == "file" {
119+
// this is system dependent, and a best guess
120+
ext := filepath.Ext(entry.GetName())
121+
mimeType = mime.TypeByExtension(ext)
122+
if ext == ".md" {
123+
mimeType = "text/markdown"
76124
}
77-
resources = append(resources, mcp.TextResourceContents{
78-
URI: entry.GetHTMLURL(),
79-
MIMEType: mimeType,
80-
Text: entry.GetName(),
81-
})
125+
}
126+
resources = append(resources, mcp.TextResourceContents{
127+
URI: entry.GetHTMLURL(),
128+
MIMEType: mimeType,
129+
Text: entry.GetName(),
130+
})
131+
132+
}
133+
return resources, nil
82134

135+
}
136+
if fileContent != nil {
137+
if fileContent.Content != nil {
138+
// download the file content from fileContent.GetDownloadURL() and use the content-type header to determine the MIME type
139+
// and return the content as a blob unless it is a text file, where you can return the content as text
140+
req, err := http.NewRequest("GET", fileContent.GetDownloadURL(), nil)
141+
if err != nil {
142+
return nil, fmt.Errorf("failed to create request: %w", err)
83143
}
84-
return resources, nil
85144

86-
} else if fileContent != nil {
87-
// Process the file content and return it as a binary resource
145+
resp, err := client.Client().Do(req)
146+
if err != nil {
147+
return nil, fmt.Errorf("failed to send request: %w", err)
148+
}
149+
defer func() { _ = resp.Body.Close() }()
88150

89-
if fileContent.Content != nil {
90-
decodedContent, err := fileContent.GetContent()
151+
if resp.StatusCode != http.StatusOK {
152+
body, err := io.ReadAll(resp.Body)
91153
if err != nil {
92-
return nil, err
154+
return nil, fmt.Errorf("failed to read response body: %w", err)
93155
}
156+
return nil, fmt.Errorf("failed to fetch file content: %s", string(body))
157+
}
94158

95-
mimeType := mime.TypeByExtension(filepath.Ext(fileContent.GetName()))
96-
97-
// Check if the file is text-based
98-
if strings.HasPrefix(mimeType, "text") {
99-
// Return as TextResourceContents
100-
return []mcp.ResourceContents{
101-
mcp.TextResourceContents{
102-
URI: request.Params.URI,
103-
MIMEType: mimeType,
104-
Text: decodedContent,
105-
},
106-
}, nil
159+
ext := filepath.Ext(fileContent.GetName())
160+
mimeType := resp.Header.Get("Content-Type")
161+
if ext == ".md" {
162+
mimeType = "text/markdown"
163+
} else if mimeType == "" {
164+
// backstop to the file extension if the content type is not set
165+
mimeType = mime.TypeByExtension(filepath.Ext(fileContent.GetName()))
166+
}
167+
168+
// if the content is a string, return it as text
169+
if strings.HasPrefix(mimeType, "text") {
170+
content, err := io.ReadAll(resp.Body)
171+
if err != nil {
172+
return nil, fmt.Errorf("failed to parse the response body: %w", err)
107173
}
108174

109-
// Otherwise, return as BlobResourceContents
110175
return []mcp.ResourceContents{
111-
mcp.BlobResourceContents{
176+
mcp.TextResourceContents{
112177
URI: request.Params.URI,
113178
MIMEType: mimeType,
114-
Blob: base64.StdEncoding.EncodeToString([]byte(decodedContent)), // Encode content as Base64
179+
Text: string(content),
115180
},
116181
}, nil
117182
}
118-
}
183+
// otherwise, read the content and encode it as base64
184+
decodedContent, err := io.ReadAll(resp.Body)
185+
if err != nil {
186+
return nil, fmt.Errorf("failed to parse the response body: %w", err)
187+
}
119188

120-
return nil, nil
189+
return []mcp.ResourceContents{
190+
mcp.BlobResourceContents{
191+
URI: request.Params.URI,
192+
MIMEType: mimeType,
193+
Blob: base64.StdEncoding.EncodeToString(decodedContent), // Encode content as Base64
194+
},
195+
}, nil
196+
}
121197
}
198+
199+
return nil, errors.New("no repository resource content found")
200+
}
122201
}

0 commit comments

Comments
 (0)