From 1152eb9c5557f4446a94e530cb12834f9875cfb2 Mon Sep 17 00:00:00 2001 From: yonaka15 Date: Sun, 29 Jun 2025 07:59:20 +0900 Subject: [PATCH 1/6] feat: add include_sha parameter to get_file_contents tool - Add include_sha boolean parameter to control metadata vs raw content retrieval - When include_sha=true, always use GitHub Contents API to ensure SHA is returned - When include_sha=false (default), maintain existing behavior with raw content preference - Unify GitHub API processing to handle both files and directories consistently - Improve error handling and fallback logic for better reliability - Update tool description and parameter documentation - Update toolsnap test file to reflect new parameter This enhancement allows users to reliably obtain file metadata (SHA, size, type) regardless of file size, while maintaining backward compatibility with existing clients. --- .../__toolsnaps__/get_file_contents.snap | 6 +- pkg/github/repositories.go | 56 +++++++++++++------ 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/pkg/github/__toolsnaps__/get_file_contents.snap b/pkg/github/__toolsnaps__/get_file_contents.snap index b3975abbc..62e4cf03a 100644 --- a/pkg/github/__toolsnaps__/get_file_contents.snap +++ b/pkg/github/__toolsnaps__/get_file_contents.snap @@ -3,7 +3,7 @@ "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. Set `include_sha` to `true` to return file metadata (including SHA, size, type) instead of raw content.", "inputSchema": { "properties": { "owner": { @@ -25,6 +25,10 @@ "sha": { "description": "Accepts optional git sha, if sha is specified it will be used instead of ref", "type": "string" + }, + "include_sha": { + "description": "Whether to return file metadata (including SHA, size, type) instead of raw content", + "type": "boolean" } }, "required": [ diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index fa5d7338a..18c603706 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -445,7 +445,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. Set `include_sha` to `true` to return file metadata (including SHA, size, type) instead of raw content.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_GET_FILE_CONTENTS_USER_TITLE", "Get file or directory contents"), ReadOnlyHint: ToBoolPtr(true), @@ -468,7 +468,10 @@ 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("include_sha", + mcp.Description("Whether to return file metadata (including SHA, size, type) instead of raw content"), + ), + ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](request, "owner") if err != nil { @@ -490,6 +493,10 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t if err != nil { return mcp.NewToolResultError(err.Error()), nil } + includeSha, err := OptionalParam[bool](request, "include_sha") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } rawOpts := &raw.RawContentOpts{} @@ -518,7 +525,8 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t 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, "/") { + // Skip raw content if include_sha is true + if !includeSha && path != "" && !strings.HasSuffix(path, "/") { rawClient, err := getRawClient(ctx) if err != nil { @@ -586,28 +594,44 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t if sha != "" { ref = sha } - if strings.HasSuffix(path, "/") { - opts := &github.RepositoryContentGetOptions{Ref: ref} - _, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) + // Try to get file/directory contents using GitHub API + opts := &github.RepositoryContentGetOptions{Ref: ref} + 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() }() + + if resp.StatusCode != 200 { + body, err := io.ReadAll(resp.Body) if err != nil { - return mcp.NewToolResultError("failed to get file contents"), nil + return mcp.NewToolResultError("failed to read response body"), nil } - defer func() { _ = resp.Body.Close() }() + return mcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s", string(body))), nil + } - 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 + // Handle directory contents + if dirContent != nil { + r, err := json.Marshal(dirContent) + if err != nil { + return mcp.NewToolResultError("failed to marshal directory contents"), nil } + return mcp.NewToolResultText(string(r)), nil + } - r, err := json.Marshal(dirContent) + // Handle file contents + if fileContent != nil { + r, err := json.Marshal(fileContent) if err != nil { - return mcp.NewToolResultError("failed to marshal response"), nil + return mcp.NewToolResultError("failed to marshal file contents"), nil } return mcp.NewToolResultText(string(r)), 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 } } From cc5747ba78880097aa050f31e7b9e3f68f68f5cc Mon Sep 17 00:00:00 2001 From: yonaka15 Date: Sun, 29 Jun 2025 08:53:18 +0900 Subject: [PATCH 2/6] feat: add include_sha parameter to get_file_contents tool Add optional boolean parameter to retrieve file metadata instead of raw content. When include_sha=true, returns SHA, size, and other metadata via GitHub Contents API. Maintains backward compatibility and performance with Raw API as default. - Add include_sha parameter with comprehensive test coverage - Support metadata retrieval for both files and directories - Update tool description and schema validation --- pkg/github/repositories_test.go | 106 ++++++++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 4 deletions(-) diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index b621cec43..74dd610bc 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -33,11 +33,25 @@ func Test_GetFileContents(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "path") assert.Contains(t, tool.InputSchema.Properties, "ref") assert.Contains(t, tool.InputSchema.Properties, "sha") + assert.Contains(t, tool.InputSchema.Properties, "include_sha") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "path"}) // Mock response for raw content mockRawContent := []byte("# Test Repository\n\nThis is a test repository.") + // Setup mock file content for include_sha test + mockFileContent := &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"), + DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/README.md"), + Content: github.Ptr(base64.StdEncoding.EncodeToString(mockRawContent)), + Encoding: github.Ptr("base64"), + } + // Setup mock directory content for success case mockDirContent := []*github.RepositoryContent{ { @@ -140,6 +154,68 @@ func Test_GetFileContents(t *testing.T) { expectError: false, expectedResult: mockDirContent, }, + { + name: "successful file metadata fetch with include_sha", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposContentsByOwnerByRepoByPath, + expectQueryParams(t, map[string]string{"ref": "refs/heads/main"}).andThen( + mockResponse(t, http.StatusOK, mockFileContent), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "path": "README.md", + "ref": "refs/heads/main", + "include_sha": true, + }, + expectError: false, + expectedResult: mockFileContent, + }, + { + name: "successful text content fetch with include_sha false", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + raw.GetRawReposContentsByOwnerByRepoByBranchByPath, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/markdown") + _, _ = w.Write(mockRawContent) + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "path": "README.md", + "ref": "refs/heads/main", + "include_sha": false, + }, + expectError: false, + expectedResult: mcp.TextResourceContents{ + URI: "repo://owner/repo/refs/heads/main/contents/README.md", + Text: "# Test Repository\n\nThis is a test repository.", + MIMEType: "text/markdown", + }, + }, + { + name: "successful directory metadata fetch with include_sha", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposContentsByOwnerByRepoByPath, + mockResponse(t, http.StatusOK, mockDirContent), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "path": "src/", + "include_sha": true, + }, + expectError: false, + expectedResult: mockDirContent, + }, { name: "content fetch fails", mockedClient: mock.NewMockedHTTPClient( @@ -165,7 +241,8 @@ func Test_GetFileContents(t *testing.T) { "ref": "refs/heads/main", }, expectError: false, - expectedResult: 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."), + expectedResult: nil, + expectedErrMsg: "failed to get file contents", }, } @@ -190,6 +267,17 @@ func Test_GetFileContents(t *testing.T) { } require.NoError(t, err) + + // Check for tool errors (API errors that return as tool results) + if tc.expectedErrMsg != "" { + require.True(t, result.IsError) + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + return + } + + // Process successful results + require.False(t, result.IsError) // Use the correct result helper based on the expected type switch expected := tc.expectedResult.(type) { case mcp.TextResourceContents: @@ -210,9 +298,19 @@ func Test_GetFileContents(t *testing.T) { assert.Equal(t, *expected[i].Path, *content.Path) assert.Equal(t, *expected[i].Type, *content.Type) } - case mcp.TextContent: - textContent := getErrorResult(t, result) - require.Equal(t, textContent, expected) + case *github.RepositoryContent: + // File metadata fetch returns a text result (JSON object) + textContent := getTextResult(t, result) + var returnedContent github.RepositoryContent + err = json.Unmarshal([]byte(textContent.Text), &returnedContent) + require.NoError(t, err) + assert.Equal(t, *expected.Name, *returnedContent.Name) + assert.Equal(t, *expected.Path, *returnedContent.Path) + assert.Equal(t, *expected.SHA, *returnedContent.SHA) + assert.Equal(t, *expected.Size, *returnedContent.Size) + if expected.Content != nil { + assert.Equal(t, *expected.Content, *returnedContent.Content) + } } }) } From 995e43d39f9c06fed994eeffd0e2aeb235000dd4 Mon Sep 17 00:00:00 2001 From: yonaka15 Date: Sun, 29 Jun 2025 09:31:32 +0900 Subject: [PATCH 3/6] chore: add documentation --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4458b95ab..36afebe35 100644 --- a/README.md +++ b/README.md @@ -849,6 +849,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `sha`: Commit SHA, branch name, or tag name (string, required) - **get_file_contents** - Get file or directory contents + - `include_sha`: Whether to return file metadata (including SHA, size, type) instead of raw content (boolean, optional) - `owner`: Repository owner (username or organization) (string, required) - `path`: Path to file/directory (directories must end with a slash '/') (string, required) - `ref`: Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head` (string, optional) From 23c7ab5e81b1f7bc94d7316aae7b428e98bbb6cf Mon Sep 17 00:00:00 2001 From: yonaka15 Date: Sun, 29 Jun 2025 09:32:36 +0900 Subject: [PATCH 4/6] chore: lint --- pkg/github/repositories.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 18c603706..7db74c587 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -469,9 +469,9 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t mcp.Description("Accepts optional git sha, if sha is specified it will be used instead of ref"), ), mcp.WithBoolean("include_sha", - mcp.Description("Whether to return file metadata (including SHA, size, type) instead of raw content"), + mcp.Description("Whether to return file metadata (including SHA, size, type) instead of raw content"), ), - ), + ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](request, "owner") if err != nil { From f891c92712e0fdb3d2d1ed875a6f7c783cb9cfd2 Mon Sep 17 00:00:00 2001 From: yonaka15 Date: Sun, 29 Jun 2025 09:40:09 +0900 Subject: [PATCH 5/6] chore: Reorders the parameters for the tool --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36afebe35..3da325d3b 100644 --- a/README.md +++ b/README.md @@ -849,12 +849,12 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `sha`: Commit SHA, branch name, or tag name (string, required) - **get_file_contents** - Get file or directory contents - - `include_sha`: Whether to return file metadata (including SHA, size, type) instead of raw content (boolean, optional) - `owner`: Repository owner (username or organization) (string, required) - `path`: Path to file/directory (directories must end with a slash '/') (string, required) - `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) - `sha`: Accepts optional git sha, if sha is specified it will be used instead of ref (string, optional) + - `include_sha`: Whether to return file metadata (including SHA, size, type) instead of raw content (boolean, optional) - **get_tag** - Get tag details - `owner`: Repository owner (string, required) From 83866a228aa3bd68cbb285b1354fba51c5f68f7b Mon Sep 17 00:00:00 2001 From: yonaka15 Date: Sun, 29 Jun 2025 09:44:12 +0900 Subject: [PATCH 6/6] Revert "chore: Reorders the parameters for the tool" This reverts commit f891c92712e0fdb3d2d1ed875a6f7c783cb9cfd2. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3da325d3b..36afebe35 100644 --- a/README.md +++ b/README.md @@ -849,12 +849,12 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `sha`: Commit SHA, branch name, or tag name (string, required) - **get_file_contents** - Get file or directory contents + - `include_sha`: Whether to return file metadata (including SHA, size, type) instead of raw content (boolean, optional) - `owner`: Repository owner (username or organization) (string, required) - `path`: Path to file/directory (directories must end with a slash '/') (string, required) - `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) - `sha`: Accepts optional git sha, if sha is specified it will be used instead of ref (string, optional) - - `include_sha`: Whether to return file metadata (including SHA, size, type) instead of raw content (boolean, optional) - **get_tag** - Get tag details - `owner`: Repository owner (string, required) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy