Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d55f2a4
chore: remove old repositories.go for replacement
yonaka15 Jun 28, 2025
a895bb5
feat: refine get_file_contents tool
yonaka15 Jun 28, 2025
765ba27
chore: remove old repositories_test.go for replacement
yonaka15 Jun 28, 2025
4f46f4f
test: adapt tests for get_file_contents changes
yonaka15 Jun 28, 2025
b5f73a2
chore: remove old repositories.go for replacement
yonaka15 Jun 28, 2025
2cd2e5d
fix: correct get_file_contents implementation
yonaka15 Jun 28, 2025
fb33766
chore: remove old repositories_test.go for replacement
yonaka15 Jun 28, 2025
96848dc
test: fix incorrect mock handler usage
yonaka15 Jun 28, 2025
8617b31
chore: remove old repositories_test.go for replacement
yonaka15 Jun 28, 2025
8d320d5
test: fix incorrect mock handler usage by wrapping in http.HandlerFunc
yonaka15 Jun 28, 2025
e6dd355
test: update tool snapshot and fix tests
yonaka15 Jun 28, 2025
1884625
chore: remove repositories.go to prepare for update
yonaka15 Jun 28, 2025
c65e4bf
chore: remove get_file_contents.snap to prepare for update
yonaka15 Jun 28, 2025
3629c64
chore: remove repositories_test.go to prepare for update
yonaka15 Jun 28, 2025
d037a89
feat: re-introduce get_file_contents.snap with backward compatibility
yonaka15 Jun 28, 2025
ebc5abb
feat: update repositories.go with allow_raw_fallback logic
yonaka15 Jun 28, 2025
c8dd741
feat: update repositories_test.go with new test cases and corrected m…
yonaka15 Jun 28, 2025
8e1b5e9
fix: remove unused time import
yonaka15 Jun 28, 2025
977d2b4
fix: remove unused time import
yonaka15 Jun 28, 2025
fc6c3e7
chore: remove buggy test file to replace it
yonaka15 Jun 28, 2025
a01319b
fix: remove unused time import from test file
yonaka15 Jun 28, 2025
eaea1de
Revert get_file_contents.snap changes for backward compatibility and …
yonaka15 Jun 28, 2025
fe32520
Update get_file_contents.snap to retain sha and ref, and add allow_ra…
yonaka15 Jun 28, 2025
418cefb
Fix repositories_test.go to match main branch style and fix return ty…
yonaka15 Jun 28, 2025
794a8c4
Fix repositories_test.go to align with main branch mock handler style…
yonaka15 Jun 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions pkg/github/__toolsnaps__/get_file_contents.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
"title": "Get file or directory contents",
"readOnlyHint": true
},
"description": "Get the contents of a file or directory from a GitHub repository",
"description": "Get the contents of a file or directory from a GitHub repository. To ensure the file SHA is returned and prevent fallback to raw content, set `allow_raw_fallback` to `false`.",
"inputSchema": {
"properties": {
"allow_raw_fallback": {
"description": "Whether to allow falling back to getting raw content when the file is too large. When this is false, the file\'s SHA will always be returned.",
"type": "boolean"
},
"owner": {
"description": "Repository owner (username or organization)",
"type": "string"
},
"path": {
"description": "Path to file/directory (directories must end with a slash '/')",
"description": "Path to file/directory (directories must end with a slash \'/\')",
"type": "string"
},
"ref": {
Expand All @@ -35,4 +39,4 @@
"type": "object"
},
"name": "get_file_contents"
}
}
144 changes: 58 additions & 86 deletions pkg/github/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"

