Merge pull request #342 from koic/json_schema_2020_12

Advertise JSON Schema 2020-12 dialect on emitted tool schemas
This commit is contained in:
Koichi ITO 2026-05-13 10:38:26 +09:00 • committed by GitHub
commit 9d2fdd02ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 196 additions and 22 deletions

View file

@ -5,6 +5,12 @@ require "json-schema"
module MCP
class Tool
class Schema
# 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
# the `json-schema` gem does not yet support 2020-12.
JSON_SCHEMA_2020_12_URI = "https://json-schema.org/draft/2020-12/schema"
attr_reader :schema
def initialize(schema = {})
@ -18,17 +24,18 @@ module MCP
end
def to_h
@schema
return @schema if @schema.key?(:"$schema")
{ "$schema": JSON_SCHEMA_2020_12_URI }.merge(@schema)
end
private
def fully_validate(data)
JSON::Validator.fully_validate(to_h, data)
JSON::Validator.fully_validate(schema_for_validation, data)
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,
@ -38,11 +45,22 @@ 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, schema_reader: schema_reader)
errors = JSON::Validator.fully_validate(metaschema, schema_for_validation, schema_reader: schema_reader)
if errors.any?
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
end
end
# The `json-schema` gem's draft-04 validator cannot resolve newer or unknown `$schema`
# dialect URIs. Strip the top-level `$schema` before validation so a dialect URI
# (whether SDK-injected by `to_h` or user-supplied) does not break the validator.
def schema_for_validation
return @schema unless @schema.key?(:"$schema")
copy = @schema.dup
copy.delete(:"$schema")
copy
end
end
end
end

View file

@ -263,7 +263,9 @@ module MCP
assert_equal "test_tool", result[:tools][0][:name]
assert_equal "Test tool", result[:tools][0][:title]
assert_equal "A test tool", result[:tools][0][:description]
assert_equal({ type: "object" }, result[:tools][0][:inputSchema])
assert_equal(
{ "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object" }, result[:tools][0][:inputSchema]
)
assert_equal({ foo: "bar" }, result[:tools][0][:_meta])
assert_instrumentation_data({ method: "tools/list" })
end
@ -284,6 +286,24 @@ module MCP
assert_equal({ foo: "bar" }, result[:tools][0][:_meta])
end
test "#handle tools/list emits 2020-12 $schema on inputSchema and outputSchema" do
tool_with_output = Tool.define(
name: "tool_with_output",
description: "tool with output schema",
input_schema: { properties: { msg: { type: "string" } } },
output_schema: { properties: { result: { type: "string" } } },
) do
Tool::Response.new([{ type: "text", content: "OK" }])
end
server = Server.new(name: "test_server", tools: [tool_with_output])
response = server.handle({ jsonrpc: "2.0", method: "tools/list", id: 1 })
tool = response[:result][:tools][0]
assert_equal "https://json-schema.org/draft/2020-12/schema", tool[:inputSchema][:"$schema"]
assert_equal "https://json-schema.org/draft/2020-12/schema", tool[:outputSchema][:"$schema"]
end
test "#handle tools/call executes tool and returns result" do
tool_name = "test_tool"
tool_args = { arg: "value" }

View file

@ -13,7 +13,12 @@ module MCP
test "to_h returns a hash representation of the input schema" do
input_schema = InputSchema.new(properties: { message: { type: "string" } }, required: ["message"])
assert_equal(
{ type: "object", properties: { message: { type: "string" } }, required: ["message"] },
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { message: { type: "string" } },
required: ["message"],
},
input_schema.to_h,
)
end
@ -21,11 +26,52 @@ module MCP
test "to_h returns a hash representation of the input schema with additionalProperties set to false" do
input_schema = InputSchema.new(properties: { message: { type: "string" } }, required: ["message"], additionalProperties: false)
assert_equal(
{ type: "object", properties: { message: { type: "string" } }, required: ["message"], additionalProperties: false },
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { message: { type: "string" } },
required: ["message"],
additionalProperties: false,
},
input_schema.to_h,
)
end
test "to_h preserves user-supplied $schema dialect" do
input_schema = InputSchema.new(
"$schema": "https://json-schema.org/draft/2019-09/schema",
properties: { message: { type: "string" } },
)
assert_equal "https://json-schema.org/draft/2019-09/schema", input_schema.to_h[:"$schema"]
end
test "validate_arguments works when user supplies a 2020-12 $schema" do
input_schema = InputSchema.new(
"$schema": "https://json-schema.org/draft/2020-12/schema",
properties: { foo: { type: "string" } },
required: ["foo"],
)
assert_nil(input_schema.validate_arguments(foo: "bar"))
assert_raises(InputSchema::ValidationError) do
input_schema.validate_arguments({ foo: 123 })
end
end
test "to_h preserves user-supplied $schema given via string key" do
# The initializer normalizes input through `JSON.parse(...,
# symbolize_names: true)`, so a string-keyed `"$schema"` should
# arrive at `schema_for_validation` the same as a symbol-keyed one.
input_schema = InputSchema.new(
{
"$schema" => "https://json-schema.org/draft/2020-12/schema",
"properties" => { "foo" => { "type" => "string" } },
"required" => ["foo"],
},
)
assert_equal "https://json-schema.org/draft/2020-12/schema", input_schema.to_h[:"$schema"]
assert_nil(input_schema.validate_arguments(foo: "bar"))
end
test "missing_required_arguments returns an array of missing required arguments" do
input_schema = InputSchema.new(properties: { message: { type: "string" } }, required: ["message"])
assert_equal ["message"], input_schema.missing_required_arguments({})
@ -38,7 +84,15 @@ module MCP
test "valid schema initialization" do
schema = InputSchema.new(properties: { foo: { type: "string" } }, required: ["foo"])
assert_equal({ type: "object", properties: { foo: { type: "string" } }, required: ["foo"] }, schema.to_h)
assert_equal(
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { foo: { type: "string" } },
required: ["foo"],
},
schema.to_h,
)
end
test "invalid schema raises argument error" do

