Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,7 @@ The following sets of tools are available:
- **pull_request_read** - Get details for a single pull request
- **OAuth Challenge Scopes**: `repo`
- `after`: Cursor for pagination, used only by the get_review_comments method. Pass the endCursor from the previous page's PageInfo to fetch the next page. (string, optional)
- `fields`: Fields to return for each changed file. Only applies when method is 'get_files'. When a nonempty list is provided, only the listed fields are returned. If 'fields' is omitted or empty, all available fields are returned, including patches. (string[], optional)
- `method`: Action to specify what pull request data needs to be retrieved from GitHub.
Possible options:
1. get - Get details of a specific pull request.
Expand Down
16 changes: 16 additions & 0 deletions pkg/github/__toolsnaps__/pull_request_read.snap
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@
"description": "Cursor for pagination, used only by the get_review_comments method. Pass the endCursor from the previous page's PageInfo to fetch the next page.",
"type": "string"
},
"fields": {
"description": "Fields to return for each changed file. Only applies when method is 'get_files'. When a nonempty list is provided, only the listed fields are returned. If 'fields' is omitted or empty, all available fields are returned, including patches.",
"items": {
"enum": [
"filename",
"status",
"additions",
"deletions",
"changes",
"patch",
"previous_filename"
],
"type": "string"
},
"type": "array"
},
"method": {
"description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get combined commit status of a head commit in a pull request.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_commits - Get the list of commits on a pull request. Use with pagination parameters to control the number of results returned.\n 6. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns thread metadata and comments with nullable current and original line-range coordinates (line, start_line, original_line, original_start_line). Current coordinates are omitted when unavailable, such as for outdated comments. Use cursor-based pagination (perPage, after) to control results.\n 7. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. Use with pagination parameters to control the number of results returned.\n 8. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.\n 9. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.\n",
"enum": [
Expand Down
7 changes: 7 additions & 0 deletions pkg/github/minimal_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ var codeSearchItemFieldEnum = []any{"name", "path", "sha", "repository", "text_m
// the requested path is a directory; ignored for single files.
var fileContentFieldEnum = []any{"type", "name", "path", "size", "sha", "url", "git_url", "html_url", "download_url"}

// pullRequestFilesItemFieldEnum lists the selectable fields for pull_request_read
// get_files results, matching MinimalPRFile. Omitting patch reduces response size
// when only filenames or file metadata are needed.
var pullRequestFilesItemFieldEnum = []any{
"filename", "status", "additions", "deletions", "changes", "patch", "previous_filename",
}

// listIssuesItemFieldEnum lists the selectable fields for list_issues result
// items, matching the JSON field names MinimalIssue actually populates via the
// list_issues GraphQL fragment (fragmentToMinimalIssue). Fields that only the
Expand Down
19 changes: 17 additions & 2 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ Possible options:
Required: []string{"method", "owner", "repo", "pullNumber"},
}
WithPagination(schema)
schema.Properties["fields"] = fieldsSchemaProperty(
"Fields to return for each changed file. Only applies when method is 'get_files'. When a nonempty list is provided, only the listed fields are returned. If 'fields' is omitted or empty, all available fields are returned, including patches.",
pullRequestFilesItemFieldEnum,
)
// get_review_comments uses GraphQL cursor-based pagination and accepts the
// `after` cursor. Other methods rely on the `page`/`perPage` parameters
// added by WithPagination and ignore `after`.
Expand Down Expand Up @@ -128,7 +132,11 @@ Possible options:
result, err := GetPullRequestStatus(ctx, client, owner, repo, pullNumber)
return attachIFC(result), nil, err
case "get_files":
result, err := GetPullRequestFiles(ctx, client, deps, owner, repo, pullNumber, pagination)
fields, err := OptionalStringArrayParam(args, "fields")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
result, err := GetPullRequestFiles(ctx, client, deps, owner, repo, pullNumber, pagination, fields)
return attachIFC(result), nil, err
case "get_commits":
result, err := GetPullRequestCommits(ctx, client, deps, owner, repo, pullNumber, pagination)
Expand Down Expand Up @@ -369,7 +377,7 @@ func GetPullRequestCheckRuns(ctx context.Context, client *github.Client, owner,
return utils.NewToolResultText(string(r)), nil
}

func GetPullRequestFiles(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
func GetPullRequestFiles(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int, pagination PaginationParams, fields []string) (*mcp.CallToolResult, error) {
if restricted, err := enforcePullRequestLockdown(ctx, client, deps, owner, repo, pullNumber); restricted != nil || err != nil {
return restricted, err
}
Expand Down Expand Up @@ -397,6 +405,13 @@ func GetPullRequestFiles(ctx context.Context, client *github.Client, deps ToolDe
}

minimalFiles := convertToMinimalPRFiles(files)
if len(fields) > 0 {
filteredFiles, err := filterEachField(minimalFiles, fields)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to filter pull request files", err), nil
}
return MarshalledTextResult(filteredFiles), nil
}

return MarshalledTextResult(minimalFiles), nil
}
Expand Down
107 changes: 107 additions & 0 deletions pkg/github/pullrequests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,113 @@ func Test_GetPullRequestFiles(t *testing.T) {
}
}

func Test_GetPullRequestFiles_Fields(t *testing.T) {
files := []*github.CommitFile{
{
Filename: github.Ptr("new.go"),
Status: github.Ptr("renamed"),
Additions: github.Ptr(2),
Deletions: github.Ptr(1),
Changes: github.Ptr(3),
Patch: github.Ptr("@@ -1 +1,2 @@\n-old\n+new\n+line"),
PreviousFilename: github.Ptr("old.go"),
},
{
Filename: github.Ptr("image.png"),
Status: github.Ptr("added"),
},
}
full := `[{"filename":"new.go","status":"renamed","additions":2,"deletions":1,"changes":3,"patch":"@@ -1 +1,2 @@\n-old\n+new\n+line","previous_filename":"old.go"},{"filename":"image.png","status":"added"}]`

for _, tc := range []struct {
name string
fields any
omit bool
empty bool
want string
wantErr string
}{
{
name: "omitted fields preserves patches",
omit: true,
want: full,
},
{
name: "empty fields preserves patches",
fields: []any{},
want: full,
},
{
name: "filenames only",
fields: []any{"filename"},
want: `[{"filename":"new.go"},{"filename":"image.png"}]`,
},
{
name: "metadata without patches",
fields: []any{"filename", "status", "changes", "previous_filename"},
want: `[{"filename":"new.go","status":"renamed","changes":3,"previous_filename":"old.go"},{"filename":"image.png","status":"added"}]`,
},
{
name: "explicit patch with absent patch omitted",
fields: []any{"filename", "patch"},
want: `[{"filename":"new.go","patch":"@@ -1 +1,2 @@\n-old\n+new\n+line"},{"filename":"image.png"}]`,
},
{
name: "empty results",
fields: []any{"filename"},
empty: true,
want: `[]`,
},
{
name: "invalid fields type",
fields: "filename",
wantErr: "fields",
},
{
name: "invalid field element",
fields: []any{42},
wantErr: "fields",
},
} {
t.Run(tc.name, func(t *testing.T) {
response := files
if tc.empty {
response = []*github.CommitFile{}
}
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetReposPullsFilesByOwnerByRepoByPullNumber: func(w http.ResponseWriter, r *http.Request) {
require.Empty(t, tc.wantErr, "invalid fields must be rejected before fetching files")
expectQueryParams(t, map[string]string{"page": "2", "per_page": "10"}).andThen(mockResponse(t, http.StatusOK, response))(w, r)
},
}))
deps := BaseDeps{Client: client}
args := map[string]any{
"method": "get_files",
"owner": "owner",
"repo": "repo",
"pullNumber": float64(42),
"page": float64(2),
"perPage": float64(10),
}
if !tc.omit {
args["fields"] = tc.fields
}
request := createMCPRequest(args)
tool := PullRequestRead(translations.NullTranslationHelper)
handler := tool.Handler(deps)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
if tc.wantErr != "" {
require.True(t, result.IsError)
assert.Contains(t, getErrorResult(t, result).Text, tc.wantErr)
return
}
require.False(t, result.IsError)
assert.JSONEq(t, tc.want, getTextResult(t, result).Text)
})
}
}

func Test_GetPullRequestCommits(t *testing.T) {
// Verify tool definition once
serverTool := PullRequestRead(translations.NullTranslationHelper)
Expand Down