diff --git a/lib/mcp/server.rb b/lib/mcp/server.rb index 80cacd6c..819b10fd 100644 --- a/lib/mcp/server.rb +++ b/lib/mcp/server.rb @@ -219,6 +219,14 @@ def validate! message = "Error occurred in server_info. `description` is not supported in protocol version 2025-06-18 or earlier" raise ArgumentError, message end + + tools_with_ref = @tools.each_with_object([]) do |(tool_name, tool), names| + names << tool_name if schema_contains_ref?(tool.input_schema_value.to_h) + end + unless tools_with_ref.empty? + message = "Error occurred in #{tools_with_ref.join(", ")}. `$ref` in input schemas is supported by protocol version 2025-11-25 or higher" + raise ArgumentError, message + end end if @configuration.protocol_version <= "2025-03-26" @@ -259,6 +267,17 @@ def validate_tool_name! raise ToolNotUnique, duplicated_tool_names unless duplicated_tool_names.empty? end + def schema_contains_ref?(schema) + case schema + when Hash + schema.any? { |key, value| key.to_s == "$ref" || schema_contains_ref?(value) } + when Array + schema.any? { |element| schema_contains_ref?(element) } + else + false + end + end + def handle_request(request, method) handler = @handlers[method] unless handler diff --git a/lib/mcp/tool/schema.rb b/lib/mcp/tool/schema.rb index f35d18e9..0b908f37 100644 --- a/lib/mcp/tool/schema.rb +++ b/lib/mcp/tool/schema.rb @@ -31,10 +31,6 @@ 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 diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index d6a9b1b7..b7c0e998 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -1184,6 +1184,44 @@ class Example < Tool assert_equal("Error occurred in Test resource. `title` is not supported in protocol version 2025-03-26 or earlier", exception.message) end + test "allows `$ref` in tool input schema with protocol version 2025-11-25" do + tool = Tool.define( + name: "ref_tool", + description: "Tool with $ref", + input_schema: { + type: "object", + "$defs": { address: { type: "object", properties: { city: { type: "string" } } } }, + properties: { address: { "$ref": "#/$defs/address" } }, + }, + ) + configuration = Configuration.new(protocol_version: "2025-11-25") + + assert_nothing_raised do + Server.new(name: "test_server", tools: [tool], configuration: configuration) + end + end + + test "raises error if `$ref` in tool input schema is used with protocol version 2025-06-18" do + tool = Tool.define( + name: "ref_tool", + description: "Tool with $ref", + input_schema: { + type: "object", + "$defs": { address: { type: "object", properties: { city: { type: "string" } } } }, + properties: { address: { "$ref": "#/$defs/address" } }, + }, + ) + configuration = Configuration.new(protocol_version: "2025-06-18") + + exception = assert_raises(ArgumentError) do + Server.new(name: "test_server", tools: [tool], configuration: configuration) + end + assert_equal( + "Error occurred in ref_tool. `$ref` in input schemas is supported by protocol version 2025-11-25 or higher", + exception.message, + ) + end + test "raises error if `title` of resource template is used with protocol version 2025-03-26" do configuration = Configuration.new(protocol_version: "2025-03-26") diff --git a/test/mcp/tool/input_schema_test.rb b/test/mcp/tool/input_schema_test.rb index 45f98a0a..827eb589 100644 --- a/test/mcp/tool/input_schema_test.rb +++ b/test/mcp/tool/input_schema_test.rb @@ -92,16 +92,41 @@ class InputSchemaTest < ActiveSupport::TestCase end end - test "rejects schemas with $ref references" do - assert_raises(ArgumentError) do - InputSchema.new(properties: { foo: { "$ref" => "#/definitions/bar" } }, required: ["foo"]) - end - end - - test "rejects schemas with symbol $ref references" do - assert_raises(ArgumentError) do - InputSchema.new(properties: { foo: { :$ref => "#/definitions/bar" } }, required: ["foo"]) - end + test "accepts schemas with $ref references" do + schema = InputSchema.new( + properties: { + foo: { type: "string" }, + }, + definitions: { + bar: { type: "string" }, + }, + required: ["foo"], + ) + assert_includes schema.to_h.keys, :definitions + end + + test "accepts schemas with $ref string key and includes $ref in to_h" do + schema = InputSchema.new({ + "properties" => { + "foo" => { "$ref" => "#/definitions/bar" }, + }, + "definitions" => { + "bar" => { "type" => "string" }, + }, + }) + assert_equal "#/definitions/bar", schema.to_h[:properties][:foo][:$ref] + end + + test "accepts schemas with $ref symbol key and includes $ref in to_h" do + schema = InputSchema.new({ + properties: { + foo: { :$ref => "#/definitions/bar" }, + }, + definitions: { + bar: { type: "string" }, + }, + }) + assert_equal "#/definitions/bar", schema.to_h[:properties][:foo][:$ref] end test "== compares two input schemas with the same properties, required fields" do diff --git a/test/mcp/tool/output_schema_test.rb b/test/mcp/tool/output_schema_test.rb index 0eb160d4..5445462d 100644 --- a/test/mcp/tool/output_schema_test.rb +++ b/test/mcp/tool/output_schema_test.rb @@ -82,16 +82,41 @@ class OutputSchemaTest < ActiveSupport::TestCase end end - test "rejects schemas with $ref references" do - assert_raises(ArgumentError) do - OutputSchema.new(properties: { foo: { "$ref" => "#/definitions/bar" } }, required: ["foo"]) - end + test "accepts schemas with $ref references" do + schema = OutputSchema.new( + properties: { + foo: { type: "string" }, + }, + definitions: { + bar: { type: "string" }, + }, + required: ["foo"], + ) + assert_includes schema.to_h.keys, :definitions end - test "rejects schemas with symbol $ref references" do - assert_raises(ArgumentError) do - OutputSchema.new(properties: { foo: { :$ref => "#/definitions/bar" } }, required: ["foo"]) - end + test "accepts schemas with $ref string key and includes $ref in to_h" do + schema = OutputSchema.new({ + "properties" => { + "foo" => { "$ref" => "#/definitions/bar" }, + }, + "definitions" => { + "bar" => { "type" => "string" }, + }, + }) + assert_equal "#/definitions/bar", schema.to_h[:properties][:foo][:$ref] + end + + test "accepts schemas with $ref symbol key and includes $ref in to_h" do + schema = OutputSchema.new({ + properties: { + foo: { :$ref => "#/definitions/bar" }, + }, + definitions: { + bar: { type: "string" }, + }, + }) + assert_equal "#/definitions/bar", schema.to_h[:properties][:foo][:$ref] end test "== compares two output schemas with the same properties and required fields" do diff --git a/test/mcp/tool_test.rb b/test/mcp/tool_test.rb index ede3fbfe..a0175ad4 100644 --- a/test/mcp/tool_test.rb +++ b/test/mcp/tool_test.rb @@ -320,7 +320,7 @@ def call(message, server_context: nil) assert_equal [{ type: "text", content: "OK" }], response.content end - test "input_schema rejects any $ref in schema" do + test "input_schema accepts $ref in schema" do schema_with_ref = { properties: { foo: { "$ref" => "#/definitions/bar" }, @@ -330,12 +330,10 @@ def call(message, server_context: nil) bar: { type: "string" }, }, } - error = assert_raises(ArgumentError) do - Class.new(MCP::Tool) do - input_schema schema_with_ref - end + tool_class = Class.new(MCP::Tool) do + input_schema schema_with_ref end - assert_match(/Invalid JSON Schema/, error.message) + assert_equal "#/definitions/bar", tool_class.input_schema.to_h[:properties][:foo][:$ref] end test "#to_h includes outputSchema when present" do @@ -409,7 +407,7 @@ class OutputSchemaObjectTool < Tool assert_includes error.message, "string did not match the following type: number" end - test "output_schema rejects any $ref in schema" do + test "output_schema accepts $ref in schema" do schema_with_ref = { properties: { foo: { "$ref" => "#/definitions/bar" }, @@ -419,12 +417,10 @@ class OutputSchemaObjectTool < Tool bar: { type: "string" }, }, } - error = assert_raises(ArgumentError) do - Class.new(MCP::Tool) do - output_schema schema_with_ref - end + tool_class = Class.new(MCP::Tool) do + output_schema schema_with_ref end - assert_match(/Invalid JSON Schema/, error.message) + assert_equal "#/definitions/bar", tool_class.output_schema.to_h[:properties][:foo][:$ref] end test ".define allows definition of tools with output_schema" do