View file

@ -8,11 +8,38 @@ module MCP
test "to_h returns a hash representation of the output schema" do
output_schema = OutputSchema.new(properties: { result: { type: "string" } }, required: ["result"])
assert_equal(
{ type: "object", properties: { result: { type: "string" } }, required: ["result"] },
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { result: { type: "string" } },
required: ["result"],
},
output_schema.to_h,
)
end
test "to_h preserves user-supplied $schema dialect" do
output_schema = OutputSchema.new(
"$schema": "https://json-schema.org/draft/2019-09/schema",
properties: { result: { type: "string" } },
)
assert_equal "https://json-schema.org/draft/2019-09/schema", output_schema.to_h[:"$schema"]
end
test "validate_result works when user supplies a 2020-12 $schema" do
output_schema = OutputSchema.new(
"$schema": "https://json-schema.org/draft/2020-12/schema",
properties: { result: { type: "string" } },
required: ["result"],
)
assert_nothing_raised do
output_schema.validate_result({ result: "success" })
end
assert_raises(OutputSchema::ValidationError) do
output_schema.validate_result({ result: 123 })
end
end
test "validate_result validates result against the schema" do
output_schema = OutputSchema.new(properties: { result: { type: "string" } }, required: ["result"])
assert_nothing_raised do
@ -57,7 +84,15 @@ module MCP
test "valid schema initialization" do
schema = OutputSchema.new(properties: { foo: { type: "string" } }, required: ["foo"])
assert_equal({ type: "object", properties: { foo: { type: "string" } }, required: ["foo"] }, schema.to_h)
assert_equal(
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { foo: { type: "string" } },
required: ["foo"],
},
schema.to_h,
)
end
test "invalid schema raises argument error" do
@ -136,7 +171,10 @@ module MCP
test "empty schema is valid" do
schema = OutputSchema.new
assert_equal({ type: "object" }, schema.to_h)
assert_equal(
{ "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object" },
schema.to_h,
)
end
test "validates complex nested schemas" do
@ -187,6 +225,7 @@ module MCP
})
assert_equal(
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "array",
items: {
properties: { foo: { type: "string" } },

View file

@ -33,7 +33,7 @@ module MCP
title: "Mock Tool",
description: "a mock tool for testing",
icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
inputSchema: { type: "object" },
inputSchema: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object" },
}
tool = Tool.define(
name: "mock_tool",
@ -109,7 +109,15 @@ module MCP
tool = MockTool
assert_equal "my_mock_tool", tool.name_value
assert_equal "a mock tool for testing", tool.description
assert_equal({ type: "object", properties: { message: { type: "string" } }, required: ["message"] }, tool.input_schema.to_h)
assert_equal(
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { message: { type: "string" } },
required: ["message"],
},
tool.input_schema.to_h,
)
end
test "defaults to class name as tool name" do
@ -126,7 +134,7 @@ module MCP
tool = NoInputSchemaTool
expected = { type: "object" }
expected = { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object" }
assert_equal expected, tool.input_schema.to_h
end
@ -137,7 +145,12 @@ module MCP
tool = InputSchemaTool
expected = { type: "object", properties: { message: { type: "string" } }, required: ["message"] }
expected = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { message: { type: "string" } },
required: ["message"],
}
assert_equal expected, tool.input_schema.to_h
end
@ -342,8 +355,13 @@ module MCP
title: "Mock Tool",
description: "a mock tool for testing",
icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
inputSchema: { type: "object" },
outputSchema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] },
inputSchema: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object" },
outputSchema: {
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { result: { type: "string" } },
required: ["result"],
},
}
tool = Tool.define(
name: "mock_tool",
@ -376,7 +394,12 @@ module MCP
end
tool = HashOutputSchemaTool
expected = { type: "object", properties: { result: { type: "string" } }, required: ["result"] }
expected = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { result: { type: "string" } },
required: ["result"],
}
assert_equal expected, tool.output_schema.to_h
end
@ -386,7 +409,12 @@ module MCP
end
tool = OutputSchemaObjectTool
expected = { type: "object", properties: { result: { type: "string" } }, required: ["result"] }
expected = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { result: { type: "string" } },
required: ["result"],
}
assert_equal expected, tool.output_schema.to_h
end
@ -436,7 +464,12 @@ module MCP
assert_equal "mock_tool", tool.name_value
assert_equal "a mock tool for testing", tool.description
assert_instance_of Tool::OutputSchema, tool.output_schema
expected_output_schema = { type: "object", properties: { result: { type: "string" } }, required: ["result"] }
expected_output_schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { result: { type: "string" } },
required: ["result"],
}
assert_equal expected_output_schema, tool.output_schema.to_h
end
@ -458,10 +491,20 @@ module MCP
assert_equal "test_tool_with_output", tool.name_value
assert_equal "a test tool with output schema", tool.description
expected_input = { type: "object", properties: { message: { type: "string" } }, required: ["message"] }
expected_input = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { message: { type: "string" } },
required: ["message"],
}
assert_equal expected_input, tool.input_schema.to_h
expected_output = { type: "object", properties: { result: { type: "string" }, success: { type: "boolean" } }, required: ["result", "success"] }
expected_output = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { result: { type: "string" }, success: { type: "boolean" } },
required: ["result", "success"],
}
assert_equal expected_output, tool.output_schema.to_h
end