Visitar URL original
Add opt-in PR readiness fields to list tools · github/github-mcp-server@61304ae · GitHub
Skip to content

Commit 61304ae

Browse files
committed
Add opt-in PR readiness fields to list tools
1 parent 85598ba commit 61304ae

8 files changed

Lines changed: 388 additions & 15 deletions

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,7 +1252,7 @@ The following sets of tools are available:
12521252
- **OAuth Challenge Scopes**: `repo`
12531253
- `base`: Filter by base branch (string, optional)
12541254
- `direction`: Sort direction (string, optional)
1255-
- `fields`: Subset of fields to return for each pull request. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields; omitting 'body' in particular drops the largest per-result data. (string[], optional)
1255+
- `fields`: Subset of fields to return for each pull request. If omitted, all standard fields are returned. Select review_decision for the nullable review decision and status_check_rollup for the nullable aggregate check state of the PR's latest commit; these add one batch GraphQL request per page. Omitting 'body' drops the largest per-result data. (string[], optional)
12561256
- `head`: Filter by head user/org and branch (string, optional)
12571257
- `owner`: Repository owner (string, required)
12581258
- `page`: Page number for pagination (min 1) (number, optional)
@@ -1305,7 +1305,7 @@ The following sets of tools are available:
13051305

13061306
- **search_pull_requests** - Search pull requests
13071307
- **OAuth Challenge Scopes**: `repo`
1308-
- `fields`: Subset of fields to return for each pull request result. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields; omitting 'body', 'reactions', and 'labels' in particular drops the largest per-result data. (string[], optional)
1308+
- `fields`: Subset of fields to return for each pull request result. If omitted, all standard fields are returned. Select review_decision for the nullable review decision and status_check_rollup for the nullable aggregate check state of the PR's latest commit; these add one batch GraphQL request per page. Omitting 'body', 'reactions', and 'labels' drops the largest per-result data. (string[], optional)
13091309
- `order`: Sort order (string, optional)
13101310
- `owner`: Optional repository owner. If provided with repo, only pull requests for this repository are listed. (string, optional)
13111311
- `page`: Page number for pagination (min 1) (number, optional)

