Skip to content

Commit 09d38df

Browse files
authored
Use Contents API instead of raw endpoint to fetch file content (#1998)
* Use Contents API instead of raw endpoint to fetch file content * Address Copilot comment * Fix linter errors
1 parent 1b829dc commit 09d38df

File tree

4 files changed

+216
-126
lines changed

4 files changed

+216
-126
lines changed

pkg/github/repositories.go

Lines changed: 53 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package github
22

33
import (
44
"context"
5+
"encoding/base64"
56
"encoding/json"
67
"fmt"
78
"io"
@@ -715,86 +716,72 @@ func GetFileContents(t translations.TranslationHelperFunc) inventory.ServerTool
715716

716717
if fileContent != nil && fileContent.SHA != nil {
717718
fileSHA = *fileContent.SHA
718-
719-
rawClient, err := deps.GetRawClient(ctx)
719+
fileSize := fileContent.GetSize()
720+
// Build resource URI for the file using URI templates
721+
pathParts := strings.Split(path, "/")
722+
resourceURI, err := expandRepoResourceURI(owner, repo, sha, ref, pathParts)
720723
if err != nil {
721-
return utils.NewToolResultError("failed to get GitHub raw content client"), nil, nil
724+
return utils.NewToolResultError("failed to build resource URI"), nil, nil
722725
}
723-
resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts)
724-
if err != nil {
725-
return utils.NewToolResultError("failed to get raw repository content"), nil, nil
726+
727+
// main branch ref passed in ref parameter but it doesn't exist - default branch was used
728+
var successNote string
729+
if fallbackUsed {
730+
successNote = fmt.Sprintf(" Note: the provided ref '%s' does not exist, default branch '%s' was used instead.", originalRef, rawOpts.Ref)
726731
}
727-
defer func() {
728-
_ = resp.Body.Close()
729-
}()
730732

731-
if resp.StatusCode == http.StatusOK {
732-
// If the raw content is found, return it directly
733-
body, err := io.ReadAll(resp.Body)
734-
if err != nil {
735-
return ghErrors.NewGitHubRawAPIErrorResponse(ctx, "failed to get raw repository content", resp, err), nil, nil
736-
}
737-
contentType := resp.Header.Get("Content-Type")
738-
739-
var resourceURI string
740-
switch {
741-
case sha != "":
742-
resourceURI, err = url.JoinPath("repo://", owner, repo, "sha", sha, "contents", path)
743-
if err != nil {
744-
return nil, nil, fmt.Errorf("failed to create resource URI: %w", err)
745-
}
746-
case ref != "":
747-
resourceURI, err = url.JoinPath("repo://", owner, repo, ref, "contents", path)
748-
if err != nil {
749-
return nil, nil, fmt.Errorf("failed to create resource URI: %w", err)
750-
}
751-
default:
752-
resourceURI, err = url.JoinPath("repo://", owner, repo, "contents", path)
753-
if err != nil {
754-
return nil, nil, fmt.Errorf("failed to create resource URI: %w", err)
755-
}
733+
// For files >= 1MB, return a ResourceLink instead of content
734+
const maxContentSize = 1024 * 1024 // 1MB
735+
if fileSize >= maxContentSize {
736+
size := int64(fileSize)
737+
resourceLink := &mcp.ResourceLink{
738+
URI: resourceURI,
739+
Name: fileContent.GetName(),
740+
Title: fmt.Sprintf("File: %s", path),
741+
Size: &size,
756742
}
743+
return utils.NewToolResultResourceLink(
744+
fmt.Sprintf("File %s is too large to display (%d bytes). Use the download URL to fetch the content: %s (SHA: %s)%s",
745+
path, fileSize, fileContent.GetDownloadURL(), fileSHA, successNote),
746+
resourceLink), nil, nil
747+
}
757748

758-
// main branch ref passed in ref parameter but it doesn't exist - default branch was used
759-
var successNote string
760-
if fallbackUsed {
761-
successNote = fmt.Sprintf(" Note: the provided ref '%s' does not exist, default branch '%s' was used instead.", originalRef, rawOpts.Ref)
762-
}
749+
// For files < 1MB, get content directly from Contents API
750+
content, err := fileContent.GetContent()
751+
if err != nil {
752+
return utils.NewToolResultError(fmt.Sprintf("failed to decode file content: %s", err)), nil, nil
753+
}
763754

764-
// Determine if content is text or binary
765-
isTextContent := strings.HasPrefix(contentType, "text/") ||
766-
contentType == "application/json" ||
767-
contentType == "application/xml" ||
768-
strings.HasSuffix(contentType, "+json") ||
769-
strings.HasSuffix(contentType, "+xml")
770-
771-
if isTextContent {
772-
result := &mcp.ResourceContents{
773-
URI: resourceURI,
774-
Text: string(body),
775-
MIMEType: contentType,
776-
}
777-
// Include SHA in the result metadata
778-
if fileSHA != "" {
779-
return utils.NewToolResultResource(fmt.Sprintf("successfully downloaded text file (SHA: %s)", fileSHA)+successNote, result), nil, nil
780-
}
781-
return utils.NewToolResultResource("successfully downloaded text file"+successNote, result), nil, nil
782-
}
755+
// Detect content type from the actual content bytes,
756+
// mirroring the original approach of using the Content-Type header
757+
// from the raw API response.
758+
contentBytes := []byte(content)
759+
contentType := http.DetectContentType(contentBytes)
760+
761+
// Determine if content is text or binary based on detected content type
762+
isTextContent := strings.HasPrefix(contentType, "text/") ||
763+
contentType == "application/json" ||
764+
contentType == "application/xml" ||
765+
strings.HasSuffix(contentType, "+json") ||
766+
strings.HasSuffix(contentType, "+xml")
783767

768+
if isTextContent {
784769
result := &mcp.ResourceContents{
785770
URI: resourceURI,
786-
Blob: body,
771+
Text: content,
787772
MIMEType: contentType,
788773
}
789-
// Include SHA in the result metadata
790-
if fileSHA != "" {
791-
return utils.NewToolResultResource(fmt.Sprintf("successfully downloaded binary file (SHA: %s)", fileSHA)+successNote, result), nil, nil
792-
}
793-
return utils.NewToolResultResource("successfully downloaded binary file"+successNote, result), nil, nil
774+
return utils.NewToolResultResource(fmt.Sprintf("successfully downloaded text file (SHA: %s)%s", fileSHA, successNote), result), nil, nil
794775
}
795776

796-
// Raw API call failed
797-
return matchFiles(ctx, client, owner, repo, ref, path, rawOpts, resp.StatusCode)
777+
// Binary content - encode as base64 blob
778+
blobContent := base64.StdEncoding.EncodeToString(contentBytes)
779+
result := &mcp.ResourceContents{
780+
URI: resourceURI,
781+
Blob: []byte(blobContent),
782+
MIMEType: contentType,
783+
}
784+
return utils.NewToolResultResource(fmt.Sprintf("successfully downloaded binary file (SHA: %s)%s", fileSHA, successNote), result), nil, nil
798785
} else if dirContent != nil {
799786
// file content or file SHA is nil which means it's a directory
800787
r, err := json.Marshal(dirContent)

0 commit comments

Comments
 (0)