From ab8eb710cc3a69a467204a9edb598d8443835b8a Mon Sep 17 00:00:00 2001 From: Koichi Okuda Date: Tue, 10 Jun 2025 11:51:12 +0900 Subject: [PATCH] feat: add raw parameter to get_file_contents tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add optional `raw` boolean parameter to get_file_contents tool to return plain text content instead of base64 encoded content. This reduces token consumption for LLM interactions by avoiding the need to decode base64. - When `raw=true`, fetches file content via GitHub's download URL - When `raw=false` or unset, returns base64 encoded content (existing behavior) - Returns appropriate error for directories when raw=true - Includes comprehensive test coverage for edge cases Fixes #372 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- pkg/github/repositories.go | 57 ++++++++++++++++++++ pkg/github/repositories_test.go | 92 +++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 093e5fdcd..19eb68e1c 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -431,6 +431,9 @@ func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc mcp.WithString("branch", mcp.Description("Branch to get contents from"), ), + mcp.WithBoolean("raw", + mcp.Description("Return raw file contents instead of base64 encoded (only applies to files, not directories)"), + ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](request, "owner") @@ -449,11 +452,65 @@ func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc if err != nil { return mcp.NewToolResultError(err.Error()), nil } + raw, err := OptionalParam[bool](request, "raw") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } + + // If raw is requested, we need to get the download URL and fetch raw content + if raw { + // First check if the path is a file by making a regular request + opts := &github.RepositoryContentGetOptions{Ref: branch} + fileContent, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) + if err != nil { + return nil, fmt.Errorf("failed to get file contents: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 200 { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return mcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s", string(body))), nil + } + + // If it's a directory, return an error since raw doesn't apply to directories + if dirContent != nil { + return mcp.NewToolResultError("raw option only applies to files, not directories"), nil + } + + // If it's a file, use the download URL to get raw content + if fileContent != nil && fileContent.DownloadURL != nil { + // Make HTTP request to download URL to get raw content + httpResp, err := http.Get(*fileContent.DownloadURL) + if err != nil { + return nil, fmt.Errorf("failed to download raw file content: %w", err) + } + defer func() { _ = httpResp.Body.Close() }() + + if httpResp.StatusCode != 200 { + return mcp.NewToolResultError(fmt.Sprintf("failed to download raw file content (status: %d)", httpResp.StatusCode)), nil + } + + // Read the raw content + rawContent, err := io.ReadAll(httpResp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read raw file content: %w", err) + } + + return mcp.NewToolResultText(string(rawContent)), nil + } + + return mcp.NewToolResultError("file does not have a download URL"), nil + } + + // Regular (non-raw) request opts := &github.RepositoryContentGetOptions{Ref: branch} fileContent, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) if err != nil { diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index f7924b2f9..7f0640f1a 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -25,6 +25,7 @@ func Test_GetFileContents(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "repo") assert.Contains(t, tool.InputSchema.Properties, "path") assert.Contains(t, tool.InputSchema.Properties, "branch") + assert.Contains(t, tool.InputSchema.Properties, "raw") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "path"}) // Setup mock file content for success case @@ -175,6 +176,97 @@ func Test_GetFileContents(t *testing.T) { } } +func Test_GetFileContents_RawParameter(t *testing.T) { + mockDirContent := []*github.RepositoryContent{ + { + Type: github.Ptr("dir"), + Name: github.Ptr("testdir"), + Path: github.Ptr("testdir"), + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedErrMsg string + }{ + { + name: "raw parameter with directory should fail", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposContentsByOwnerByRepoByPath, + mockResponse(t, http.StatusOK, mockDirContent), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "path": "testdir", + "raw": true, + }, + expectError: true, + expectedErrMsg: "raw option only applies to files, not directories", + }, + { + name: "raw file without download URL should fail", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposContentsByOwnerByRepoByPath, + mockResponse(t, http.StatusOK, &github.RepositoryContent{ + Type: github.Ptr("file"), + Name: github.Ptr("test.txt"), + Path: github.Ptr("test.txt"), + // No DownloadURL + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "path": "test.txt", + "raw": true, + }, + expectError: true, + expectedErrMsg: "file does not have a download URL", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetFileContents(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + if err != nil { + assert.Contains(t, err.Error(), tc.expectedErrMsg) + } else { + // Error should be in the result + require.NotNil(t, result) + require.True(t, result.IsError) + // Check error message in result content + textContent := getTextResult(t, result) + assert.Contains(t, textContent.Text, tc.expectedErrMsg) + } + return + } + + require.NoError(t, err) + require.NotNil(t, result) + require.False(t, result.IsError) + }) + } +} + func Test_ForkRepository(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) 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