Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9d2fdd0

Browse files
authored
Merge pull request #342 from koic/json_schema_2020_12
Advertise JSON Schema 2020-12 dialect on emitted tool schemas
2 parents 075ae28 + 1fc698b commit 9d2fdd0

5 files changed

Lines changed: 196 additions & 22 deletions

File tree

lib/mcp/tool/schema.rb

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
module MCP
66
class Tool
77
class Schema
8+
# JSON Schema 2020-12 is the default dialect for MCP schema definitions
9+
# per MCP 2025-11-25 (SEP-1613). Note: emission only — runtime validation
10+
# is still performed against the JSON Schema draft-04 metaschema because
11+
# the `json-schema` gem does not yet support 2020-12.
12+
JSON_SCHEMA_2020_12_URI = "https://json-schema.org/draft/2020-12/schema"
13+
814
attr_reader :schema
915

1016
def initialize(schema = {})
@@ -18,17 +24,18 @@ def ==(other)
1824
end
1925

2026
def to_h
21-
@schema
27+
return @schema if @schema.key?(:"$schema")
28+
29+
{ "$schema": JSON_SCHEMA_2020_12_URI }.merge(@schema)
2230
end
2331

2432
private
2533

2634
def fully_validate(data)
27-
JSON::Validator.fully_validate(to_h, data)
35+
JSON::Validator.fully_validate(schema_for_validation, data)
2836
end
2937

