ruby-sdk/lib/mcp/tool/schema.rb
Edgars Beigarts a3c24d2bcc
Refactor schema validation to reduce duplication and add additionalProperties support
This refactoring extracts common schema validation logic into a base Schema
class, eliminating ~100 lines of duplicate code between InputSchema and
OutputSchema. It also adds support for the JSON Schema `additionalProperties`
keyword to allow fine-grained control over extra properties in tool arguments.

Previously `additionalParameters` could be set only on OutputSchema.

Key changes:

- Created MCP::Tool::Schema base class consolidating:
  - Schema validation against JSON Schema Draft 4 metaschema
  - Deep key transformation (string to symbol conversion)
  - $ref disallowance checking
  - Common validation methods

- Refactored InputSchema to inherit from Schema:
  - Simplified constructor to accept full schema hash
  - Changed required fields storage from symbols to strings
  - Removed ~40 lines of duplicated validation logic

- Refactored OutputSchema to inherit from Schema:
  - Removed ~54 lines of duplicated validation logic
  - Now shares all common functionality with InputSchema

- Updated Tool.input_schema setter:
  - Simplified to pass full hash directly to InputSchema

- Added additionalProperties support:
  - By default, tools now allow additional properties (JSON Schema default)
  - Can explicitly set `additionalProperties: false` to disallow extras
  - Added comprehensive test coverage for both behaviors
2025-12-12 12:49:52 +02:00

65 lines
1.9 KiB
Ruby

# frozen_string_literal: true
require "json-schema"
module MCP
class Tool
class Schema
attr_reader :schema
def initialize(schema = {})
@schema = deep_transform_keys(JSON.parse(JSON.dump(schema)), &:to_sym)
@schema[:type] ||= "object"
validate_schema!
end
def ==(other)
other.is_a?(self.class) && schema == other.schema
end
def to_h
@schema
end
private
def fully_validate(data)
JSON::Validator.fully_validate(to_h, data)
end
def deep_transform_keys(schema, &block)
case schema
when Hash
schema.each_with_object({}) do |(key, value), result|
if key.casecmp?("$ref")
raise ArgumentError, "Invalid JSON Schema: $ref is not allowed in tool schemas"
end
result[yield(key)] = deep_transform_keys(value, &block)
end
when Array
schema.map { |e| deep_transform_keys(e, &block) }
else
schema
end
end
def validate_schema!
schema = to_h
gem_path = File.realpath(Gem.loaded_specs["json-schema"].full_gem_path)
schema_reader = JSON::Schema::Reader.new(
accept_uri: false,
accept_file: ->(path) { File.realpath(path.to_s).start_with?(gem_path) },
)
metaschema_path = Pathname.new(JSON::Validator.validator_for_name("draft4").metaschema)
# Converts metaschema to a file URI for cross-platform compatibility
metaschema_uri = JSON::Util::URI.file_uri(metaschema_path.expand_path.cleanpath.to_s.tr("\\", "/"))
metaschema = metaschema_uri.to_s
errors = JSON::Validator.fully_validate(metaschema, schema, schema_reader: schema_reader)
if errors.any?
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
end
end
end
end
end