Module: MCPClient::SchemaValidator::InputRequirements

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

Overview

What an input schema requires of every instance, read through the applicators that apply unconditionally: the root, what its $ref chain reaches, and each allOf member (each of those recursively). The tools specification says clients SHOULD follow $ref resolution when validating tool inputs, and SEP-2106 made $ref and allOf legal on an inputSchema: a required behind them is as required as one at the root. A branch the instance may or may not satisfy (anyOf, oneOf, if) decides nothing here and is left to the server, which MUST validate the arguments anyway. Extended into SchemaValidator, so the methods are its own.

Instance Method Summary collapse

Instance Method Details

#input_requirements(schema) ⇒ Array(Array<String>, Hash{String => Object})

Returns the required property names, and the declared properties by name (a property declared nearer the root wins).

Parameters:

  • schema (Object) —

    the input schema (string or symbol keys)

Returns:

  • (Array(Array<String>, Hash{String => Object})) —

    the required property names, and the declared properties by name (a property declared nearer the root wins)



20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/mcp_client/schema_validator/input_requirements.rb', line 20

def input_requirements(schema)
  root = normalize_schema(schema)
  return [[], {}] unless root.is_a?(Hash)

  declared = dialect(root)
  scan = { count: 0, dialect: declared && canonical_dialect(declared), anchors: nil,
           walked: {}.compare_by_identity, required: [], properties: {}, pending: [root] }
  read_requirement_positions(root, scan)
  [scan[:required].uniq, scan[:properties]]
rescue TooLarge
  # Unusable anyway: the preflight reports it.
  [[], {}]
end

#position_dialect(node, root, scan) ⇒ String?

The dialect in force at a position: the one the anchor index recorded for its resource, else the root's.

Returns:

  • (String, nil)


68
69
70
71
# File 'lib/mcp_client/schema_validator/input_requirements.rb', line 68

def position_dialect(node, root, scan)
  scan[:anchors] ||= anchor_index(root, scan[:dialect])
  indexed_dialect(node, scan) || scan[:dialect]
end

#queue_referenced(node, ref, root, scan) ⇒ void

This method returns an undefined value.

Queue what a local reference reaches (an external one is never dereferenced, and an unresolvable one is the preflight's to report).



76
77
78
79
80
81
# File 'lib/mcp_client/schema_validator/input_requirements.rb', line 76

def queue_referenced(node, ref, root, scan)
  return if external_ref?(ref, root, scan[:dialect], scan, from: node)

  target = resolve_reference(root, ref, scan[:dialect], scan, from: node)
  scan[:pending] << target unless target.equal?(UNRESOLVED)
end

#read_requirement_positions(root, scan) ⇒ void

This method returns an undefined value.

Read every queued position, bounded like the preflight walk.



36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/mcp_client/schema_validator/input_requirements.rb', line 36

def read_requirement_positions(root, scan)
  until scan[:pending].empty?
    node = scan[:pending].pop
    next unless node.is_a?(Hash) && !scan[:walked].key?(node)

    scan[:walked][node] = true
    scan[:count] += 1
    return if scan[:count] > MAX_SUBSCHEMAS

    read_requirements(node, root, scan)
  end
end

#read_requirements(node, root, scan) ⇒ void

This method returns an undefined value.

One position's requirements, and what it applies next. Under draft-07 nothing beside a $ref is applied (draft-07 Core Section 8.3) — the draft-07 of the resource the position belongs to, which an embedded resource declares for itself (2020-12 Core Section 9.3.2), not the document root's.



55
56
57
58
59
60
61
62
63
# File 'lib/mcp_client/schema_validator/input_requirements.rb', line 55

def read_requirements(node, root, scan)
  ref = node['$ref']
  queue_referenced(node, ref, root, scan) if ref.is_a?(String)
  return if position_dialect(node, root, scan) == DRAFT_07 && node.key?('$ref')

  scan[:required].concat(node['required'].map(&:to_s)) if node['required'].is_a?(Array)
  scan[:properties] = node['properties'].merge(scan[:properties]) if node['properties'].is_a?(Hash)
  scan[:pending].concat(node['allOf'].reverse) if node['allOf'].is_a?(Array)
end