Module: MCPClient::SchemaValidator::Scalars

Included in:
MCPClient::SchemaValidator
Defined in:
lib/mcp_client/schema_validator/scalars.rb

Overview

The string and number keywords, and the budget a peer-supplied pattern is matched under. Both a pattern and the values it runs against come from the remote peer, so matching is bounded by the validation-wide deadline. Extended into SchemaValidator, so the methods are its own.

Instance Method Summary collapse

Instance Method Details

#multiple_of?(data, factor) ⇒ Boolean

Whether dividing the value by the factor gives an integer (JSON Schema 2020-12 Validation Section 6.2.1). The division is exact: 0.0075 is a multiple of 0.0001, which binary floating point says it is not, so the decimal each number was written as decides.

Parameters:

  • data (Numeric) —

    the instance

  • factor (Numeric) —

    the multipleOf value

Returns:

  • (Boolean)


115
116
117
118
119
120
121
122
123
# File 'lib/mcp_client/schema_validator/scalars.rb', line 115

def multiple_of?(data, factor)
  return (data % factor).zero? if data.is_a?(Integer) && factor.is_a?(Integer)

  (Rational(data.to_s) / Rational(factor.to_s)).denominator == 1
rescue ArgumentError, ZeroDivisionError, FloatDomainError, TypeError
  # A value no decimal describes (an infinity a Ruby caller passed in;
  # JSON carries none) falls back to the floating-point remainder.
  (data % factor).zero?
end

#pattern_budget_remaining(deadline) ⇒ Float

Time left in the validation-wide budget.

Parameters:

  • deadline (Float, nil) —

    monotonic deadline, or nil for a lone match

Returns:

  • (Float) —

    seconds available; 0.0 when exhausted



65
66
67
68
69
70
71
72
# File 'lib/mcp_client/schema_validator/scalars.rb', line 65

def pattern_budget_remaining(deadline)
  return PATTERN_MATCH_TIMEOUT unless deadline

  remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
  return 0.0 if remaining <= 0

  [remaining, MIN_PATTERN_MATCH_TIMEOUT].max
end

#validate_number(data, schema, path, _dialect = nil) ⇒ Array<String>

Validate a number against its bounds. minimum / maximum and exclusiveMinimum / exclusiveMaximum are four independent numeric assertions in every supported dialect (draft-07 validation Sections 6.2.2-6.2.5); each present one is applied.

Parameters:

  • data (Numeric) —

    the number

  • schema (Hash) —

    string-keyed schema

  • path (String) —

    location for error messages

Returns:

  • (Array<String>) —

    validation errors



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/mcp_client/schema_validator/scalars.rb', line 82

def validate_number(data, schema, path, _dialect = nil)
  errors = []
  minimum = schema['minimum']
  maximum = schema['maximum']
  exclusive_min = schema['exclusiveMinimum']
  exclusive_max = schema['exclusiveMaximum']
  shown = clip_value(data)
  if minimum.is_a?(Numeric) && data < minimum
    errors << "#{path}: value #{shown} is less than minimum #{clip_value(minimum)}"
  end
  if maximum.is_a?(Numeric) && data > maximum
    errors << "#{path}: value #{shown} is greater than maximum #{clip_value(maximum)}"
  end
  if exclusive_min.is_a?(Numeric) && data <= exclusive_min
    errors << "#{path}: value #{shown} must be greater than exclusiveMinimum #{clip_value(exclusive_min)}"
  end
  if exclusive_max.is_a?(Numeric) && data >= exclusive_max
    errors << "#{path}: value #{shown} must be less than exclusiveMaximum #{clip_value(exclusive_max)}"
  end
  factor = schema['multipleOf']
  if factor.is_a?(Numeric) && factor.positive? && !multiple_of?(data, factor)
    errors << "#{path}: value #{shown} is not a multiple of #{clip_value(factor)}"
  end
  errors
end

#validate_pattern(data, pattern, path, deadline = nil) ⇒ Array<String>

Validate a string against a regular-expression pattern. A pattern that is no ECMA-262 expression is a malformed keyword the preflight refuses; should one reach here it is an error, never a pass.

The pattern comes from the tool's outputSchema, i.e. from the remote server, so matching runs against the validation-wide deadline: neither a single expensive expression nor many cheap-looking ones can pin the calling thread. A match that exceeds the budget aborts the validation rather than silently accepting the value — the value was never shown to satisfy the schema.

Parameters:

  • data (String) —

    the string

  • pattern (Object) —

    the pattern keyword value

  • path (String) —

    location for error messages

  • deadline (Float, nil) (defaults to: nil) —

    monotonic deadline for the whole validation

Returns:

  • (Array<String>) —

    validation errors

Raises:

  • (Aborted) —

    when the budget is exhausted



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/mcp_client/schema_validator/scalars.rb', line 45

def validate_pattern(data, pattern, path, deadline = nil)
  return [] unless pattern.is_a?(String)

  remaining = pattern_budget_remaining(deadline)
  raise Aborted, "validation time budget exhausted before pattern #{clip(pattern.inspect)}" if remaining.zero?

  return [] if data.match?(ecma_regexp(pattern, remaining, deadline))

  ["#{path}: string does not match pattern #{clip(pattern.inspect)}"]
rescue Regexp::TimeoutError
  raise Aborted, "pattern #{clip(pattern.inspect)} exceeded the #{PATTERN_MATCH_TIMEOUT}s matching budget"
rescue EcmaPatterns::Untranslatable => e
  ["#{path}: pattern #{clip(pattern.inspect)} cannot be evaluated faithfully (#{clip(e.message)})"]
rescue RegexpError => e
  ["#{path}: pattern #{clip(pattern.inspect)} is not an ECMA-262 regular expression (#{clip(e.message)})"]
end

#validate_string(data, schema, path, deadline = nil) ⇒ Array<String>

Validate a string against minLength/maxLength/pattern.

Parameters:

  • data (String) —

    the string

  • schema (Hash) —

    string-keyed schema

  • path (String) —

    location for error messages

Returns:

  • (Array<String>) —

    validation errors



15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/mcp_client/schema_validator/scalars.rb', line 15

def validate_string(data, schema, path, deadline = nil)
  errors = []
  min_length = schema['minLength']
  max_length = schema['maxLength']
  if min_length.is_a?(Numeric) && data.length < min_length
    errors << "#{path}: string is shorter than minLength #{min_length}"
  end
  if max_length.is_a?(Numeric) && data.length > max_length
    errors << "#{path}: string is longer than maxLength #{max_length}"
  end
  errors.concat(validate_pattern(data, schema['pattern'], path, deadline))
  errors
end