Skip to content

Tool Schema Builder — Ruby source

Build function-calling and MCP tool schemas that pass strict mode on the first try, and lint pasted ones against the strict-mode contract — additionalProperties, required-sync, defaults, enums — with one-click autofix for every mechanical violation. Runs entirely in your browser.

This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.

# frozen_string_literal: true

# Tool Schema Builder — strict-mode validation of function-calling / MCP
# tool definitions (OpenAI strict mode / MCP inputSchema contract).
# CosmoDev polyglot showcase port, from src/lib/tool-schema.ts
# (the canonical TypeScript implementation). Ruby 3.2, stdlib only.

require 'json'

module ToolSchema
  SUPPORTED_TYPES = %w[string number integer boolean object array].freeze
  NAME_RE = /\A[a-z0-9_-]{1,64}\z/

  module_function

  # The recursive strict-mode walk — every object nests the same rules.
  def check_object(path, obj, issues)
    issues << ['no-additional-properties', path] if obj['additionalProperties'] != false
    props = obj['properties'].is_a?(Hash) ? obj['properties'] : {}
    keys = props.keys.select { |k| props[k].is_a?(Hash) }
    required = obj['required'].is_a?(Array) ? obj['required'] : []
    missing = keys - required
    issues << ['all-required', "#{path}: required missing #{missing.join(', ')}"] unless missing.empty?
    keys.each do |key|
      prop = props[key]
      p = "#{path}.properties.#{key}"
      issues << ['no-defaults', p] if prop.key?('default')
      issues << ['description-present', p] if prop['description'].to_s.strip.empty?
      issues << ['typed-properties', p] unless SUPPORTED_TYPES.include?(prop['type'])
      enum = prop['enum']
      if enum.is_a?(Array)
        kinds = enum.map { |v| v.class.name }.uniq
        bad = kinds.size > 1 || kinds.include?('Hash') || kinds.include?('NilClass')
        issues << ['enum-values', p] if enum.empty? || bad
      end
      issues << ['array-items', p] if prop['type'] == 'array' && !prop['items'].is_a?(Hash)
      if prop['type'] == 'object' && prop['properties'].is_a?(Hash)
        check_object(p, prop, issues)
      end
    end
  end

  # Validate a JSON tool definition; returns [rule, where] issue pairs.
  def validate_tool_schema(text)
    root = JSON.parse(text)
    return [['json-parseable', '$: input must be a JSON object']] unless root.is_a?(Hash)

    issues = []
    unless root['name'].to_s.match?(NAME_RE)
      issues << ['non-empty-name', 'name: must be 1-64 chars of [a-z0-9_-]']
    end
    if root['description'].to_s.strip.empty?
      issues << ['description-present', 'description: the tool needs a description']
    end
    schema = root['input_schema']
    if schema.is_a?(Hash) && schema['type'] == 'object'
      check_object('input_schema', schema, issues)
    else
      issues << ['json-parseable', 'input_schema: must be an object with type: "object"']
    end
    issues
  rescue JSON::ParserError => e
    [["json-parseable", "$: #{e.message}"]]
  end
end

if $PROGRAM_NAME == __FILE__
  broken = JSON.generate('name' => 'Get_Weather', 'input_schema' => {
    'type' => 'object',
    'properties' => {
      'city' => { 'type' => 'string', 'default' => 'Paris' },
      'unit' => { 'type' => 'string', 'description' => 'celsius or fahrenheit', 'enum' => ['c', 2] },
      'tags' => { 'type' => 'array' } },
    'required' => ['city'] })
  ToolSchema.validate_tool_schema(broken).each { |rule, where| puts "#{rule}  #{where}" }
end

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →