diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 093e5fdc..19eb68e1 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 f7924b2f..7f0640f1 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)