‎pkg/github/__toolsnaps__/list_pull_requests.snap‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"type": "string"
2121
},
2222
"fields": {
23-
"description": "Subset of fields to return for each pull request. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields; omitting 'body' in particular drops the largest per-result data.",
23+
"description": "Subset of fields to return for each pull request. If omitted, all standard fields are returned. Select review_decision for the nullable review decision and status_check_rollup for the nullable aggregate check state of the PR's latest commit; these add one batch GraphQL request per page. Omitting 'body' drops the largest per-result data.",
2424
"items": {
2525
"enum": [
2626
"number",
@@ -47,7 +47,9 @@
4747
"updated_at",
4848
"closed_at",
4949
"merged_at",
50-
"milestone"
50+
"milestone",
51+
"review_decision",
52+
"status_check_rollup"
5153
],
5254
"type": "string"
5355
},

‎pkg/github/__toolsnaps__/search_pull_requests.snap‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"inputSchema": {
99
"properties": {
1010
"fields": {
11-
"description": "Subset of fields to return for each pull request result. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields; omitting 'body', 'reactions', and 'labels' in particular drops the largest per-result data.",
11+
"description": "Subset of fields to return for each pull request result. If omitted, all standard fields are returned. Select review_decision for the nullable review decision and status_check_rollup for the nullable aggregate check state of the PR's latest commit; these add one batch GraphQL request per page. Omitting 'body', 'reactions', and 'labels' drops the largest per-result data.",
1212
"items": {
1313
"enum": [
1414
"number",
@@ -32,7 +32,9 @@
3232
"closed_at",
3333
"closed_by",
3434
"pull_request",
35-
"repository_url"
35+
"repository_url",
36+
"review_decision",
37+
"status_check_rollup"
3638
],
3739
"type": "string"
3840
},

‎pkg/github/minimal_types.go‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,15 @@ var listIssuesItemFieldEnum = []any{
4141
}
4242

4343
// listPullRequestsItemFieldEnum lists the selectable fields for
44-
// list_pull_requests result items, matching the JSON field names of
45-
// MinimalPullRequest. The body field is the heaviest, so omitting it is the main
46-
// lever for shrinking large result sets.
44+
// list_pull_requests result items. Most fields come from MinimalPullRequest;
45+
// review_decision and status_check_rollup are opt-in GraphQL enrichments. The
46+
// body field is the heaviest, so omitting it shrinks large result sets.
4747
var listPullRequestsItemFieldEnum = []any{
4848
"number", "title", "body", "state", "draft", "merged", "mergeable_state",
4949
"html_url", "user", "labels", "assignees", "requested_reviewers", "merged_by",
5050
"head", "base", "additions", "deletions", "changed_files", "commits",
5151
"comments", "created_at", "updated_at", "closed_at", "merged_at", "milestone",
52+
"review_decision", "status_check_rollup",
5253
}
5354

5455
// listCommitsItemFieldEnum lists the selectable fields for list_commits result
@@ -83,14 +84,15 @@ var searchIssuesItemFieldEnum = []any{
8384

8485
// searchPullRequestsItemFieldEnum lists the selectable fields for
8586
// search_pull_requests result items. Issue search returns pull requests as
86-
// github.Issue objects, so this is a curated subset of those JSON field names.
87-
// The body, reactions, and labels fields are the heaviest, so omitting them is
88-
// the main lever for shrinking large result sets.
87+
// github.Issue objects, so most fields are a curated subset of those JSON
88+
// names. review_decision and status_check_rollup are opt-in GraphQL enrichments.
89+
// Omitting body, reactions, and labels shrinks large result sets.
8990
var searchPullRequestsItemFieldEnum = []any{
9091
"number", "title", "body", "state", "state_reason", "draft", "locked",
9192
"html_url", "user", "author_association", "labels", "assignee", "assignees",
9293
"milestone", "comments", "reactions", "created_at", "updated_at", "closed_at",
9394
"closed_by", "pull_request", "repository_url",
95+
"review_decision", "status_check_rollup",
9496
}
9597

9698
// filterFields marshals v to a JSON object and returns a map containing only the

‎pkg/github/pullrequests.go‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,7 +1364,7 @@ func ListPullRequests(t translations.TranslationHelperFunc) inventory.ServerTool
13641364
Required: []string{"owner", "repo"},
13651365
}
13661366
schema.Properties["fields"] = fieldsSchemaProperty(
1367-
"Subset of fields to return for each pull request. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields; omitting 'body' in particular drops the largest per-result data.",
1367+
"Subset of fields to return for each pull request. If omitted, all standard fields are returned. Select review_decision for the nullable review decision and status_check_rollup for the nullable aggregate check state of the PR's latest commit; these add one batch GraphQL request per page. Omitting 'body' drops the largest per-result data.",
13681368
listPullRequestsItemFieldEnum,
13691369
)
13701370
WithPagination(schema)
@@ -1454,9 +1454,11 @@ func ListPullRequests(t translations.TranslationHelperFunc) inventory.ServerTool
14541454
}
14551455

14561456
minimalPRs := make([]MinimalPullRequest, 0, len(prs))
1457+
nodeIDs := make([]string, 0, len(prs))
14571458
for _, pr := range prs {
14581459
if pr != nil {
14591460
minimalPRs = append(minimalPRs, convertToMinimalPullRequest(pr))
1461+
nodeIDs = append(nodeIDs, pr.GetNodeID())
14601462
}
14611463
}
14621464

@@ -1467,6 +1469,15 @@ func ListPullRequests(t translations.TranslationHelperFunc) inventory.ServerTool
14671469
if err != nil {
14681470
return utils.NewToolResultErrorFromErr("failed to filter pull requests", err), nil, nil
14691471
}
1472+
if requestedPullRequestReadiness(fields) {
1473+
readiness, err := fetchPullRequestReadiness(ctx, deps, nodeIDs, fields)
1474+
if err != nil {
1475+
return utils.NewToolResultErrorFromErr("failed to fetch pull request readiness", err), nil, nil
1476+
}
1477+
if err := addPullRequestReadinessFields(filteredPRs, nodeIDs, fields, readiness); err != nil {
1478+
return utils.NewToolResultErrorFromErr("failed to enrich pull request readiness", err), nil, nil
1479+
}
1480+
}
14701481
payload = filteredPRs
14711482
filtered = true
14721483
}
@@ -1647,7 +1658,7 @@ func SearchPullRequests(t translations.TranslationHelperFunc) inventory.ServerTo
16471658
Required: []string{"query"},
16481659
}
16491660
schema.Properties["fields"] = fieldsSchemaProperty(
1650-
"Subset of fields to return for each pull request result. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields; omitting 'body', 'reactions', and 'labels' in particular drops the largest per-result data.",
1661+
"Subset of fields to return for each pull request result. If omitted, all standard fields are returned. Select review_decision for the nullable review decision and status_check_rollup for the nullable aggregate check state of the PR's latest commit; these add one batch GraphQL request per page. Omitting 'body', 'reactions', and 'labels' drops the largest per-result data.",
16511662
searchPullRequestsItemFieldEnum,
16521663
)
16531664
WithPagination(schema)
@@ -1671,6 +1682,21 @@ func SearchPullRequests(t translations.TranslationHelperFunc) inventory.ServerTo
16711682
return utils.NewToolResultError(err.Error()), nil, nil
16721683
}
16731684
options = append(options, withFieldsFiltering(deps, "search_pull_requests", fields))
1685+
if requestedPullRequestReadiness(fields) {
1686+
options = append(options, withSearchFieldsEnrichment(func(ctx context.Context, issues []*github.Issue, items []map[string]any) error {
1687+
nodeIDs := make([]string, len(issues))
1688+
for i, issue := range issues {
1689+
if issue != nil {
1690+
nodeIDs[i] = issue.GetNodeID()
1691+
}
1692+
}
1693+
readiness, err := fetchPullRequestReadiness(ctx, deps, nodeIDs, fields)
1694+
if err != nil {
1695+
return err
1696+
}
1697+
return addPullRequestReadinessFields(items, nodeIDs, fields, readiness)
1698+
}))
1699+
}
16741700
result, err := searchHandler(ctx, deps.GetClient, args, "pr", "failed to search pull requests", options...)
16751701
return result, nil, err
16761702
})
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/shurcooL/githubv4"
8+
)
9+
10+
// Pull request readiness is queried only for explicitly selected fields. REST
11+
// list/search responses carry node IDs, so one nodes query enriches the whole
12+
// page, including search pages spanning several repositories.
13+
type pullRequestReadinessQuery struct {
14+
Nodes []struct {
15+
PullRequest struct {
16+
ID githubv4.ID
17+
ReviewDecision githubv4.String `graphql:"reviewDecision @include(if: $includeReview)"`
18+
Commits struct {
19+
Nodes []struct {
20+
Commit struct {
21+
StatusCheckRollup struct {
22+
State githubv4.String
23+
}
24+
}
25+
}
26+
} `graphql:"commits(last: 1) @include(if: $includeChecks)"`
27+
} `graphql:"... on PullRequest"`
28+
} `graphql:"nodes(ids: $ids)"`
29+
}
30+
31+
type pullRequestReadiness struct {
32+
ReviewDecision *string
33+
StatusCheckRollup *string
34+
}
35+
36+
func requestedPullRequestReadiness(fields []string) bool {
37+
for _, field := range fields {
38+
if field == "review_decision" || field == "status_check_rollup" {
39+
return true
40+
}
41+
}
42+
return false
43+
}
44+
45+
func fetchPullRequestReadiness(ctx context.Context, deps ToolDependencies, nodeIDs []string, fields []string) (map[string]pullRequestReadiness, error) {
46+
if len(nodeIDs) == 0 {
47+
return nil, nil
48+
}
49+
ids := make([]githubv4.ID, len(nodeIDs))
50+
for i, id := range nodeIDs {
51+
if id == "" {
52+
return nil, fmt.Errorf("pull request readiness requires a node ID for item %d", i+1)
53+
}
54+
ids[i] = githubv4.ID(id)
55+
}
56+
client, err := deps.GetGQLClient(ctx)
57+
if err != nil {
58+
return nil, fmt.Errorf("get GraphQL client for pull request readiness: %w", err)
59+
}
60+
if client == nil {
61+
return nil, fmt.Errorf("GraphQL client for pull request readiness is unavailable")
62+
}
63+
var includeReview, includeChecks bool
64+
for _, field := range fields {
65+
switch field {
66+
case "review_decision":
67+
includeReview = true
68+
case "status_check_rollup":
69+
includeChecks = true
70+
}
71+
}
72+
var q pullRequestReadinessQuery
73+
if err := client.Query(ctx, &q, map[string]any{
74+
"ids": ids,
75+
"includeReview": githubv4.Boolean(includeReview),
76+
"includeChecks": githubv4.Boolean(includeChecks),
77+
}); err != nil {
78+
return nil, fmt.Errorf("query pull request readiness: %w", err)
79+
}
80+
result := make(map[string]pullRequestReadiness, len(q.Nodes))
81+
for _, node := range q.Nodes {
82+
id, ok := node.PullRequest.ID.(string)
83+
if !ok || id == "" {
84+
return nil, fmt.Errorf("pull request readiness response contained a missing or non-PR node")
85+
}
86+
var item pullRequestReadiness
87+
if value := string(node.PullRequest.ReviewDecision); value != "" {
88+
item.ReviewDecision = &value
89+
}
90+
if commits := node.PullRequest.Commits.Nodes; len(commits) > 0 {
91+
if value := string(commits[0].Commit.StatusCheckRollup.State); value != "" {
92+
item.StatusCheckRollup = &value
93+
}
94+
}
95+
result[id] = item
96+
}
97+
for _, id := range nodeIDs {
98+
if _, ok := result[id]; !ok {
99+
return nil, fmt.Errorf("pull request readiness response omitted node %s", id)
100+
}
101+
}
102+
return result, nil
103+
}
104+
105+
func addPullRequestReadinessFields(items []map[string]any, nodeIDs []string, fields []string, readiness map[string]pullRequestReadiness) error {
106+
if len(items) != len(nodeIDs) {
107+
return fmt.Errorf("pull request readiness item count mismatch")
108+
}
109+
for i, id := range nodeIDs {
110+
value, ok := readiness[id]
111+
if !ok {
112+
return fmt.Errorf("pull request readiness missing for node %s", id)
113+
}
114+
for _, field := range fields {
115+
switch field {
116+
case "review_decision":
117+
items[i][field] = value.ReviewDecision
118+
case "status_check_rollup":
119+
items[i][field] = value.StatusCheckRollup
120+
}
121+
}
122+
}
123+
return nil
124+
}

0 commit comments

Comments
 (0)