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)
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: