Skip to content

Commit bf64678

Browse files
authored
Reduce context usage for list_issues (#2098)
* reduce context usage for list_issues * address copilot feedback, align pagination tags to camelCase
1 parent b222072 commit bf64678

File tree

3 files changed

+77
-85
lines changed

3 files changed

+77
-85
lines changed

pkg/github/issues.go

Lines changed: 4 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -201,33 +201,6 @@ func getIssueQueryType(hasLabels bool, hasSince bool) any {
201201
}
202202
}
203203

204-
func fragmentToIssue(fragment IssueFragment) *github.Issue {
205-
// Convert GraphQL labels to GitHub API labels format
206-
var foundLabels []*github.Label
207-
for _, labelNode := range fragment.Labels.Nodes {
208-
foundLabels = append(foundLabels, &github.Label{
209-
Name: github.Ptr(string(labelNode.Name)),
210-
NodeID: github.Ptr(string(labelNode.ID)),
211-
Description: github.Ptr(string(labelNode.Description)),
212-
})
213-
}
214-
215-
return &github.Issue{
216-
Number: github.Ptr(int(fragment.Number)),
217-
Title: github.Ptr(sanitize.Sanitize(string(fragment.Title))),
218-
CreatedAt: &github.Timestamp{Time: fragment.CreatedAt.Time},
219-
UpdatedAt: &github.Timestamp{Time: fragment.UpdatedAt.Time},
220-
User: &github.User{
221-
Login: github.Ptr(string(fragment.Author.Login)),
222-
},
223-
State: github.Ptr(string(fragment.State)),
224-
ID: github.Ptr(fragment.DatabaseID),
225-
Body: github.Ptr(sanitize.Sanitize(string(fragment.Body))),
226-
Labels: foundLabels,
227-
Comments: github.Ptr(int(fragment.Comments.TotalCount)),
228-
}
229-
}
230-
231204
// IssueRead creates a tool to get details of a specific issue in a GitHub repository.
232205
func IssueRead(t translations.TranslationHelperFunc) inventory.ServerTool {
233206
schema := &jsonschema.Schema{
@@ -1584,41 +1557,12 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
15841557
), nil, nil
15851558
}
15861559

1587-
// Extract and convert all issue nodes using the common interface
1588-
var issues []*github.Issue
1589-
var pageInfo struct {
1590-
HasNextPage githubv4.Boolean
1591-
HasPreviousPage githubv4.Boolean
1592-
StartCursor githubv4.String
1593-
EndCursor githubv4.String
1594-
}
1595-
var totalCount int
1596-
1560+
var resp MinimalIssuesResponse
15971561
if queryResult, ok := issueQuery.(IssueQueryResult); ok {
1598-
fragment := queryResult.GetIssueFragment()
1599-
for _, issue := range fragment.Nodes {
1600-
issues = append(issues, fragmentToIssue(issue))
1601-
}
1602-
pageInfo = fragment.PageInfo
1603-
totalCount = fragment.TotalCount
1604-
}
1605-
1606-
// Create response with issues
1607-
response := map[string]any{
1608-
"issues": issues,
1609-
"pageInfo": map[string]any{
1610-
"hasNextPage": pageInfo.HasNextPage,
1611-
"hasPreviousPage": pageInfo.HasPreviousPage,
1612-
"startCursor": string(pageInfo.StartCursor),
1613-
"endCursor": string(pageInfo.EndCursor),
1614-
},
1615-
"totalCount": totalCount,
1562+
resp = convertToMinimalIssuesResponse(queryResult.GetIssueFragment())
16161563
}
1617-
out, err := json.Marshal(response)
1618-
if err != nil {
1619-
return nil, nil, fmt.Errorf("failed to marshal issues: %w", err)
1620-
}
1621-
return utils.NewToolResultText(string(out)), nil, nil
1564+
1565+
return MarshalledTextResult(resp), nil, nil
16221566
})
16231567
}
16241568

