Skip to content

Commit 3a88695

Browse files
Implement cursor-based pagination for MCP tools
- Replace page/perPage parameters with single cursor parameter - Fix page size at 10 items (fetch 11 to detect more data) - Add cursor parsing and encoding functions - Update all paginated tools to return items, moreData, and cursor fields - Preserve existing metadata for search results (totalCount, etc.) This addresses issue #1362 by providing deterministic cursor values that models can pass back directly without inferring page numbers.
1 parent 05e0f8f commit 3a88695

File tree

9 files changed

+256
-170
lines changed

9 files changed

+256
-170
lines changed

pkg/github/actions.go

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func ListWorkflows(getClient GetClientFn, t translations.TranslationHelperFunc)
6363

6464
// Set up list options
6565
opts := &github.ListOptions{
66-
PerPage: pagination.PerPage,
66+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
6767
Page: pagination.Page,
6868
}
6969

@@ -73,12 +73,7 @@ func ListWorkflows(getClient GetClientFn, t translations.TranslationHelperFunc)
7373
}
7474
defer func() { _ = resp.Body.Close() }()
7575

76-
r, err := json.Marshal(workflows)
77-
if err != nil {
78-
return nil, fmt.Errorf("failed to marshal response: %w", err)
79-
}
80-
81-
return mcp.NewToolResultText(string(r)), nil
76+
return CreatePaginatedResponse(workflows, pagination.Page)
8277
}
8378
}
8479

@@ -201,7 +196,7 @@ func ListWorkflowRuns(getClient GetClientFn, t translations.TranslationHelperFun
201196
Event: event,
202197
Status: status,
203198
ListOptions: github.ListOptions{
204-
PerPage: pagination.PerPage,
199+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
205200
Page: pagination.Page,
206201
},
207202
}
@@ -212,12 +207,7 @@ func ListWorkflowRuns(getClient GetClientFn, t translations.TranslationHelperFun
212207
}
213208
defer func() { _ = resp.Body.Close() }()
214209

215-
r, err := json.Marshal(workflowRuns)
216-
if err != nil {
217-
return nil, fmt.Errorf("failed to marshal response: %w", err)
218-
}
219-
220-
return mcp.NewToolResultText(string(r)), nil
210+
return CreatePaginatedResponse(workflowRuns, pagination.Page)
221211
}
222212
}
223213

@@ -504,7 +494,7 @@ func ListWorkflowJobs(getClient GetClientFn, t translations.TranslationHelperFun
504494
opts := &github.ListWorkflowJobsOptions{
505495
Filter: filter,
506496
ListOptions: github.ListOptions{
507-
PerPage: pagination.PerPage,
497+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
508498
Page: pagination.Page,
509499
},
510500
}
@@ -515,9 +505,24 @@ func ListWorkflowJobs(getClient GetClientFn, t translations.TranslationHelperFun
515505
}
516506
defer func() { _ = resp.Body.Close() }()
517507

518-
// Add optimization tip for failed job debugging
508+
// Note: This response includes optimization_tip, so we need to handle it differently
509+
// For now, we'll wrap jobs in pagination and include tip separately
510+
// Get the jobs array from the response
511+
paginatedJobs, err := CreatePaginatedResponse(jobs, pagination.Page)
512+
if err != nil {
513+
return nil, err
514+
}
515+
516+
// Parse the paginated response to add the tip
517+
var paginatedData PaginatedResponse
518+
if err := json.Unmarshal([]byte(paginatedJobs.GetText()), &paginatedData); err != nil {
519+
return paginatedJobs, nil // Return as-is if parsing fails
520+
}
521+
519522
response := map[string]any{
520-
"jobs": jobs,
523+
"items": paginatedData.Items,
524+
"moreData": paginatedData.MoreData,
525+
"cursor": paginatedData.Cursor,
521526
"optimization_tip": "For debugging failed jobs, consider using get_job_logs with failed_only=true and run_id=" + fmt.Sprintf("%d", runID) + " to get logs directly without needing to list jobs first",
522527
}
523528

@@ -1019,7 +1024,7 @@ func ListWorkflowRunArtifacts(getClient GetClientFn, t translations.TranslationH
10191024

10201025
// Set up list options
10211026
opts := &github.ListOptions{
1022-
PerPage: pagination.PerPage,
1027+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
10231028
Page: pagination.Page,
10241029
}
10251030

@@ -1029,12 +1034,7 @@ func ListWorkflowRunArtifacts(getClient GetClientFn, t translations.TranslationH
10291034
}
10301035
defer func() { _ = resp.Body.Close() }()
10311036

1032-
r, err := json.Marshal(artifacts)
1033-
if err != nil {
1034-
return nil, fmt.Errorf("failed to marshal response: %w", err)
1035-
}
1036-
1037-
return mcp.NewToolResultText(string(r)), nil
1037+
return CreatePaginatedResponse(artifacts, pagination.Page)
10381038
}
10391039
}
10401040

pkg/github/gists.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func ListGists(getClient GetClientFn, t translations.TranslationHelperFunc) (too
4848
opts := &github.GistListOptions{
4949
ListOptions: github.ListOptions{
5050
Page: pagination.Page,
51-
PerPage: pagination.PerPage,
51+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
5252
},
5353
}
5454

@@ -80,12 +80,7 @@ func ListGists(getClient GetClientFn, t translations.TranslationHelperFunc) (too
8080
return mcp.NewToolResultError(fmt.Sprintf("failed to list gists: %s", string(body))), nil
8181
}
8282

83-
r, err := json.Marshal(gists)
84-
if err != nil {
85-
return nil, fmt.Errorf("failed to marshal response: %w", err)
86-
}
87-
88-
return mcp.NewToolResultText(string(r)), nil
83+
return CreatePaginatedResponse(gists, pagination.Page)
8984
}
9085
}
9186

pkg/github/issues.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ func GetIssueComments(ctx context.Context, client *github.Client, owner string,
346346
opts := &github.IssueListCommentsOptions{
347347
ListOptions: github.ListOptions{
348348
Page: pagination.Page,
349-
PerPage: pagination.PerPage,
349+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
350350
},
351351
}
352352

@@ -364,19 +364,14 @@ func GetIssueComments(ctx context.Context, client *github.Client, owner string,
364364
return mcp.NewToolResultError(fmt.Sprintf("failed to get issue comments: %s", string(body))), nil
365365
}
366366

367-
r, err := json.Marshal(comments)
368-
if err != nil {
369-
return nil, fmt.Errorf("failed to marshal response: %w", err)
370-
}
371-
372-
return mcp.NewToolResultText(string(r)), nil
367+
return CreatePaginatedResponse(comments, pagination.Page)
373368
}
374369

375370
func GetSubIssues(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
376371
opts := &github.IssueListOptions{
377372
ListOptions: github.ListOptions{
378373
Page: pagination.Page,
379-
PerPage: pagination.PerPage,
374+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
380375
},
381376
}
382377

@@ -399,12 +394,7 @@ func GetSubIssues(ctx context.Context, client *github.Client, owner string, repo
399394
return mcp.NewToolResultError(fmt.Sprintf("failed to list sub-issues: %s", string(body))), nil
400395
}
401396

402-
r, err := json.Marshal(subIssues)
403-
if err != nil {
404-
return nil, fmt.Errorf("failed to marshal response: %w", err)
405-
}
406-
407-
return mcp.NewToolResultText(string(r)), nil
397+
return CreatePaginatedResponse(subIssues, pagination.Page)
408398
}
409399

410400
func GetIssueLabels(ctx context.Context, client *githubv4.Client, owner string, repo string, issueNumber int) (*mcp.CallToolResult, error) {

pkg/github/notifications.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFu
8989
Participating: filter == FilterOnlyParticipating,
9090
ListOptions: github.ListOptions{
9191
Page: paginationParams.Page,
92-
PerPage: paginationParams.PerPage,
92+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
9393
},
9494
}
9595

