From 5300483d3ce2566178465377396b257803928b61 Mon Sep 17 00:00:00 2001 From: adbcodes Date: Tue, 8 Sep 2026 04:24:28 +0530 Subject: [PATCH 1/2] Allow selecting fields in pull request file responses --- README.md | 1 + .../__toolsnaps__/pull_request_read.snap | 16 +++ pkg/github/minimal_types.go | 7 ++ pkg/github/pullrequests.go | 19 +++- pkg/github/pullrequests_test.go | 107 ++++++++++++++++++ 5 files changed, 148 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b90f64a58..05913f771e 100644 --- a/README.md +++ b/README.md @@ -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 include in each file returned by get_files. Use ["filename"] for paths only, or omit patch to reduce response size. Returns all fields when omitted or empty. Ignored by other methods. (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. diff --git a/pkg/github/__toolsnaps__/pull_request_read.snap b/pkg/github/__toolsnaps__/pull_request_read.snap index d518c7cad9..efaf29fcb3 100644 --- a/pkg/github/__toolsnaps__/pull_request_read.snap +++ b/pkg/github/__toolsnaps__/pull_request_read.snap @@ -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 include in each file returned by get_files. Use [\"filename\"] for paths only, or omit patch to reduce response size. Returns all fields when omitted or empty. Ignored by other methods.", + "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": [ diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go index 2eba9a1628..4724d50f56 100644 --- a/pkg/github/minimal_types.go +++ b/pkg/github/minimal_types.go @@ -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 diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 5cee8b3231..3391fac817 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -59,6 +59,10 @@ Possible options: Required: []string{"method", "owner", "repo", "pullNumber"}, } WithPagination(schema) + schema.Properties["fields"] = fieldsSchemaProperty( + "Fields to include in each file returned by get_files. Use [\"filename\"] for paths only, or omit patch to reduce response size. Returns all fields when omitted or empty. Ignored by other methods.", + 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`. @@ -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) @@ -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 } @@ -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 } diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index c0e392aea6..cd3ddd287e 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -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) From ee436bbf98d41ffa72c8d2cda8289b2de8d2fbb8 Mon Sep 17 00:00:00 2001 From: adbcodes Date: Tue, 8 Sep 2026 05:11:01 +0530 Subject: [PATCH 2/2] Clarify pull request file field selection description --- README.md | 2 +- pkg/github/__toolsnaps__/pull_request_read.snap | 2 +- pkg/github/pullrequests.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 05913f771e..6248493cdf 100644 --- a/README.md +++ b/README.md @@ -1267,7 +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 include in each file returned by get_files. Use ["filename"] for paths only, or omit patch to reduce response size. Returns all fields when omitted or empty. Ignored by other methods. (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. diff --git a/pkg/github/__toolsnaps__/pull_request_read.snap b/pkg/github/__toolsnaps__/pull_request_read.snap index efaf29fcb3..e0f30074f3 100644 --- a/pkg/github/__toolsnaps__/pull_request_read.snap +++ b/pkg/github/__toolsnaps__/pull_request_read.snap @@ -12,7 +12,7 @@ "type": "string" }, "fields": { - "description": "Fields to include in each file returned by get_files. Use [\"filename\"] for paths only, or omit patch to reduce response size. Returns all fields when omitted or empty. Ignored by other methods.", + "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", diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 3391fac817..2007f2598a 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -60,7 +60,7 @@ Possible options: } WithPagination(schema) schema.Properties["fields"] = fieldsSchemaProperty( - "Fields to include in each file returned by get_files. Use [\"filename\"] for paths only, or omit patch to reduce response size. Returns all fields when omitted or empty. Ignored by other methods.", + "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