Module: SolidLoop::Mcp::Toolset::SchemaValidator

Defined in:
lib/solid_loop/mcp/toolset.rb

Overview

A focused, dependency-free validator for the subset of JSON Schema the DSL actually uses: object required and top-level properties types (plus additionalProperties: false to reject unknown fields). It is not a full JSON-Schema implementation — the aim is to keep obviously wrong calls out of handlers, not to validate deep structures.

Constant Summary collapse

TYPE_CHECKS =

JSON Schema type name => a predicate on the (string-keyed, JSON-parsed) argument value.

{
  "string"  => ->(v) { v.is_a?(String) },
  "integer" => ->(v) { v.is_a?(Integer) },
  "number"  => ->(v) { v.is_a?(Numeric) && !v.is_a?(TrueClass) && !v.is_a?(FalseClass) },
  "boolean" => ->(v) { v == true || v == false },
  "array"   => ->(v) { v.is_a?(Array) },
  "object"  => ->(v) { v.is_a?(Hash) },
  "null"    => ->(v) { v.nil? }
}.freeze

Class Method Summary collapse

Class Method Details

.additional_violation(schema, arguments) ⇒ Object



72
73
74
75
76
77
78
# File 'lib/solid_loop/mcp/toolset.rb', line 72

def self.additional_violation(schema, arguments)
  return nil unless schema["additionalProperties"] == false

  allowed = (schema["properties"] || {}).keys
  extra   = arguments.keys - allowed
  "unexpected argument(s): #{extra.join(', ')}" if extra.any?
end

.normalize(value) ⇒ Object

Schemas are declared with symbol keys in the DSL but arrive JSON-parsed (string keys) over the wire; normalize to string keys so both match.



103
104
105
106
107
# File 'lib/solid_loop/mcp/toolset.rb', line 103

def self.normalize(value)
  return value unless value.is_a?(Hash) || value.is_a?(Array)

  JSON.parse(JSON.generate(value))
end

.property_type_violation(schema, arguments) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/solid_loop/mcp/toolset.rb', line 80

def self.property_type_violation(schema, arguments)
  properties = schema["properties"]
  return nil unless properties.is_a?(Hash)

  properties.each do |name, spec|
    next unless arguments.key?(name)

    expected = spec.is_a?(Hash) ? spec["type"] : nil
    next unless expected

    checks = Array(expected).map { |t| TYPE_CHECKS[t] }
    # If any declared type is unknown to us, don't enforce this property
    # (best-effort — we can't judge what we don't understand).
    next if checks.any?(&:nil?)

    ok = checks.any? { |check| check.call(arguments[name]) }
    return "argument '#{name}' must be #{Array(expected).join(' or ')}" unless ok
  end
  nil
end

.required_violation(schema, arguments) ⇒ Object



67
68
69
70
# File 'lib/solid_loop/mcp/toolset.rb', line 67

def self.required_violation(schema, arguments)
  missing = Array(schema["required"]).reject { |name| arguments.key?(name.to_s) }
  "missing required argument(s): #{missing.join(', ')}" if missing.any?
end

.violation(input_schema, arguments) ⇒ Object

Returns a human-readable violation string, or nil when arguments satisfy the schema. A non-object schema (or a schema whose type is not "object") is treated as "no constraints" — validation is best-effort.



54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/solid_loop/mcp/toolset.rb', line 54

def self.violation(input_schema, arguments)
  schema = normalize(input_schema)
  return nil unless schema.is_a?(Hash)

  type = schema["type"]
  return nil if type && type != "object"
  return "arguments must be an object" unless arguments.is_a?(Hash)

  required_violation(schema, arguments) ||
    additional_violation(schema, arguments) ||
    property_type_violation(schema, arguments)
end