chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/go-github/v89/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, A map[string]any](args A, 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.
|
||||
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 optionalBoolParam, 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 {
|
||||
return pageInfo{
|
||||
HasNextPage: resp.After != "",
|
||||
HasPreviousPage: resp.Before != "",
|
||||
NextCursor: resp.After,
|
||||
PrevCursor: resp.Before,
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user