Skip to content

Commit 848479a

Browse files
authored
refactor(mcp): eliminate duplicate cursor cycle detection by delegating paginateAll to PaginateAll (#8049)
`paginateAll` and `PaginateAll` in `internal/mcp/pagination.go` each independently implemented cursor cycle detection and page-limit enforcement — identical safety logic duplicated across ~30 lines, with subtly divergent ordering and error messages. ## Changes - **`internal/mcp/pagination.go`** — `paginateAll` is now a thin adapter over `PaginateAll`. A closure bridges the `paginatedPage[T]` fetch signature to `PaginateAll`'s `([]T, string, error)` contract, adding per-page logging. Errors are wrapped with `serverID`/`itemKind` context to preserve richer diagnostics: ```go func paginateAll[T any](serverID, itemKind string, fetch func(cursor string) (paginatedPage[T], error)) ([]T, error) { result, err := PaginateAll(paginateAllMaxPages, func(cursor string) ([]T, string, error) { page, err := fetch(cursor) if err != nil { return nil, "", err } logConn.Printf("list%s: received page of %d %s from serverID=%s", itemKind, len(page.Items), itemKind, serverID) return page.Items, page.NextCursor, nil }) if err != nil { return nil, fmt.Errorf("list%s: backend serverID=%s: %w", itemKind, serverID, err) } logConn.Printf("list%s: received %d %s total from serverID=%s", itemKind, len(result), itemKind, serverID) return result, nil } ``` - **`internal/mcp/pagination_test.go`**, **`internal/mcp/connection_test.go`** — Updated two test cases whose error-message assertions referenced the old `paginateAll` wording (`"more than"` / `"pages"`) to match the `PaginateAll` format (`"exceeded"` / `"page limit"`). The mid-pagination "total so far" log is intentionally dropped; `PaginateAll` logs its own per-page progress, and the final total is still emitted after all pages are collected.
2 parents 3780159 + da8a2f7 commit 848479a

3 files changed

Lines changed: 20 additions & 32 deletions

File tree

internal/mcp/connection_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -990,8 +990,8 @@ func TestPaginateAll(t *testing.T) {
990990
return paginatedPage[string]{Items: []string{"x"}, NextCursor: nextCursor}, nil
991991
})
992992
require.Error(t, err)
993-
assert.ErrorContains(t, err, "more than")
994-
assert.ErrorContains(t, err, "pages")
993+
assert.ErrorContains(t, err, "exceeded")
994+
assert.ErrorContains(t, err, "page limit")
995995
// Must stop at the page limit, not run forever.
996996
assert.Equal(t, paginateAllMaxPages, callCount)
997997
})

internal/mcp/pagination.go

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -61,43 +61,30 @@ func PaginateAll[T any](maxPages int, fetch func(cursor string) ([]T, string, er
6161
}
6262

6363
// paginateAll collects all items across paginated SDK list calls.
64-
// It returns an error if the backend returns more than paginateAllMaxPages pages,
65-
// protecting against runaway backends.
66-
// The canonical shared algorithm is PaginateAll; paginateAll adds server-specific
67-
// logging and richer error context on top of it.
64+
// It delegates to PaginateAll for the core pagination algorithm (cursor cycle
65+
// detection and page-limit enforcement) and adds server-specific logging and
66+
// richer error context on top of it.
6867
func paginateAll[T any](
6968
serverID string,
7069
itemKind string,
7170
fetch func(cursor string) (paginatedPage[T], error),
7271
) ([]T, error) {
73-
first, err := fetch("")
74-
if err != nil {
75-
return nil, err
76-
}
77-
all := make([]T, len(first.Items), max(len(first.Items), 1))
78-
copy(all, first.Items)
79-
logConn.Printf("list%s: received page of %d %s from serverID=%s", itemKind, len(first.Items), itemKind, serverID)
80-
81-
cursor := first.NextCursor
82-
seenCursors := make(map[string]struct{})
83-
for pageCount := 1; cursor != ""; pageCount++ {
84-
if pageCount >= paginateAllMaxPages {
85-
return nil, fmt.Errorf("list%s: backend serverID=%s returned more than %d pages; aborting to prevent unbounded memory growth", itemKind, serverID, paginateAllMaxPages)
86-
}
87-
if _, seen := seenCursors[cursor]; seen {
88-
return nil, fmt.Errorf("list%s: backend serverID=%s returned cyclical cursor %q", itemKind, serverID, cursor)
89-
}
90-
seenCursors[cursor] = struct{}{}
72+
result, err := PaginateAll(paginateAllMaxPages, func(cursor string) ([]T, string, error) {
9173
page, err := fetch(cursor)
9274
if err != nil {
93-
return nil, err
75+
return nil, "", err
9476
}
95-
all = append(all, page.Items...)
96-
logConn.Printf("list%s: received page of %d %s (total so far: %d) from serverID=%s", itemKind, len(page.Items), itemKind, len(all), serverID)
97-
cursor = page.NextCursor
77+
logConn.Printf("list%s: received page of %d %s from serverID=%s", itemKind, len(page.Items), itemKind, serverID)
78+
return page.Items, page.NextCursor, nil
79+
})
80+
if err != nil {
81+
return nil, fmt.Errorf("list%s: backend serverID=%s: %w", itemKind, serverID, err)
9882
}
99-
logConn.Printf("list%s: received %d %s total from serverID=%s", itemKind, len(all), itemKind, serverID)
100-
return all, nil
83+
if result == nil {
84+
result = []T{}
85+
}
86+
logConn.Printf("list%s: received %d %s total from serverID=%s", itemKind, len(result), itemKind, serverID)
87+
return result, nil
10188
}
10289

10390
// listMCPItems is a generic helper for the list* family of MCP operations.

internal/mcp/pagination_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ func TestPaginateAllHelper(t *testing.T) {
246246
items, err := paginateAll("server1", "Tools", fetch)
247247

248248
require.NoError(t, err)
249+
assert.NotNil(t, items)
249250
assert.Empty(t, items)
250251
})
251252

@@ -377,8 +378,8 @@ func TestPaginateAllHelper(t *testing.T) {
377378
items, err := paginateAll("server1", "Tools", fetch)
378379

379380
require.Error(t, err)
380-
assert.ErrorContains(t, err, "more than")
381-
assert.ErrorContains(t, err, "pages")
381+
assert.ErrorContains(t, err, "exceeded")
382+
assert.ErrorContains(t, err, "page limit")
382383
assert.Nil(t, items)
383384
})
384385

0 commit comments

Comments
 (0)