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:
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user