@@ -135,13 +135,7 @@ func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFu
135135
return mcp.NewToolResultError(fmt.Sprintf("failed to get notifications: %s", string(body))), nil
136136
}
137137

138-
// Marshal response to JSON
139-
r, err := json.Marshal(notifications)
140-
if err != nil {
141-
return nil, fmt.Errorf("failed to marshal response: %w", err)
142-
}
143-
144-
return mcp.NewToolResultText(string(r)), nil
138+
return CreatePaginatedResponse(notifications, paginationParams.Page)
145139
}
146140
}
147141

pkg/github/pullrequests.go

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, rep
220220

221221
func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
222222
opts := &github.ListOptions{
223-
PerPage: pagination.PerPage,
223+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
224224
Page: pagination.Page,
225225
}
226226
files, resp, err := client.PullRequests.ListFiles(ctx, owner, repo, pullNumber, opts)
@@ -241,18 +241,13 @@ func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo
241241
return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request files: %s", string(body))), nil
242242
}
243243

244-
r, err := json.Marshal(files)
245-
if err != nil {
246-
return nil, fmt.Errorf("failed to marshal response: %w", err)
247-
}
248-
249-
return mcp.NewToolResultText(string(r)), nil
244+
return CreatePaginatedResponse(files, pagination.Page)
250245
}
251246

252247
func GetPullRequestReviewComments(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
253248
opts := &github.PullRequestListCommentsOptions{
254249
ListOptions: github.ListOptions{
255-
PerPage: pagination.PerPage,
250+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
256251
Page: pagination.Page,
257252
},
258253
}
@@ -275,12 +270,7 @@ func GetPullRequestReviewComments(ctx context.Context, client *github.Client, ow
275270
return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request review comments: %s", string(body))), nil
276271
}
277272

278-
r, err := json.Marshal(comments)
279-
if err != nil {
280-
return nil, fmt.Errorf("failed to marshal response: %w", err)
281-
}
282-
283-
return mcp.NewToolResultText(string(r)), nil
273+
return CreatePaginatedResponse(comments, pagination.Page)
284274
}
285275

286276
func GetPullRequestReviews(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) {
@@ -788,7 +778,7 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun
788778
Sort: sort,
789779
Direction: direction,
790780
ListOptions: github.ListOptions{
791-
PerPage: pagination.PerPage,
781+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
792782
Page: pagination.Page,
793783
},
794784
}
@@ -828,12 +818,7 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun
828818
}
829819
}
830820

831-
r, err := json.Marshal(prs)
832-
if err != nil {
833-
return nil, fmt.Errorf("failed to marshal response: %w", err)
834-
}
835-
836-
return mcp.NewToolResultText(string(r)), nil
821+
return CreatePaginatedResponse(prs, pagination.Page)
837822
}
838823
}
839824

pkg/github/repositories.go

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (too
6767

6868
opts := &github.ListOptions{
6969
Page: pagination.Page,
70-
PerPage: pagination.PerPage,
70+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
7171
}
7272

7373
client, err := getClient(ctx)
@@ -149,17 +149,12 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t
149149
if err != nil {
150150
return mcp.NewToolResultError(err.Error()), nil
151151
}
152-
// Set default perPage to 30 if not provided
153-
perPage := pagination.PerPage
154-
if perPage == 0 {
155-
perPage = 30
156-
}
157152
opts := &github.CommitsListOptions{
158153
SHA: sha,
159154
Author: author,
160155
ListOptions: github.ListOptions{
161156
Page: pagination.Page,
162-
PerPage: perPage,
157+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
163158
},
164159
}
165160

@@ -191,12 +186,7 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t
191186
minimalCommits[i] = convertToMinimalCommit(commit, false)
192187
}
193188

194-
r, err := json.Marshal(minimalCommits)
195-
if err != nil {
196-
return nil, fmt.Errorf("failed to marshal response: %w", err)
197-
}
198-
199-
return mcp.NewToolResultText(string(r)), nil
189+
return CreatePaginatedResponse(minimalCommits, pagination.Page)
200190
}
201191
}
202192