ghErrors "github.com/github/github-mcp-server/pkg/errors"
Expand Down Expand Up @@ -445,7 +444,7 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun
// GetFileContents creates a tool to get the contents of a file or directory from a GitHub repository.
func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_file_contents",
mcp.WithDescription(t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository")),
mcp.WithDescription(t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository. To ensure the file SHA is returned and prevent fallback to raw content, set `allow_raw_fallback` to `false`.")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_GET_FILE_CONTENTS_USER_TITLE", "Get file or directory contents"),
ReadOnlyHint: ToBoolPtr(true),
Expand All @@ -468,6 +467,9 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
mcp.WithString("sha",
mcp.Description("Accepts optional git sha, if sha is specified it will be used instead of ref"),
),
mcp.WithBoolean("allow_raw_fallback",
mcp.Description("Whether to allow falling back to getting raw content when the file is too large. When this is false, the file's SHA will always be returned."),
),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := RequiredParam[string](request, "owner")
Expand All @@ -490,91 +492,66 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
allowRawFallback, err := OptionalParam[bool](request, "allow_raw_fallback")
if err != nil {
// If the parameter is not present, default to true
allowRawFallback = true
}

rawOpts := &raw.RawContentOpts{}

if strings.HasPrefix(ref, "refs/pull/") {
prNumber := strings.TrimSuffix(strings.TrimPrefix(ref, "refs/pull/"), "/head")
if len(prNumber) > 0 {
// fetch the PR from the API to get the latest commit and use SHA
githubClient, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
prNum, err := strconv.Atoi(prNumber)
if err != nil {
return nil, fmt.Errorf("invalid pull request number: %w", err)
}
pr, _, err := githubClient.PullRequests.Get(ctx, owner, repo, prNum)
if err != nil {
return nil, fmt.Errorf("failed to get pull request: %w", err)
}
sha = pr.GetHead().GetSHA()
ref = ""
}
var refOrSha string
if sha != "" {
refOrSha = sha
} else {
refOrSha = ref
}

rawOpts.SHA = sha
rawOpts.Ref = ref

// If the path is (most likely) not to be a directory, we will first try to get the raw content from the GitHub raw content API.
if path != "" && !strings.HasSuffix(path, "/") {
rawOpts := &raw.RawContentOpts{}
if refOrSha != "" {
rawOpts.Ref = refOrSha
}

// If the path is (most likely) not to be a directory, we will first try to get the raw content from the GitHub raw content API.
if path != "" && !strings.HasSuffix(path, "/") && allowRawFallback {
rawClient, err := getRawClient(ctx)
if err != nil {
return mcp.NewToolResultError("failed to get GitHub raw content client"), nil
}
resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts)
if err != nil {
return mcp.NewToolResultError("failed to get raw repository content"), nil
}
defer func() {
_ = resp.Body.Close()
}()

if resp.StatusCode != http.StatusOK {
// If the raw content is not found, we will fall back to the GitHub API (in case it is a directory)
// Fallback to the GitHub API if there is an error
} else {
// If the raw content is found, return it directly
body, err := io.ReadAll(resp.Body)
if err != nil {
return mcp.NewToolResultError("failed to read response body"), nil
}
contentType := resp.Header.Get("Content-Type")
defer func() {
_ = resp.Body.Close()
}()

var resourceURI string
switch {
case sha != "":
resourceURI, err = url.JoinPath("repo://", owner, repo, "sha", sha, "contents", path)
if resp.StatusCode == http.StatusOK {
// If the raw content is found, return it directly
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to create resource URI: %w", err)
return mcp.NewToolResultError("failed to read response body"), nil
}
case ref != "":
resourceURI, err = url.JoinPath("repo://", owner, repo, ref, "contents", path)
contentType := resp.Header.Get("Content-Type")

resourceURI, err := url.JoinPath("repo://", owner, repo, refOrSha, "contents", path)
if err != nil {
return nil, fmt.Errorf("failed to create resource URI: %w", err)
}
default:
resourceURI, err = url.JoinPath("repo://", owner, repo, "contents", path)
if err != nil {
return nil, fmt.Errorf("failed to create resource URI: %w", err)

if strings.HasPrefix(contentType, "application") || strings.HasPrefix(contentType, "text") {
return mcp.NewToolResultResource("successfully downloaded text file", mcp.TextResourceContents{
URI: resourceURI,
Text: string(body),
MIMEType: contentType,
}), nil
}
}

if strings.HasPrefix(contentType, "application") || strings.HasPrefix(contentType, "text") {
return mcp.NewToolResultResource("successfully downloaded text file", mcp.TextResourceContents{
return mcp.NewToolResultResource("successfully downloaded binary file", mcp.BlobResourceContents{
URI: resourceURI,
Text: string(body),
Blob: base64.StdEncoding.EncodeToString(body),
MIMEType: contentType,
}), nil
}

return mcp.NewToolResultResource("successfully downloaded binary file", mcp.BlobResourceContents{
URI: resourceURI,
Blob: base64.StdEncoding.EncodeToString(body),
MIMEType: contentType,
}), nil

}
}

Expand All @@ -583,35 +560,30 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
return mcp.NewToolResultError("failed to get GitHub client"), nil
}

if sha != "" {
ref = sha
opts := &github.RepositoryContentGetOptions{}
if refOrSha != "" {
opts.Ref = refOrSha
}
if strings.HasSuffix(path, "/") {
opts := &github.RepositoryContentGetOptions{Ref: ref}
_, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
if err != nil {
return mcp.NewToolResultError("failed to get file contents"), nil
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != 200 {
body, err := io.ReadAll(resp.Body)
if err != nil {
return mcp.NewToolResultError("failed to read response body"), nil
}
return mcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s", string(body))), nil
}
fileContent, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get file contents", resp, err), nil
}
defer func() { _ = resp.Body.Close() }()

r, err := json.Marshal(dirContent)
if err != nil {
return mcp.NewToolResultError("failed to marshal response"), nil
}
return mcp.NewToolResultText(string(r)), nil
var r []byte
if dirContent != nil {
r, err = json.Marshal(dirContent)
} else {
r, err = json.Marshal(fileContent)
}
if err != nil {
return mcp.NewToolResultError("failed to marshal response"), nil
}
return mcp.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), nil
return mcp.NewToolResultText(string(r)), nil
}
}


// ForkRepository creates a tool to fork a repository.
func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("fork_repository",
Expand Down
Loading
Loading