From afcb99c1f8f76bb337311253f6abec1e67571f19 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 2 Apr 2025 14:54:25 +0200 Subject: [PATCH 1/5] use raw repo URIs for resources --- pkg/github/repository_resource.go | 45 +++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index c909e9c3b..655656f4f 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -4,7 +4,10 @@ import ( "context" "encoding/base64" "errors" + "fmt" + "io" "mime" + "net/http" "path/filepath" "strings" @@ -113,6 +116,7 @@ func repositoryResourceContentsHandler(client *github.Client) func(ctx context.C for _, entry := range directoryContent { mimeType := "text/directory" if entry.GetType() == "file" { + // this is system dependent, and a best guess mimeType = mime.TypeByExtension(filepath.Ext(entry.GetName())) } resources = append(resources, mcp.TextResourceContents{ @@ -127,28 +131,59 @@ func repositoryResourceContentsHandler(client *github.Client) func(ctx context.C } if fileContent != nil { if fileContent.Content != nil { - decodedContent, err := fileContent.GetContent() + // download the file content from fileContent.GetDownloadURL() and use the content-type header to determine the MIME type + // and return the content as a blob unless it is a text file, where you can return the content as text + req, err := http.NewRequest("GET", fileContent.GetDownloadURL(), nil) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create request: %w", err) } - mimeType := mime.TypeByExtension(filepath.Ext(fileContent.GetName())) + resp, err := client.Client().Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return nil, fmt.Errorf("failed to get security analysis settings: %s", string(body)) + } + + mimeType := resp.Header.Get("Content-Type") + if mimeType == "" { + // backstop to the file extension if the content type is not set + mime.TypeByExtension(filepath.Ext(fileContent.GetName())) + } + // if the content is a string, return it as text if strings.HasPrefix(mimeType, "text") { + content, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to parse the response body: %w", err) + } + return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: request.Params.URI, MIMEType: mimeType, - Text: decodedContent, + Text: string(content), }, }, nil } + // otherwise, read the content and encode it as base64 + decodedContent, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to parse the response body: %w", err) + } return []mcp.ResourceContents{ mcp.BlobResourceContents{ URI: request.Params.URI, MIMEType: mimeType, - Blob: base64.StdEncoding.EncodeToString([]byte(decodedContent)), // Encode content as Base64 + Blob: base64.StdEncoding.EncodeToString(decodedContent), // Encode content as Base64 }, }, nil } From f1d6aa53d0fae0f9c10c08d03ed693c3dfa46566 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 2 Apr 2025 23:10:19 +0200 Subject: [PATCH 2/5] fetch repository content from raw urls --- pkg/github/repository_resource.go | 13 +++- pkg/github/repository_resource_test.go | 91 +++++++++++++++++++++----- 2 files changed, 83 insertions(+), 21 deletions(-) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 655656f4f..cbf66c029 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -117,7 +117,11 @@ func repositoryResourceContentsHandler(client *github.Client) func(ctx context.C mimeType := "text/directory" if entry.GetType() == "file" { // this is system dependent, and a best guess - mimeType = mime.TypeByExtension(filepath.Ext(entry.GetName())) + ext := filepath.Ext(entry.GetName()) + mimeType = mime.TypeByExtension(ext) + if ext == ".md" { + mimeType = "text/markdown" + } } resources = append(resources, mcp.TextResourceContents{ URI: entry.GetHTMLURL(), @@ -152,10 +156,13 @@ func repositoryResourceContentsHandler(client *github.Client) func(ctx context.C return nil, fmt.Errorf("failed to get security analysis settings: %s", string(body)) } + ext := filepath.Ext(fileContent.GetName()) mimeType := resp.Header.Get("Content-Type") - if mimeType == "" { + if ext == ".md" { + mimeType = "text/markdown" + } else if mimeType == "" { // backstop to the file extension if the content type is not set - mime.TypeByExtension(filepath.Ext(fileContent.GetName())) + mimeType = mime.TypeByExtension(filepath.Ext(fileContent.GetName())) } // if the content is a string, return it as text diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index 702a488ca..05d35b7a4 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -2,7 +2,6 @@ package github import ( "context" - "encoding/base64" "net/http" "testing" @@ -13,28 +12,35 @@ import ( "github.com/stretchr/testify/require" ) +var GetRawReposContentsByOwnerByRepoByPath mock.EndpointPattern = mock.EndpointPattern{ + Pattern: "/{owner}/{repo}/main/{path:.+}", + Method: "GET", +} + func Test_repositoryResourceContentsHandler(t *testing.T) { mockDirContent := []*github.RepositoryContent{ { - Type: github.Ptr("file"), - Name: github.Ptr("README.md"), - Path: github.Ptr("README.md"), - SHA: github.Ptr("abc123"), - Size: github.Ptr(42), - HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"), + Type: github.Ptr("file"), + Name: github.Ptr("README.md"), + Path: github.Ptr("README.md"), + SHA: github.Ptr("abc123"), + Size: github.Ptr(42), + HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"), + DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/README.md"), }, { - Type: github.Ptr("dir"), - Name: github.Ptr("src"), - Path: github.Ptr("src"), - SHA: github.Ptr("def456"), - HTMLURL: github.Ptr("https://github.com/owner/repo/tree/main/src"), + Type: github.Ptr("dir"), + Name: github.Ptr("src"), + Path: github.Ptr("src"), + SHA: github.Ptr("def456"), + HTMLURL: github.Ptr("https://github.com/owner/repo/tree/main/src"), + DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/src"), }, } expectedDirContent := []mcp.TextResourceContents{ { URI: "https://github.com/owner/repo/blob/main/README.md", - MIMEType: "", + MIMEType: "text/markdown", Text: "README.md", }, { @@ -44,20 +50,41 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { }, } - mockFileContent := &github.RepositoryContent{ + mockTextContent := &github.RepositoryContent{ Type: github.Ptr("file"), Name: github.Ptr("README.md"), Path: github.Ptr("README.md"), - Content: github.Ptr("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku"), // Base64 encoded "# Test Repository\n\nThis is a test repository." + Content: github.Ptr("# Test Repository\n\nThis is a test repository."), SHA: github.Ptr("abc123"), Size: github.Ptr(42), HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"), DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/README.md"), } + mockFileContent := &github.RepositoryContent{ + Type: github.Ptr("file"), + Name: github.Ptr("data.png"), + Path: github.Ptr("data.png"), + Content: github.Ptr("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku"), // Base64 encoded "# Test Repository\n\nThis is a test repository." + SHA: github.Ptr("abc123"), + Size: github.Ptr(42), + HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/data.png"), + DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/data.png"), + } + expectedFileContent := []mcp.BlobResourceContents{ { - Blob: base64.StdEncoding.EncodeToString([]byte("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku")), + Blob: "IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku", + MIMEType: "image/png", + URI: "", + }, + } + + expectedTextContent := []mcp.TextResourceContents{ + { + Text: "# Test Repository\n\nThis is a test repository.", + MIMEType: "text/markdown", + URI: "", }, } @@ -94,21 +121,49 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { expectError: "repo is required", }, { - name: "successful file content fetch", + name: "successful blob content fetch", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetReposContentsByOwnerByRepoByPath, mockFileContent, ), + mock.WithRequestMatchHandler( + GetRawReposContentsByOwnerByRepoByPath, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "image/png") + // as this is given as a png, it will return the content as a blob + w.Write([]byte("# Test Repository\n\nThis is a test repository.")) + }), + ), ), requestArgs: map[string]any{ "owner": []string{"owner"}, "repo": []string{"repo"}, - "path": []string{"README.md"}, + "path": []string{"data.png"}, "branch": []string{"main"}, }, expectedResult: expectedFileContent, }, + { + name: "successful text content fetch", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposContentsByOwnerByRepoByPath, + mockTextContent, + ), + mock.WithRequestMatch( + GetRawReposContentsByOwnerByRepoByPath, + []byte("# Test Repository\n\nThis is a test repository."), + ), + ), + requestArgs: map[string]any{ + "owner": []string{"owner"}, + "repo": []string{"repo"}, + "path": []string{"data.png"}, + "branch": []string{"main"}, + }, + expectedResult: expectedTextContent, + }, { name: "successful directory content fetch", mockedClient: mock.NewMockedHTTPClient( From c699e24a8c3d172f9449a5a10038a546ada7ec5f Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 2 Apr 2025 23:17:27 +0200 Subject: [PATCH 3/5] ensure no error in test write --- pkg/github/repository_resource_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index 05d35b7a4..65d4d368c 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -132,7 +132,8 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "image/png") // as this is given as a png, it will return the content as a blob - w.Write([]byte("# Test Repository\n\nThis is a test repository.")) + _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository.")) + require.NoError(t, err) }), ), ), From 0213f400753185233653c01fd813c13c5997c095 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 2 Apr 2025 23:25:06 +0200 Subject: [PATCH 4/5] Update pkg/github/repository_resource.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/github/repository_resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index cbf66c029..9fa74c3c6 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -153,7 +153,7 @@ func repositoryResourceContentsHandler(client *github.Client) func(ctx context.C if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - return nil, fmt.Errorf("failed to get security analysis settings: %s", string(body)) + return nil, fmt.Errorf("failed to fetch file content: %s", string(body)) } ext := filepath.Ext(fileContent.GetName()) From 332b3602924658e3850fd7f9ad3ba66e9902811f Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 2 Apr 2025 23:27:33 +0200 Subject: [PATCH 5/5] use appropriate file name for text file test --- pkg/github/repository_resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index 65d4d368c..0a5b0b0f0 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -160,7 +160,7 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { requestArgs: map[string]any{ "owner": []string{"owner"}, "repo": []string{"repo"}, - "path": []string{"data.png"}, + "path": []string{"README.md"}, "branch": []string{"main"}, }, expectedResult: expectedTextContent,