Skip to content

Commit ae6dc68

Browse files
committed
Updating pagination for Graphql ListIssues
1 parent 18553d5 commit ae6dc68

File tree

1 file changed

+113
-44
lines changed

1 file changed

+113
-44
lines changed

pkg/github/issues.go

Lines changed: 113 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,34 @@ import (
1818
"github.com/shurcooL/githubv4"
1919
)
2020

21+
var ListIssuesQuery struct {
22+
Repository struct {
23+
Issues struct {
24+
Nodes []struct {
25+
Number githubv4.Int
26+
Title githubv4.String
27+
Body githubv4.String
28+
Author struct {
29+
Login githubv4.String
30+
}
31+
CreatedAt githubv4.DateTime
32+
Labels struct {
33+
Nodes []struct {
34+
Name githubv4.String
35+
}
36+
} `graphql:"labels(first: 10)"`
37+
}
38+
PageInfo struct {
39+
HasNextPage githubv4.Boolean
40+
HasPreviousPage githubv4.Boolean
41+
StartCursor githubv4.String
42+
EndCursor githubv4.String
43+
}
44+
TotalCount int
45+
} `graphql:"issues(first: $first, after: $after, states: $states, orderBy: {field: $orderBy, direction: $direction})"`
46+
} `graphql:"repository(owner: $owner, name: $repo)"`
47+
}
48+
2149
// GetIssue creates a tool to get details of a specific issue in a GitHub repository.
2250
func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
2351
return mcp.NewTool("get_issue",
@@ -726,7 +754,7 @@ func CreateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t
726754
// ListIssues creates a tool to list and filter repository issues
727755
func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
728756
return mcp.NewTool("list_issues",
729-
mcp.WithDescription(t("TOOL_LIST_ISSUES_DESCRIPTION", "List issues in a GitHub repository.")),
757+
mcp.WithDescription(t("TOOL_LIST_ISSUES_DESCRIPTION", "List issues in a GitHub repository. For pagination, use the 'endCursor' from the previous response's 'pageInfo' in the 'after' parameter.")),
730758
mcp.WithToolAnnotation(mcp.ToolAnnotation{
731759
Title: t("TOOL_LIST_ISSUES_USER_TITLE", "List issues"),
732760
ReadOnlyHint: ToBoolPtr(true),
@@ -740,6 +768,7 @@ func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun
740768
mcp.Description("Repository name"),
741769
),
742770
mcp.WithString("state",
771+
mcp.Required(),
743772
mcp.Description("Filter by state"),
744773
mcp.Enum("OPEN", "CLOSED"),
745774
),
@@ -752,17 +781,17 @@ func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun
752781
),
753782
),
754783
mcp.WithString("orderBy",
755-
mcp.Description("Order discussions by field. If provided, the 'direction' also needs to be provided."),
784+
mcp.Description("Order issues by field. If provided, the 'direction' also needs to be provided."),
756785
mcp.Enum("CREATED_AT", "UPDATED_AT"),
757786
),
758787
mcp.WithString("direction",
759-
mcp.Description("Order direction."),
788+
mcp.Description("Order direction. If provided, the 'orderBy' also needs to be provided."),
760789
mcp.Enum("ASC", "DESC"),
761790
),
762791
mcp.WithString("since",
763792
mcp.Description("Filter by date (ISO 8601 timestamp)"),
764793
),
765-
WithPagination(),
794+
WithCursorPagination(),
766795
),
767796
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
768797
owner, err := RequiredParam[string](request, "owner")
@@ -775,16 +804,13 @@ func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun
775804
}
776805

777806
// Set optional parameters if provided
778-
state, err := OptionalParam[string](request, "state")
807+
state, err := RequiredParam[string](request, "state")
779808
if err != nil {
780809
return mcp.NewToolResultError(err.Error()), nil
781810
}
782-
if state == "" {
783-
state = "OPEN" // Default to OPEN if not provided
784-
}
785811

786812
// Get labels
787-
//labels, err := OptionalStringArrayParam(request, "labels")
813+
labels, err := OptionalStringArrayParam(request, "labels")
788814
if err != nil {
789815
return mcp.NewToolResultError(err.Error()), nil
790816
}
@@ -793,6 +819,7 @@ func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun
793819
if err != nil {
794820
return mcp.NewToolResultError(err.Error()), nil
795821
}
822+
//If orderBy is empty, default to CREATED_AT
796823
if orderBy == "" {
797824
orderBy = "CREATED_AT"
798825
}
@@ -801,6 +828,7 @@ func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun
801828
if err != nil {
802829
return mcp.NewToolResultError(err.Error()), nil
803830
}
831+
//If direction is empty, default to DESC
804832
if direction == "" {
805833
direction = "DESC"
806834
}
@@ -809,64 +837,99 @@ func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun
809837
if err != nil {
810838
return mcp.NewToolResultError(err.Error()), nil
811839
}
840+
841+
var sinceTime time.Time
812842
if since != "" {
813-
//timestamp, err := parseISOTimestamp(since)
843+
sinceTime, err = parseISOTimestamp(since)
814844
if err != nil {
815845
return mcp.NewToolResultError(fmt.Sprintf("failed to list issues: %s", err.Error())), nil
816846
}
817-
//since = timestamp
847+
}
848+
// Get pagination parameters and convert to GraphQL format
849+
pagination, err := OptionalCursorPaginationParams(request)
850+
if err != nil {
851+
return nil, err
852+
}
853+
854+
// Check if someone tried to use page-based pagination instead of cursor-based
855+
if _, pageProvided := request.GetArguments()["page"]; pageProvided {
856+
return mcp.NewToolResultError("This tool uses cursor-based pagination. Use the 'after' parameter with the 'endCursor' value from the previous response instead of 'page'."), nil
818857
}
819858

