Skip to content

Commit 8e6dfa9

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 4eab486 commit 8e6dfa9

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

@@ -51,6 +60,7 @@ def initialize(
5160
@title = title
5261
@version = version
5362
@instructions = instructions
63+
@tool_names = tools.map(&:name_value)
5464
@tools = tools.to_h { |t| [t.name_value, t] }
5565
@prompts = prompts.to_h { |p| [p.name_value, p] }
5666
@resources = resources
@@ -99,7 +109,10 @@ def handle_json(request)
99109

100110
def define_tool(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, meta: nil, &block)
101111
tool = Tool.define(name:, title:, description:, input_schema:, annotations:, meta:, &block)
102-
@tools[tool.name_value] = tool
112+
tool_name = tool.name_value
113+
114+
@tool_names << tool_name
115+
@tools[tool_name] = tool
103116

104117
validate!
105118
end
@@ -174,6 +187,8 @@ def prompts_get_handler(&block)
174187
private
175188

176189
def validate!
190+
validate_tool_name!
191+
177192
# NOTE: The draft protocol version is the next version after 2025-03-26.
178193
if @configuration.protocol_version <= "2025-03-26"
179194
if server_info.key?(:title)
@@ -207,6 +222,12 @@ def validate!
207222
end
208223
end
209224

225+
def validate_tool_name!
226+
duplicated_tool_names = @tool_names.tally.filter_map { |name, count| name if count >= 2 }
227+
228+
raise ToolNotUnique, duplicated_tool_names unless duplicated_tool_names.empty?
229+
end
230+
210231
def handle_request(request, method)
211232
handler = @handlers[method]
212233
unless handler

test/mcp/server_test.rb

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

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

974+
test "#define_tool adds a tool with duplicated tool name to the server" do
975+
error = assert_raises(MCP::ToolNotUnique) do
976+
@server.define_tool(
977+
name: "test_tool", # NOTE: Already registered tool name
978+
description: "Defined tool",
979+
input_schema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] },
980+
meta: { foo: "bar" },
981+
) do |message:|
982+
Tool::Response.new(message)
983+
end
984+
end
985+
assert_match(/\ATool names should be unique. Use `tool_name` to assign unique names to/, error.message)
986+
end
987+
936988
test "#define_tool call definition allows tool arguments and server context" do
937989
@server.server_context = { user_id: "123" }
938990

0 commit comments

Comments
 (0)