Skip to content

Commit dee80d2

Browse files
committed
Raise an error when duplicate tool names are registered
This raises an exception when duplicate tool names are registered, instead of silently overwriting tools. Tool names are required to be unique within a server, so this behavior aligns with the MCP specification. > Tool names SHOULD be unique within a server. https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names Validation for tool names could be made more strictly spec-compliant in a separate effort. Fixes #197
1 parent 171baa8 commit dee80d2

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)