Skip to content

Commit 78c5531

Browse files
shivasuryaclaude
andcommitted
feat(mcp): add cursor-based pagination for large result sets
Implement pagination support for tools that return potentially large result sets: - Add PaginateSlice generic function for slicing with cursor support - Add EncodeCursor/DecodeCursor for opaque base64-encoded cursors - Add ExtractPaginationParams for validation and default limits - Update find_symbol, get_callers, get_callees to support pagination - Default limit: 50, max limit: 500 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent cce2c7b commit 78c5531

File tree

4 files changed

+529
-58
lines changed

4 files changed

+529
-58
lines changed

sast-engine/mcp/pagination.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package mcp
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"fmt"
7+
)
8+
9+
// Default and max limits.
10+
const (
11+
DefaultLimit = 50
12+
MaxLimit = 500
13+
)
14+
15+
// PaginationParams holds pagination parameters from request.
16+
type PaginationParams struct {
17+
Limit int `json:"limit"`
18+
Cursor string `json:"cursor"`
19+
}
20+
21+
// PaginationInfo holds pagination metadata for response.
22+
type PaginationInfo struct {
23+
Total int `json:"total"`
24+
Returned int `json:"returned"`
25+
HasMore bool `json:"hasMore"`
26+
NextCursor string `json:"nextCursor,omitempty"`
27+
}
28+
29+
// Cursor represents an opaque pagination cursor.
30+
type Cursor struct {
31+
Offset int `json:"o"`
32+
Query string `json:"q,omitempty"`
33+
}
34+
35+
// EncodeCursor creates an opaque cursor string.
36+
func EncodeCursor(offset int, query string) string {
37+
c := Cursor{Offset: offset, Query: query}
38+
bytes, _ := json.Marshal(c)
39+
return base64.URLEncoding.EncodeToString(bytes)
40+
}
41+
42+
// DecodeCursor parses a cursor string.
43+
func DecodeCursor(cursor string) (*Cursor, error) {
44+
if cursor == "" {
45+
return &Cursor{Offset: 0}, nil
46+
}
47+
48+
bytes, err := base64.URLEncoding.DecodeString(cursor)
49+
if err != nil {
50+
return nil, fmt.Errorf("invalid cursor: %w", err)
51+
}
52+
53+
var c Cursor
54+
if err := json.Unmarshal(bytes, &c); err != nil {
55+
return nil, fmt.Errorf("invalid cursor format: %w", err)
56+
}
57+
58+
return &c, nil
59+
}
60+
61+
// ExtractPaginationParams extracts and validates pagination params.
62+
func ExtractPaginationParams(args map[string]interface{}) (*PaginationParams, *RPCError) {
63+
params := &PaginationParams{
64+
Limit: DefaultLimit,
65+
}
66+
67+
// Extract limit.
68+
if limitVal, ok := args["limit"]; ok {
69+
switch v := limitVal.(type) {
70+
case float64:
71+
params.Limit = int(v)
72+
case int:
73+
params.Limit = v
74+
default:
75+
return nil, InvalidParamsError("limit must be a number")
76+
}
77+
}
78+
79+
// Validate limit.
80+
if params.Limit <= 0 {
81+
params.Limit = DefaultLimit
82+
}
83+
if params.Limit > MaxLimit {
84+
params.Limit = MaxLimit
85+
}
86+
87+
// Extract cursor.
88+
if cursorVal, ok := args["cursor"].(string); ok {
89+
params.Cursor = cursorVal
90+
}
91+
92+
return params, nil
93+
}
94+
95+
// PaginateSlice applies pagination to a slice of any type.
96+
func PaginateSlice[T any](items []T, params *PaginationParams) ([]T, *PaginationInfo) {
97+
total := len(items)
98+
99+
// Decode cursor to get offset.
100+
cursor, err := DecodeCursor(params.Cursor)
101+
if err != nil {
102+
cursor = &Cursor{Offset: 0}
103+
}
104+
105+
offset := cursor.Offset
106+
limit := params.Limit
107+
108+
// Bounds check.
109+
if offset >= total {
110+
return []T{}, &PaginationInfo{
111+
Total: total,
112+
Returned: 0,
113+
HasMore: false,
114+
}
115+
}
116+
117+
end := offset + limit
118+
if end > total {
119+
end = total
120+
}
121+
122+
result := items[offset:end]
123+
hasMore := end < total
124+
125+
info := &PaginationInfo{
126+
Total: total,
127+
Returned: len(result),
128+
HasMore: hasMore,
129+
}
130+
131+
if hasMore {
132+
info.NextCursor = EncodeCursor(end, cursor.Query)
133+
}
134+
135+
return result, info
136+
}
137+
138+
// PaginatedResult wraps results with pagination info.
139+
type PaginatedResult struct {
140+
Items interface{} `json:"items"`
141+
Pagination PaginationInfo `json:"pagination"`
142+
}
143+
144+
// NewPaginatedResult creates a paginated result.
145+
func NewPaginatedResult(items interface{}, info *PaginationInfo) *PaginatedResult {
146+
return &PaginatedResult{
147+
Items: items,
148+
Pagination: *info,
149+
}
150+
}

0 commit comments

Comments
 (0)