Skip to content

Commit 8d68fc0

Browse files
authored
Merge pull request #199 from koic/fix_tool_name_colisions_across_namespaces
Raise an error when duplicate tool names are registered
2 parents 149d977 + dee80d2 commit 8d68fc0

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

lib/mcp/server.rb

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
require_relative "methods"
66

77
module MCP
8+
class ToolNotUnique < StandardError
9+
def initialize(duplicated_tool_names)
10+
super(<<~MESSAGE)
11+
Tool names should be unique. Use `tool_name` to assign unique names to:
12+
#{duplicated_tool_names.join(", ")}
13+
MESSAGE
14+
end
15+
end
16+
817
class Server
918
DEFAULT_VERSION = "0.1.0"
1019

@@ -53,6 +62,7 @@ def initialize(
5362
@title = title
5463
@version = version
5564
@instructions = instructions
65+
@tool_names = tools.map(&:name_value)
5666
@tools = tools.to_h { |t| [t.name_value, t] }
5767
@prompts = prompts.to_h { |p| [p.name_value, p] }
5868
@resources = resources
@@ -101,7 +111,10 @@ def handle_json(request)
101111

102112
def define_tool(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, meta: nil, &block)
103113
tool = Tool.define(name:, title:, description:, input_schema:, annotations:, meta:, &block)
104-
@tools[tool.name_value] = tool
114+
tool_name = tool.name_value
115+
116+
@tool_names << tool_name
117+
@tools[tool_name] = tool
105118

106119
validate!
107120
end
@@ -176,6 +189,8 @@ def prompts_get_handler(&block)
176189
private
177190

178191
def validate!
192+
validate_tool_name!
193+
179194
# NOTE: The draft protocol version is the next version after 2025-11-25.
180195
if @configuration.protocol_version <= "2025-06-18"
181196
if server_info.key?(:description)
@@ -216,6 +231,12 @@ def validate!
216231
end
217232
end
218233

234+
def validate_tool_name!
235+
duplicated_tool_names = @tool_names.tally.filter_map { |name, count| name if count >= 2 }
236+
237+
raise ToolNotUnique, duplicated_tool_names unless duplicated_tool_names.empty?
238+
end
239+
219240
def handle_request(request, method)
220241
handler = @handlers[method]
221242
unless handler

test/mcp/server_test.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,44 @@ def call(message:, server_context: nil)
374374
assert_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises" })
375375
end
376376

377+
test "registers tools with the same class name in different namespaces" do
378+
module Foo
379+
class Example < Tool
380+
end
381+
end
382+
383+
module Bar
384+
class Example < Tool
385+
end
386+
end
387+
388+
error = assert_raises(MCP::ToolNotUnique) { Server.new(tools: [Foo::Example, Bar::Example]) }
389+
assert_equal(<<~MESSAGE, error.message)
390+
Tool names should be unique. Use `tool_name` to assign unique names to:
391+
example
392+
MESSAGE
393+
end
394+
395+
test "registers tools with the same tool name" do
396+
module Baz
397+
class Example < Tool
398+
tool_name "foo"
399+
end
400+
end
401+
402+
module Qux
403+
class Example < Tool
404+
tool_name "foo"
405+
end
406+
end
407+
408+
error = assert_raises(MCP::ToolNotUnique) { Server.new(tools: [Baz::Example, Qux::Example]) }
409+
assert_equal(<<~MESSAGE, error.message)
410+
Tool names should be unique. Use `tool_name` to assign unique names to:
411+
foo
412+
MESSAGE
413+
end
414+
377415
test "#handle_json returns error response with isError true if the tool raises an error" do
378416
request = JSON.generate({
379417
jsonrpc: "2.0",
@@ -950,6 +988,20 @@ def call(message:, server_context: nil)
950988
assert_equal({ content: "success", isError: false }, response[:result])
951989
end
952990

991+
test "#define_tool adds a tool with duplicated tool name to the server" do
992+
error = assert_raises(MCP::ToolNotUnique) do
993+
@server.define_tool(
994+
name: "test_tool", # NOTE: Already registered tool name
995+
description: "Defined tool",
996+
input_schema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] },
997+
meta: { foo: "bar" },
998+
) do |message:|
999+
Tool::Response.new(message)
1000+
end
1001+
end
1002+
assert_match(/\ATool names should be unique. Use `tool_name` to assign unique names to/, error.message)
1003+
end
1004+
9531005
test "#define_tool call definition allows tool arguments and server context" do
9541006
@server.server_context = { user_id: "123" }
9551007

0 commit comments

Comments
 (0)