11# frozen_string_literal: true
22
33require "digest"
4- require "json-schema "
4+ require "json_schemer "
55
66module MCP
77 class Tool
@@ -38,11 +38,10 @@ def clear
3838
3939 # JSON Schema 2020-12 is the default dialect for MCP schema definitions
4040 # per MCP 2025-11-25 (SEP-1613). Note: emission only — runtime validation
41- # is still performed against the JSON Schema draft-04 metaschema because
42- # the `json-schema` gem does not yet support 2020-12.
41+ # is still performed against the JSON Schema draft-04 metaschema.
4342 JSON_SCHEMA_2020_12_URI = "https://json-schema.org/draft/2020-12/schema"
4443
45- attr_reader : schema
44+ DRAFT4_META_SCHEMA_URI = "http://json- schema.org/draft-04/schema#"
4645
4746 def initialize ( schema = { } )
4847 @schema = JSON . parse ( JSON . dump ( schema ) , symbolize_names : true )
@@ -51,7 +50,7 @@ def initialize(schema = {})
5150 end
5251
5352 def ==( other )
54- other . is_a? ( self . class ) && schema == other . schema
53+ other . is_a? ( self . class ) && @ schema == other . instance_variable_get ( :@ schema)
5554 end
5655
5756 def to_h
@@ -62,8 +61,38 @@ def to_h
6261
6362 private
6463
64+ def stringify ( obj )
65+ case obj
66+ when Hash
67+ obj . each_with_object ( { } ) { |( k , v ) , h | h [ k . to_s ] = stringify ( v ) }
68+ when Array
69+ obj . map { |v | stringify ( v ) }
70+ when Symbol
71+ obj . to_s
72+ else
73+ obj
74+ end
75+ end
76+
77+ # Lazily built so a cache hit in `validate_schema!` avoids the schemer construction cost.
78+ # Memoized per Schema instance because schema content is fixed at construction,
79+ # so the compiled schemer is reusable across many `fully_validate` calls.
80+ #
81+ # `format: false` preserves the legacy behavior of the previous `json-schema` based implementation,
82+ # which did not enforce `format` keywords. `RegexpError` from a malformed `pattern` is re-raised as
83+ # `ArgumentError` so callers see the same exception class they used to.
84+ def schemer
85+ @schemer ||= JSONSchemer . schema (
86+ stringify ( schema_for_validation ) ,
87+ meta_schema : DRAFT4_META_SCHEMA_URI ,
88+ format : false ,
89+ )
90+ rescue RegexpError => e
91+ raise ArgumentError , "Invalid JSON Schema: #{ e . message } "
92+ end
93+
6594 def fully_validate ( data )
66- JSON :: Validator . fully_validate ( schema_for_validation , data )
95+ schemer . validate ( stringify ( data ) ) . map { | validation_error | validation_error . fetch ( "error" ) }
6796 end
6897
6998 def validate_schema!
@@ -75,26 +104,16 @@ def validate_schema!
75104 key = Digest ::SHA256 . hexdigest ( JSON . generate ( target , max_nesting : false ) )
76105 return if VALIDATION_CACHE . validated? ( key )
77106
78- gem_path = File . realpath ( Gem . loaded_specs [ "json-schema" ] . full_gem_path )
79- schema_reader = JSON ::Schema ::Reader . new (
80- accept_uri : false ,
81- accept_file : -> ( path ) { File . realpath ( path . to_s ) . start_with? ( gem_path ) } ,
82- )
83- metaschema_path = Pathname . new ( JSON ::Validator . validator_for_name ( "draft4" ) . metaschema )
84- # Converts metaschema to a file URI for cross-platform compatibility
85- metaschema_uri = JSON ::Util ::URI . file_uri ( metaschema_path . expand_path . cleanpath . to_s . tr ( "\\ " , "/" ) )
86- metaschema = metaschema_uri . to_s
87- errors = JSON ::Validator . fully_validate ( metaschema , target , schema_reader : schema_reader )
107+ errors = schemer . validate_schema . map { |validation_error | validation_error . fetch ( "error" ) }
88108 if errors . any?
89109 raise ArgumentError , "Invalid JSON Schema: #{ errors . join ( ", " ) } "
90110 end
91111
92112 VALIDATION_CACHE . store ( key )
93113 end
94114
95- # The `json-schema` gem's draft-04 validator cannot resolve newer or unknown `$schema`
96- # dialect URIs. Strip the top-level `$schema` before validation so a dialect URI
97- # (whether SDK-injected by `to_h` or user-supplied) does not break the validator.
115+ # `json_schemer` is pinned to the draft-04 metaschema, so strip top-level `$schema` before validation:
116+ # this preserves the legacy behavior of ignoring the advertised dialect URI when the SDK validates schemas.
98117 def schema_for_validation
99118 return @schema unless @schema . key? ( :"$schema" )
100119
0 commit comments