Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@ func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (
mcp.Description("Sort order"),
mcp.Enum("asc", "desc"),
),
WithPagination(),
WithFixedCursorPagination(),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return searchHandler(ctx, getClient, request, "issue", "failed to search issues")
Expand Down
2 changes: 1 addition & 1 deletion pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@ func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperF
mcp.Description("Sort order"),
mcp.Enum("asc", "desc"),
),
WithPagination(),
WithFixedCursorPagination(),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return searchHandler(ctx, getClient, request, "pr", "failed to search pull requests")
Expand Down
89 changes: 84 additions & 5 deletions pkg/github/search_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package github

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -73,18 +74,27 @@ func searchHandler(
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
pagination, err := OptionalPaginationParams(request)
pagination, err := OptionalFixedCursorPaginationParams(request)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

// WithFixedCursorPagination: fetch exactly pageSize items, use TotalCount to determine if there's more
pageSize := pagination.PerPage
// Determine current page from After cursor
page := 1
if pagination.After != "" {
decoded, err := decodePageCursor(pagination.After)
if err == nil && decoded > 0 {
page = decoded
}
}
opts := &github.SearchOptions{
// Default to "created" if no sort is provided, as it's a common use case.
Sort: sort,
Order: order,
ListOptions: github.ListOptions{
Page: pagination.Page,
PerPage: pagination.PerPage,
Page: page,
PerPage: pageSize,
},
}

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

r, err := json.Marshal(result)
// Prepare paginated results
items := result.Issues
totalCount := result.GetTotal()

// Calculate if there's a next page based on total count and current position
currentItemCount := len(items)
itemsSeenSoFar := (page-1)*pageSize + currentItemCount
hasNextPage := itemsSeenSoFar < totalCount

nextCursor := ""
if hasNextPage {
nextPage := page + 1
nextCursor = encodePageCursor(nextPage)
}

pageInfo := struct {
HasNextPage bool `json:"hasNextPage"`
EndCursor string `json:"endCursor,omitempty"`
}{
HasNextPage: hasNextPage,
EndCursor: nextCursor,
}

response := struct {
TotalCount int `json:"totalCount"`
IncompleteResults bool `json:"incompleteResults"`
Items []*github.Issue `json:"items"`
PageInfo interface{} `json:"pageInfo"`
}{
TotalCount: totalCount,
IncompleteResults: result.GetIncompleteResults(),
Items: items,
PageInfo: pageInfo,
}

r, err := json.Marshal(response)
if err != nil {
return nil, fmt.Errorf("%s: failed to marshal response: %w", errorPrefix, err)
}

return mcp.NewToolResultText(string(r)), nil
}

// encodePageCursor encodes the page number as a base64 string
func encodePageCursor(page int) string {
s := fmt.Sprintf("page=%d", page)
return b64Encode(s)
}

// decodePageCursor decodes a base64 cursor and extracts the page number
func decodePageCursor(cursor string) (int, error) {
data, err := b64Decode(cursor)
if err != nil {
return 1, err
}
var page int
n, err := fmt.Sscanf(data, "page=%d", &page)
if err != nil || n != 1 {
return 1, fmt.Errorf("invalid cursor format")
}
return page, nil
}

// b64Encode encodes a string to base64
func b64Encode(s string) string {
return base64.StdEncoding.EncodeToString([]byte(s))
}

// b64Decode decodes a base64 string
func b64Decode(s string) (string, error) {
data, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", err
}
return string(data), nil
}
22 changes: 22 additions & 0 deletions pkg/github/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,15 @@ func WithUnifiedPagination() mcp.ToolOption {
}
}

// WithFixedCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter).
func WithFixedCursorPagination() mcp.ToolOption {
return func(tool *mcp.Tool) {
mcp.WithString("cursor",
mcp.Description("Cursor for pagination. Use the endCursor from the previous page's PageInfo."),
)(tool)
}
}

// WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter).
func WithCursorPagination() mcp.ToolOption {
return func(tool *mcp.Tool) {
Expand Down Expand Up @@ -273,6 +282,19 @@ func OptionalPaginationParams(r mcp.CallToolRequest) (PaginationParams, error) {
}, nil
}

// OptionalFixedCursorPaginationParams returns the "perPage" and "after" parameters from the request,
// without the "page" parameter, suitable for cursor-based pagination only.
func OptionalFixedCursorPaginationParams(r mcp.CallToolRequest) (CursorPaginationParams, error) {
cursor, err := OptionalParam[string](r, "cursor")
if err != nil {
return CursorPaginationParams{}, err
}
return CursorPaginationParams{
PerPage: 10,
After: cursor,
}, nil
}

// OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request,
// without the "page" parameter, suitable for cursor-based pagination only.
func OptionalCursorPaginationParams(r mcp.CallToolRequest) (CursorPaginationParams, error) {
Expand Down
Loading