Improvements & refactoring of get_file_contents (#1582)

* Improvements & refactoring of get_file_contents

* Fix logical path when file or directory not found

* Fix comment

* Docs update

* Do file matching when raw API returns error
This commit is contained in:
Ksenia Bobrova
2025-12-15 11:05:01 +01:00
committed by GitHub
parent adaa6a1d86
commit a48e306da7
3 changed files with 62 additions and 68 deletions
+1 -1
View File
@@ -1092,7 +1092,7 @@ The following sets of tools are available:
- **get_file_contents** - Get file or directory contents - **get_file_contents** - Get file or directory contents
- `owner`: Repository owner (username or organization) (string, required) - `owner`: Repository owner (username or organization) (string, required)
- `path`: Path to file/directory (directories must end with a slash '/') (string, optional) - `path`: Path to file/directory (string, optional)
- `ref`: Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head` (string, optional) - `ref`: Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head` (string, optional)
- `repo`: Repository name (string, required) - `repo`: Repository name (string, required)
- `sha`: Accepts optional commit SHA. If specified, it will be used instead of ref (string, optional) - `sha`: Accepts optional commit SHA. If specified, it will be used instead of ref (string, optional)
@@ -17,7 +17,7 @@
}, },
"path": { "path": {
"type": "string", "type": "string",
"description": "Path to file/directory (directories must end with a slash '/')", "description": "Path to file/directory",
"default": "/" "default": "/"
}, },
"ref": { "ref": {
+60 -66
View File
@@ -560,7 +560,7 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
}, },
"path": { "path": {
Type: "string", Type: "string",
Description: "Path to file/directory (directories must end with a slash '/')", Description: "Path to file/directory",
Default: json.RawMessage(`"/"`), Default: json.RawMessage(`"/"`),
}, },
"ref": { "ref": {
@@ -608,28 +608,26 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
return utils.NewToolResultError(fmt.Sprintf("failed to resolve git reference: %s", err)), nil, nil return utils.NewToolResultError(fmt.Sprintf("failed to resolve git reference: %s", err)), nil, nil
} }
// If the path is (most likely) not to be a directory, we will if rawOpts.SHA != "" {
// first try to get the raw content from the GitHub raw content API. ref = rawOpts.SHA
}
var rawAPIResponseCode int var fileSHA string
if path != "" && !strings.HasSuffix(path, "/") { opts := &github.RepositoryContentGetOptions{Ref: ref}
// First, get file info from Contents API to retrieve SHA
var fileSHA string // Always call GitHub Contents API first to get metadata including SHA and determine if it's a file or directory
opts := &github.RepositoryContentGetOptions{Ref: ref} fileContent, dirContent, respContents, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
fileContent, _, respContents, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) if respContents != nil {
if respContents != nil { defer func() { _ = respContents.Body.Close() }()
defer func() { _ = respContents.Body.Close() }() }
}
if err != nil { // The path does not point to a file or directory.
return ghErrors.NewGitHubAPIErrorResponse(ctx, // Instead let's try to find it in the Git Tree by matching the end of the path.
"failed to get file SHA", if err != nil || (fileContent == nil && dirContent == nil) {
respContents, return matchFiles(ctx, client, owner, repo, ref, path, rawOpts, 0)
err, }
), nil, nil
} if fileContent != nil && fileContent.SHA != nil {
if fileContent == nil || fileContent.SHA == nil {
return utils.NewToolResultError("file content SHA is nil, if a directory was requested, path parameters should end with a trailing slash '/'"), nil, nil
}
fileSHA = *fileContent.SHA fileSHA = *fileContent.SHA
rawClient, err := getRawClient(ctx) rawClient, err := getRawClient(ctx)
@@ -702,55 +700,19 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
} }
return utils.NewToolResultResource("successfully downloaded binary file", result), nil, nil return utils.NewToolResultResource("successfully downloaded binary file", result), nil, nil
} }
rawAPIResponseCode = resp.StatusCode
}
if rawOpts.SHA != "" { // Raw API call failed
ref = rawOpts.SHA return matchFiles(ctx, client, owner, repo, ref, path, rawOpts, resp.StatusCode)
} } else if dirContent != nil {
if strings.HasSuffix(path, "/") { // file content or file SHA is nil which means it's a directory
opts := &github.RepositoryContentGetOptions{Ref: ref} r, err := json.Marshal(dirContent)
_, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
if err == nil && resp.StatusCode == http.StatusOK {
defer func() { _ = resp.Body.Close() }()
r, err := json.Marshal(dirContent)
if err != nil {
return utils.NewToolResultError("failed to marshal response"), nil, nil
}
return utils.NewToolResultText(string(r)), nil, nil
}
}
// The path does not point to a file or directory.
// Instead let's try to find it in the Git Tree by matching the end of the path.
// Step 1: Get Git Tree recursively
tree, resp, err := client.Git.GetTree(ctx, owner, repo, ref, true)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to get git tree",
resp,
err,
), nil, nil
}
defer func() { _ = resp.Body.Close() }()
// Step 2: Filter tree for matching paths
const maxMatchingFiles = 3
matchingFiles := filterPaths(tree.Entries, path, maxMatchingFiles)
if len(matchingFiles) > 0 {
matchingFilesJSON, err := json.Marshal(matchingFiles)
if err != nil { if err != nil {
return utils.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s", err)), nil, nil return utils.NewToolResultError("failed to marshal response"), nil, nil
} }
resolvedRefs, err := json.Marshal(rawOpts) return utils.NewToolResultText(string(r)), nil, nil
if err != nil {
return utils.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s), but the raw content API returned an unexpected status code %d.", string(resolvedRefs), string(matchingFilesJSON), rawAPIResponseCode)), nil, nil
} }
return utils.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, nil return utils.NewToolResultError("failed to get file contents"), nil, nil
}) })
return tool, handler return tool, handler
@@ -2115,3 +2077,35 @@ func UnstarRepository(getClient GetClientFn, t translations.TranslationHelperFun
return tool, handler return tool, handler
} }
func matchFiles(ctx context.Context, client *github.Client, owner, repo, ref, path string, rawOpts *raw.ContentOpts, rawAPIResponseCode int) (*mcp.CallToolResult, any, error) {
// Step 1: Get Git Tree recursively
tree, response, err := client.Git.GetTree(ctx, owner, repo, ref, true)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to get git tree",
response,
err,
), nil, nil
}
defer func() { _ = response.Body.Close() }()
// Step 2: Filter tree for matching paths
const maxMatchingFiles = 3
matchingFiles := filterPaths(tree.Entries, path, maxMatchingFiles)
if len(matchingFiles) > 0 {
matchingFilesJSON, err := json.Marshal(matchingFiles)
if err != nil {
return utils.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s", err)), nil, nil
}
resolvedRefs, err := json.Marshal(rawOpts)
if err != nil {
return utils.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil, nil
}
if rawAPIResponseCode > 0 {
return utils.NewToolResultText(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s), but the content API returned an unexpected status code %d.", string(resolvedRefs), string(matchingFilesJSON), rawAPIResponseCode)), nil, nil
}
return utils.NewToolResultText(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s).", string(resolvedRefs), string(matchingFilesJSON))), nil, nil
}
return utils.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, nil
}