diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index c909e9c3b..9fa74c3c6 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,7 +116,12 @@ func repositoryResourceContentsHandler(client *github.Client) func(ctx context.C for _, entry := range directoryContent { mimeType := "text/directory" if entry.GetType() == "file" { - mimeType = mime.TypeByExtension(filepath.Ext(entry.GetName())) + // this is system dependent, and a best guess + ext := filepath.Ext(entry.GetName()) + mimeType = mime.TypeByExtension(ext) + if ext == ".md" { + mimeType = "text/markdown" + } } resources = append(resources, mcp.TextResourceContents{ URI: entry.GetHTMLURL(), @@ -127,28 +135,62 @@ 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 fetch file content: %s", string(body)) + } + + ext := filepath.Ext(fileContent.GetName()) + mimeType := resp.Header.Get("Content-Type") + if ext == ".md" { + mimeType = "text/markdown" + } else if mimeType == "" { + // backstop to the file extension if the content type is not set + mimeType = 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 } diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index 702a488ca..0a5b0b0f0 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,50 @@ 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 + _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository.")) + require.NoError(t, err) + }), + ), ), 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{"README.md"}, + "branch": []string{"main"}, + }, + expectedResult: expectedTextContent, + }, { name: "successful directory content fetch", mockedClient: mock.NewMockedHTTPClient(
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: