@@ -191,10 +191,24 @@ func WithPagination() mcp.ToolOption {
191191 }
192192}
193193
194- // WithGraphQLPagination adds GraphQL cursor-based pagination parameters to a tool.
195- // https://docs.github.com/en/graphql/reference/objects#connection
196- func WithGraphQLPagination () mcp.ToolOption {
194+ // WithUnifiedPagination adds both REST and GraphQL pagination parameters to a tool.
195+ // This allows tools to accept both page/perPage and cursor-based pagination parameters.
196+ // GraphQL tools should use this and convert page/perPage to first/after internally.
197+ func WithUnifiedPagination () mcp.ToolOption {
197198 return func (tool * mcp.Tool ) {
199+ // REST API pagination parameters
200+ mcp .WithNumber ("page" ,
201+ mcp .Description ("Page number for pagination (min 1)" ),
202+ mcp .Min (1 ),
203+ )(tool )
204+
205+ mcp .WithNumber ("perPage" ,
206+ mcp .Description ("Results per page for pagination (min 1, max 100)" ),
207+ mcp .Min (1 ),
208+ mcp .Max (100 ),
209+ )(tool )
210+
211+ // GraphQL cursor-based pagination parameters
198212 mcp .WithNumber ("first" ,
199213 mcp .Description ("Number of items to return per page (min 1, max 100)" ),
200214 mcp .Min (1 ),
@@ -249,6 +263,110 @@ type GraphQLPaginationParams struct {
249263 Before * string
250264}
251265
266+ // UnifiedPaginationParams contains both REST and GraphQL pagination parameters
267+ type UnifiedPaginationParams struct {
268+ // REST API pagination
269+ Page int
270+ PerPage int
271+
272+ // GraphQL cursor-based pagination
273+ First * int32
274+ Last * int32
275+ After * string
276+ Before * string
277+ }
278+
279+ // ToGraphQLParams converts unified pagination parameters to GraphQL-specific parameters.
280+ // If cursor-based parameters (first/last/after/before) are provided, they take precedence.
281+ // Otherwise, page/perPage are converted to first/after equivalent.
282+ func (u UnifiedPaginationParams ) ToGraphQLParams () GraphQLPaginationParams {
283+ // If any cursor-based parameters are explicitly set, use them directly
284+ if u .First != nil || u .Last != nil || u .After != nil || u .Before != nil {
285+ return GraphQLPaginationParams {
286+ First : u .First ,
287+ Last : u .Last ,
288+ After : u .After ,
289+ Before : u .Before ,
290+ }
291+ }
292+
293+ // Convert page/perPage to GraphQL parameters
294+ // For GraphQL, we use 'first' for perPage and ignore page for the initial request
295+ // (subsequent requests would use 'after' cursor from previous response)
296+ first := int32 (u .PerPage )
297+ return GraphQLPaginationParams {
298+ First : & first ,
299+ Last : nil ,
300+ After : nil ,
301+ Before : nil ,
302+ }
303+ }
304+
305+ // OptionalUnifiedPaginationParams returns unified pagination parameters from the request.
306+ // It accepts both REST API (page/perPage) and GraphQL (first/last/after/before) parameters.
307+ func OptionalUnifiedPaginationParams (r mcp.CallToolRequest ) (UnifiedPaginationParams , error ) {
308+ var params UnifiedPaginationParams
309+
310+ // Get REST API pagination parameters with defaults
311+ page , err := OptionalIntParamWithDefault (r , "page" , 1 )
312+ if err != nil {
313+ return UnifiedPaginationParams {}, err
314+ }
315+ params .Page = page
316+
317+ perPage , err := OptionalIntParamWithDefault (r , "perPage" , 30 )
318+ if err != nil {
319+ return UnifiedPaginationParams {}, err
320+ }
321+ params .PerPage = perPage
322+
323+ // Get GraphQL pagination parameters
324+ if val , err := OptionalParam [float64 ](r , "first" ); err != nil {
325+ return UnifiedPaginationParams {}, err
326+ } else if val != 0 {
327+ first := int32 (val )
328+ params .First = & first
329+ }
330+
331+ if val , err := OptionalParam [float64 ](r , "last" ); err != nil {
332+ return UnifiedPaginationParams {}, err
333+ } else if val != 0 {
334+ last := int32 (val )
335+ params .Last = & last
336+ }
337+
338+ if val , err := OptionalParam [string ](r , "after" ); err != nil {
339+ return UnifiedPaginationParams {}, err
340+ } else if val != "" {
341+ params .After = & val
342+ }
343+
344+ if val , err := OptionalParam [string ](r , "before" ); err != nil {
345+ return UnifiedPaginationParams {}, err
346+ } else if val != "" {
347+ params .Before = & val
348+ }
349+
350+ // Validate GraphQL pagination parameters according to GraphQL connection spec
351+ // Only validate if any GraphQL parameters are provided
352+ if params .First != nil || params .Last != nil || params .After != nil || params .Before != nil {
353+ if params .First != nil && params .Last != nil {
354+ return UnifiedPaginationParams {}, fmt .Errorf ("only one of 'first' or 'last' may be specified" )
355+ }
356+ if params .After != nil && params .Before != nil {
357+ return UnifiedPaginationParams {}, fmt .Errorf ("only one of 'after' or 'before' may be specified" )
358+ }
359+ if params .After != nil && params .Last != nil {
360+ return UnifiedPaginationParams {}, fmt .Errorf ("'after' cannot be used with 'last'. Did you mean to use 'before' instead?" )
361+ }
362+ if params .Before != nil && params .First != nil {
363+ return UnifiedPaginationParams {}, fmt .Errorf ("'before' cannot be used with 'first'. Did you mean to use 'after' instead?" )
364+ }
365+ }
366+
367+ return params , nil
368+ }
369+
252370// OptionalGraphQLPaginationParams returns the GraphQL cursor-based pagination parameters from the request.
253371// It validates that the parameters are used correctly according to GraphQL connection spec.
254372func OptionalGraphQLPaginationParams (r mcp.CallToolRequest ) (GraphQLPaginationParams , error ) {
0 commit comments