820-
//if page, ok := request.GetArguments()["page"].(float64); ok {
821-
//listOptions.Page = int(page)
822-
//}
859+
// Check if pagination parameters were explicitly provided
860+
_, perPageProvided := request.GetArguments()["perPage"]
861+
paginationExplicit := perPageProvided
823862

824-
//if perPage, ok := request.GetArguments()["perPage"].(float64); ok {
825-
//.PerPage = int(perPage)
826-
//}
863+
paginationParams, err := pagination.ToGraphQLParams()
864+
if err != nil {
865+
return nil, err
866+
}
867+
868+
if paginationParams.After == nil {
869+
defaultAfter := string("")
870+
paginationParams.After = &defaultAfter
871+
}
872+
873+
// Use default of 30 if pagination was not explicitly provided
874+
if !paginationExplicit {
875+
defaultFirst := int32(DefaultGraphQLPageSize)
876+
paginationParams.First = &defaultFirst
877+
}
827878

828879
client, err := getGQLClient(ctx)
829880
if err != nil {
830881
return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil
831882
}
832883

833-
var q struct {
834-
Repository struct {
835-
Issues struct {
836-
Nodes []struct {
837-
Number githubv4.Int
838-
Title githubv4.String
839-
Body githubv4.String
840-
Author struct {
841-
Login githubv4.String
842-
}
843-
CreatedAt githubv4.DateTime
844-
Labels struct {
845-
Nodes []struct {
846-
Name githubv4.String
847-
}
848-
} `graphql:"labels(first: 10)"`
849-
}
850-
} `graphql:"issues(first: $first, states: $states, orderBy: {field: $orderBy, direction: $direction})"`
851-
} `graphql:"repository(owner: $owner, name: $repo)"`
852-
}
853884
vars := map[string]interface{}{
854885
"owner": githubv4.String(owner),
855886
"repo": githubv4.String(repo),
856-
"first": githubv4.Int(100),
857887
"states": []githubv4.IssueState{githubv4.IssueState(state)},
858888
"orderBy": githubv4.IssueOrderField(orderBy),
859889
"direction": githubv4.OrderDirection(direction),
890+
"first": githubv4.Int(*paginationParams.First),
891+
}
892+
893+
if paginationParams.After != nil {
894+
vars["after"] = githubv4.String(*paginationParams.After)
895+
} else {
896+
vars["after"] = (*githubv4.String)(nil)
860897
}
861-
if err := client.Query(ctx, &q, vars); err != nil {
898+
899+
if err := client.Query(ctx, &ListIssuesQuery, vars); err != nil {
862900
return mcp.NewToolResultError(err.Error()), nil
863901
}
864902

903+
//We must filter based on labels after fetching all issues
865904
var issues []map[string]interface{}
866-
for _, issue := range q.Repository.Issues.Nodes {
867-
var labels []string
905+
for _, issue := range ListIssuesQuery.Repository.Issues.Nodes {
906+
var issueLabels []string
868907
for _, label := range issue.Labels.Nodes {
869-
labels = append(labels, string(label.Name))
908+
issueLabels = append(issueLabels, string(label.Name))
909+
}
910+
911+
// Filter by since date if specified
912+
if !sinceTime.IsZero() && issue.CreatedAt.Time.Before(sinceTime) {
913+
continue // Skip issues created before the since date
914+
}
915+
916+
// Filter by labels if specified
917+
if len(labels) > 0 {
918+
hasMatchingLabel := false
919+
for _, requestedLabel := range labels {
920+
for _, issueLabel := range issueLabels {
921+
if strings.EqualFold(requestedLabel, issueLabel) {
922+
hasMatchingLabel = true
923+
break
924+
}
925+
}
926+
if hasMatchingLabel {
927+
break
928+
}
929+
}
930+
if !hasMatchingLabel {
931+
continue // Skip this issue as it doesn't match any requested labels
932+
}
870933
}
871934

872935
issues = append(issues, map[string]interface{}{
@@ -875,15 +938,21 @@ func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun
875938
"body": string(issue.Body),
876939
"author": string(issue.Author.Login),
877940
"createdAt": issue.CreatedAt.Time,
878-
"labels": labels,
941+
"labels": issueLabels,
879942
})
880943
}
881944

882945
// Create response with issues
883946
response := map[string]interface{}{
884947
"issues": issues,
948+
"pageInfo": map[string]interface{}{
949+
"hasNextPage": ListIssuesQuery.Repository.Issues.PageInfo.HasNextPage,
950+
"hasPreviousPage": ListIssuesQuery.Repository.Issues.PageInfo.HasPreviousPage,
951+
"startCursor": string(ListIssuesQuery.Repository.Issues.PageInfo.StartCursor),
952+
"endCursor": string(ListIssuesQuery.Repository.Issues.PageInfo.EndCursor),
953+
},
954+
"totalCount": ListIssuesQuery.Repository.Issues.TotalCount,
885955
}
886-
887956
out, err := json.Marshal(response)
888957
if err != nil {
889958
return nil, fmt.Errorf("failed to marshal issues: %w", err)

0 commit comments

Comments
 (0)