pkg/github/issues_test.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,7 +1187,6 @@ func Test_ListIssues(t *testing.T) {
11871187
expectError bool
11881188
errContains string
11891189
expectedCount int
1190-
verifyOrder func(t *testing.T, issues []*github.Issue)
11911190
}{
11921191
{
11931192
name: "list all issues",
@@ -1296,31 +1295,32 @@ func Test_ListIssues(t *testing.T) {
12961295
require.NoError(t, err)
12971296

12981297
// Parse the structured response with pagination info
1299-
var response struct {
1300-
Issues []*github.Issue `json:"issues"`
1301-
PageInfo struct {
1302-
HasNextPage bool `json:"hasNextPage"`
1303-
HasPreviousPage bool `json:"hasPreviousPage"`
1304-
StartCursor string `json:"startCursor"`
1305-
EndCursor string `json:"endCursor"`
1306-
} `json:"pageInfo"`
1307-
TotalCount int `json:"totalCount"`
1308-
}
1298+
var response MinimalIssuesResponse
13091299
err = json.Unmarshal([]byte(text), &response)
13101300
require.NoError(t, err)
13111301

13121302
assert.Len(t, response.Issues, tc.expectedCount, "Expected %d issues, got %d", tc.expectedCount, len(response.Issues))
13131303

1314-
// Verify order if verifyOrder function is provided
1315-
if tc.verifyOrder != nil {
1316-
tc.verifyOrder(t, response.Issues)
1317-
}
1304+
// Verify pagination metadata
1305+
assert.Equal(t, tc.expectedCount, response.TotalCount)
1306+
assert.False(t, response.PageInfo.HasNextPage)
1307+
assert.False(t, response.PageInfo.HasPreviousPage)
13181308

13191309
// Verify that returned issues have expected structure
13201310
for _, issue := range response.Issues {
1321-
assert.NotNil(t, issue.Number, "Issue should have number")
1322-
assert.NotNil(t, issue.Title, "Issue should have title")
1323-
assert.NotNil(t, issue.State, "Issue should have state")
1311+
assert.NotZero(t, issue.Number, "Issue should have number")
1312+
assert.NotEmpty(t, issue.Title, "Issue should have title")
1313+
assert.NotEmpty(t, issue.State, "Issue should have state")
1314+
assert.NotEmpty(t, issue.CreatedAt, "Issue should have created_at")
1315+
assert.NotEmpty(t, issue.UpdatedAt, "Issue should have updated_at")
1316+
assert.NotNil(t, issue.User, "Issue should have user")
1317+
assert.NotEmpty(t, issue.User.Login, "Issue user should have login")
1318+
assert.Empty(t, issue.HTMLURL, "html_url should be empty (not populated by GraphQL fragment)")
1319+
1320+
// Labels should be flattened to name strings
1321+
for _, label := range issue.Labels {
1322+
assert.NotEmpty(t, label, "Label should be a non-empty string")
1323+
}
13241324
}
13251325
})
13261326
}

pkg/github/minimal_types.go

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"time"
55

66
"github.com/google/go-github/v82/github"
7+
8+
"github.com/github/github-mcp-server/pkg/sanitize"
79
)
810

