diff --git a/lib/mcp/tool.rb b/lib/mcp/tool.rb index 8175511..d72de72 100644 --- a/lib/mcp/tool.rb +++ b/lib/mcp/tool.rb @@ -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 @@ -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 @@ -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 diff --git a/test/mcp/tool_test.rb b/test/mcp/tool_test.rb index 0de4278..27729d5 100644 --- a/test/mcp/tool_test.rb +++ b/test/mcp/tool_test.rb @@ -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