Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,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, meta: nil, &block)
tool = Tool.define(name:, title:, description:, input_schema:, annotations:, meta:, &block)
@tools[tool.name_value] = tool

validate!
Expand Down
14 changes: 13 additions & 1 deletion lib/mcp/tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class << self
attr_reader :title_value
attr_reader :description_value
attr_reader :annotations_value
attr_reader :meta_value

def call(*args, server_context: nil)
raise NotImplementedError, "Subclasses must implement call"
Expand All @@ -21,6 +22,7 @@ def to_h
inputSchema: input_schema_value.to_h,
outputSchema: @output_schema_value&.to_h,
annotations: annotations_value&.to_h,
_meta: meta_value,
}.compact
end

Expand All @@ -32,6 +34,7 @@ def inherited(subclass)
subclass.instance_variable_set(:@input_schema_value, nil)
subclass.instance_variable_set(:@output_schema_value, nil)
subclass.instance_variable_set(:@annotations_value, nil)
subclass.instance_variable_set(:@meta_value, nil)
end

def tool_name(value = NOT_SET)
Expand Down Expand Up @@ -90,6 +93,14 @@ def output_schema(value = NOT_SET)
end
end

def meta(value = NOT_SET)
if value == NOT_SET
@meta_value
else
@meta_value = value
end
end

def annotations(hash = NOT_SET)
if hash == NOT_SET
@annotations_value
Expand All @@ -98,12 +109,13 @@ def annotations(hash = NOT_SET)
end
end

def define(name: nil, title: nil, description: nil, input_schema: nil, output_schema: nil, annotations: nil, &block)
def define(name: nil, title: nil, description: nil, input_schema: nil, output_schema: nil, meta: nil, annotations: nil, &block)
Class.new(self) do
tool_name name
title title
description description
input_schema input_schema
meta meta
output_schema output_schema
self.annotations(annotations) if annotations
define_singleton_method(:call, &block) if block
Expand Down
4 changes: 4 additions & 0 deletions test/mcp/server_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class ServerTest < ActiveSupport::TestCase
name: "test_tool",
title: "Test tool",
description: "A test tool",
meta: { foo: "bar" },
)

@tool_that_raises = Tool.define(
Expand Down Expand Up @@ -195,6 +196,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

Expand All @@ -211,6 +213,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
Expand Down Expand Up @@ -848,6 +851,7 @@ def call(message:, server_context: nil)
name: "defined_tool",
description: "Defined tool",
input_schema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] },
meta: { foo: "bar" },
) do |message:|
Tool::Response.new(message)
end
Expand Down
52 changes: 52 additions & 0 deletions test/mcp/tool_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class TestTool < Tool
read_only_hint: true,
title: "Test Tool",
)
meta(
foo: "bar",
)

class << self
def call(message:, server_context: nil)
Expand Down Expand Up @@ -52,6 +55,14 @@ def call(message:, server_context: nil)
assert_equal expected_annotations, tool.to_h[:annotations]
end

test "#to_h includes meta when present" do
tool = TestTool
expected_meta = {
foo: "bar",
}
assert_equal expected_meta, tool.to_h[:_meta]
end

test "#call invokes the tool block and returns the response" do
tool = TestTool
response = tool.call(message: "test")
Expand Down Expand Up @@ -152,6 +163,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 meta" do
tool = Tool.define(
name: "mock_tool",
title: "Mock Tool",
description: "a mock tool for testing",
meta: { 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.meta_value)
end

test "Tool class method annotations can be set and retrieved" do
class AnnotationsTestTool < Tool
tool_name "annotations_test"
Expand Down Expand Up @@ -180,6 +208,30 @@ class UpdatableAnnotationsTool < Tool
assert_equal "Updated", tool.annotations_value.title
end

test "Tool class method meta can be set and retrieved" do
class MetaTestTool < Tool
tool_name "meta_test"
meta(foo: "bar")
end

tool = MetaTestTool
assert_instance_of Hash, tool.meta_value
assert_equal "bar", tool.meta_value[:foo]
end

test "Tool class method meta can be updated" do
class UpdatableMetaTool < Tool
tool_name "updatable_meta"
end

tool = UpdatableMetaTool
tool.meta(foo: "baz")
assert_equal({ foo: "baz" }, tool.meta_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
class TypedTestTool < Tool
tool_name "test_tool"
Expand Down