From 66e34311826bf5ae306f73f4b7ad3a113cedc281 Mon Sep 17 00:00:00 2001 From: kerobbi Date: Wed, 25 Feb 2026 18:30:20 +0000 Subject: [PATCH 1/2] reduce context usage for list_issues --- pkg/github/issues.go | 64 +++---------------------------------- pkg/github/issues_test.go | 23 +++---------- pkg/github/minimal_types.go | 50 ++++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 80 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index ce3c4e945..b5bc4ebb8 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -201,33 +201,6 @@ func getIssueQueryType(hasLabels bool, hasSince bool) any { } } -func fragmentToIssue(fragment IssueFragment) *github.Issue { - // Convert GraphQL labels to GitHub API labels format - var foundLabels []*github.Label - for _, labelNode := range fragment.Labels.Nodes { - foundLabels = append(foundLabels, &github.Label{ - Name: github.Ptr(string(labelNode.Name)), - NodeID: github.Ptr(string(labelNode.ID)), - Description: github.Ptr(string(labelNode.Description)), - }) - } - - return &github.Issue{ - Number: github.Ptr(int(fragment.Number)), - Title: github.Ptr(sanitize.Sanitize(string(fragment.Title))), - CreatedAt: &github.Timestamp{Time: fragment.CreatedAt.Time}, - UpdatedAt: &github.Timestamp{Time: fragment.UpdatedAt.Time}, - User: &github.User{ - Login: github.Ptr(string(fragment.Author.Login)), - }, - State: github.Ptr(string(fragment.State)), - ID: github.Ptr(fragment.DatabaseID), - Body: github.Ptr(sanitize.Sanitize(string(fragment.Body))), - Labels: foundLabels, - Comments: github.Ptr(int(fragment.Comments.TotalCount)), - } -} - // IssueRead creates a tool to get details of a specific issue in a GitHub repository. func IssueRead(t translations.TranslationHelperFunc) inventory.ServerTool { schema := &jsonschema.Schema{ @@ -1584,41 +1557,12 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool { ), nil, nil } - // Extract and convert all issue nodes using the common interface - var issues []*github.Issue - var pageInfo struct { - HasNextPage githubv4.Boolean - HasPreviousPage githubv4.Boolean - StartCursor githubv4.String - EndCursor githubv4.String - } - var totalCount int - + var resp MinimalIssuesResponse if queryResult, ok := issueQuery.(IssueQueryResult); ok { - fragment := queryResult.GetIssueFragment() - for _, issue := range fragment.Nodes { - issues = append(issues, fragmentToIssue(issue)) - } - pageInfo = fragment.PageInfo - totalCount = fragment.TotalCount - } - - // Create response with issues - response := map[string]any{ - "issues": issues, - "pageInfo": map[string]any{ - "hasNextPage": pageInfo.HasNextPage, - "hasPreviousPage": pageInfo.HasPreviousPage, - "startCursor": string(pageInfo.StartCursor), - "endCursor": string(pageInfo.EndCursor), - }, - "totalCount": totalCount, + resp = convertToMinimalIssuesResponse(queryResult.GetIssueFragment()) } - out, err := json.Marshal(response) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal issues: %w", err) - } - return utils.NewToolResultText(string(out)), nil, nil + + return MarshalledTextResult(resp), nil, nil }) } diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index f9b8c7c62..036a6553c 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -1187,7 +1187,6 @@ func Test_ListIssues(t *testing.T) { expectError bool errContains string expectedCount int - verifyOrder func(t *testing.T, issues []*github.Issue) }{ { name: "list all issues", @@ -1296,31 +1295,17 @@ func Test_ListIssues(t *testing.T) { require.NoError(t, err) // Parse the structured response with pagination info - var response struct { - Issues []*github.Issue `json:"issues"` - PageInfo struct { - HasNextPage bool `json:"hasNextPage"` - HasPreviousPage bool `json:"hasPreviousPage"` - StartCursor string `json:"startCursor"` - EndCursor string `json:"endCursor"` - } `json:"pageInfo"` - TotalCount int `json:"totalCount"` - } + var response MinimalIssuesResponse err = json.Unmarshal([]byte(text), &response) require.NoError(t, err) assert.Len(t, response.Issues, tc.expectedCount, "Expected %d issues, got %d", tc.expectedCount, len(response.Issues)) - // Verify order if verifyOrder function is provided - if tc.verifyOrder != nil { - tc.verifyOrder(t, response.Issues) - } - // Verify that returned issues have expected structure for _, issue := range response.Issues { - assert.NotNil(t, issue.Number, "Issue should have number") - assert.NotNil(t, issue.Title, "Issue should have title") - assert.NotNil(t, issue.State, "Issue should have state") + assert.NotZero(t, issue.Number, "Issue should have number") + assert.NotEmpty(t, issue.Title, "Issue should have title") + assert.NotEmpty(t, issue.State, "Issue should have state") } }) } diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go index 3eabf2163..5c635ac0b 100644 --- a/pkg/github/minimal_types.go +++ b/pkg/github/minimal_types.go @@ -4,6 +4,8 @@ import ( "time" "github.com/google/go-github/v82/github" + + "github.com/github/github-mcp-server/pkg/sanitize" ) // MinimalUser is the output type for user and organization search results. @@ -176,7 +178,7 @@ type MinimalIssue struct { StateReason string `json:"state_reason,omitempty"` Draft bool `json:"draft,omitempty"` Locked bool `json:"locked,omitempty"` - HTMLURL string `json:"html_url"` + HTMLURL string `json:"html_url,omitempty"` User *MinimalUser `json:"user,omitempty"` AuthorAssociation string `json:"author_association,omitempty"` Labels []string `json:"labels,omitempty"` @@ -191,6 +193,13 @@ type MinimalIssue struct { IssueType string `json:"issue_type,omitempty"` } +// MinimalIssuesResponse is the trimmed output for a paginated list of issues. +type MinimalIssuesResponse struct { + Issues []MinimalIssue `json:"issues"` + TotalCount int `json:"total_count"` + PageInfo MinimalPageInfo `json:"page_info"` +} + // MinimalIssueComment is the trimmed output type for issue comment objects to reduce verbosity. type MinimalIssueComment struct { ID int64 `json:"id"` @@ -376,6 +385,45 @@ func convertToMinimalIssue(issue *github.Issue) MinimalIssue { return m } +func fragmentToMinimalIssue(fragment IssueFragment) MinimalIssue { + m := MinimalIssue{ + Number: int(fragment.Number), + Title: sanitize.Sanitize(string(fragment.Title)), + Body: sanitize.Sanitize(string(fragment.Body)), + State: string(fragment.State), + Comments: int(fragment.Comments.TotalCount), + CreatedAt: fragment.CreatedAt.Format(time.RFC3339), + UpdatedAt: fragment.UpdatedAt.Format(time.RFC3339), + User: &MinimalUser{ + Login: string(fragment.Author.Login), + }, + } + + for _, label := range fragment.Labels.Nodes { + m.Labels = append(m.Labels, string(label.Name)) + } + + return m +} + +func convertToMinimalIssuesResponse(fragment IssueQueryFragment) MinimalIssuesResponse { + minimalIssues := make([]MinimalIssue, 0, len(fragment.Nodes)) + for _, issue := range fragment.Nodes { + minimalIssues = append(minimalIssues, fragmentToMinimalIssue(issue)) + } + + return MinimalIssuesResponse{ + Issues: minimalIssues, + TotalCount: fragment.TotalCount, + PageInfo: MinimalPageInfo{ + HasNextPage: bool(fragment.PageInfo.HasNextPage), + HasPreviousPage: bool(fragment.PageInfo.HasPreviousPage), + StartCursor: string(fragment.PageInfo.StartCursor), + EndCursor: string(fragment.PageInfo.EndCursor), + }, + } +} + func convertToMinimalIssueComment(comment *github.IssueComment) MinimalIssueComment { m := MinimalIssueComment{ ID: comment.GetID(), From 5d2628acb5f1d5246ca0bfa05e9af8a1d23731a6 Mon Sep 17 00:00:00 2001 From: kerobbi Date: Wed, 25 Feb 2026 20:00:46 +0000 Subject: [PATCH 2/2] address copilot feedback, align pagination tags to camelCase --- pkg/github/issues_test.go | 15 +++++++++++++++ pkg/github/minimal_types.go | 16 ++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 036a6553c..e78a03fcb 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -1301,11 +1301,26 @@ func Test_ListIssues(t *testing.T) { assert.Len(t, response.Issues, tc.expectedCount, "Expected %d issues, got %d", tc.expectedCount, len(response.Issues)) + // Verify pagination metadata + assert.Equal(t, tc.expectedCount, response.TotalCount) + assert.False(t, response.PageInfo.HasNextPage) + assert.False(t, response.PageInfo.HasPreviousPage) + // Verify that returned issues have expected structure for _, issue := range response.Issues { assert.NotZero(t, issue.Number, "Issue should have number") assert.NotEmpty(t, issue.Title, "Issue should have title") assert.NotEmpty(t, issue.State, "Issue should have state") + assert.NotEmpty(t, issue.CreatedAt, "Issue should have created_at") + assert.NotEmpty(t, issue.UpdatedAt, "Issue should have updated_at") + assert.NotNil(t, issue.User, "Issue should have user") + assert.NotEmpty(t, issue.User.Login, "Issue user should have login") + assert.Empty(t, issue.HTMLURL, "html_url should be empty (not populated by GraphQL fragment)") + + // Labels should be flattened to name strings + for _, label := range issue.Labels { + assert.NotEmpty(t, label, "Label should be a non-empty string") + } } }) } diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go index 5c635ac0b..a8757c51c 100644 --- a/pkg/github/minimal_types.go +++ b/pkg/github/minimal_types.go @@ -196,8 +196,8 @@ type MinimalIssue struct { // MinimalIssuesResponse is the trimmed output for a paginated list of issues. type MinimalIssuesResponse struct { Issues []MinimalIssue `json:"issues"` - TotalCount int `json:"total_count"` - PageInfo MinimalPageInfo `json:"page_info"` + TotalCount int `json:"totalCount"` + PageInfo MinimalPageInfo `json:"pageInfo"` } // MinimalIssueComment is the trimmed output type for issue comment objects to reduce verbosity. @@ -698,10 +698,10 @@ func convertToMinimalCommit(commit *github.RepositoryCommit, includeDiffs bool) // MinimalPageInfo contains pagination cursor information. type MinimalPageInfo struct { - HasNextPage bool `json:"has_next_page"` - HasPreviousPage bool `json:"has_previous_page"` - StartCursor string `json:"start_cursor,omitempty"` - EndCursor string `json:"end_cursor,omitempty"` + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + StartCursor string `json:"startCursor,omitempty"` + EndCursor string `json:"endCursor,omitempty"` } // MinimalReviewComment is the trimmed output type for PR review comment objects. @@ -727,8 +727,8 @@ type MinimalReviewThread struct { // MinimalReviewThreadsResponse is the trimmed output for a paginated list of PR review threads. type MinimalReviewThreadsResponse struct { ReviewThreads []MinimalReviewThread `json:"review_threads"` - TotalCount int `json:"total_count"` - PageInfo MinimalPageInfo `json:"page_info"` + TotalCount int `json:"totalCount"` + PageInfo MinimalPageInfo `json:"pageInfo"` } func convertToMinimalPRFiles(files []*github.CommitFile) []MinimalPRFile {