Skip to content

Commit 84ff1fc

Browse files
committed
Make tool names stricter
Follow-up to #199 (comment) Tool names are now validated to comply with the specification. https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names Since the tool name is optional, `nil` value is treated as "not specified" and is ignored without validation.
1 parent 771163c commit 84ff1fc

File tree

2 files changed

+61
-1
lines changed

2 files changed

+61
-1
lines changed

lib/mcp/tool.rb

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module MCP
44
class Tool
55
class << self
66
NOT_SET = Object.new
7+
MAX_LENGTH_OF_NAME = 128
78

89
attr_reader :title_value
910
attr_reader :description_value
@@ -42,11 +43,13 @@ def tool_name(value = NOT_SET)
4243
name_value
4344
else
4445
@name_value = value
46+
47+
validate!
4548
end
4649
end
4750

4851
def name_value
49-
@name_value || StringUtils.handle_from_class_name(name)
52+
@name_value || (name.nil? ? nil : StringUtils.handle_from_class_name(name))
5053
end
5154

5255
def input_schema_value
@@ -117,6 +120,22 @@ def define(name: nil, title: nil, description: nil, input_schema: nil, output_sc
117120
output_schema output_schema
118121
self.annotations(annotations) if annotations
119122
define_singleton_method(:call, &block) if block
123+
end.tap(&:validate!)
124+
end
125+
126+
# It complies with the following tool name specification:
127+
# https://modelcontextprotocol.io/specification/latest/server/tools#tool-names
128+
def validate!
129+
return true unless tool_name
130+
131+
if tool_name.empty? || tool_name.length > MAX_LENGTH_OF_NAME
132+
raise ArgumentError, "Tool names should be between 1 and 128 characters in length (inclusive)."
133+
end
134+
135+
unless tool_name.match?(/\A[A-Za-z\d_\-\.]+\z/)
136+
raise ArgumentError, <<~MESSAGE
137+
Tool names only allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.).
138+
MESSAGE
120139
end
121140
end
122141
end

test/mcp/tool_test.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,5 +434,46 @@ def call(message:, server_context: nil)
434434
expected_output = { type: "object", properties: { result: { type: "string" }, success: { type: "boolean" } }, required: ["result", "success"] }
435435
assert_equal expected_output, tool.output_schema.to_h
436436
end
437+
438+
test "accepts valid tool names" do
439+
assert Tool.define(name: "getUser")
440+
assert Tool.define(name: "DATA_EXPORT_v2")
441+
assert Tool.define(name: "admin.tools.list")
442+
assert Tool.define(name: "a" * 128)
443+
end
444+
445+
test "raises an error when tool name is empty in class definition" do
446+
error = assert_raises(ArgumentError) do
447+
class EmptyTitleNameTool < Tool
448+
tool_name ""
449+
end
450+
end
451+
assert_equal("Tool names should be between 1 and 128 characters in length (inclusive).", error.message)
452+
end
453+
454+
test "allows nil tool name in class definition" do
455+
assert_nothing_raised do
456+
class EmptyTitleNameTool < Tool
457+
tool_name nil
458+
end
459+
end
460+
end
461+
462+
test "raises an error when tool name is empty" do
463+
error = assert_raises(ArgumentError) { Tool.define(name: "") }
464+
assert_equal("Tool names should be between 1 and 128 characters in length (inclusive).", error.message)
465+
end
466+
467+
test "raises an error when tool name exceeds 128 characters" do
468+
error = assert_raises(ArgumentError) { Tool.define(name: "a" * 129) }
469+
assert_equal("Tool names should be between 1 and 128 characters in length (inclusive).", error.message)
470+
end
471+
472+
test "raises an error when tool name includes invalid characters (e.g., spaces)" do
473+
error = assert_raises(ArgumentError) { Tool.define(name: "foo bar") }
474+
assert_equal(<<~MESSAGE, error.message)
475+
Tool names only allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.).
476+
MESSAGE
477+
end
437478
end
438479
end

0 commit comments

Comments
 (0)