Skip to content

Commit ad8a9d1

Browse files
experiment with fake cursor pagination approach
1 parent bc4555f commit ad8a9d1

File tree

4 files changed

+108
-7
lines changed

4 files changed

+108
-7
lines changed

pkg/github/issues.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,7 @@ func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (
831831
mcp.Description("Sort order"),
832832
mcp.Enum("asc", "desc"),
833833
),
834-
WithPagination(),
834+
WithFixedCursorPagination(),
835835
),
836836
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
837837
return searchHandler(ctx, getClient, request, "issue", "failed to search issues")

pkg/github/pullrequests.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,7 @@ func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperF
968968
mcp.Description("Sort order"),
969969
mcp.Enum("asc", "desc"),
970970
),
971-
WithPagination(),
971+
WithFixedCursorPagination(),
972972
),
973973
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
974974
return searchHandler(ctx, getClient, request, "pr", "failed to search pull requests")

pkg/github/search_utils.go

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package github
22

33
import (
44
"context"
5+
"encoding/base64"
56
"encoding/json"
67
"fmt"
78
"io"
@@ -73,18 +74,27 @@ func searchHandler(
7374
if err != nil {
7475
return mcp.NewToolResultError(err.Error()), nil
7576
}
76-
pagination, err := OptionalPaginationParams(request)
77+
pagination, err := OptionalFixedCursorPaginationParams(request)
7778
if err != nil {
7879
return mcp.NewToolResultError(err.Error()), nil
7980
}
8081

82+
// WithFixedCursorPagination: fetch exactly pageSize items, use TotalCount to determine if there's more
83+
pageSize := pagination.PerPage
84+
// Determine current page from After cursor
85+
page := 1
86+
if pagination.After != "" {
87+
decoded, err := decodePageCursor(pagination.After)
88+
if err == nil && decoded > 0 {
89+
page = decoded
90+
}
91+
}
8192
opts := &github.SearchOptions{
82-
// Default to "created" if no sort is provided, as it's a common use case.
8393
Sort: sort,
8494
Order: order,
8595
ListOptions: github.ListOptions{
86-
Page: pagination.Page,
87-
PerPage: pagination.PerPage,
96+
Page: page,
97+
PerPage: pageSize,
8898
},
8999
}
90100

@@ -106,10 +116,79 @@ func searchHandler(
106116
return mcp.NewToolResultError(fmt.Sprintf("%s: %s", errorPrefix, string(body))), nil
107117
}
108118

109-
r, err := json.Marshal(result)
119+
// Prepare paginated results
120+
items := result.Issues
121+
totalCount := result.GetTotal()
122+
123+
// Calculate if there's a next page based on total count and current position
124+
currentItemCount := len(items)
125+
itemsSeenSoFar := (page-1)*pageSize + currentItemCount
126+
hasNextPage := itemsSeenSoFar < totalCount
127+
128+
nextCursor := ""
129+
if hasNextPage {
130+
nextPage := page + 1
131+
nextCursor = encodePageCursor(nextPage)
132+
}
133+
134+
pageInfo := struct {
135+
HasNextPage bool `json:"hasNextPage"`
136+
EndCursor string `json:"endCursor,omitempty"`
137+
}{
138+
HasNextPage: hasNextPage,
139+
EndCursor: nextCursor,
140+
}
141+
142+
response := struct {
143+
TotalCount int `json:"totalCount"`
144+
IncompleteResults bool `json:"incompleteResults"`
145+
Items []*github.Issue `json:"items"`
146+
PageInfo interface{} `json:"pageInfo"`
147+
}{
148+
TotalCount: totalCount,
149+
IncompleteResults: result.GetIncompleteResults(),
150+
Items: items,
151+
PageInfo: pageInfo,
152+
}
153+
154+
r, err := json.Marshal(response)
110155
if err != nil {
111156
return nil, fmt.Errorf("%s: failed to marshal response: %w", errorPrefix, err)
112157
}
113158

114159
return mcp.NewToolResultText(string(r)), nil
115160
}
161+
162+
// encodePageCursor encodes the page number as a base64 string
163+
func encodePageCursor(page int) string {
164+
s := fmt.Sprintf("page=%d", page)
165+
return b64Encode(s)
166+
}
167+
168+
// decodePageCursor decodes a base64 cursor and extracts the page number
169+
func decodePageCursor(cursor string) (int, error) {
170+
data, err := b64Decode(cursor)
171+
if err != nil {
172+
return 1, err
173+
}
174+
var page int
175+
n, err := fmt.Sscanf(data, "page=%d", &page)
176+
if err != nil || n != 1 {
177+
return 1, fmt.Errorf("invalid cursor format")
178+
}
179+
return page, nil
180+
}
181+
182+
// b64Encode encodes a string to base64
183+
func b64Encode(s string) string {
184+
return base64.StdEncoding.EncodeToString([]byte(s))
185+
}
186+
187+
// b64Decode decodes a base64 string
188+
func b64Decode(s string) (string, error) {
189+
data, err := base64.StdEncoding.DecodeString(s)
190+
if err != nil {
191+
return "", err
192+
}
193+
return string(data), nil
194+
}

pkg/github/server.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,15 @@ func WithUnifiedPagination() mcp.ToolOption {
227227
}
228228
}
229229

230+
// WithFixedCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter).
231+
func WithFixedCursorPagination() mcp.ToolOption {
232+
return func(tool *mcp.Tool) {
233+
mcp.WithString("cursor",
234+
mcp.Description("Cursor for pagination. Use the endCursor from the previous page's PageInfo."),
235+
)(tool)
236+
}
237+
}
238+
230239
// WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter).
231240
func WithCursorPagination() mcp.ToolOption {
232241
return func(tool *mcp.Tool) {
@@ -273,6 +282,19 @@ func OptionalPaginationParams(r mcp.CallToolRequest) (PaginationParams, error) {
273282
}, nil
274283
}
275284

285+
// OptionalFixedCursorPaginationParams returns the "perPage" and "after" parameters from the request,
286+
// without the "page" parameter, suitable for cursor-based pagination only.
287+
func OptionalFixedCursorPaginationParams(r mcp.CallToolRequest) (CursorPaginationParams, error) {
288+
cursor, err := OptionalParam[string](r, "cursor")
289+
if err != nil {
290+
return CursorPaginationParams{}, err
291+
}
292+
return CursorPaginationParams{
293+
PerPage: 10,
294+
After: cursor,
295+
}, nil
296+
}
297+
276298
// OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request,
277299
// without the "page" parameter, suitable for cursor-based pagination only.
278300
func OptionalCursorPaginationParams(r mcp.CallToolRequest) (CursorPaginationParams, error) {

0 commit comments

Comments
 (0)