From 3127bb69725ec5fce18640a613dfb4be72f5143d Mon Sep 17 00:00:00 2001 From: Tyler Untisz Date: Sun, 31 Aug 2025 11:57:40 -0600 Subject: [PATCH 1/5] Add support for _meta field on tool schema --- lib/mcp/server.rb | 4 ++-- lib/mcp/tool.rb | 14 ++++++++++- test/mcp/server_test.rb | 4 ++++ test/mcp/tool_test.rb | 52 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/lib/mcp/server.rb b/lib/mcp/server.rb index f62ee03b..0f927775 100644 --- a/lib/mcp/server.rb +++ b/lib/mcp/server.rb @@ -99,8 +99,8 @@ def handle_json(request) end end - def define_tool(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, &block) - tool = Tool.define(name:, title:, description:, input_schema:, annotations:, &block) + def define_tool(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, metadata: nil, &block) + tool = Tool.define(name:, title:, description:, input_schema:, annotations:, metadata:, &block) @tools[tool.name_value] = tool end diff --git a/lib/mcp/tool.rb b/lib/mcp/tool.rb index 024704df..86cf21c2 100644 --- a/lib/mcp/tool.rb +++ b/lib/mcp/tool.rb @@ -8,6 +8,7 @@ class << self attr_reader :title_value attr_reader :description_value attr_reader :annotations_value + attr_reader :metadata_value def call(*args, server_context: nil) raise NotImplementedError, "Subclasses must implement call" @@ -20,6 +21,7 @@ def to_h description: description_value, inputSchema: input_schema_value.to_h, } + result[:_meta] = metadata_value if metadata_value result[:annotations] = annotations_value.to_h if annotations_value result end @@ -31,6 +33,7 @@ def inherited(subclass) subclass.instance_variable_set(:@description_value, nil) subclass.instance_variable_set(:@input_schema_value, nil) subclass.instance_variable_set(:@annotations_value, nil) + subclass.instance_variable_set(:@metadata_value, nil) end def tool_name(value = NOT_SET) @@ -77,6 +80,14 @@ def input_schema(value = NOT_SET) end end + def metadata(value = NOT_SET) + if value == NOT_SET + @metadata_value + else + @metadata_value = value + end + end + def annotations(hash = NOT_SET) if hash == NOT_SET @annotations_value @@ -85,12 +96,13 @@ def annotations(hash = NOT_SET) end end - def define(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, &block) + def define(name: nil, title: nil, description: nil, input_schema: nil, metadata: nil, annotations: nil, &block) Class.new(self) do tool_name name title title description description input_schema input_schema + metadata metadata self.annotations(annotations) if annotations define_singleton_method(:call, &block) if block end diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index a8527bc0..002561b5 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -11,6 +11,7 @@ class ServerTest < ActiveSupport::TestCase name: "test_tool", title: "Test tool", description: "A test tool", + metadata: { foo: "bar" }, ) @tool_that_raises = Tool.define( @@ -196,6 +197,7 @@ class ServerTest < ActiveSupport::TestCase 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({ foo: "bar" }, result[:tools][0][:_meta]) assert_instrumentation_data({ method: "tools/list" }) end @@ -212,6 +214,7 @@ class ServerTest < ActiveSupport::TestCase 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({ foo: "bar" }, result[:tools][0][:_meta]) end test "#tools_list_handler sets the tools/list handler" do @@ -827,6 +830,7 @@ def call(message:, server_context: nil) name: "defined_tool", description: "Defined tool", input_schema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] }, + metadata: { foo: "bar" }, ) do |message:| Tool::Response.new(message) end diff --git a/test/mcp/tool_test.rb b/test/mcp/tool_test.rb index a8dafcaa..0ded3edf 100644 --- a/test/mcp/tool_test.rb +++ b/test/mcp/tool_test.rb @@ -16,6 +16,9 @@ class TestTool < Tool read_only_hint: true, title: "Test Tool", ) + metadata( + foo: "bar", + ) class << self def call(message:, server_context: nil) @@ -45,6 +48,14 @@ def call(message:, server_context: nil) assert_equal expected_annotations, tool.to_h[:annotations] end + test "#to_h includes metadata when present" do + tool = TestTool + expected_metadata = { + foo: "bar", + } + assert_equal expected_metadata, tool.to_h[:_meta] + end + test "#call invokes the tool block and returns the response" do tool = TestTool response = tool.call(message: "test") @@ -145,6 +156,23 @@ class InputSchemaTool < Tool assert_equal({ destructiveHint: true, idempotentHint: false, openWorldHint: true, readOnlyHint: true, title: "Mock Tool" }, tool.annotations_value.to_h) end + test ".define allows definition of tools with metadata" do + tool = Tool.define( + name: "mock_tool", + title: "Mock Tool", + description: "a mock tool for testing", + metadata: { foo: "bar" }, + ) do |_| + Tool::Response.new([{ type: "text", content: "OK" }]) + end + + assert_equal "mock_tool", tool.name_value + assert_equal "Mock Tool", tool.title + assert_equal "a mock tool for testing", tool.description + assert_equal tool.input_schema, Tool::InputSchema.new + assert_equal({ foo: "bar" }, tool.metadata_value) + end + test "Tool class method annotations can be set and retrieved" do class AnnotationsTestTool < Tool tool_name "annotations_test" @@ -173,6 +201,30 @@ class UpdatableAnnotationsTool < Tool assert_equal "Updated", tool.annotations_value.title end + test "Tool class method metadata can be set and retrieved" do + class MetadataTestTool < Tool + tool_name "annotations_test" + metadata(foo: "bar") + end + + tool = MetadataTestTool + assert_instance_of Hash, tool.metadata_value + assert_equal "bar", tool.metadata_value[:foo] + end + + test "Tool class method metadata can be updated" do + class UpdatableMetadataTool < Tool + tool_name "updatable_metadata" + end + + tool = UpdatableMetadataTool + tool.metadata(foo: "baz") + assert_equal({ foo: "baz" }, tool.metadata_value) + + tool.metadata(foo: "qux") + assert_equal({ foo: "qux" }, tool.metadata_value) + end + test "#call with Sorbet typed tools invokes the tool block and returns the response" do class TypedTestTool < Tool tool_name "test_tool" From 547571ebdc7ad11aff7db7fe00475757976f419d Mon Sep 17 00:00:00 2001 From: Topher Bullock Date: Wed, 1 Oct 2025 15:39:14 -0400 Subject: [PATCH 2/5] use `meta` naming for `_meta` field on tool schema --- lib/mcp/server.rb | 4 ++-- lib/mcp/tool.rb | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/mcp/server.rb b/lib/mcp/server.rb index 0f927775..25622ecc 100644 --- a/lib/mcp/server.rb +++ b/lib/mcp/server.rb @@ -99,8 +99,8 @@ def handle_json(request) end end - def define_tool(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, metadata: nil, &block) - tool = Tool.define(name:, title:, description:, input_schema:, annotations:, metadata:, &block) + def define_tool(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, meta: nil, &block) + tool = Tool.define(name:, title:, description:, input_schema:, annotations:, meta:, &block) @tools[tool.name_value] = tool end diff --git a/lib/mcp/tool.rb b/lib/mcp/tool.rb index 86cf21c2..8de5050c 100644 --- a/lib/mcp/tool.rb +++ b/lib/mcp/tool.rb @@ -8,7 +8,7 @@ class << self attr_reader :title_value attr_reader :description_value attr_reader :annotations_value - attr_reader :metadata_value + attr_reader :meta_value def call(*args, server_context: nil) raise NotImplementedError, "Subclasses must implement call" @@ -21,7 +21,7 @@ def to_h description: description_value, inputSchema: input_schema_value.to_h, } - result[:_meta] = metadata_value if metadata_value + result[:_meta] = meta_value if meta_value result[:annotations] = annotations_value.to_h if annotations_value result end @@ -33,7 +33,7 @@ def inherited(subclass) subclass.instance_variable_set(:@description_value, nil) subclass.instance_variable_set(:@input_schema_value, nil) subclass.instance_variable_set(:@annotations_value, nil) - subclass.instance_variable_set(:@metadata_value, nil) + subclass.instance_variable_set(:@meta_value, nil) end def tool_name(value = NOT_SET) @@ -80,11 +80,11 @@ def input_schema(value = NOT_SET) end end - def metadata(value = NOT_SET) + def meta(value = NOT_SET) if value == NOT_SET - @metadata_value + @meta_value else - @metadata_value = value + @meta_value = value end end @@ -96,7 +96,7 @@ def annotations(hash = NOT_SET) end end - def define(name: nil, title: nil, description: nil, input_schema: nil, metadata: nil, annotations: nil, &block) + def define(name: nil, title: nil, description: nil, input_schema: nil, meta: nil, annotations: nil, &block) Class.new(self) do tool_name name title title From 42e8a640f981e19cbbe2d5996de7fd4965bd054f Mon Sep 17 00:00:00 2001 From: Topher Bullock Date: Wed, 1 Oct 2025 15:53:05 -0400 Subject: [PATCH 3/5] apply suggestions to fix `_meta` tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ateş Göral --- test/mcp/server_test.rb | 4 ++-- test/mcp/tool_test.rb | 44 ++++++++++++++++++++--------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index c432d8a7..f714e17d 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -10,7 +10,7 @@ class ServerTest < ActiveSupport::TestCase name: "test_tool", title: "Test tool", description: "A test tool", - metadata: { foo: "bar" }, + meta: { foo: "bar" }, ) @tool_that_raises = Tool.define( @@ -851,7 +851,7 @@ def call(message:, server_context: nil) name: "defined_tool", description: "Defined tool", input_schema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] }, - metadata: { foo: "bar" }, + meta: { foo: "bar" }, ) do |message:| Tool::Response.new(message) end diff --git a/test/mcp/tool_test.rb b/test/mcp/tool_test.rb index dec64a81..b7172676 100644 --- a/test/mcp/tool_test.rb +++ b/test/mcp/tool_test.rb @@ -15,7 +15,7 @@ class TestTool < Tool read_only_hint: true, title: "Test Tool", ) - metadata( + meta( foo: "bar", ) @@ -55,12 +55,12 @@ def call(message:, server_context: nil) assert_equal expected_annotations, tool.to_h[:annotations] end - test "#to_h includes metadata when present" do + test "#to_h includes meta when present" do tool = TestTool - expected_metadata = { + expected_meta = { foo: "bar", } - assert_equal expected_metadata, tool.to_h[:_meta] + assert_equal expected_meta, tool.to_h[:_meta] end test "#call invokes the tool block and returns the response" do @@ -163,12 +163,12 @@ class InputSchemaTool < Tool assert_equal({ destructiveHint: true, idempotentHint: false, openWorldHint: true, readOnlyHint: true, title: "Mock Tool" }, tool.annotations_value.to_h) end - test ".define allows definition of tools with metadata" do + test ".define allows definition of tools with meta" do tool = Tool.define( name: "mock_tool", title: "Mock Tool", description: "a mock tool for testing", - metadata: { foo: "bar" }, + meta: { foo: "bar" }, ) do |_| Tool::Response.new([{ type: "text", content: "OK" }]) end @@ -177,7 +177,7 @@ class InputSchemaTool < Tool assert_equal "Mock Tool", tool.title assert_equal "a mock tool for testing", tool.description assert_equal tool.input_schema, Tool::InputSchema.new - assert_equal({ foo: "bar" }, tool.metadata_value) + assert_equal({ foo: "bar" }, tool.meta_value) end test "Tool class method annotations can be set and retrieved" do @@ -208,28 +208,28 @@ class UpdatableAnnotationsTool < Tool assert_equal "Updated", tool.annotations_value.title end - test "Tool class method metadata can be set and retrieved" do - class MetadataTestTool < Tool - tool_name "annotations_test" - metadata(foo: "bar") + test "Tool class method meta can be set and retrieved" do + class MetaTestTool < Tool + tool_name "meta_test" + meta(foo: "bar") end - tool = MetadataTestTool - assert_instance_of Hash, tool.metadata_value - assert_equal "bar", tool.metadata_value[:foo] + tool = MetaTestTool + assert_instance_of Hash, tool.meta_value + assert_equal "bar", tool.meta_value[:foo] end - test "Tool class method metadata can be updated" do - class UpdatableMetadataTool < Tool - tool_name "updatable_metadata" + test "Tool class method meta can be updated" do + class UpdatableMetaTool < Tool + tool_name "updatable_meta" end - tool = UpdatableMetadataTool - tool.metadata(foo: "baz") - assert_equal({ foo: "baz" }, tool.metadata_value) + tool = UpdatableMetaTool + tool.meta(foo: "baz") + assert_equal({ foo: "baz" }, tool.meta_value) - tool.metadata(foo: "qux") - assert_equal({ foo: "qux" }, tool.metadata_value) + tool.meta(foo: "qux") + assert_equal({ foo: "qux" }, tool.meta_value) end test "#call with Sorbet typed tools invokes the tool block and returns the response" do From 0abbdf81c2b7038b742ed8cce387e7e8db3d13aa Mon Sep 17 00:00:00 2001 From: Topher Bullock Date: Wed, 1 Oct 2025 16:16:39 -0400 Subject: [PATCH 4/5] Update lib/mcp/tool.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ateş Göral --- lib/mcp/tool.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/mcp/tool.rb b/lib/mcp/tool.rb index fdb117fc..7d740677 100644 --- a/lib/mcp/tool.rb +++ b/lib/mcp/tool.rb @@ -92,7 +92,6 @@ def output_schema(value = NOT_SET) @output_schema_value = value end end - def meta(value = NOT_SET) if value == NOT_SET @meta_value From 3dfb11f8fa4349b3937e12c968018490c20cfa4d Mon Sep 17 00:00:00 2001 From: Topher Bullock Date: Wed, 1 Oct 2025 16:58:55 -0400 Subject: [PATCH 5/5] Update lib/mcp/tool.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ateş Göral --- lib/mcp/tool.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mcp/tool.rb b/lib/mcp/tool.rb index 7d740677..6ddc7e60 100644 --- a/lib/mcp/tool.rb +++ b/lib/mcp/tool.rb @@ -92,6 +92,7 @@ def output_schema(value = NOT_SET) @output_schema_value = value end end + def meta(value = NOT_SET) if value == NOT_SET @meta_value