Module: MCPClient::ElicitationValidator
- Defined in:
- lib/mcp_client/elicitation_validator.rb
Overview
Validates elicitation schemas and content per MCP 2025-11-25 spec. Schemas are restricted to flat objects with primitive property types:
string (with optional enum, pattern, format, minLength, maxLength)
number / integer (with optional minimum, maximum)
boolean
array (multi-select enum only, with items containing enum or anyOf)
Constant Summary collapse
- PRIMITIVE_TYPES =
Allowed primitive types for schema properties
%w[string number integer boolean].freeze
- STRING_FORMATS =
Allowed string formats per MCP spec
%w[email uri date date-time].freeze
Class Method Summary collapse
-
.parseable?(klass, value) ⇒ Boolean
Whether the value parses.
-
.valid_uri?(value) ⇒ Boolean
Whether the value is an absolute URI.
-
.validate_array_property(name, prop) ⇒ Array<String>
Validate an array property (multi-select enum only).
-
.validate_array_value(field, value, prop) ⇒ Array<String>
Validate an array value against its property schema (multi-select enum).
-
.validate_content(content, schema) ⇒ Array<String>
Validate content against a requestedSchema.
-
.validate_number_value(field, value, prop) ⇒ Array<String>
Validate a number value against its property schema.
-
.validate_primitive_property(name, prop) ⇒ Array<String>
Validate a primitive property (string, number, integer, boolean).
-
.validate_property(name, prop) ⇒ Array<String>
Validate a single property definition.
-
.validate_schema(schema) ⇒ Array<String>
Validate that a requestedSchema conforms to MCP elicitation constraints.
-
.validate_string_format(field, value, format) ⇒ Array<String>
Validate a string value against the schema's format constraint.
-
.validate_string_value(field, value, prop) ⇒ Array<String>
Validate a string value against its property schema.
-
.validate_value(field, value, prop) ⇒ Array<String>
Validate a single value against its property schema.
Class Method Details
.parseable?(klass, value) ⇒ Boolean
Returns whether the value parses.
239 240 241 242 243 244 |
# File 'lib/mcp_client/elicitation_validator.rb', line 239 def self.parseable?(klass, value) klass.iso8601(value) true rescue ArgumentError false end |
.valid_uri?(value) ⇒ Boolean
Returns whether the value is an absolute URI.
229 230 231 232 233 234 |
# File 'lib/mcp_client/elicitation_validator.rb', line 229 def self.valid_uri?(value) uri = URI.parse(value) !uri.scheme.nil? rescue URI::InvalidURIError false end |
.validate_array_property(name, prop) ⇒ Array<String>
Validate an array property (multi-select enum only).
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/mcp_client/elicitation_validator.rb', line 95 def self.validate_array_property(name, prop) errors = [] items = prop['items'] unless items.is_a?(Hash) errors << "Property '#{name}' array type requires 'items' definition" return errors end has_enum = items['enum'].is_a?(Array) has_any_of = items['anyOf'].is_a?(Array) errors << "Property '#{name}' array items must have 'enum' or 'anyOf'" unless has_enum || has_any_of errors end |
.validate_array_value(field, value, prop) ⇒ Array<String>
Validate an array value against its property schema (multi-select enum).
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'lib/mcp_client/elicitation_validator.rb', line 273 def self.validate_array_value(field, value, prop) errors = [] unless value.is_a?(Array) errors << "Field '#{field}' must be an array" return errors end items = prop['items'] || {} allowed = if items['enum'].is_a?(Array) items['enum'] elsif items['anyOf'].is_a?(Array) items['anyOf'].map { |o| o['const'] } end if allowed value.each do |v| errors << "Field '#{field}' contains invalid value '#{v}'" unless allowed.include?(v) end end if prop['minItems'] && value.length < prop['minItems'] errors << "Field '#{field}' must have at least #{prop['minItems']} items" end if prop['maxItems'] && value.length > prop['maxItems'] errors << "Field '#{field}' must have at most #{prop['maxItems']} items" end errors end |
.validate_content(content, schema) ⇒ Array<String>
Validate content against a requestedSchema. Returns an array of error messages (empty if valid).
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/mcp_client/elicitation_validator.rb', line 117 def self.validate_content(content, schema) errors = [] return errors unless content.is_a?(Hash) && schema.is_a?(Hash) properties = schema['properties'] || {} required = Array(schema['required']) # Check required fields required.each do |field| field_s = field.to_s errors << "Missing required field '#{field_s}'" unless content.key?(field_s) || content.key?(field_s.to_sym) end # Validate each provided field content.each do |field, value| prop = properties[field.to_s] next unless prop.is_a?(Hash) errors.concat(validate_value(field.to_s, value, prop)) end errors end |
.validate_number_value(field, value, prop) ⇒ Array<String>
Validate a number value against its property schema.
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/mcp_client/elicitation_validator.rb', line 251 def self.validate_number_value(field, value, prop) errors = [] unless value.is_a?(Numeric) errors << "Field '#{field}' must be a number" return errors end errors << "Field '#{field}' must be an integer" if prop['type'] == 'integer' && !value.is_a?(Integer) errors << "Field '#{field}' must be >= #{prop['minimum']}" if prop['minimum'] && value < prop['minimum'] errors << "Field '#{field}' must be <= #{prop['maximum']}" if prop['maximum'] && value > prop['maximum'] errors end |
.validate_primitive_property(name, prop) ⇒ Array<String>
Validate a primitive property (string, number, integer, boolean).
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/mcp_client/elicitation_validator.rb', line 69 def self.validate_primitive_property(name, prop) errors = [] type = prop['type'] case type when 'string' if prop['format'] && !STRING_FORMATS.include?(prop['format']) errors << "Property '#{name}' has unsupported format '#{prop['format']}'" end errors << "Property '#{name}' enum must be an array" if prop['enum'] && !prop['enum'].is_a?(Array) when 'number', 'integer' if prop.key?('minimum') && !prop['minimum'].is_a?(Numeric) errors << "Property '#{name}' minimum must be numeric" end if prop.key?('maximum') && !prop['maximum'].is_a?(Numeric) errors << "Property '#{name}' maximum must be numeric" end end errors end |
.validate_property(name, prop) ⇒ Array<String>
Validate a single property definition.
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/mcp_client/elicitation_validator.rb', line 48 def self.validate_property(name, prop) errors = [] return errors unless prop.is_a?(Hash) type = prop['type'] if type == 'array' errors.concat(validate_array_property(name, prop)) elsif PRIMITIVE_TYPES.include?(type) errors.concat(validate_primitive_property(name, prop)) else errors << "Property '#{name}' has unsupported type '#{type}'" end errors end |
.validate_schema(schema) ⇒ Array<String>
Validate that a requestedSchema conforms to MCP elicitation constraints. Returns an array of error messages (empty if valid).
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/mcp_client/elicitation_validator.rb', line 25 def self.validate_schema(schema) errors = [] return errors unless schema.is_a?(Hash) unless schema['type'] == 'object' errors << "Schema type must be 'object', got '#{schema['type']}'" return errors end properties = schema['properties'] return errors unless properties.is_a?(Hash) properties.each do |name, prop| errors.concat(validate_property(name, prop)) end errors end |
.validate_string_format(field, value, format) ⇒ Array<String>
Validate a string value against the schema's format constraint. The MCP elicitation schema supports email, uri, date, and date-time.
215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/mcp_client/elicitation_validator.rb', line 215 def self.validate_string_format(field, value, format) valid = case format when 'email' then value.match?(URI::MailTo::EMAIL_REGEXP) when 'uri' then valid_uri?(value) when 'date' then parseable?(Date, value) when 'date-time' then parseable?(Time, value) else true # No format declared, or an unknown format: not validated end valid ? [] : ["Field '#{field}' must be a valid #{format}"] end |
.validate_string_value(field, value, prop) ⇒ Array<String>
Validate a string value against its property schema.
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/mcp_client/elicitation_validator.rb', line 169 def self.validate_string_value(field, value, prop) errors = [] unless value.is_a?(String) errors << "Field '#{field}' must be a string" return errors end if prop['enum'].is_a?(Array) && !prop['enum'].include?(value) errors << "Field '#{field}' must be one of: #{prop['enum'].join(', ')}" end if prop['oneOf'].is_a?(Array) allowed = prop['oneOf'].map { |o| o['const'] } errors << "Field '#{field}' must be one of: #{allowed.join(', ')}" unless allowed.include?(value) end if prop['pattern'] begin unless value.match?(Regexp.new(prop['pattern'])) errors << "Field '#{field}' must match pattern '#{prop['pattern']}'" end rescue RegexpError # Skip pattern validation if the pattern is invalid end end if prop['minLength'] && value.length < prop['minLength'] errors << "Field '#{field}' must be at least #{prop['minLength']} characters" end if prop['maxLength'] && value.length > prop['maxLength'] errors << "Field '#{field}' must be at most #{prop['maxLength']} characters" end errors.concat(validate_string_format(field, value, prop['format'])) errors end |
.validate_value(field, value, prop) ⇒ Array<String>
Validate a single value against its property schema.
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/mcp_client/elicitation_validator.rb', line 146 def self.validate_value(field, value, prop) errors = [] type = prop['type'] case type when 'string' errors.concat(validate_string_value(field, value, prop)) when 'number', 'integer' errors.concat(validate_number_value(field, value, prop)) when 'boolean' errors << "Field '#{field}' must be a boolean" unless [true, false].include?(value) when 'array' errors.concat(validate_array_value(field, value, prop)) end errors end |