From 658d41a2a69b86b9b8406e583ee2f658c9a8e625 Mon Sep 17 00:00:00 2001 From: fazal-ul-rehman Date: Sun, 5 Jul 2026 00:15:14 +0200 Subject: [PATCH 1/3] Add guidelines for Android Firebase build configuration kami33133 --- cmd/github-mcp-server/helpers.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/cmd/github-mcp-server/helpers.go b/cmd/github-mcp-server/helpers.go index c5f498813d..06b1fa386a 100644 --- a/cmd/github-mcp-server/helpers.go +++ b/cmd/github-mcp-server/helpers.go @@ -27,3 +27,33 @@ func formatToolsetName(name string) string { return strings.Join(parts, " ") } } +# Copilot Space: Android Firebase Build Configuration Expert + +## Role +You are a **Build Configuration & Dependency Management Specialist** focused on Android projects using Gradle, Firebase, and Google Cloud services. + +## Framework & Patterns to Follow +- **Gradle Best Practices**: Follow Android Gradle Plugin conventions (v8.5.1+) +- **Dependency Management**: Use dependency locking (LockMode.STRICT) and managed repositories (Google, Maven Central) +- **Firebase Integration**: Properly configure Google Services plugin, Firebase Crashlytics, and Performance Monitoring +- **Security First**: Validate classpath dependencies and plugin versions against official Google/Firebase releases +- **Repository Consistency**: Maintain parallel repository declarations in `buildscript` and `allprojects` blocks + +## Primary Tasks +1. Review and optimize Gradle configuration (`build.gradle`) +2. Debug Firebase plugin integration issues +3. Manage dependency versions and conflicts +4. Assist with dependency locking strategies +5. Help create feature requests and bug reports following project templates + +## Avoid +- ❌ Recommending deprecated Firebase plugins or outdated Gradle versions +- ❌ Suggesting custom repository mirrors without explicit project approval +- ❌ Bypassing dependency locking in STRICT mode without justification +- ❌ Providing general Java/Kotlin coding advice unless it relates to Gradle/build configuration +- ❌ Modifying classpath dependencies without explaining the rationale and version compatibility + +## When to Escalate +- Complex multi-module build issues requiring detailed profiling +- Native build tool integration (NDK, CMake) +- Custom plugin development outside standard Firebase SDKs From 59c782bcec5d235df829bdd2bd1149d29deec80a Mon Sep 17 00:00:00 2001 From: fazal-ul-rehman Date: Tue, 7 Jul 2026 17:59:09 +0200 Subject: [PATCH 2/3] Update print statement from 'Hello' to 'Goodbye' here you get this new with addition --- pkg/github/params.go | 2542 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2542 insertions(+) diff --git a/pkg/github/params.go b/pkg/github/params.go index a6b43375ef..b69703afd1 100644 --- a/pkg/github/params.go +++ b/pkg/github/params.go @@ -488,3 +488,2545 @@ func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { } return cursor.ToGraphQLParams() } +// ❌ WRONG +func OptionalParamOK[T any, A map[string]any](args A, p string) (value T, ok bool, err error) + +// ✅ CORRECT +func OptionalParamOK[T any](args map[string]any, p string) (value T, ok bool, err error) +package github + +import ( + "errors" + "fmt" + "math" + "strconv" + + "github.com/google/go-github/v87/github" + "github.com/google/jsonschema-go/jsonschema" +) + +// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. +// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. +func OptionalParamOK[T any](args map[string]any, p string) (value T, ok bool, err error) { + // Check if the parameter is present in the request + val, exists := args[p] + if !exists { + // Not present, return zero value, false, no error + return + } + + // Check if the parameter is of the expected type + value, ok = val.(T) + if !ok { + // Present but wrong type + err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) + ok = true // Set ok to true because the parameter *was* present, even if wrong type + return + } + + // Present and correct type + ok = true + return +} + +// isAcceptedError checks if the error is an accepted error. +func isAcceptedError(err error) bool { + var acceptedError *github.AcceptedError + return errors.As(err, &acceptedError) +} + +// toInt converts a value to int, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values outside the int range. +func toInt(val any) (int, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + if f > math.MaxInt || f < math.MinInt { + return 0, fmt.Errorf("numeric value out of int range: %v", f) + } + return int(f), nil +} + +// toInt64 converts a value to int64, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values that lose precision in the float64→int64 conversion. +func toInt64(val any) (int64, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + result := int64(f) + // Check round-trip to detect precision loss for large int64 values + if float64(result) != f { + return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) + } + return result, nil +} + +// RequiredParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type. +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredParam[T comparable](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + // Check if the parameter is of the expected type + val, ok := args[p].(T) + if !ok { + return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) + } + + if val == zero { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + return val, nil +} + +// RequiredInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredInt(args map[string]any, p string) (int, error) { + v, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt(v) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// RequiredBigInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value. +// 4. Validates that the float64 value can be safely converted to int64 without truncation. +func RequiredBigInt(args map[string]any, p string) (int64, error) { + val, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt64(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// OptionalParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalParam[T any](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, nil + } + + // Check if the parameter is of the expected type + if _, ok := args[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, args[p]) + } + + return args[p].(T), nil +} + +// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type (float64 or numeric string) and returns it +func OptionalIntParam(args map[string]any, p string) (int, error) { + val, ok := args[p] + if !ok { + return 0, nil + } + + result, err := toInt(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + return result, nil +} + +// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalIntParam, but it also takes a default value. +// If the parameter is not present or is zero, the default is returned. +func OptionalIntParamWithDefault(args map[string]any, p string, d int) (int, error) { + v, err := OptionalIntParam(args, p) + if err != nil { + return 0, err + } + if v == 0 { + return d, nil + } + return v, nil +} + +// OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalParam[bool], but it also takes a default value. +func OptionalBoolParamWithDefault(args map[string]any, p string, d bool) (bool, error) { + _, ok := args[p] + v, err := OptionalParam[bool](args, p) + if err != nil { + return false, err + } + if !ok { + return d, nil + } + return v, nil +} + +// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, iterates the elements and checks each is a string +func OptionalStringArrayParam(args map[string]any, p string) ([]string, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []string{}, nil + } + + switch v := args[p].(type) { + case nil: + return []string{}, nil + case []string: + return v, nil + case []any: + strSlice := make([]string, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + strSlice[i] = s + } + return strSlice, nil + default: + return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, args[p]) + } +} + +func convertStringSliceToBigIntSlice(s []string) ([]int64, error) { + int64Slice := make([]int64, len(s)) + for i, str := range s { + val, err := convertStringToBigInt(str, 0) + if err != nil { + return nil, fmt.Errorf("failed to convert element %d (%s) to int64: %w", i, str, err) + } + int64Slice[i] = val + } + return int64Slice, nil +} + +func convertStringToBigInt(s string, def int64) (int64, error) { + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return def, fmt.Errorf("failed to convert string %s to int64: %w", s, err) + } + return v, nil +} + +// OptionalBigIntArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns an empty slice +// 2. If it is present, iterates the elements, checks each is a string, and converts them to int64 values +func OptionalBigIntArrayParam(args map[string]any, p string) ([]int64, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []int64{}, nil + } + + switch v := args[p].(type) { + case nil: + return []int64{}, nil + case []string: + return convertStringSliceToBigIntSlice(v) + case []any: + int64Slice := make([]int64, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []int64{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + val, err := convertStringToBigInt(s, 0) + if err != nil { + return []int64{}, fmt.Errorf("parameter %s: failed to convert element %d (%s) to int64: %w", p, i, s, err) + } + int64Slice[i] = val + } + return int64Slice, nil + default: + return []int64{}, fmt.Errorf("parameter %s could not be coerced to []int64, is %T", p, args[p]) + } +} + +// WithPagination adds REST API pagination parameters to a tool. +// https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api +func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + return schema +} + +// WithUnifiedPagination adds REST API pagination parameters to a tool. +// GraphQL tools will use this and convert page/perPage to GraphQL cursor parameters internally. +func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + } + + return schema +} + +// WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter). +func WithCursorPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the cursor from the previous response.", + } + + return schema +} + +type PaginationParams struct { + Page int + PerPage int + After string +} + +// OptionalPaginationParams returns the "page", "perPage", and "after" parameters from the request, +// or their default values if not present, "page" default is 1, "perPage" default is 30. +// In future, we may want to make the default values configurable, or even have this +// function returned from `withPagination`, where the defaults are provided alongside +// the min/max values. +func OptionalPaginationParams(args map[string]any) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(args, "page", 1) + if err != nil { + return PaginationParams{}, err + } + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return PaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return PaginationParams{}, err + } + return PaginationParams{ + Page: page, + PerPage: perPage, + After: after, + }, nil +} + +// OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request, +// without the "page" parameter, suitable for cursor-based pagination only. +func OptionalCursorPaginationParams(args map[string]any) (CursorPaginationParams, error) { + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return CursorPaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return CursorPaginationParams{}, err + } + return CursorPaginationParams{ + PerPage: perPage, + After: after, + }, nil +} + +type CursorPaginationParams struct { + PerPage int + After string +} + +type pageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + NextCursor string `json:"nextCursor,omitempty"` + PrevCursor string `json:"prevCursor,omitempty"` +} + +func buildPageInfo(resp *github.Response) pageInfo { + info := pageInfo{ + HasNextPage: false, + HasPreviousPage: false, + } + + if resp != nil { + if resp.After != "" { + info.HasNextPage = true + info.NextCursor = resp.After + } + if resp.Before != "" { + info.HasPreviousPage = true + info.PrevCursor = resp.Before + } + } + + return info +} + +// ToGraphQLParams converts cursor pagination parameters to GraphQL-specific parameters. +func (p CursorPaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + if p.PerPage > 100 { + return nil, fmt.Errorf("perPage value %d exceeds maximum of 100", p.PerPage) + } + if p.PerPage < 0 { + return nil, fmt.Errorf("perPage value %d cannot be negative", p.PerPage) + } + first := int32(p.PerPage) + + var after *string + if p.After != "" { + after = &p.After + } + + return &GraphQLPaginationParams{ + First: &first, + After: after, + }, nil +} + +type GraphQLPaginationParams struct { + First *int32 + After *string +} + +// ToGraphQLParams converts REST API pagination parameters to GraphQL-specific parameters. +// This converts page/perPage to first parameter for GraphQL queries. +// If After is provided, it takes precedence over page-based pagination. +func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + // Convert to CursorPaginationParams and delegate to avoid duplication + cursor := CursorPaginationParams{ + PerPage: p.PerPage, + After: p.After, + } + return cursor.ToGraphQLParams() +} +package github + +import ( + "errors" + "fmt" + "math" + "strconv" + + "github.com/google/go-github/v87/github" + "github.com/google/jsonschema-go/jsonschema" +) + +// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. +// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. +func OptionalParamOK[T any](args map[string]any, p string) (value T, ok bool, err error) { + // Check if the parameter is present in the request + val, exists := args[p] + if !exists { + // Not present, return zero value, false, no error + return + } + + // Check if the parameter is of the expected type + value, ok = val.(T) + if !ok { + // Present but wrong type + err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) + ok = true // Set ok to true because the parameter *was* present, even if wrong type + return + } + + // Present and correct type + ok = true + return +} + +// isAcceptedError checks if the error is an accepted error. +func isAcceptedError(err error) bool { + var acceptedError *github.AcceptedError + return errors.As(err, &acceptedError) +} + +// toInt converts a value to int, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values outside the int range. +func toInt(val any) (int, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + if f > math.MaxInt || f < math.MinInt { + return 0, fmt.Errorf("numeric value out of int range: %v", f) + } + return int(f), nil +} + +// toInt64 converts a value to int64, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values that lose precision in the float64→int64 conversion. +func toInt64(val any) (int64, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + result := int64(f) + // Check round-trip to detect precision loss for large int64 values + if float64(result) != f { + return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) + } + return result, nil +} + +// RequiredParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type. +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredParam[T comparable](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + // Check if the parameter is of the expected type + val, ok := args[p].(T) + if !ok { + return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) + } + + if val == zero { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + return val, nil +} + +// RequiredInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredInt(args map[string]any, p string) (int, error) { + v, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt(v) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// RequiredBigInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value. +// 4. Validates that the float64 value can be safely converted to int64 without truncation. +func RequiredBigInt(args map[string]any, p string) (int64, error) { + val, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt64(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// OptionalParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalParam[T any](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, nil + } + + // Check if the parameter is of the expected type + if _, ok := args[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, args[p]) + } + + return args[p].(T), nil +} + +// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type (float64 or numeric string) and returns it +func OptionalIntParam(args map[string]any, p string) (int, error) { + val, ok := args[p] + if !ok { + return 0, nil + } + + result, err := toInt(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + return result, nil +} + +// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalIntParam, but it also takes a default value. +// If the parameter is not present or is zero, the default is returned. +func OptionalIntParamWithDefault(args map[string]any, p string, d int) (int, error) { + v, err := OptionalIntParam(args, p) + if err != nil { + return 0, err + } + if v == 0 { + return d, nil + } + return v, nil +} + +// OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalParam[bool], but it also takes a default value. +func OptionalBoolParamWithDefault(args map[string]any, p string, d bool) (bool, error) { + _, ok := args[p] + v, err := OptionalParam[bool](args, p) + if err != nil { + return false, err + } + if !ok { + return d, nil + } + return v, nil +} + +// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, iterates the elements and checks each is a string +func OptionalStringArrayParam(args map[string]any, p string) ([]string, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []string{}, nil + } + + switch v := args[p].(type) { + case nil: + return []string{}, nil + case []string: + return v, nil + case []any: + strSlice := make([]string, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + strSlice[i] = s + } + return strSlice, nil + default: + return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, args[p]) + } +} + +func convertStringSliceToBigIntSlice(s []string) ([]int64, error) { + int64Slice := make([]int64, len(s)) + for i, str := range s { + val, err := convertStringToBigInt(str, 0) + if err != nil { + return nil, fmt.Errorf("failed to convert element %d (%s) to int64: %w", i, str, err) + } + int64Slice[i] = val + } + return int64Slice, nil +} + +func convertStringToBigInt(s string, def int64) (int64, error) { + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return def, fmt.Errorf("failed to convert string %s to int64: %w", s, err) + } + return v, nil +} + +// OptionalBigIntArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns an empty slice +// 2. If it is present, iterates the elements, checks each is a string, and converts them to int64 values +func OptionalBigIntArrayParam(args map[string]any, p string) ([]int64, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []int64{}, nil + } + + switch v := args[p].(type) { + case nil: + return []int64{}, nil + case []string: + return convertStringSliceToBigIntSlice(v) + case []any: + int64Slice := make([]int64, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []int64{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + val, err := convertStringToBigInt(s, 0) + if err != nil { + return []int64{}, fmt.Errorf("parameter %s: failed to convert element %d (%s) to int64: %w", p, i, s, err) + } + int64Slice[i] = val + } + return int64Slice, nil + default: + return []int64{}, fmt.Errorf("parameter %s could not be coerced to []int64, is %T", p, args[p]) + } +} + +// WithPagination adds REST API pagination parameters to a tool. +// https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api +func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + return schema +} + +// WithUnifiedPagination adds REST API pagination parameters to a tool. +// GraphQL tools will use this and convert page/perPage to GraphQL cursor parameters internally. +func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + } + + return schema +} + +// WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter). +func WithCursorPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the cursor from the previous response.", + } + + return schema +} + +type PaginationParams struct { + Page int + PerPage int + After string +} + +// OptionalPaginationParams returns the "page", "perPage", and "after" parameters from the request, +// or their default values if not present, "page" default is 1, "perPage" default is 30. +// In future, we may want to make the default values configurable, or even have this +// function returned from `withPagination`, where the defaults are provided alongside +// the min/max values. +func OptionalPaginationParams(args map[string]any) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(args, "page", 1) + if err != nil { + return PaginationParams{}, err + } + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return PaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return PaginationParams{}, err + } + return PaginationParams{ + Page: page, + PerPage: perPage, + After: after, + }, nil +} + +// OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request, +// without the "page" parameter, suitable for cursor-based pagination only. +func OptionalCursorPaginationParams(args map[string]any) (CursorPaginationParams, error) { + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return CursorPaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return CursorPaginationParams{}, err + } + return CursorPaginationParams{ + PerPage: perPage, + After: after, + }, nil +} + +type CursorPaginationParams struct { + PerPage int + After string +} + +type pageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + NextCursor string `json:"nextCursor,omitempty"` + PrevCursor string `json:"prevCursor,omitempty"` +} + +func buildPageInfo(resp *github.Response) pageInfo { + info := pageInfo{ + HasNextPage: false, + HasPreviousPage: false, + } + + if resp != nil { + if resp.After != "" { + info.HasNextPage = true + info.NextCursor = resp.After + } + if resp.Before != "" { + info.HasPreviousPage = true + info.PrevCursor = resp.Before + } + } + + return info +} + +// ToGraphQLParams converts cursor pagination parameters to GraphQL-specific parameters. +func (p CursorPaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + if p.PerPage > 100 { + return nil, fmt.Errorf("perPage value %d exceeds maximum of 100", p.PerPage) + } + if p.PerPage < 0 { + return nil, fmt.Errorf("perPage value %d cannot be negative", p.PerPage) + } + first := int32(p.PerPage) + + var after *string + if p.After != "" { + after = &p.After + } + + return &GraphQLPaginationParams{ + First: &first, + After: after, + }, nil +} + +type GraphQLPaginationParams struct { + First *int32 + After *string +} + +// ToGraphQLParams converts REST API pagination parameters to GraphQL-specific parameters. +// This converts page/perPage to first parameter for GraphQL queries. +// If After is provided, it takes precedence over page-based pagination. +func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + // Convert to CursorPaginationParams and delegate to avoid duplication + cursor := CursorPaginationParams{ + PerPage: p.PerPage, + After: p.After, + } + return cursor.ToGraphQLParams() +} +package github + +import ( + "errors" + "fmt" + "math" + "strconv" + + "github.com/google/go-github/v87/github" + "github.com/google/jsonschema-go/jsonschema" +) + +// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. +// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. +func OptionalParamOK[T any](args map[string]any, p string) (value T, ok bool, err error) { + // Check if the parameter is present in the request + val, exists := args[p] + if !exists { + // Not present, return zero value, false, no error + return + } + + // Check if the parameter is of the expected type + value, ok = val.(T) + if !ok { + // Present but wrong type + err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) + ok = true // Set ok to true because the parameter *was* present, even if wrong type + return + } + + // Present and correct type + ok = true + return +} + +// isAcceptedError checks if the error is an accepted error. +func isAcceptedError(err error) bool { + var acceptedError *github.AcceptedError + return errors.As(err, &acceptedError) +} + +// toInt converts a value to int, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values outside the int range. +func toInt(val any) (int, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + if f > math.MaxInt || f < math.MinInt { + return 0, fmt.Errorf("numeric value out of int range: %v", f) + } + return int(f), nil +} + +// toInt64 converts a value to int64, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values that lose precision in the float64→int64 conversion. +func toInt64(val any) (int64, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + result := int64(f) + // Check round-trip to detect precision loss for large int64 values + if float64(result) != f { + return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) + } + return result, nil +} + +// RequiredParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type. +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredParam[T comparable](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + // Check if the parameter is of the expected type + val, ok := args[p].(T) + if !ok { + return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) + } + + if val == zero { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + return val, nil +} + +// RequiredInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredInt(args map[string]any, p string) (int, error) { + v, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt(v) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// RequiredBigInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value. +// 4. Validates that the float64 value can be safely converted to int64 without truncation. +func RequiredBigInt(args map[string]any, p string) (int64, error) { + val, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt64(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// OptionalParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalParam[T any](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, nil + } + + // Check if the parameter is of the expected type + if _, ok := args[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, args[p]) + } + + return args[p].(T), nil +} + +// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type (float64 or numeric string) and returns it +func OptionalIntParam(args map[string]any, p string) (int, error) { + val, ok := args[p] + if !ok { + return 0, nil + } + + result, err := toInt(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + return result, nil +} + +// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalIntParam, but it also takes a default value. +// If the parameter is not present or is zero, the default is returned. +func OptionalIntParamWithDefault(args map[string]any, p string, d int) (int, error) { + v, err := OptionalIntParam(args, p) + if err != nil { + return 0, err + } + if v == 0 { + return d, nil + } + return v, nil +} + +// OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalParam[bool], but it also takes a default value. +func OptionalBoolParamWithDefault(args map[string]any, p string, d bool) (bool, error) { + _, ok := args[p] + v, err := OptionalParam[bool](args, p) + if err != nil { + return false, err + } + if !ok { + return d, nil + } + return v, nil +} + +// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, iterates the elements and checks each is a string +func OptionalStringArrayParam(args map[string]any, p string) ([]string, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []string{}, nil + } + + switch v := args[p].(type) { + case nil: + return []string{}, nil + case []string: + return v, nil + case []any: + strSlice := make([]string, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + strSlice[i] = s + } + return strSlice, nil + default: + return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, args[p]) + } +} + +func convertStringSliceToBigIntSlice(s []string) ([]int64, error) { + int64Slice := make([]int64, len(s)) + for i, str := range s { + val, err := convertStringToBigInt(str, 0) + if err != nil { + return nil, fmt.Errorf("failed to convert element %d (%s) to int64: %w", i, str, err) + } + int64Slice[i] = val + } + return int64Slice, nil +} + +func convertStringToBigInt(s string, def int64) (int64, error) { + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return def, fmt.Errorf("failed to convert string %s to int64: %w", s, err) + } + return v, nil +} + +// OptionalBigIntArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns an empty slice +// 2. If it is present, iterates the elements, checks each is a string, and converts them to int64 values +func OptionalBigIntArrayParam(args map[string]any, p string) ([]int64, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []int64{}, nil + } + + switch v := args[p].(type) { + case nil: + return []int64{}, nil + case []string: + return convertStringSliceToBigIntSlice(v) + case []any: + int64Slice := make([]int64, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []int64{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + val, err := convertStringToBigInt(s, 0) + if err != nil { + return []int64{}, fmt.Errorf("parameter %s: failed to convert element %d (%s) to int64: %w", p, i, s, err) + } + int64Slice[i] = val + } + return int64Slice, nil + default: + return []int64{}, fmt.Errorf("parameter %s could not be coerced to []int64, is %T", p, args[p]) + } +} + +// WithPagination adds REST API pagination parameters to a tool. +// https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api +func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + return schema +} + +// WithUnifiedPagination adds REST API pagination parameters to a tool. +// GraphQL tools will use this and convert page/perPage to GraphQL cursor parameters internally. +func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + } + + return schema +} + +// WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter). +func WithCursorPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the cursor from the previous response.", + } + + return schema +} + +type PaginationParams struct { + Page int + PerPage int + After string +} + +// OptionalPaginationParams returns the "page", "perPage", and "after" parameters from the request, +// or their default values if not present, "page" default is 1, "perPage" default is 30. +// In future, we may want to make the default values configurable, or even have this +// function returned from `withPagination`, where the defaults are provided alongside +// the min/max values. +func OptionalPaginationParams(args map[string]any) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(args, "page", 1) + if err != nil { + return PaginationParams{}, err + } + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return PaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return PaginationParams{}, err + } + return PaginationParams{ + Page: page, + PerPage: perPage, + After: after, + }, nil +} + +// OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request, +// without the "page" parameter, suitable for cursor-based pagination only. +func OptionalCursorPaginationParams(args map[string]any) (CursorPaginationParams, error) { + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return CursorPaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return CursorPaginationParams{}, err + } + return CursorPaginationParams{ + PerPage: perPage, + After: after, + }, nil +} + +type CursorPaginationParams struct { + PerPage int + After string +} + +type pageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + NextCursor string `json:"nextCursor,omitempty"` + PrevCursor string `json:"prevCursor,omitempty"` +} + +func buildPageInfo(resp *github.Response) pageInfo { + info := pageInfo{ + HasNextPage: false, + HasPreviousPage: false, + } + + if resp != nil { + if resp.After != "" { + info.HasNextPage = true + info.NextCursor = resp.After + } + if resp.Before != "" { + info.HasPreviousPage = true + info.PrevCursor = resp.Before + } + } + + return info +} + +// ToGraphQLParams converts cursor pagination parameters to GraphQL-specific parameters. +func (p CursorPaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + if p.PerPage > 100 { + return nil, fmt.Errorf("perPage value %d exceeds maximum of 100", p.PerPage) + } + if p.PerPage < 0 { + return nil, fmt.Errorf("perPage value %d cannot be negative", p.PerPage) + } + first := int32(p.PerPage) + + var after *string + if p.After != "" { + after = &p.After + } + + return &GraphQLPaginationParams{ + First: &first, + After: after, + }, nil +} + +type GraphQLPaginationParams struct { + First *int32 + After *string +} + +// ToGraphQLParams converts REST API pagination parameters to GraphQL-specific parameters. +// This converts page/perPage to first parameter for GraphQL queries. +// If After is provided, it takes precedence over page-based pagination. +func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + // Convert to CursorPaginationParams and delegate to avoid duplication + cursor := CursorPaginationParams{ + PerPage: p.PerPage, + After: p.After, + } + return cursor.ToGraphQLParams() +} +package github + +import ( + "errors" + "fmt" + "math" + "strconv" + + "github.com/google/go-github/v87/github" + "github.com/google/jsonschema-go/jsonschema" +) + +// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. +// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. +func OptionalParamOK[T any](args map[string]any, p string) (value T, ok bool, err error) { + // Check if the parameter is present in the request + val, exists := args[p] + if !exists { + // Not present, return zero value, false, no error + return + } + + // Check if the parameter is of the expected type + value, ok = val.(T) + if !ok { + // Present but wrong type + err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) + ok = true // Set ok to true because the parameter *was* present, even if wrong type + return + } + + // Present and correct type + ok = true + return +} + +// isAcceptedError checks if the error is an accepted error. +func isAcceptedError(err error) bool { + var acceptedError *github.AcceptedError + return errors.As(err, &acceptedError) +} + +// toInt converts a value to int, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values outside the int range. +func toInt(val any) (int, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + if f > math.MaxInt || f < math.MinInt { + return 0, fmt.Errorf("numeric value out of int range: %v", f) + } + return int(f), nil +} + +// toInt64 converts a value to int64, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values that lose precision in the float64→int64 conversion. +func toInt64(val any) (int64, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + result := int64(f) + // Check round-trip to detect precision loss for large int64 values + if float64(result) != f { + return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) + } + return result, nil +} + +// RequiredParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type. +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredParam[T comparable](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + // Check if the parameter is of the expected type + val, ok := args[p].(T) + if !ok { + return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) + } + + if val == zero { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + return val, nil +} + +// RequiredInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredInt(args map[string]any, p string) (int, error) { + v, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt(v) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// RequiredBigInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value. +// 4. Validates that the float64 value can be safely converted to int64 without truncation. +func RequiredBigInt(args map[string]any, p string) (int64, error) { + val, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt64(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// OptionalParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalParam[T any](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, nil + } + + // Check if the parameter is of the expected type + if _, ok := args[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, args[p]) + } + + return args[p].(T), nil +} + +// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type (float64 or numeric string) and returns it +func OptionalIntParam(args map[string]any, p string) (int, error) { + val, ok := args[p] + if !ok { + return 0, nil + } + + result, err := toInt(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + return result, nil +} + +// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalIntParam, but it also takes a default value. +// If the parameter is not present or is zero, the default is returned. +func OptionalIntParamWithDefault(args map[string]any, p string, d int) (int, error) { + v, err := OptionalIntParam(args, p) + if err != nil { + return 0, err + } + if v == 0 { + return d, nil + } + return v, nil +} + +// OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalParam[bool], but it also takes a default value. +func OptionalBoolParamWithDefault(args map[string]any, p string, d bool) (bool, error) { + _, ok := args[p] + v, err := OptionalParam[bool](args, p) + if err != nil { + return false, err + } + if !ok { + return d, nil + } + return v, nil +} + +// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, iterates the elements and checks each is a string +func OptionalStringArrayParam(args map[string]any, p string) ([]string, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []string{}, nil + } + + switch v := args[p].(type) { + case nil: + return []string{}, nil + case []string: + return v, nil + case []any: + strSlice := make([]string, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + strSlice[i] = s + } + return strSlice, nil + default: + return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, args[p]) + } +} + +func convertStringSliceToBigIntSlice(s []string) ([]int64, error) { + int64Slice := make([]int64, len(s)) + for i, str := range s { + val, err := convertStringToBigInt(str, 0) + if err != nil { + return nil, fmt.Errorf("failed to convert element %d (%s) to int64: %w", i, str, err) + } + int64Slice[i] = val + } + return int64Slice, nil +} + +func convertStringToBigInt(s string, def int64) (int64, error) { + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return def, fmt.Errorf("failed to convert string %s to int64: %w", s, err) + } + return v, nil +} + +// OptionalBigIntArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns an empty slice +// 2. If it is present, iterates the elements, checks each is a string, and converts them to int64 values +func OptionalBigIntArrayParam(args map[string]any, p string) ([]int64, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []int64{}, nil + } + + switch v := args[p].(type) { + case nil: + return []int64{}, nil + case []string: + return convertStringSliceToBigIntSlice(v) + case []any: + int64Slice := make([]int64, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []int64{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + val, err := convertStringToBigInt(s, 0) + if err != nil { + return []int64{}, fmt.Errorf("parameter %s: failed to convert element %d (%s) to int64: %w", p, i, s, err) + } + int64Slice[i] = val + } + return int64Slice, nil + default: + return []int64{}, fmt.Errorf("parameter %s could not be coerced to []int64, is %T", p, args[p]) + } +} + +// WithPagination adds REST API pagination parameters to a tool. +// https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api +func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + return schema +} + +// WithUnifiedPagination adds REST API pagination parameters to a tool. +// GraphQL tools will use this and convert page/perPage to GraphQL cursor parameters internally. +func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + } + + return schema +} + +// WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter). +func WithCursorPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the cursor from the previous response.", + } + + return schema +} + +type PaginationParams struct { + Page int + PerPage int + After string +} + +// OptionalPaginationParams returns the "page", "perPage", and "after" parameters from the request, +// or their default values if not present, "page" default is 1, "perPage" default is 30. +// In future, we may want to make the default values configurable, or even have this +// function returned from `withPagination`, where the defaults are provided alongside +// the min/max values. +func OptionalPaginationParams(args map[string]any) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(args, "page", 1) + if err != nil { + return PaginationParams{}, err + } + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return PaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return PaginationParams{}, err + } + return PaginationParams{ + Page: page, + PerPage: perPage, + After: after, + }, nil +} + +// OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request, +// without the "page" parameter, suitable for cursor-based pagination only. +func OptionalCursorPaginationParams(args map[string]any) (CursorPaginationParams, error) { + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return CursorPaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return CursorPaginationParams{}, err + } + return CursorPaginationParams{ + PerPage: perPage, + After: after, + }, nil +} + +type CursorPaginationParams struct { + PerPage int + After string +} + +type pageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + NextCursor string `json:"nextCursor,omitempty"` + PrevCursor string `json:"prevCursor,omitempty"` +} + +func buildPageInfo(resp *github.Response) pageInfo { + info := pageInfo{ + HasNextPage: false, + HasPreviousPage: false, + } + + if resp != nil { + if resp.After != "" { + info.HasNextPage = true + info.NextCursor = resp.After + } + if resp.Before != "" { + info.HasPreviousPage = true + info.PrevCursor = resp.Before + } + } + + return info +} + +// ToGraphQLParams converts cursor pagination parameters to GraphQL-specific parameters. +func (p CursorPaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + if p.PerPage > 100 { + return nil, fmt.Errorf("perPage value %d exceeds maximum of 100", p.PerPage) + } + if p.PerPage < 0 { + return nil, fmt.Errorf("perPage value %d cannot be negative", p.PerPage) + } + first := int32(p.PerPage) + + var after *string + if p.After != "" { + after = &p.After + } + + return &GraphQLPaginationParams{ + First: &first, + After: after, + }, nil +} + +type GraphQLPaginationParams struct { + First *int32 + After *string +} + +// ToGraphQLParams converts REST API pagination parameters to GraphQL-specific parameters. +// This converts page/perPage to first parameter for GraphQL queries. +// If After is provided, it takes precedence over page-based pagination. +func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + // Convert to CursorPaginationParams and delegate to avoid duplication + cursor := CursorPaginationParams{ + PerPage: p.PerPage, + After: p.After, + } + return cursor.ToGraphQLParams() +} +package github + +import ( + "errors" + "fmt" + "math" + "strconv" + + "github.com/google/go-github/v87/github" + "github.com/google/jsonschema-go/jsonschema" +) + +// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. +// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. +func OptionalParamOK[T any](args map[string]any, p string) (value T, ok bool, err error) { + // Check if the parameter is present in the request + val, exists := args[p] + if !exists { + // Not present, return zero value, false, no error + return + } + + // Check if the parameter is of the expected type + value, ok = val.(T) + if !ok { + // Present but wrong type + err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) + ok = true // Set ok to true because the parameter *was* present, even if wrong type + return + } + + // Present and correct type + ok = true + return +} + +// isAcceptedError checks if the error is an accepted error. +func isAcceptedError(err error) bool { + var acceptedError *github.AcceptedError + return errors.As(err, &acceptedError) +} + +// toInt converts a value to int, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values outside the int range. +func toInt(val any) (int, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + if f > math.MaxInt || f < math.MinInt { + return 0, fmt.Errorf("numeric value out of int range: %v", f) + } + return int(f), nil +} + +// toInt64 converts a value to int64, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values that lose precision in the float64→int64 conversion. +func toInt64(val any) (int64, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + result := int64(f) + // Check round-trip to detect precision loss for large int64 values + if float64(result) != f { + return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) + } + return result, nil +} + +// RequiredParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type. +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredParam[T comparable](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + // Check if the parameter is of the expected type + val, ok := args[p].(T) + if !ok { + return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) + } + + if val == zero { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + return val, nil +} + +// RequiredInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredInt(args map[string]any, p string) (int, error) { + v, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt(v) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// RequiredBigInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value. +// 4. Validates that the float64 value can be safely converted to int64 without truncation. +func RequiredBigInt(args map[string]any, p string) (int64, error) { + val, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt64(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// OptionalParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalParam[T any](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, nil + } + + // Check if the parameter is of the expected type + if _, ok := args[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, args[p]) + } + + return args[p].(T), nil +} + +// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type (float64 or numeric string) and returns it +func OptionalIntParam(args map[string]any, p string) (int, error) { + val, ok := args[p] + if !ok { + return 0, nil + } + + result, err := toInt(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + return result, nil +} + +// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalIntParam, but it also takes a default value. +// If the parameter is not present or is zero, the default is returned. +func OptionalIntParamWithDefault(args map[string]any, p string, d int) (int, error) { + v, err := OptionalIntParam(args, p) + if err != nil { + return 0, err + } + if v == 0 { + return d, nil + } + return v, nil +} + +// OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalParam[bool], but it also takes a default value. +func OptionalBoolParamWithDefault(args map[string]any, p string, d bool) (bool, error) { + _, ok := args[p] + v, err := OptionalParam[bool](args, p) + if err != nil { + return false, err + } + if !ok { + return d, nil + } + return v, nil +} + +// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, iterates the elements and checks each is a string +func OptionalStringArrayParam(args map[string]any, p string) ([]string, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []string{}, nil + } + + switch v := args[p].(type) { + case nil: + return []string{}, nil + case []string: + return v, nil + case []any: + strSlice := make([]string, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + strSlice[i] = s + } + return strSlice, nil + default: + return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, args[p]) + } +} + +func convertStringSliceToBigIntSlice(s []string) ([]int64, error) { + int64Slice := make([]int64, len(s)) + for i, str := range s { + val, err := convertStringToBigInt(str, 0) + if err != nil { + return nil, fmt.Errorf("failed to convert element %d (%s) to int64: %w", i, str, err) + } + int64Slice[i] = val + } + return int64Slice, nil +} + +func convertStringToBigInt(s string, def int64) (int64, error) { + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return def, fmt.Errorf("failed to convert string %s to int64: %w", s, err) + } + return v, nil +} + +// OptionalBigIntArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns an empty slice +// 2. If it is present, iterates the elements, checks each is a string, and converts them to int64 values +func OptionalBigIntArrayParam(args map[string]any, p string) ([]int64, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []int64{}, nil + } + + switch v := args[p].(type) { + case nil: + return []int64{}, nil + case []string: + return convertStringSliceToBigIntSlice(v) + case []any: + int64Slice := make([]int64, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []int64{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + val, err := convertStringToBigInt(s, 0) + if err != nil { + return []int64{}, fmt.Errorf("parameter %s: failed to convert element %d (%s) to int64: %w", p, i, s, err) + } + int64Slice[i] = val + } + return int64Slice, nil + default: + return []int64{}, fmt.Errorf("parameter %s could not be coerced to []int64, is %T", p, args[p]) + } +} + +// WithPagination adds REST API pagination parameters to a tool. +// https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api +func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + return schema +} + +// WithUnifiedPagination adds REST API pagination parameters to a tool. +// GraphQL tools will use this and convert page/perPage to GraphQL cursor parameters internally. +func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + } + + return schema +} + +// WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter). +func WithCursorPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the cursor from the previous response.", + } + + return schema +} + +type PaginationParams struct { + Page int + PerPage int + After string +} + +// OptionalPaginationParams returns the "page", "perPage", and "after" parameters from the request, +// or their default values if not present, "page" default is 1, "perPage" default is 30. +// In future, we may want to make the default values configurable, or even have this +// function returned from `withPagination`, where the defaults are provided alongside +// the min/max values. +func OptionalPaginationParams(args map[string]any) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(args, "page", 1) + if err != nil { + return PaginationParams{}, err + } + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return PaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return PaginationParams{}, err + } + return PaginationParams{ + Page: page, + PerPage: perPage, + After: after, + }, nil +} + +// OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request, +// without the "page" parameter, suitable for cursor-based pagination only. +func OptionalCursorPaginationParams(args map[string]any) (CursorPaginationParams, error) { + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return CursorPaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return CursorPaginationParams{}, err + } + return CursorPaginationParams{ + PerPage: perPage, + After: after, + }, nil +} + +type CursorPaginationParams struct { + PerPage int + After string +} + +type pageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + NextCursor string `json:"nextCursor,omitempty"` + PrevCursor string `json:"prevCursor,omitempty"` +} + +func buildPageInfo(resp *github.Response) pageInfo { + info := pageInfo{ + HasNextPage: false, + HasPreviousPage: false, + } + + if resp != nil { + if resp.After != "" { + info.HasNextPage = true + info.NextCursor = resp.After + } + if resp.Before != "" { + info.HasPreviousPage = true + info.PrevCursor = resp.Before + } + } + + return info +} + +// ToGraphQLParams converts cursor pagination parameters to GraphQL-specific parameters. +func (p CursorPaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + if p.PerPage > 100 { + return nil, fmt.Errorf("perPage value %d exceeds maximum of 100", p.PerPage) + } + if p.PerPage < 0 { + return nil, fmt.Errorf("perPage value %d cannot be negative", p.PerPage) + } + first := int32(p.PerPage) + + var after *string + if p.After != "" { + after = &p.After + } + + return &GraphQLPaginationParams{ + First: &first, + After: after, + }, nil +} + +type GraphQLPaginationParams struct { + First *int32 + After *string +} + +// ToGraphQLParams converts REST API pagination parameters to GraphQL-specific parameters. +// This converts page/perPage to first parameter for GraphQL queries. +// If After is provided, it takes precedence over page-based pagination. +func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + // Convert to CursorPaginationParams and delegate to avoid duplication + cursor := CursorPaginationParams{ + PerPage: p.PerPage, + After: p.After, + } + return cursor.ToGraphQLParams() +} +func OptionalParamOK[T any, A map[string]any](args A, p string) +func OptionalParamOK[T any](args map[string]any, p string) +func buildPageInfo(resp *github.Response) pageInfo { + return pageInfo{ + HasNextPage: resp.After != "", + HasPreviousPage: resp.Before != "", + NextCursor: resp.After, + PrevCursor: resp.Before, + } +} +func buildPageInfo(resp *github.Response) pageInfo { + info := pageInfo{ + HasNextPage: false, + HasPreviousPage: false, + } + if resp != nil { + if resp.After != "" { + info.HasNextPage = true + info.NextCursor = resp.After + } + if resp.Before != "" { + info.HasPreviousPage = true + info.PrevCursor = resp.Before + } + } + return info +} From f07cabd4798e66527f14d7e07e600c71dc603f85 Mon Sep 17 00:00:00 2001 From: fazal-ul-rehman Date: Tue, 7 Jul 2026 18:01:00 +0200 Subject: [PATCH 3/3] Update print statement from 'Hello' to 'Goodbye' --- pkg/github/params.go | 2542 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2542 insertions(+) diff --git a/pkg/github/params.go b/pkg/github/params.go index a6b43375ef..b69703afd1 100644 --- a/pkg/github/params.go +++ b/pkg/github/params.go @@ -488,3 +488,2545 @@ func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { } return cursor.ToGraphQLParams() } +// ❌ WRONG +func OptionalParamOK[T any, A map[string]any](args A, p string) (value T, ok bool, err error) + +// ✅ CORRECT +func OptionalParamOK[T any](args map[string]any, p string) (value T, ok bool, err error) +package github + +import ( + "errors" + "fmt" + "math" + "strconv" + + "github.com/google/go-github/v87/github" + "github.com/google/jsonschema-go/jsonschema" +) + +// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. +// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. +func OptionalParamOK[T any](args map[string]any, p string) (value T, ok bool, err error) { + // Check if the parameter is present in the request + val, exists := args[p] + if !exists { + // Not present, return zero value, false, no error + return + } + + // Check if the parameter is of the expected type + value, ok = val.(T) + if !ok { + // Present but wrong type + err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) + ok = true // Set ok to true because the parameter *was* present, even if wrong type + return + } + + // Present and correct type + ok = true + return +} + +// isAcceptedError checks if the error is an accepted error. +func isAcceptedError(err error) bool { + var acceptedError *github.AcceptedError + return errors.As(err, &acceptedError) +} + +// toInt converts a value to int, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values outside the int range. +func toInt(val any) (int, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + if f > math.MaxInt || f < math.MinInt { + return 0, fmt.Errorf("numeric value out of int range: %v", f) + } + return int(f), nil +} + +// toInt64 converts a value to int64, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values that lose precision in the float64→int64 conversion. +func toInt64(val any) (int64, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + result := int64(f) + // Check round-trip to detect precision loss for large int64 values + if float64(result) != f { + return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) + } + return result, nil +} + +// RequiredParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type. +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredParam[T comparable](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + // Check if the parameter is of the expected type + val, ok := args[p].(T) + if !ok { + return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) + } + + if val == zero { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + return val, nil +} + +// RequiredInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredInt(args map[string]any, p string) (int, error) { + v, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt(v) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// RequiredBigInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value. +// 4. Validates that the float64 value can be safely converted to int64 without truncation. +func RequiredBigInt(args map[string]any, p string) (int64, error) { + val, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt64(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// OptionalParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalParam[T any](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, nil + } + + // Check if the parameter is of the expected type + if _, ok := args[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, args[p]) + } + + return args[p].(T), nil +} + +// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type (float64 or numeric string) and returns it +func OptionalIntParam(args map[string]any, p string) (int, error) { + val, ok := args[p] + if !ok { + return 0, nil + } + + result, err := toInt(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + return result, nil +} + +// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalIntParam, but it also takes a default value. +// If the parameter is not present or is zero, the default is returned. +func OptionalIntParamWithDefault(args map[string]any, p string, d int) (int, error) { + v, err := OptionalIntParam(args, p) + if err != nil { + return 0, err + } + if v == 0 { + return d, nil + } + return v, nil +} + +// OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalParam[bool], but it also takes a default value. +func OptionalBoolParamWithDefault(args map[string]any, p string, d bool) (bool, error) { + _, ok := args[p] + v, err := OptionalParam[bool](args, p) + if err != nil { + return false, err + } + if !ok { + return d, nil + } + return v, nil +} + +// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, iterates the elements and checks each is a string +func OptionalStringArrayParam(args map[string]any, p string) ([]string, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []string{}, nil + } + + switch v := args[p].(type) { + case nil: + return []string{}, nil + case []string: + return v, nil + case []any: + strSlice := make([]string, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + strSlice[i] = s + } + return strSlice, nil + default: + return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, args[p]) + } +} + +func convertStringSliceToBigIntSlice(s []string) ([]int64, error) { + int64Slice := make([]int64, len(s)) + for i, str := range s { + val, err := convertStringToBigInt(str, 0) + if err != nil { + return nil, fmt.Errorf("failed to convert element %d (%s) to int64: %w", i, str, err) + } + int64Slice[i] = val + } + return int64Slice, nil +} + +func convertStringToBigInt(s string, def int64) (int64, error) { + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return def, fmt.Errorf("failed to convert string %s to int64: %w", s, err) + } + return v, nil +} + +// OptionalBigIntArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns an empty slice +// 2. If it is present, iterates the elements, checks each is a string, and converts them to int64 values +func OptionalBigIntArrayParam(args map[string]any, p string) ([]int64, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []int64{}, nil + } + + switch v := args[p].(type) { + case nil: + return []int64{}, nil + case []string: + return convertStringSliceToBigIntSlice(v) + case []any: + int64Slice := make([]int64, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []int64{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + val, err := convertStringToBigInt(s, 0) + if err != nil { + return []int64{}, fmt.Errorf("parameter %s: failed to convert element %d (%s) to int64: %w", p, i, s, err) + } + int64Slice[i] = val + } + return int64Slice, nil + default: + return []int64{}, fmt.Errorf("parameter %s could not be coerced to []int64, is %T", p, args[p]) + } +} + +// WithPagination adds REST API pagination parameters to a tool. +// https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api +func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + return schema +} + +// WithUnifiedPagination adds REST API pagination parameters to a tool. +// GraphQL tools will use this and convert page/perPage to GraphQL cursor parameters internally. +func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + } + + return schema +} + +// WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter). +func WithCursorPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the cursor from the previous response.", + } + + return schema +} + +type PaginationParams struct { + Page int + PerPage int + After string +} + +// OptionalPaginationParams returns the "page", "perPage", and "after" parameters from the request, +// or their default values if not present, "page" default is 1, "perPage" default is 30. +// In future, we may want to make the default values configurable, or even have this +// function returned from `withPagination`, where the defaults are provided alongside +// the min/max values. +func OptionalPaginationParams(args map[string]any) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(args, "page", 1) + if err != nil { + return PaginationParams{}, err + } + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return PaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return PaginationParams{}, err + } + return PaginationParams{ + Page: page, + PerPage: perPage, + After: after, + }, nil +} + +// OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request, +// without the "page" parameter, suitable for cursor-based pagination only. +func OptionalCursorPaginationParams(args map[string]any) (CursorPaginationParams, error) { + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return CursorPaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return CursorPaginationParams{}, err + } + return CursorPaginationParams{ + PerPage: perPage, + After: after, + }, nil +} + +type CursorPaginationParams struct { + PerPage int + After string +} + +type pageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + NextCursor string `json:"nextCursor,omitempty"` + PrevCursor string `json:"prevCursor,omitempty"` +} + +func buildPageInfo(resp *github.Response) pageInfo { + info := pageInfo{ + HasNextPage: false, + HasPreviousPage: false, + } + + if resp != nil { + if resp.After != "" { + info.HasNextPage = true + info.NextCursor = resp.After + } + if resp.Before != "" { + info.HasPreviousPage = true + info.PrevCursor = resp.Before + } + } + + return info +} + +// ToGraphQLParams converts cursor pagination parameters to GraphQL-specific parameters. +func (p CursorPaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + if p.PerPage > 100 { + return nil, fmt.Errorf("perPage value %d exceeds maximum of 100", p.PerPage) + } + if p.PerPage < 0 { + return nil, fmt.Errorf("perPage value %d cannot be negative", p.PerPage) + } + first := int32(p.PerPage) + + var after *string + if p.After != "" { + after = &p.After + } + + return &GraphQLPaginationParams{ + First: &first, + After: after, + }, nil +} + +type GraphQLPaginationParams struct { + First *int32 + After *string +} + +// ToGraphQLParams converts REST API pagination parameters to GraphQL-specific parameters. +// This converts page/perPage to first parameter for GraphQL queries. +// If After is provided, it takes precedence over page-based pagination. +func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + // Convert to CursorPaginationParams and delegate to avoid duplication + cursor := CursorPaginationParams{ + PerPage: p.PerPage, + After: p.After, + } + return cursor.ToGraphQLParams() +} +package github + +import ( + "errors" + "fmt" + "math" + "strconv" + + "github.com/google/go-github/v87/github" + "github.com/google/jsonschema-go/jsonschema" +) + +// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. +// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. +func OptionalParamOK[T any](args map[string]any, p string) (value T, ok bool, err error) { + // Check if the parameter is present in the request + val, exists := args[p] + if !exists { + // Not present, return zero value, false, no error + return + } + + // Check if the parameter is of the expected type + value, ok = val.(T) + if !ok { + // Present but wrong type + err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) + ok = true // Set ok to true because the parameter *was* present, even if wrong type + return + } + + // Present and correct type + ok = true + return +} + +// isAcceptedError checks if the error is an accepted error. +func isAcceptedError(err error) bool { + var acceptedError *github.AcceptedError + return errors.As(err, &acceptedError) +} + +// toInt converts a value to int, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values outside the int range. +func toInt(val any) (int, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + if f > math.MaxInt || f < math.MinInt { + return 0, fmt.Errorf("numeric value out of int range: %v", f) + } + return int(f), nil +} + +// toInt64 converts a value to int64, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values that lose precision in the float64→int64 conversion. +func toInt64(val any) (int64, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + result := int64(f) + // Check round-trip to detect precision loss for large int64 values + if float64(result) != f { + return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) + } + return result, nil +} + +// RequiredParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type. +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredParam[T comparable](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + // Check if the parameter is of the expected type + val, ok := args[p].(T) + if !ok { + return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) + } + + if val == zero { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + return val, nil +} + +// RequiredInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredInt(args map[string]any, p string) (int, error) { + v, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt(v) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// RequiredBigInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value. +// 4. Validates that the float64 value can be safely converted to int64 without truncation. +func RequiredBigInt(args map[string]any, p string) (int64, error) { + val, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt64(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// OptionalParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalParam[T any](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, nil + } + + // Check if the parameter is of the expected type + if _, ok := args[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, args[p]) + } + + return args[p].(T), nil +} + +// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type (float64 or numeric string) and returns it +func OptionalIntParam(args map[string]any, p string) (int, error) { + val, ok := args[p] + if !ok { + return 0, nil + } + + result, err := toInt(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + return result, nil +} + +// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalIntParam, but it also takes a default value. +// If the parameter is not present or is zero, the default is returned. +func OptionalIntParamWithDefault(args map[string]any, p string, d int) (int, error) { + v, err := OptionalIntParam(args, p) + if err != nil { + return 0, err + } + if v == 0 { + return d, nil + } + return v, nil +} + +// OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalParam[bool], but it also takes a default value. +func OptionalBoolParamWithDefault(args map[string]any, p string, d bool) (bool, error) { + _, ok := args[p] + v, err := OptionalParam[bool](args, p) + if err != nil { + return false, err + } + if !ok { + return d, nil + } + return v, nil +} + +// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, iterates the elements and checks each is a string +func OptionalStringArrayParam(args map[string]any, p string) ([]string, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []string{}, nil + } + + switch v := args[p].(type) { + case nil: + return []string{}, nil + case []string: + return v, nil + case []any: + strSlice := make([]string, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + strSlice[i] = s + } + return strSlice, nil + default: + return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, args[p]) + } +} + +func convertStringSliceToBigIntSlice(s []string) ([]int64, error) { + int64Slice := make([]int64, len(s)) + for i, str := range s { + val, err := convertStringToBigInt(str, 0) + if err != nil { + return nil, fmt.Errorf("failed to convert element %d (%s) to int64: %w", i, str, err) + } + int64Slice[i] = val + } + return int64Slice, nil +} + +func convertStringToBigInt(s string, def int64) (int64, error) { + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return def, fmt.Errorf("failed to convert string %s to int64: %w", s, err) + } + return v, nil +} + +// OptionalBigIntArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns an empty slice +// 2. If it is present, iterates the elements, checks each is a string, and converts them to int64 values +func OptionalBigIntArrayParam(args map[string]any, p string) ([]int64, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []int64{}, nil + } + + switch v := args[p].(type) { + case nil: + return []int64{}, nil + case []string: + return convertStringSliceToBigIntSlice(v) + case []any: + int64Slice := make([]int64, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []int64{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + val, err := convertStringToBigInt(s, 0) + if err != nil { + return []int64{}, fmt.Errorf("parameter %s: failed to convert element %d (%s) to int64: %w", p, i, s, err) + } + int64Slice[i] = val + } + return int64Slice, nil + default: + return []int64{}, fmt.Errorf("parameter %s could not be coerced to []int64, is %T", p, args[p]) + } +} + +// WithPagination adds REST API pagination parameters to a tool. +// https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api +func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + return schema +} + +// WithUnifiedPagination adds REST API pagination parameters to a tool. +// GraphQL tools will use this and convert page/perPage to GraphQL cursor parameters internally. +func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + } + + return schema +} + +// WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter). +func WithCursorPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the cursor from the previous response.", + } + + return schema +} + +type PaginationParams struct { + Page int + PerPage int + After string +} + +// OptionalPaginationParams returns the "page", "perPage", and "after" parameters from the request, +// or their default values if not present, "page" default is 1, "perPage" default is 30. +// In future, we may want to make the default values configurable, or even have this +// function returned from `withPagination`, where the defaults are provided alongside +// the min/max values. +func OptionalPaginationParams(args map[string]any) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(args, "page", 1) + if err != nil { + return PaginationParams{}, err + } + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return PaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return PaginationParams{}, err + } + return PaginationParams{ + Page: page, + PerPage: perPage, + After: after, + }, nil +} + +// OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request, +// without the "page" parameter, suitable for cursor-based pagination only. +func OptionalCursorPaginationParams(args map[string]any) (CursorPaginationParams, error) { + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return CursorPaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return CursorPaginationParams{}, err + } + return CursorPaginationParams{ + PerPage: perPage, + After: after, + }, nil +} + +type CursorPaginationParams struct { + PerPage int + After string +} + +type pageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + NextCursor string `json:"nextCursor,omitempty"` + PrevCursor string `json:"prevCursor,omitempty"` +} + +func buildPageInfo(resp *github.Response) pageInfo { + info := pageInfo{ + HasNextPage: false, + HasPreviousPage: false, + } + + if resp != nil { + if resp.After != "" { + info.HasNextPage = true + info.NextCursor = resp.After + } + if resp.Before != "" { + info.HasPreviousPage = true + info.PrevCursor = resp.Before + } + } + + return info +} + +// ToGraphQLParams converts cursor pagination parameters to GraphQL-specific parameters. +func (p CursorPaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + if p.PerPage > 100 { + return nil, fmt.Errorf("perPage value %d exceeds maximum of 100", p.PerPage) + } + if p.PerPage < 0 { + return nil, fmt.Errorf("perPage value %d cannot be negative", p.PerPage) + } + first := int32(p.PerPage) + + var after *string + if p.After != "" { + after = &p.After + } + + return &GraphQLPaginationParams{ + First: &first, + After: after, + }, nil +} + +type GraphQLPaginationParams struct { + First *int32 + After *string +} + +// ToGraphQLParams converts REST API pagination parameters to GraphQL-specific parameters. +// This converts page/perPage to first parameter for GraphQL queries. +// If After is provided, it takes precedence over page-based pagination. +func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + // Convert to CursorPaginationParams and delegate to avoid duplication + cursor := CursorPaginationParams{ + PerPage: p.PerPage, + After: p.After, + } + return cursor.ToGraphQLParams() +} +package github + +import ( + "errors" + "fmt" + "math" + "strconv" + + "github.com/google/go-github/v87/github" + "github.com/google/jsonschema-go/jsonschema" +) + +// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. +// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. +func OptionalParamOK[T any](args map[string]any, p string) (value T, ok bool, err error) { + // Check if the parameter is present in the request + val, exists := args[p] + if !exists { + // Not present, return zero value, false, no error + return + } + + // Check if the parameter is of the expected type + value, ok = val.(T) + if !ok { + // Present but wrong type + err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) + ok = true // Set ok to true because the parameter *was* present, even if wrong type + return + } + + // Present and correct type + ok = true + return +} + +// isAcceptedError checks if the error is an accepted error. +func isAcceptedError(err error) bool { + var acceptedError *github.AcceptedError + return errors.As(err, &acceptedError) +} + +// toInt converts a value to int, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values outside the int range. +func toInt(val any) (int, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + if f > math.MaxInt || f < math.MinInt { + return 0, fmt.Errorf("numeric value out of int range: %v", f) + } + return int(f), nil +} + +// toInt64 converts a value to int64, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values that lose precision in the float64→int64 conversion. +func toInt64(val any) (int64, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + result := int64(f) + // Check round-trip to detect precision loss for large int64 values + if float64(result) != f { + return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) + } + return result, nil +} + +// RequiredParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type. +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredParam[T comparable](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + // Check if the parameter is of the expected type + val, ok := args[p].(T) + if !ok { + return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) + } + + if val == zero { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + return val, nil +} + +// RequiredInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredInt(args map[string]any, p string) (int, error) { + v, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt(v) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// RequiredBigInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value. +// 4. Validates that the float64 value can be safely converted to int64 without truncation. +func RequiredBigInt(args map[string]any, p string) (int64, error) { + val, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt64(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// OptionalParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalParam[T any](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, nil + } + + // Check if the parameter is of the expected type + if _, ok := args[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, args[p]) + } + + return args[p].(T), nil +} + +// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type (float64 or numeric string) and returns it +func OptionalIntParam(args map[string]any, p string) (int, error) { + val, ok := args[p] + if !ok { + return 0, nil + } + + result, err := toInt(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + return result, nil +} + +// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalIntParam, but it also takes a default value. +// If the parameter is not present or is zero, the default is returned. +func OptionalIntParamWithDefault(args map[string]any, p string, d int) (int, error) { + v, err := OptionalIntParam(args, p) + if err != nil { + return 0, err + } + if v == 0 { + return d, nil + } + return v, nil +} + +// OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalParam[bool], but it also takes a default value. +func OptionalBoolParamWithDefault(args map[string]any, p string, d bool) (bool, error) { + _, ok := args[p] + v, err := OptionalParam[bool](args, p) + if err != nil { + return false, err + } + if !ok { + return d, nil + } + return v, nil +} + +// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, iterates the elements and checks each is a string +func OptionalStringArrayParam(args map[string]any, p string) ([]string, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []string{}, nil + } + + switch v := args[p].(type) { + case nil: + return []string{}, nil + case []string: + return v, nil + case []any: + strSlice := make([]string, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + strSlice[i] = s + } + return strSlice, nil + default: + return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, args[p]) + } +} + +func convertStringSliceToBigIntSlice(s []string) ([]int64, error) { + int64Slice := make([]int64, len(s)) + for i, str := range s { + val, err := convertStringToBigInt(str, 0) + if err != nil { + return nil, fmt.Errorf("failed to convert element %d (%s) to int64: %w", i, str, err) + } + int64Slice[i] = val + } + return int64Slice, nil +} + +func convertStringToBigInt(s string, def int64) (int64, error) { + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return def, fmt.Errorf("failed to convert string %s to int64: %w", s, err) + } + return v, nil +} + +// OptionalBigIntArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns an empty slice +// 2. If it is present, iterates the elements, checks each is a string, and converts them to int64 values +func OptionalBigIntArrayParam(args map[string]any, p string) ([]int64, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []int64{}, nil + } + + switch v := args[p].(type) { + case nil: + return []int64{}, nil + case []string: + return convertStringSliceToBigIntSlice(v) + case []any: + int64Slice := make([]int64, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []int64{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + val, err := convertStringToBigInt(s, 0) + if err != nil { + return []int64{}, fmt.Errorf("parameter %s: failed to convert element %d (%s) to int64: %w", p, i, s, err) + } + int64Slice[i] = val + } + return int64Slice, nil + default: + return []int64{}, fmt.Errorf("parameter %s could not be coerced to []int64, is %T", p, args[p]) + } +} + +// WithPagination adds REST API pagination parameters to a tool. +// https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api +func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + return schema +} + +// WithUnifiedPagination adds REST API pagination parameters to a tool. +// GraphQL tools will use this and convert page/perPage to GraphQL cursor parameters internally. +func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + } + + return schema +} + +// WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter). +func WithCursorPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the cursor from the previous response.", + } + + return schema +} + +type PaginationParams struct { + Page int + PerPage int + After string +} + +// OptionalPaginationParams returns the "page", "perPage", and "after" parameters from the request, +// or their default values if not present, "page" default is 1, "perPage" default is 30. +// In future, we may want to make the default values configurable, or even have this +// function returned from `withPagination`, where the defaults are provided alongside +// the min/max values. +func OptionalPaginationParams(args map[string]any) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(args, "page", 1) + if err != nil { + return PaginationParams{}, err + } + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return PaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return PaginationParams{}, err + } + return PaginationParams{ + Page: page, + PerPage: perPage, + After: after, + }, nil +} + +// OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request, +// without the "page" parameter, suitable for cursor-based pagination only. +func OptionalCursorPaginationParams(args map[string]any) (CursorPaginationParams, error) { + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return CursorPaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return CursorPaginationParams{}, err + } + return CursorPaginationParams{ + PerPage: perPage, + After: after, + }, nil +} + +type CursorPaginationParams struct { + PerPage int + After string +} + +type pageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + NextCursor string `json:"nextCursor,omitempty"` + PrevCursor string `json:"prevCursor,omitempty"` +} + +func buildPageInfo(resp *github.Response) pageInfo { + info := pageInfo{ + HasNextPage: false, + HasPreviousPage: false, + } + + if resp != nil { + if resp.After != "" { + info.HasNextPage = true + info.NextCursor = resp.After + } + if resp.Before != "" { + info.HasPreviousPage = true + info.PrevCursor = resp.Before + } + } + + return info +} + +// ToGraphQLParams converts cursor pagination parameters to GraphQL-specific parameters. +func (p CursorPaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + if p.PerPage > 100 { + return nil, fmt.Errorf("perPage value %d exceeds maximum of 100", p.PerPage) + } + if p.PerPage < 0 { + return nil, fmt.Errorf("perPage value %d cannot be negative", p.PerPage) + } + first := int32(p.PerPage) + + var after *string + if p.After != "" { + after = &p.After + } + + return &GraphQLPaginationParams{ + First: &first, + After: after, + }, nil +} + +type GraphQLPaginationParams struct { + First *int32 + After *string +} + +// ToGraphQLParams converts REST API pagination parameters to GraphQL-specific parameters. +// This converts page/perPage to first parameter for GraphQL queries. +// If After is provided, it takes precedence over page-based pagination. +func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + // Convert to CursorPaginationParams and delegate to avoid duplication + cursor := CursorPaginationParams{ + PerPage: p.PerPage, + After: p.After, + } + return cursor.ToGraphQLParams() +} +package github + +import ( + "errors" + "fmt" + "math" + "strconv" + + "github.com/google/go-github/v87/github" + "github.com/google/jsonschema-go/jsonschema" +) + +// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. +// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. +func OptionalParamOK[T any](args map[string]any, p string) (value T, ok bool, err error) { + // Check if the parameter is present in the request + val, exists := args[p] + if !exists { + // Not present, return zero value, false, no error + return + } + + // Check if the parameter is of the expected type + value, ok = val.(T) + if !ok { + // Present but wrong type + err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) + ok = true // Set ok to true because the parameter *was* present, even if wrong type + return + } + + // Present and correct type + ok = true + return +} + +// isAcceptedError checks if the error is an accepted error. +func isAcceptedError(err error) bool { + var acceptedError *github.AcceptedError + return errors.As(err, &acceptedError) +} + +// toInt converts a value to int, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values outside the int range. +func toInt(val any) (int, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + if f > math.MaxInt || f < math.MinInt { + return 0, fmt.Errorf("numeric value out of int range: %v", f) + } + return int(f), nil +} + +// toInt64 converts a value to int64, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values that lose precision in the float64→int64 conversion. +func toInt64(val any) (int64, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + result := int64(f) + // Check round-trip to detect precision loss for large int64 values + if float64(result) != f { + return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) + } + return result, nil +} + +// RequiredParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type. +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredParam[T comparable](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + // Check if the parameter is of the expected type + val, ok := args[p].(T) + if !ok { + return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) + } + + if val == zero { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + return val, nil +} + +// RequiredInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredInt(args map[string]any, p string) (int, error) { + v, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt(v) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// RequiredBigInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value. +// 4. Validates that the float64 value can be safely converted to int64 without truncation. +func RequiredBigInt(args map[string]any, p string) (int64, error) { + val, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt64(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// OptionalParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalParam[T any](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, nil + } + + // Check if the parameter is of the expected type + if _, ok := args[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, args[p]) + } + + return args[p].(T), nil +} + +// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type (float64 or numeric string) and returns it +func OptionalIntParam(args map[string]any, p string) (int, error) { + val, ok := args[p] + if !ok { + return 0, nil + } + + result, err := toInt(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + return result, nil +} + +// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalIntParam, but it also takes a default value. +// If the parameter is not present or is zero, the default is returned. +func OptionalIntParamWithDefault(args map[string]any, p string, d int) (int, error) { + v, err := OptionalIntParam(args, p) + if err != nil { + return 0, err + } + if v == 0 { + return d, nil + } + return v, nil +} + +// OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalParam[bool], but it also takes a default value. +func OptionalBoolParamWithDefault(args map[string]any, p string, d bool) (bool, error) { + _, ok := args[p] + v, err := OptionalParam[bool](args, p) + if err != nil { + return false, err + } + if !ok { + return d, nil + } + return v, nil +} + +// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, iterates the elements and checks each is a string +func OptionalStringArrayParam(args map[string]any, p string) ([]string, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []string{}, nil + } + + switch v := args[p].(type) { + case nil: + return []string{}, nil + case []string: + return v, nil + case []any: + strSlice := make([]string, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + strSlice[i] = s + } + return strSlice, nil + default: + return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, args[p]) + } +} + +func convertStringSliceToBigIntSlice(s []string) ([]int64, error) { + int64Slice := make([]int64, len(s)) + for i, str := range s { + val, err := convertStringToBigInt(str, 0) + if err != nil { + return nil, fmt.Errorf("failed to convert element %d (%s) to int64: %w", i, str, err) + } + int64Slice[i] = val + } + return int64Slice, nil +} + +func convertStringToBigInt(s string, def int64) (int64, error) { + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return def, fmt.Errorf("failed to convert string %s to int64: %w", s, err) + } + return v, nil +} + +// OptionalBigIntArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns an empty slice +// 2. If it is present, iterates the elements, checks each is a string, and converts them to int64 values +func OptionalBigIntArrayParam(args map[string]any, p string) ([]int64, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []int64{}, nil + } + + switch v := args[p].(type) { + case nil: + return []int64{}, nil + case []string: + return convertStringSliceToBigIntSlice(v) + case []any: + int64Slice := make([]int64, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []int64{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + val, err := convertStringToBigInt(s, 0) + if err != nil { + return []int64{}, fmt.Errorf("parameter %s: failed to convert element %d (%s) to int64: %w", p, i, s, err) + } + int64Slice[i] = val + } + return int64Slice, nil + default: + return []int64{}, fmt.Errorf("parameter %s could not be coerced to []int64, is %T", p, args[p]) + } +} + +// WithPagination adds REST API pagination parameters to a tool. +// https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api +func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + return schema +} + +// WithUnifiedPagination adds REST API pagination parameters to a tool. +// GraphQL tools will use this and convert page/perPage to GraphQL cursor parameters internally. +func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + } + + return schema +} + +// WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter). +func WithCursorPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the cursor from the previous response.", + } + + return schema +} + +type PaginationParams struct { + Page int + PerPage int + After string +} + +// OptionalPaginationParams returns the "page", "perPage", and "after" parameters from the request, +// or their default values if not present, "page" default is 1, "perPage" default is 30. +// In future, we may want to make the default values configurable, or even have this +// function returned from `withPagination`, where the defaults are provided alongside +// the min/max values. +func OptionalPaginationParams(args map[string]any) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(args, "page", 1) + if err != nil { + return PaginationParams{}, err + } + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return PaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return PaginationParams{}, err + } + return PaginationParams{ + Page: page, + PerPage: perPage, + After: after, + }, nil +} + +// OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request, +// without the "page" parameter, suitable for cursor-based pagination only. +func OptionalCursorPaginationParams(args map[string]any) (CursorPaginationParams, error) { + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return CursorPaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return CursorPaginationParams{}, err + } + return CursorPaginationParams{ + PerPage: perPage, + After: after, + }, nil +} + +type CursorPaginationParams struct { + PerPage int + After string +} + +type pageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + NextCursor string `json:"nextCursor,omitempty"` + PrevCursor string `json:"prevCursor,omitempty"` +} + +func buildPageInfo(resp *github.Response) pageInfo { + info := pageInfo{ + HasNextPage: false, + HasPreviousPage: false, + } + + if resp != nil { + if resp.After != "" { + info.HasNextPage = true + info.NextCursor = resp.After + } + if resp.Before != "" { + info.HasPreviousPage = true + info.PrevCursor = resp.Before + } + } + + return info +} + +// ToGraphQLParams converts cursor pagination parameters to GraphQL-specific parameters. +func (p CursorPaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + if p.PerPage > 100 { + return nil, fmt.Errorf("perPage value %d exceeds maximum of 100", p.PerPage) + } + if p.PerPage < 0 { + return nil, fmt.Errorf("perPage value %d cannot be negative", p.PerPage) + } + first := int32(p.PerPage) + + var after *string + if p.After != "" { + after = &p.After + } + + return &GraphQLPaginationParams{ + First: &first, + After: after, + }, nil +} + +type GraphQLPaginationParams struct { + First *int32 + After *string +} + +// ToGraphQLParams converts REST API pagination parameters to GraphQL-specific parameters. +// This converts page/perPage to first parameter for GraphQL queries. +// If After is provided, it takes precedence over page-based pagination. +func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + // Convert to CursorPaginationParams and delegate to avoid duplication + cursor := CursorPaginationParams{ + PerPage: p.PerPage, + After: p.After, + } + return cursor.ToGraphQLParams() +} +package github + +import ( + "errors" + "fmt" + "math" + "strconv" + + "github.com/google/go-github/v87/github" + "github.com/google/jsonschema-go/jsonschema" +) + +// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. +// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. +func OptionalParamOK[T any](args map[string]any, p string) (value T, ok bool, err error) { + // Check if the parameter is present in the request + val, exists := args[p] + if !exists { + // Not present, return zero value, false, no error + return + } + + // Check if the parameter is of the expected type + value, ok = val.(T) + if !ok { + // Present but wrong type + err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) + ok = true // Set ok to true because the parameter *was* present, even if wrong type + return + } + + // Present and correct type + ok = true + return +} + +// isAcceptedError checks if the error is an accepted error. +func isAcceptedError(err error) bool { + var acceptedError *github.AcceptedError + return errors.As(err, &acceptedError) +} + +// toInt converts a value to int, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values outside the int range. +func toInt(val any) (int, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + if f > math.MaxInt || f < math.MinInt { + return 0, fmt.Errorf("numeric value out of int range: %v", f) + } + return int(f), nil +} + +// toInt64 converts a value to int64, handling both float64 and string representations. +// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, +// fractional values, and values that lose precision in the float64→int64 conversion. +func toInt64(val any) (int64, error) { + var f float64 + switch v := val.(type) { + case float64: + f = v + case string: + var err error + f, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %s", v) + } + default: + return 0, fmt.Errorf("expected number, got %T", val) + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("non-finite numeric value") + } + if f != math.Trunc(f) { + return 0, fmt.Errorf("non-integer numeric value: %v", f) + } + result := int64(f) + // Check round-trip to detect precision loss for large int64 values + if float64(result) != f { + return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) + } + return result, nil +} + +// RequiredParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type. +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredParam[T comparable](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + // Check if the parameter is of the expected type + val, ok := args[p].(T) + if !ok { + return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) + } + + if val == zero { + return zero, fmt.Errorf("missing required parameter: %s", p) + } + + return val, nil +} + +// RequiredInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value +func RequiredInt(args map[string]any, p string) (int, error) { + v, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt(v) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// RequiredBigInt is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request. +// 2. Checks if the parameter is of the expected type (float64 or numeric string). +// 3. Checks if the parameter is not empty, i.e: non-zero value. +// 4. Validates that the float64 value can be safely converted to int64 without truncation. +func RequiredBigInt(args map[string]any, p string) (int64, error) { + val, ok := args[p] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + result, err := toInt64(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + if result == 0 { + return 0, fmt.Errorf("missing required parameter: %s", p) + } + + return result, nil +} + +// OptionalParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type and returns it +func OptionalParam[T any](args map[string]any, p string) (T, error) { + var zero T + + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return zero, nil + } + + // Check if the parameter is of the expected type + if _, ok := args[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, args[p]) + } + + return args[p].(T), nil +} + +// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, it checks if the parameter is of the expected type (float64 or numeric string) and returns it +func OptionalIntParam(args map[string]any, p string) (int, error) { + val, ok := args[p] + if !ok { + return 0, nil + } + + result, err := toInt(val) + if err != nil { + return 0, fmt.Errorf("parameter %s is not a valid number: %w", p, err) + } + + return result, nil +} + +// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalIntParam, but it also takes a default value. +// If the parameter is not present or is zero, the default is returned. +func OptionalIntParamWithDefault(args map[string]any, p string, d int) (int, error) { + v, err := OptionalIntParam(args, p) + if err != nil { + return 0, err + } + if v == 0 { + return d, nil + } + return v, nil +} + +// OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// similar to OptionalParam[bool], but it also takes a default value. +func OptionalBoolParamWithDefault(args map[string]any, p string, d bool) (bool, error) { + _, ok := args[p] + v, err := OptionalParam[bool](args, p) + if err != nil { + return false, err + } + if !ok { + return d, nil + } + return v, nil +} + +// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns its zero-value +// 2. If it is present, iterates the elements and checks each is a string +func OptionalStringArrayParam(args map[string]any, p string) ([]string, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []string{}, nil + } + + switch v := args[p].(type) { + case nil: + return []string{}, nil + case []string: + return v, nil + case []any: + strSlice := make([]string, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + strSlice[i] = s + } + return strSlice, nil + default: + return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, args[p]) + } +} + +func convertStringSliceToBigIntSlice(s []string) ([]int64, error) { + int64Slice := make([]int64, len(s)) + for i, str := range s { + val, err := convertStringToBigInt(str, 0) + if err != nil { + return nil, fmt.Errorf("failed to convert element %d (%s) to int64: %w", i, str, err) + } + int64Slice[i] = val + } + return int64Slice, nil +} + +func convertStringToBigInt(s string, def int64) (int64, error) { + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return def, fmt.Errorf("failed to convert string %s to int64: %w", s, err) + } + return v, nil +} + +// OptionalBigIntArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// It does the following checks: +// 1. Checks if the parameter is present in the request, if not, it returns an empty slice +// 2. If it is present, iterates the elements, checks each is a string, and converts them to int64 values +func OptionalBigIntArrayParam(args map[string]any, p string) ([]int64, error) { + // Check if the parameter is present in the request + if _, ok := args[p]; !ok { + return []int64{}, nil + } + + switch v := args[p].(type) { + case nil: + return []int64{}, nil + case []string: + return convertStringSliceToBigIntSlice(v) + case []any: + int64Slice := make([]int64, len(v)) + for i, v := range v { + s, ok := v.(string) + if !ok { + return []int64{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) + } + val, err := convertStringToBigInt(s, 0) + if err != nil { + return []int64{}, fmt.Errorf("parameter %s: failed to convert element %d (%s) to int64: %w", p, i, s, err) + } + int64Slice[i] = val + } + return int64Slice, nil + default: + return []int64{}, fmt.Errorf("parameter %s could not be coerced to []int64, is %T", p, args[p]) + } +} + +// WithPagination adds REST API pagination parameters to a tool. +// https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api +func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + return schema +} + +// WithUnifiedPagination adds REST API pagination parameters to a tool. +// GraphQL tools will use this and convert page/perPage to GraphQL cursor parameters internally. +func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + } + + return schema +} + +// WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter). +func WithCursorPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), + } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "string", + Description: "Cursor for pagination. Use the cursor from the previous response.", + } + + return schema +} + +type PaginationParams struct { + Page int + PerPage int + After string +} + +// OptionalPaginationParams returns the "page", "perPage", and "after" parameters from the request, +// or their default values if not present, "page" default is 1, "perPage" default is 30. +// In future, we may want to make the default values configurable, or even have this +// function returned from `withPagination`, where the defaults are provided alongside +// the min/max values. +func OptionalPaginationParams(args map[string]any) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(args, "page", 1) + if err != nil { + return PaginationParams{}, err + } + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return PaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return PaginationParams{}, err + } + return PaginationParams{ + Page: page, + PerPage: perPage, + After: after, + }, nil +} + +// OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request, +// without the "page" parameter, suitable for cursor-based pagination only. +func OptionalCursorPaginationParams(args map[string]any) (CursorPaginationParams, error) { + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) + if err != nil { + return CursorPaginationParams{}, err + } + after, err := OptionalParam[string](args, "after") + if err != nil { + return CursorPaginationParams{}, err + } + return CursorPaginationParams{ + PerPage: perPage, + After: after, + }, nil +} + +type CursorPaginationParams struct { + PerPage int + After string +} + +type pageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + NextCursor string `json:"nextCursor,omitempty"` + PrevCursor string `json:"prevCursor,omitempty"` +} + +func buildPageInfo(resp *github.Response) pageInfo { + info := pageInfo{ + HasNextPage: false, + HasPreviousPage: false, + } + + if resp != nil { + if resp.After != "" { + info.HasNextPage = true + info.NextCursor = resp.After + } + if resp.Before != "" { + info.HasPreviousPage = true + info.PrevCursor = resp.Before + } + } + + return info +} + +// ToGraphQLParams converts cursor pagination parameters to GraphQL-specific parameters. +func (p CursorPaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + if p.PerPage > 100 { + return nil, fmt.Errorf("perPage value %d exceeds maximum of 100", p.PerPage) + } + if p.PerPage < 0 { + return nil, fmt.Errorf("perPage value %d cannot be negative", p.PerPage) + } + first := int32(p.PerPage) + + var after *string + if p.After != "" { + after = &p.After + } + + return &GraphQLPaginationParams{ + First: &first, + After: after, + }, nil +} + +type GraphQLPaginationParams struct { + First *int32 + After *string +} + +// ToGraphQLParams converts REST API pagination parameters to GraphQL-specific parameters. +// This converts page/perPage to first parameter for GraphQL queries. +// If After is provided, it takes precedence over page-based pagination. +func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { + // Convert to CursorPaginationParams and delegate to avoid duplication + cursor := CursorPaginationParams{ + PerPage: p.PerPage, + After: p.After, + } + return cursor.ToGraphQLParams() +} +func OptionalParamOK[T any, A map[string]any](args A, p string) +func OptionalParamOK[T any](args map[string]any, p string) +func buildPageInfo(resp *github.Response) pageInfo { + return pageInfo{ + HasNextPage: resp.After != "", + HasPreviousPage: resp.Before != "", + NextCursor: resp.After, + PrevCursor: resp.Before, + } +} +func buildPageInfo(resp *github.Response) pageInfo { + info := pageInfo{ + HasNextPage: false, + HasPreviousPage: false, + } + if resp != nil { + if resp.After != "" { + info.HasNextPage = true + info.NextCursor = resp.After + } + if resp.Before != "" { + info.HasPreviousPage = true + info.PrevCursor = resp.Before + } + } + return info +}