From 2508defc79915168814b380af05e2830dc272403 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 30 Jul 2025 18:29:01 +0200 Subject: [PATCH] inputSchema interface requires type key When defining a tool without any arguments taken, it should be allowed to not have to define the `input_schema` at all, and the default value for such a scenario should be `{ type: "object" }` as per the schema. https://github.com/modelcontextprotocol/modelcontextprotocol/blob/16d91abb68bbc824944c5bda4ac2e2ccaaa74b0f/schema/2025-06-18/schema.ts#L896-L900 --- lib/mcp/tool.rb | 5 ++++- lib/mcp/tool/input_schema.rb | 7 ++++++- test/mcp/server_test.rb | 2 +- test/mcp/tool/input_schema_test.rb | 15 +++++++++++++++ test/mcp/tool_test.rb | 15 ++++++++++++--- 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/lib/mcp/tool.rb b/lib/mcp/tool.rb index 441c0488..32dd6197 100644 --- a/lib/mcp/tool.rb +++ b/lib/mcp/tool.rb @@ -6,7 +6,6 @@ class << self NOT_SET = Object.new attr_reader :description_value - attr_reader :input_schema_value attr_reader :annotations_value def call(*args, server_context: nil) @@ -43,6 +42,10 @@ def name_value @name_value || StringUtils.handle_from_class_name(name) end + def input_schema_value + @input_schema_value || InputSchema.new + end + def description(value = NOT_SET) if value == NOT_SET @description_value diff --git a/lib/mcp/tool/input_schema.rb b/lib/mcp/tool/input_schema.rb index b0c7cdff..b542801b 100644 --- a/lib/mcp/tool/input_schema.rb +++ b/lib/mcp/tool/input_schema.rb @@ -15,8 +15,13 @@ def initialize(properties: {}, required: []) validate_schema! end + def ==(other) + other.is_a?(InputSchema) && properties == other.properties && required == other.required + end + def to_h - { type: "object", properties: }.tap do |hsh| + { type: "object" }.tap do |hsh| + hsh[:properties] = properties if properties.any? hsh[:required] = required if required.any? end end diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index ef966daa..e68cdb8b 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -164,7 +164,7 @@ class ServerTest < ActiveSupport::TestCase assert_kind_of Array, result[:tools] assert_equal "test_tool", result[:tools][0][:name] assert_equal "Test tool", result[:tools][0][:description] - assert_empty(result[:tools][0][:inputSchema]) + assert_equal({ type: "object" }, result[:tools][0][:inputSchema]) assert_instrumentation_data({ method: "tools/list" }) end diff --git a/test/mcp/tool/input_schema_test.rb b/test/mcp/tool/input_schema_test.rb index 5aad8951..98ed54ff 100644 --- a/test/mcp/tool/input_schema_test.rb +++ b/test/mcp/tool/input_schema_test.rb @@ -79,6 +79,21 @@ class InputSchemaTest < ActiveSupport::TestCase InputSchema.new(properties: { foo: { :$ref => "#/definitions/bar" } }, required: [:foo]) end end + + test "== compares two input schemas with the same properties and required fields" do + schema1 = InputSchema.new(properties: { foo: { type: "string" } }, required: [:foo]) + schema2 = InputSchema.new(properties: { foo: { type: "string" } }, required: [:foo]) + assert_equal schema1, schema2 + + schema3 = InputSchema.new(properties: { bar: { type: "string" } }, required: [:bar]) + refute_equal schema1, schema3 + + schema4 = InputSchema.new(properties: { foo: { type: "string" } }, required: [:bar]) + refute_equal schema1, schema4 + + schema5 = InputSchema.new(properties: { bar: { type: "string" } }, required: [:foo]) + refute_equal schema1, schema5 + end end end end diff --git a/test/mcp/tool_test.rb b/test/mcp/tool_test.rb index 1f9d056b..abd3fcb3 100644 --- a/test/mcp/tool_test.rb +++ b/test/mcp/tool_test.rb @@ -26,7 +26,7 @@ def call(message:, server_context: nil) test "#to_h returns a hash with name, description, and inputSchema" do tool = Tool.define(name: "mock_tool", description: "a mock tool for testing") - assert_equal tool.to_h, { name: "mock_tool", description: "a mock tool for testing", inputSchema: {} } + assert_equal tool.to_h, { name: "mock_tool", description: "a mock tool for testing", inputSchema: { type: "object" } } end test "#to_h includes annotations when present" do @@ -71,6 +71,15 @@ class DefaultNameTool < Tool assert_equal tool.tool_name, "default_name_tool" end + test "input schema defaults to an empty hash" do + class NoInputSchemaTool < Tool; end + + tool = NoInputSchemaTool + + expected = { type: "object" } + assert_equal expected, tool.input_schema.to_h + end + test "accepts input schema as an InputSchema object" do class InputSchemaTool < Tool input_schema InputSchema.new(properties: { message: { type: "string" } }, required: [:message]) @@ -106,7 +115,7 @@ class InputSchemaTool < Tool assert_equal tool.name_value, "mock_tool" assert_equal tool.description, "a mock tool for testing" - assert_equal tool.input_schema, nil + assert_equal tool.input_schema, Tool::InputSchema.new end test ".define allows definition of tools with annotations" do @@ -123,7 +132,7 @@ class InputSchemaTool < Tool assert_equal tool.name_value, "mock_tool" assert_equal tool.description, "a mock tool for testing" - assert_equal tool.input_schema, nil + assert_equal tool.input_schema, Tool::InputSchema.new assert_equal tool.annotations_value.to_h, { title: "Mock Tool", readOnlyHint: true } end