Skip to content

feat: add raw parameter to get_file_contents tool #497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions pkg/github/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down
92 changes: 92 additions & 0 deletions pkg/github/repositories_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down