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(