911
// MinimalUser is the output type for user and organization search results.
@@ -176,7 +178,7 @@ type MinimalIssue struct {
176178
StateReason string `json:"state_reason,omitempty"`
177179
Draft bool `json:"draft,omitempty"`
178180
Locked bool `json:"locked,omitempty"`
179-
HTMLURL string `json:"html_url"`
181+
HTMLURL string `json:"html_url,omitempty"`
180182
User *MinimalUser `json:"user,omitempty"`
181183
AuthorAssociation string `json:"author_association,omitempty"`
182184
Labels []string `json:"labels,omitempty"`
@@ -191,6 +193,13 @@ type MinimalIssue struct {
191193
IssueType string `json:"issue_type,omitempty"`
192194
}
193195

196+
// MinimalIssuesResponse is the trimmed output for a paginated list of issues.
197+
type MinimalIssuesResponse struct {
198+
Issues []MinimalIssue `json:"issues"`
199+
TotalCount int `json:"totalCount"`
200+
PageInfo MinimalPageInfo `json:"pageInfo"`
201+
}
202+
194203
// MinimalIssueComment is the trimmed output type for issue comment objects to reduce verbosity.
195204
type MinimalIssueComment struct {
196205
ID int64 `json:"id"`
@@ -376,6 +385,45 @@ func convertToMinimalIssue(issue *github.Issue) MinimalIssue {
376385
return m
377386
}
378387

388+
func fragmentToMinimalIssue(fragment IssueFragment) MinimalIssue {
389+
m := MinimalIssue{
390+
Number: int(fragment.Number),
391+
Title: sanitize.Sanitize(string(fragment.Title)),
392+
Body: sanitize.Sanitize(string(fragment.Body)),
393+
State: string(fragment.State),
394+
Comments: int(fragment.Comments.TotalCount),
395+
CreatedAt: fragment.CreatedAt.Format(time.RFC3339),
396+
UpdatedAt: fragment.UpdatedAt.Format(time.RFC3339),
397+
User: &MinimalUser{
398+
Login: string(fragment.Author.Login),
399+
},
400+
}
401+
402+
for _, label := range fragment.Labels.Nodes {
403+
m.Labels = append(m.Labels, string(label.Name))
404+
}
405+
406+
return m
407+
}
408+
409+
func convertToMinimalIssuesResponse(fragment IssueQueryFragment) MinimalIssuesResponse {
410+
minimalIssues := make([]MinimalIssue, 0, len(fragment.Nodes))
411+
for _, issue := range fragment.Nodes {
412+
minimalIssues = append(minimalIssues, fragmentToMinimalIssue(issue))
413+
}
414+
415+
return MinimalIssuesResponse{
416+
Issues: minimalIssues,
417+
TotalCount: fragment.TotalCount,
418+
PageInfo: MinimalPageInfo{
419+
HasNextPage: bool(fragment.PageInfo.HasNextPage),
420+
HasPreviousPage: bool(fragment.PageInfo.HasPreviousPage),
421+
StartCursor: string(fragment.PageInfo.StartCursor),
422+
EndCursor: string(fragment.PageInfo.EndCursor),
423+
},
424+
}
425+
}
426+
379427
func convertToMinimalIssueComment(comment *github.IssueComment) MinimalIssueComment {
380428
m := MinimalIssueComment{
381429
ID: comment.GetID(),
@@ -650,10 +698,10 @@ func convertToMinimalCommit(commit *github.RepositoryCommit, includeDiffs bool)
650698

651699
// MinimalPageInfo contains pagination cursor information.
652700
type MinimalPageInfo struct {
653-
HasNextPage bool `json:"has_next_page"`
654-
HasPreviousPage bool `json:"has_previous_page"`
655-
StartCursor string `json:"start_cursor,omitempty"`
656-
EndCursor string `json:"end_cursor,omitempty"`
701+
HasNextPage bool `json:"hasNextPage"`
702+
HasPreviousPage bool `json:"hasPreviousPage"`
703+
StartCursor string `json:"startCursor,omitempty"`
704+
EndCursor string `json:"endCursor,omitempty"`
657705
}
658706

659707
// MinimalReviewComment is the trimmed output type for PR review comment objects.
@@ -679,8 +727,8 @@ type MinimalReviewThread struct {
679727
// MinimalReviewThreadsResponse is the trimmed output for a paginated list of PR review threads.
680728
type MinimalReviewThreadsResponse struct {
681729
ReviewThreads []MinimalReviewThread `json:"review_threads"`
682-
TotalCount int `json:"total_count"`
683-
PageInfo MinimalPageInfo `json:"page_info"`
730+
TotalCount int `json:"totalCount"`
731+
PageInfo MinimalPageInfo `json:"pageInfo"`
684732
}
685733

686734
func convertToMinimalPRFiles(files []*github.CommitFile) []MinimalPRFile {

0 commit comments

Comments
 (0)