Cache Tool::Schema Validation to Avoid Re-validating Identical Schemas
## Motivation and Context `MCP::Tool::Schema#initialize` validated every instance against the draft-04 metaschema via `JSON::Validator.fully_validate`, costing ~32ms for deep schemas (up to ~100ms). When schemas are built dynamically and repeatedly, this work is wasted because the result depends only on the schema content. This caches successful validations by a content digest, so an identical schema is validated once and subsequent constructions skip the traversal (~100ms to ~0.1ms on a cache hit). The cache is bounded and thread-safe. It supersedes the `validate: false` escape hatch proposed in #362, which would have added permanent public interface and allowed invalid schemas to reach clients. Python and TypeScript SDKs do not metaschema-validate schema definitions, so this cost is specific to the Ruby SDK; caching reduces it without weakening the default or changing validation semantics. ## How Has This Been Tested? Added regression tests in `test/mcp/tool/schema_test.rb`: identical schemas validate only once, distinct schemas validate separately, a cache hit still yields a usable and correctly validated schema, invalid schemas raise on every construction and are not cached, a schema at the normalization depth limit is cached without a nesting error, and `ValidationCache` evicts the oldest entry beyond its max size. The full suite (`rake test`) and `rake rubocop` pass. ## Breaking Changes None. Validation safety is preserved: every distinct schema is still validated, and invalid schemas continue to raise. The initializer signature and validation semantics are unchanged.
This commit is contained in:
parent
fd2fd27625
commit
5c8b132dcd
1 changed files with 137 additions and 1 deletions
|
|
@ -1,10 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "digest"
|
||||
require "json-schema"
|
||||
|
||||
module MCP
|
||||
class Tool
|
||||
class Schema
|
||||
# Metaschema validation depends only on schema content, so a given schema
|
||||
# never needs to be validated more than once. Caching the result lets repeated
|
||||
# (e.g. dynamically rebuilt) schemas skip the costly traversal.
|
||||
class ValidationCache
|
||||
DEFAULT_MAX_SIZE = 1000
|
||||
|
||||
def initialize(max_size: DEFAULT_MAX_SIZE)
|
||||
@max_size = max_size
|
||||
@entries = {}
|
||||
@mutex = Mutex.new
|
||||
end
|
||||
|
||||
def validated?(key)
|
||||
@mutex.synchronize { @entries.key?(key) }
|
||||
end
|
||||
|
||||
def store(key)
|
||||
@mutex.synchronize do
|
||||
@entries.delete(key)
|
||||
@entries[key] = true
|
||||
@entries.shift while @entries.size > @max_size
|
||||
end
|
||||
end
|
||||
|
||||
def clear
|
||||
@mutex.synchronize { @entries.clear }
|
||||
end
|
||||
end
|
||||
VALIDATION_CACHE = ValidationCache.new
|
||||
|
||||
# JSON Schema 2020-12 is the default dialect for MCP schema definitions
|
||||
# per MCP 2025-11-25 (SEP-1613). Note: emission only — runtime validation
|
||||
# is still performed against the JSON Schema draft-04 metaschema because
|
||||
|
|
@ -36,6 +67,14 @@ module MCP
|
|||
end
|
||||
|
||||
def validate_schema!
|
||||
target = schema_for_validation
|
||||
|
||||
# `max_nesting: false` because normalization uses `JSON.dump` (no nesting limit),
|
||||
# so the default `JSON.generate` limit would raise on a deeply nested schema that
|
||||
# the initializer already accepted.
|
||||
key = Digest::SHA256.hexdigest(JSON.generate(target, max_nesting: false))
|
||||
return if VALIDATION_CACHE.validated?(key)
|
||||
|
||||
gem_path = File.realpath(Gem.loaded_specs["json-schema"].full_gem_path)
|
||||
schema_reader = JSON::Schema::Reader.new(
|
||||
accept_uri: false,
|
||||
|
|
@ -45,10 +84,12 @@ module MCP
|
|||
# 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_for_validation, schema_reader: schema_reader)
|
||||
errors = JSON::Validator.fully_validate(metaschema, target, schema_reader: schema_reader)
|
||||
if errors.any?
|
||||
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
|
||||
end
|
||||
|
||||
VALIDATION_CACHE.store(key)
|
||||
end
|
||||
|
||||
# The `json-schema` gem's draft-04 validator cannot resolve newer or unknown `$schema`
|
||||
|
|
|
|||
Loading…
Reference in a new issue