@@ -235,7 +225,7 @@ func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) (
235225
opts := &github.BranchListOptions{
236226
ListOptions: github.ListOptions{
237227
Page: pagination.Page,
238-
PerPage: pagination.PerPage,
228+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
239229
},
240230
}
241231

@@ -268,12 +258,7 @@ func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) (
268258
minimalBranches = append(minimalBranches, convertToMinimalBranch(branch))
269259
}
270260

271-
r, err := json.Marshal(minimalBranches)
272-
if err != nil {
273-
return nil, fmt.Errorf("failed to marshal response: %w", err)
274-
}
275-
276-
return mcp.NewToolResultText(string(r)), nil
261+
return CreatePaginatedResponse(minimalBranches, pagination.Page)
277262
}
278263
}
279264

@@ -1256,7 +1241,7 @@ func ListTags(getClient GetClientFn, t translations.TranslationHelperFunc) (tool
12561241

12571242
opts := &github.ListOptions{
12581243
Page: pagination.Page,
1259-
PerPage: pagination.PerPage,
1244+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
12601245
}
12611246

12621247
client, err := getClient(ctx)
@@ -1282,12 +1267,7 @@ func ListTags(getClient GetClientFn, t translations.TranslationHelperFunc) (tool
12821267
return mcp.NewToolResultError(fmt.Sprintf("failed to list tags: %s", string(body))), nil
12831268
}
12841269

1285-
r, err := json.Marshal(tags)
1286-
if err != nil {
1287-
return nil, fmt.Errorf("failed to marshal response: %w", err)
1288-
}
1289-
1290-
return mcp.NewToolResultText(string(r)), nil
1270+
return CreatePaginatedResponse(tags, pagination.Page)
12911271
}
12921272
}
12931273

@@ -1412,7 +1392,7 @@ func ListReleases(getClient GetClientFn, t translations.TranslationHelperFunc) (
14121392

14131393
opts := &github.ListOptions{
14141394
Page: pagination.Page,
1415-
PerPage: pagination.PerPage,
1395+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
14161396
}
14171397

14181398
client, err := getClient(ctx)
@@ -1434,12 +1414,7 @@ func ListReleases(getClient GetClientFn, t translations.TranslationHelperFunc) (
14341414
return mcp.NewToolResultError(fmt.Sprintf("failed to list releases: %s", string(body))), nil
14351415
}
14361416

1437-
r, err := json.Marshal(releases)
1438-
if err != nil {
1439-
return nil, fmt.Errorf("failed to marshal response: %w", err)
1440-
}
1441-
1442-
return mcp.NewToolResultText(string(r)), nil
1417+
return CreatePaginatedResponse(releases, pagination.Page)
14431418
}
14441419
}
14451420

@@ -1741,7 +1716,7 @@ func ListStarredRepositories(getClient GetClientFn, t translations.TranslationHe
17411716
opts := &github.ActivityListStarredOptions{
17421717
ListOptions: github.ListOptions{
17431718
Page: pagination.Page,
1744-
PerPage: pagination.PerPage,
1719+
PerPage: CursorFetchSize, // Fetch one extra to detect if more data exists
17451720
},
17461721
}
17471722
if sort != "" {
@@ -1810,12 +1785,7 @@ func ListStarredRepositories(getClient GetClientFn, t translations.TranslationHe
18101785
minimalRepos = append(minimalRepos, minimalRepo)
18111786
}
18121787

1813-
r, err := json.Marshal(minimalRepos)
1814-
if err != nil {
1815-
return nil, fmt.Errorf("failed to marshal starred repositories: %w", err)
1816-
}
1817-
1818-
return mcp.NewToolResultText(string(r)), nil
1788+
return CreatePaginatedResponse(minimalRepos, pagination.Page)
18191789
}
18201790
}
18211791

0 commit comments

Comments
 (0)