Skip to content
Open
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
21 changes: 20 additions & 1 deletion lib/mcp/tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module MCP
class Tool
class << self
NOT_SET = Object.new
MAX_LENGTH_OF_NAME = 128

attr_reader :title_value
attr_reader :description_value
Expand Down Expand Up @@ -42,11 +43,13 @@ def tool_name(value = NOT_SET)
name_value
else
@name_value = value

validate!
end
end

def name_value
@name_value || StringUtils.handle_from_class_name(name)
@name_value || (name.nil? ? nil : StringUtils.handle_from_class_name(name))
end

def input_schema_value
Expand Down Expand Up @@ -117,6 +120,22 @@ def define(name: nil, title: nil, description: nil, input_schema: nil, output_sc
output_schema output_schema
self.annotations(annotations) if annotations
define_singleton_method(:call, &block) if block
end.tap(&:validate!)
end

# It complies with the following tool name specification:
# https://modelcontextprotocol.io/specification/latest/server/tools#tool-names
def validate!
return true unless tool_name

if tool_name.empty? || tool_name.length > MAX_LENGTH_OF_NAME
raise ArgumentError, "Tool names should be between 1 and 128 characters in length (inclusive)."
end

unless tool_name.match?(/\A[A-Za-z\d_\-\.]+\z/)
raise ArgumentError, <<~MESSAGE
Tool names only allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.).
MESSAGE
end
end
end
Expand Down
41 changes: 41 additions & 0 deletions test/mcp/tool_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -434,5 +434,46 @@ def call(message:, server_context: nil)
expected_output = { type: "object", properties: { result: { type: "string" }, success: { type: "boolean" } }, required: ["result", "success"] }
assert_equal expected_output, tool.output_schema.to_h
end

test "accepts valid tool names" do
assert Tool.define(name: "getUser")
assert Tool.define(name: "DATA_EXPORT_v2")
assert Tool.define(name: "admin.tools.list")
assert Tool.define(name: "a" * 128)
end

test "raises an error when tool name is empty in class definition" do
error = assert_raises(ArgumentError) do
class EmptyTitleNameTool < Tool
tool_name ""
end
end
assert_equal("Tool names should be between 1 and 128 characters in length (inclusive).", error.message)
end

test "allows nil tool name in class definition" do
assert_nothing_raised do
class EmptyTitleNameTool < Tool
tool_name nil
end
end
end

test "raises an error when tool name is empty" do
error = assert_raises(ArgumentError) { Tool.define(name: "") }
assert_equal("Tool names should be between 1 and 128 characters in length (inclusive).", error.message)
end

test "raises an error when tool name exceeds 128 characters" do
error = assert_raises(ArgumentError) { Tool.define(name: "a" * 129) }
assert_equal("Tool names should be between 1 and 128 characters in length (inclusive).", error.message)
end

test "raises an error when tool name includes invalid characters (e.g., spaces)" do
error = assert_raises(ArgumentError) { Tool.define(name: "foo bar") }
assert_equal(<<~MESSAGE, error.message)
Tool names only allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.).
MESSAGE
end
end
end