3038
def validate_schema!
31-
schema = to_h
3239
gem_path = File.realpath(Gem.loaded_specs["json-schema"].full_gem_path)
3340
schema_reader = JSON::Schema::Reader.new(
3441
accept_uri: false,
@@ -38,11 +45,22 @@ def validate_schema!
3845
# Converts metaschema to a file URI for cross-platform compatibility
3946
metaschema_uri = JSON::Util::URI.file_uri(metaschema_path.expand_path.cleanpath.to_s.tr("\\", "/"))
4047
metaschema = metaschema_uri.to_s
41-
errors = JSON::Validator.fully_validate(metaschema, schema, schema_reader: schema_reader)
48+
errors = JSON::Validator.fully_validate(metaschema, schema_for_validation, schema_reader: schema_reader)
4249
if errors.any?
4350
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
4451
end
4552
end
53+
54+
# The `json-schema` gem's draft-04 validator cannot resolve newer or unknown `$schema`
55+
# dialect URIs. Strip the top-level `$schema` before validation so a dialect URI
56+
# (whether SDK-injected by `to_h` or user-supplied) does not break the validator.
57+
def schema_for_validation
58+
return @schema unless @schema.key?(:"$schema")
59+
60+
copy = @schema.dup
61+
copy.delete(:"$schema")
62+
copy
63+
end
4664
end
4765
end
4866
end

test/mcp/server_test.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,9 @@ class ServerTest < ActiveSupport::TestCase
263263
assert_equal "test_tool", result[:tools][0][:name]
264264
assert_equal "Test tool", result[:tools][0][:title]
265265
assert_equal "A test tool", result[:tools][0][:description]
266-
assert_equal({ type: "object" }, result[:tools][0][:inputSchema])
266+
assert_equal(
267+
{ "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object" }, result[:tools][0][:inputSchema]
268+
)
267269
assert_equal({ foo: "bar" }, result[:tools][0][:_meta])
268270
assert_instrumentation_data({ method: "tools/list" })
269271
end
@@ -284,6 +286,24 @@ class ServerTest < ActiveSupport::TestCase
284286
assert_equal({ foo: "bar" }, result[:tools][0][:_meta])
285287
end
286288

289+
test "#handle tools/list emits 2020-12 $schema on inputSchema and outputSchema" do
290+
tool_with_output = Tool.define(
291+
name: "tool_with_output",
292+
description: "tool with output schema",
293+
input_schema: { properties: { msg: { type: "string" } } },
294+
output_schema: { properties: { result: { type: "string" } } },
295+
) do
296+
Tool::Response.new([{ type: "text", content: "OK" }])
297+
end
298+
server = Server.new(name: "test_server", tools: [tool_with_output])
299+
300+
response = server.handle({ jsonrpc: "2.0", method: "tools/list", id: 1 })
301+
tool = response[:result][:tools][0]
302+
303+
assert_equal "https://json-schema.org/draft/2020-12/schema", tool[:inputSchema][:"$schema"]
304+
assert_equal "https://json-schema.org/draft/2020-12/schema", tool[:outputSchema][:"$schema"]
305+
end
306+
287307
test "#handle tools/call executes tool and returns result" do
288308
tool_name = "test_tool"
289309
tool_args = { arg: "value" }

test/mcp/tool/input_schema_test.rb

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,65 @@ class InputSchemaTest < ActiveSupport::TestCase
1313
test "to_h returns a hash representation of the input schema" do
1414
input_schema = InputSchema.new(properties: { message: { type: "string" } }, required: ["message"])
1515
assert_equal(
16-
{ type: "object", properties: { message: { type: "string" } }, required: ["message"] },
16+
{
17+
"$schema": "https://json-schema.org/draft/2020-12/schema",
18+
type: "object",
19+
properties: { message: { type: "string" } },
20+
required: ["message"],
21+
},
1722
input_schema.to_h,
1823
)
1924
end
2025

2126
test "to_h returns a hash representation of the input schema with additionalProperties set to false" do
2227
input_schema = InputSchema.new(properties: { message: { type: "string" } }, required: ["message"], additionalProperties: false)
2328
assert_equal(
24-
{ type: "object", properties: { message: { type: "string" } }, required: ["message"], additionalProperties: false },
29+
{
30+
"$schema": "https://json-schema.org/draft/2020-12/schema",
31+
type: "object",
32+
properties: { message: { type: "string" } },
33+
required: ["message"],
34+
additionalProperties: false,
35+
},
2536
input_schema.to_h,
2637
)
2738
end
2839

40+
test "to_h preserves user-supplied $schema dialect" do
41+
input_schema = InputSchema.new(
42+
"$schema": "https://json-schema.org/draft/2019-09/schema",
43+
properties: { message: { type: "string" } },
44+
)
45+
assert_equal "https://json-schema.org/draft/2019-09/schema", input_schema.to_h[:"$schema"]
46+
end
47+
48+
test "validate_arguments works when user supplies a 2020-12 $schema" do
49+
input_schema = InputSchema.new(
50+
"$schema": "https://json-schema.org/draft/2020-12/schema",
51+
properties: { foo: { type: "string" } },
52+
required: ["foo"],
53+
)
54+
assert_nil(input_schema.validate_arguments(foo: "bar"))
55+
assert_raises(InputSchema::ValidationError) do
56+
input_schema.validate_arguments({ foo: 123 })
57+
end
58+
end
59+
60+
test "to_h preserves user-supplied $schema given via string key" do
61+
# The initializer normalizes input through `JSON.parse(...,
62+
# symbolize_names: true)`, so a string-keyed `"$schema"` should
63+
# arrive at `schema_for_validation` the same as a symbol-keyed one.
64+
input_schema = InputSchema.new(
65+
{
66+
"$schema" => "https://json-schema.org/draft/2020-12/schema",
67+
"properties" => { "foo" => { "type" => "string" } },
68+
"required" => ["foo"],
69+
},
70+
)
71+
assert_equal "https://json-schema.org/draft/2020-12/schema", input_schema.to_h[:"$schema"]
72+
assert_nil(input_schema.validate_arguments(foo: "bar"))
73+
end
74+
2975
test "missing_required_arguments returns an array of missing required arguments" do
3076
input_schema = InputSchema.new(properties: { message: { type: "string" } }, required: ["message"])
3177
assert_equal ["message"], input_schema.missing_required_arguments({})
@@ -38,7 +84,15 @@ class InputSchemaTest < ActiveSupport::TestCase
3884

3985
test "valid schema initialization" do
4086
schema = InputSchema.new(properties: { foo: { type: "string" } }, required: ["foo"])
41-
assert_equal({ type: "object", properties: { foo: { type: "string" } }, required: ["foo"] }, schema.to_h)
87+
assert_equal(
88+
{
89+
"$schema": "https://json-schema.org/draft/2020-12/schema",
90+
type: "object",
91+
properties: { foo: { type: "string" } },
92+
required: ["foo"],
93+
},
94+
schema.to_h,
95+
)
4296
end
4397

4498
test "invalid schema raises argument error" do

test/mcp/tool/output_schema_test.rb

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,38 @@ class OutputSchemaTest < ActiveSupport::TestCase
88
test "to_h returns a hash representation of the output schema" do
99
output_schema = OutputSchema.new(properties: { result: { type: "string" } }, required: ["result"])
1010
assert_equal(
11-
{ type: "object", properties: { result: { type: "string" } }, required: ["result"] },
11+
{
12+
"$schema": "https://json-schema.org/draft/2020-12/schema",
13+
type: "object",
14+
properties: { result: { type: "string" } },
15+
required: ["result"],
16+
},
1217
output_schema.to_h,
1318
)
1419
end
1520

21+
test "to_h preserves user-supplied $schema dialect" do
22+
output_schema = OutputSchema.new(
23+
"$schema": "https://json-schema.org/draft/2019-09/schema",
24+
properties: { result: { type: "string" } },
25+
)
26+
assert_equal "https://json-schema.org/draft/2019-09/schema", output_schema.to_h[:"$schema"]
27+
end
28+
29+
test "validate_result works when user supplies a 2020-12 $schema" do
30+
output_schema = OutputSchema.new(
31+
"$schema": "https://json-schema.org/draft/2020-12/schema",
32+
properties: { result: { type: "string" } },
33+
required: ["result"],
34+
)
35+
assert_nothing_raised do
36+
output_schema.validate_result({ result: "success" })
37+
end
38+
assert_raises(OutputSchema::ValidationError) do
39+
output_schema.validate_result({ result: 123 })
40+
end
41+
end
42+
1643
test "validate_result validates result against the schema" do
1744
output_schema = OutputSchema.new(properties: { result: { type: "string" } }, required: ["result"])
1845
assert_nothing_raised do
@@ -57,7 +84,15 @@ class OutputSchemaTest < ActiveSupport::TestCase
5784

5885
test "valid schema initialization" do
5986
schema = OutputSchema.new(properties: { foo: { type: "string" } }, required: ["foo"])
60-
assert_equal({ type: "object", properties: { foo: { type: "string" } }, required: ["foo"] }, schema.to_h)
87+
assert_equal(
88+
{
89+
"$schema": "https://json-schema.org/draft/2020-12/schema",
90+
type: "object",
91+
properties: { foo: { type: "string" } },
92+
required: ["foo"],
93+
},
94+
schema.to_h,
95+
)
6196
end
6297

6398
test "invalid schema raises argument error" do
@@ -136,7 +171,10 @@ class OutputSchemaTest < ActiveSupport::TestCase
136171

137172
test "empty schema is valid" do
138173
schema = OutputSchema.new
139-
assert_equal({ type: "object" }, schema.to_h)
174+
assert_equal(
175+
{ "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object" },
176+
schema.to_h,
177+
)
140178
end
141179

142180
test "validates complex nested schemas" do
@@ -187,6 +225,7 @@ class OutputSchemaTest < ActiveSupport::TestCase
187225
})
188226
assert_equal(
189227
{
228+
"$schema": "https://json-schema.org/draft/2020-12/schema",
190229
type: "array",
191230
items: {
192231
properties: { foo: { type: "string" } },

test/mcp/tool_test.rb

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def call(message:, server_context: nil)
3333
title: "Mock Tool",
3434
description: "a mock tool for testing",
3535
icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
36-
inputSchema: { type: "object" },
36+
inputSchema: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object" },
3737
}
3838
tool = Tool.define(
3939
name: "mock_tool",
@@ -109,7 +109,15 @@ class MockTool < Tool
109109
tool = MockTool
110110
assert_equal "my_mock_tool", tool.name_value
111111
assert_equal "a mock tool for testing", tool.description
112-
assert_equal({ type: "object", properties: { message: { type: "string" } }, required: ["message"] }, tool.input_schema.to_h)
112+
assert_equal(
113+
{
114+
"$schema": "https://json-schema.org/draft/2020-12/schema",
115+
type: "object",
116+
properties: { message: { type: "string" } },
117+
required: ["message"],
118+
},
119+
tool.input_schema.to_h,
120+
)
113121
end
114122

115123
test "defaults to class name as tool name" do
@@ -126,7 +134,7 @@ class NoInputSchemaTool < Tool; end
126134

127135
tool = NoInputSchemaTool
128136

129-
expected = { type: "object" }
137+
expected = { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object" }
130138
assert_equal expected, tool.input_schema.to_h
131139
end
132140

@@ -137,7 +145,12 @@ class InputSchemaTool < Tool
137145

138146
tool = InputSchemaTool
139147

140-
expected = { type: "object", properties: { message: { type: "string" } }, required: ["message"] }
148+
expected = {
149+
"$schema": "https://json-schema.org/draft/2020-12/schema",
150+
type: "object",
151+
properties: { message: { type: "string" } },
152+
required: ["message"],
153+
}
141154
assert_equal expected, tool.input_schema.to_h
142155
end
143156

@@ -342,8 +355,13 @@ def call(message, server_context: nil)
342355
title: "Mock Tool",
343356
description: "a mock tool for testing",
344357
icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
345-
inputSchema: { type: "object" },
346-
outputSchema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] },
358+
inputSchema: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object" },
359+
outputSchema: {
360+
"$schema": "https://json-schema.org/draft/2020-12/schema",
361+
type: "object",
362+
properties: { result: { type: "string" } },
363+
required: ["result"],
364+
},
347365
}
348366
tool = Tool.define(
349367
name: "mock_tool",
@@ -376,7 +394,12 @@ class HashOutputSchemaTool < Tool
376394
end
377395

378396
tool = HashOutputSchemaTool
379-
expected = { type: "object", properties: { result: { type: "string" } }, required: ["result"] }
397+
expected = {
398+
"$schema": "https://json-schema.org/draft/2020-12/schema",
399+
type: "object",
400+
properties: { result: { type: "string" } },
401+
required: ["result"],
402+
}
380403
assert_equal expected, tool.output_schema.to_h
381404
end
382405

@@ -386,7 +409,12 @@ class OutputSchemaObjectTool < Tool
386409
end
387410

388411
tool = OutputSchemaObjectTool
389-
expected = { type: "object", properties: { result: { type: "string" } }, required: ["result"] }
412+
expected = {
413+
"$schema": "https://json-schema.org/draft/2020-12/schema",
414+
type: "object",
415+
properties: { result: { type: "string" } },
416+
required: ["result"],
417+
}
390418
assert_equal expected, tool.output_schema.to_h
391419
end
392420

@@ -436,7 +464,12 @@ class OutputSchemaObjectTool < Tool
436464
assert_equal "mock_tool", tool.name_value
437465
assert_equal "a mock tool for testing", tool.description
438466
assert_instance_of Tool::OutputSchema, tool.output_schema
439-
expected_output_schema = { type: "object", properties: { result: { type: "string" } }, required: ["result"] }
467+
expected_output_schema = {
468+
"$schema": "https://json-schema.org/draft/2020-12/schema",
469+
type: "object",
470+
properties: { result: { type: "string" } },
471+
required: ["result"],
472+
}
440473
assert_equal expected_output_schema, tool.output_schema.to_h
441474
end
442475

@@ -458,10 +491,20 @@ def call(message:, server_context: nil)
458491
assert_equal "test_tool_with_output", tool.name_value
459492
assert_equal "a test tool with output schema", tool.description
460493

461-
expected_input = { type: "object", properties: { message: { type: "string" } }, required: ["message"] }
494+
expected_input = {
495+
"$schema": "https://json-schema.org/draft/2020-12/schema",
496+
type: "object",
497+
properties: { message: { type: "string" } },
498+
required: ["message"],
499+
}
462500
assert_equal expected_input, tool.input_schema.to_h
463501

464-
expected_output = { type: "object", properties: { result: { type: "string" }, success: { type: "boolean" } }, required: ["result", "success"] }
502+
expected_output = {
503+
"$schema": "https://json-schema.org/draft/2020-12/schema",
504+
type: "object",
505+
properties: { result: { type: "string" }, success: { type: "boolean" } },
506+
required: ["result", "success"],
507+
}
465508
assert_equal expected_output, tool.output_schema.to_h
466509
end
467510

0 commit comments

Comments
 (0)