Skip to content

Add title to tools, prompts and resources #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ This gem provides a `MCP::Tool` class that can be used to create tools in two wa

```ruby
class MyTool < MCP::Tool
title "My Tool"
description "This tool performs specific functionality..."
input_schema(
properties: {
Expand All @@ -381,7 +382,6 @@ class MyTool < MCP::Tool
required: ["message"]
)
annotations(
title: "My Tool",
read_only_hint: true,
destructive_hint: false,
idempotent_hint: true,
Expand All @@ -401,9 +401,9 @@ tool = MyTool
```ruby
tool = MCP::Tool.define(
name: "my_tool",
title: "My Tool",
description: "This tool performs specific functionality...",
annotations: {
title: "My Tool",
read_only_hint: true
}
) do |args, server_context|
Expand Down Expand Up @@ -437,6 +437,7 @@ The `MCP::Prompt` class provides two ways to create prompts:
```ruby
class MyPrompt < MCP::Prompt
prompt_name "my_prompt" # Optional - defaults to underscored class name
title "My Prompt"
description "This prompt performs specific functionality..."
arguments [
MCP::Prompt::Argument.new(
Expand Down Expand Up @@ -473,6 +474,7 @@ prompt = MyPrompt
```ruby
prompt = MCP::Prompt.define(
name: "my_prompt",
title: "My Prompt",
description: "This prompt performs specific functionality...",
arguments: [
MCP::Prompt::Argument.new(
Expand Down Expand Up @@ -558,7 +560,8 @@ The `MCP::Resource` class provides a way to register resources with the server.
```ruby
resource = MCP::Resource.new(
uri: "https://example.com/my_resource",
name: "My Resource",
name: "my-resource",
title: "My Resource",
description: "Lorem ipsum dolor sit amet",
mime_type: "text/html",
)
Expand Down
3 changes: 2 additions & 1 deletion examples/http_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ def template(args, server_context:)
resources: [
MCP::Resource.new(
uri: "https://test_resource.invalid",
name: "Test resource",
name: "test-resource",
title: "Test Resource",
description: "Test resource that echoes back the uri as its content",
mime_type: "text/plain",
),
Expand Down
3 changes: 2 additions & 1 deletion examples/stdio_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ def template(args, server_context:)
resources: [
MCP::Resource.new(
uri: "https://test_resource.invalid",
name: "Test resource",
name: "test-resource",
title: "Test Resource",
description: "Test resource that echoes back the uri as its content",
mime_type: "text/plain",
),
Expand Down
15 changes: 13 additions & 2 deletions lib/mcp/prompt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class Prompt
class << self
NOT_SET = Object.new

attr_reader :title_value
attr_reader :description_value
attr_reader :arguments_value

Expand All @@ -14,12 +15,13 @@ def template(args, server_context: nil)
end

def to_h
{ name: name_value, description: description_value, arguments: arguments_value.map(&:to_h) }.compact
{ name: name_value, title: title_value, description: description_value, arguments: arguments_value.map(&:to_h) }.compact
end

def inherited(subclass)
super
subclass.instance_variable_set(:@name_value, nil)
subclass.instance_variable_set(:@title_value, nil)
subclass.instance_variable_set(:@description_value, nil)
subclass.instance_variable_set(:@arguments_value, nil)
end
Expand All @@ -36,6 +38,14 @@ def name_value
@name_value || StringUtils.handle_from_class_name(name)
end

def title(value = NOT_SET)
if value == NOT_SET
@title_value
else
@title_value = value
end
end

def description(value = NOT_SET)
if value == NOT_SET
@description_value
Expand All @@ -52,9 +62,10 @@ def arguments(value = NOT_SET)
end
end

def define(name: nil, description: nil, arguments: [], &block)
def define(name: nil, title: nil, description: nil, arguments: [], &block)
Class.new(self) do
prompt_name name
title title
description description
arguments arguments
define_singleton_method(:template) do |args, server_context: nil|
Expand Down
14 changes: 8 additions & 6 deletions lib/mcp/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@

module MCP
class Resource
attr_reader :uri, :name, :description, :mime_type
attr_reader :uri, :name, :title, :description, :mime_type

def initialize(uri:, name:, description: nil, mime_type: nil)
def initialize(uri:, name:, title: nil, description: nil, mime_type: nil)
@uri = uri
@name = name
@title = title
@description = description
@mime_type = mime_type
end

def to_h
{
uri: @uri,
name: @name,
description: @description,
mimeType: @mime_type,
uri: uri,
name: name,
title: title,
description: description,
mimeType: mime_type,
}.compact
end
end
Expand Down
14 changes: 8 additions & 6 deletions lib/mcp/resource_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@

module MCP
class ResourceTemplate
attr_reader :uri_template, :name, :description, :mime_type
attr_reader :uri_template, :name, :title, :description, :mime_type

def initialize(uri_template:, name:, description: nil, mime_type: nil)
def initialize(uri_template:, name:, title: nil, description: nil, mime_type: nil)
@uri_template = uri_template
@name = name
@title = title
@description = description
@mime_type = mime_type
end

def to_h
{
uriTemplate: @uri_template,
name: @name,
description: @description,
mimeType: @mime_type,
uriTemplate: uri_template,
name: name,
title: title,
description: description,
mimeType: mime_type,
}.compact
end
end
Expand Down
8 changes: 4 additions & 4 deletions lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ def handle_json(request)
end
end

def define_tool(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
tool = Tool.define(name:, description:, input_schema:, annotations:, &block)
def define_tool(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, &block)
tool = Tool.define(name:, title:, description:, input_schema:, annotations:, &block)
@tools[tool.name_value] = tool
end

def define_prompt(name: nil, description: nil, arguments: [], &block)
prompt = Prompt.define(name:, description:, arguments:, &block)
def define_prompt(name: nil, title: nil, description: nil, arguments: [], &block)
prompt = Prompt.define(name:, title:, description:, arguments:, &block)
@prompts[prompt.name_value] = prompt
end

Expand Down
14 changes: 13 additions & 1 deletion lib/mcp/tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Tool
class << self
NOT_SET = Object.new

attr_reader :title_value
attr_reader :description_value
attr_reader :annotations_value

Expand All @@ -15,6 +16,7 @@ def call(*args, server_context: nil)
def to_h
result = {
name: name_value,
title: title_value,
description: description_value,
inputSchema: input_schema_value.to_h,
}
Expand All @@ -25,6 +27,7 @@ def to_h
def inherited(subclass)
super
subclass.instance_variable_set(:@name_value, nil)
subclass.instance_variable_set(:@title_value, nil)
subclass.instance_variable_set(:@description_value, nil)
subclass.instance_variable_set(:@input_schema_value, nil)
subclass.instance_variable_set(:@annotations_value, nil)
Expand All @@ -46,6 +49,14 @@ def input_schema_value
@input_schema_value || InputSchema.new
end

def title(value = NOT_SET)
if value == NOT_SET
@title_value
else
@title_value = value
end
end

def description(value = NOT_SET)
if value == NOT_SET
@description_value
Expand Down Expand Up @@ -74,9 +85,10 @@ def annotations(hash = NOT_SET)
end
end

def define(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
def define(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, &block)
Class.new(self) do
tool_name name
title title
description description
input_schema input_schema
self.annotations(annotations) if annotations
Expand Down
1 change: 1 addition & 0 deletions test/mcp/prompt_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def template(args, server_context:)
test ".define allows definition of simple prompts with a block" do
prompt = Prompt.define(
name: "mock_prompt",
title: "Mock Prompt",
description: "a mock prompt for testing",
arguments: [
Prompt::Argument.new(name: "test_argument", description: "Test argument", required: true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ def puts(message)
@server.resources = [
MCP::Resource.new(
uri: "https://test_resource.invalid",
name: "Test Resource",
name: "test-resource",
title: "Test Resource",
description: "A test resource",
mime_type: "text/plain",
),
Expand Down
31 changes: 21 additions & 10 deletions test/mcp/server_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ module MCP
class ServerTest < ActiveSupport::TestCase
include InstrumentationTestHelper
setup do
@tool = Tool.define(name: "test_tool", description: "Test tool")
@tool = Tool.define(
name: "test_tool",
title: "Test tool",
description: "A test tool",
)

@tool_that_raises = Tool.define(
name: "tool_that_raises",
description: "Tool that raises",
title: "Tool that raises",
description: "A tool that raises",
input_schema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] },
) { raise StandardError, "Tool error" }

@tool_with_no_args = Tool.define(
name: "tool_with_no_args",
title: "Tool with no args",
description: "This tool performs specific functionality...",
annotations: {
title: "Tool with no args",
read_only_hint: true,
},
) do
Expand All @@ -28,6 +33,7 @@ class ServerTest < ActiveSupport::TestCase

@prompt = Prompt.define(
name: "test_prompt",
title: "Test Prompt",
description: "Test prompt",
arguments: [
Prompt::Argument.new(name: "test_argument", description: "Test argument", required: true),
Expand All @@ -43,14 +49,16 @@ class ServerTest < ActiveSupport::TestCase

@resource = Resource.new(
uri: "https://test_resource.invalid",
name: "Test resource",
name: "test-resource",
title: "Test Resource",
description: "Test resource",
mime_type: "text/plain",
)

@resource_template = ResourceTemplate.new(
uri_template: "https://test_resource.invalid/{id}",
name: "Test resource",
name: "test-resource",
title: "Test Resource",
description: "Test resource",
mime_type: "text/plain",
)
Expand Down Expand Up @@ -163,7 +171,8 @@ class ServerTest < ActiveSupport::TestCase
result = response[:result]
assert_kind_of Array, result[:tools]
assert_equal "test_tool", result[:tools][0][:name]
assert_equal "Test tool", result[:tools][0][:description]
assert_equal "Test tool", result[:tools][0][:title]
assert_equal "A test tool", result[:tools][0][:description]
assert_equal({ type: "object" }, result[:tools][0][:inputSchema])
assert_instrumentation_data({ method: "tools/list" })
end
Expand All @@ -179,7 +188,8 @@ class ServerTest < ActiveSupport::TestCase
result = response[:result]
assert_kind_of Array, result[:tools]
assert_equal "test_tool", result[:tools][0][:name]
assert_equal "Test tool", result[:tools][0][:description]
assert_equal "Test tool", result[:tools][0][:title]
assert_equal "A test tool", result[:tools][0][:description]
end

test "#tools_list_handler sets the tools/list handler" do
Expand Down Expand Up @@ -224,7 +234,8 @@ class ServerTest < ActiveSupport::TestCase
test "#handle tools/call returns error if required tool arguments are missing" do
tool_with_required_argument = Tool.define(
name: "test_tool",
description: "Test tool",
title: "Test tool",
description: "A test tool",
input_schema: { properties: { message: { type: "string" } }, required: ["message"] },
) do |message: nil|
Tool::Response.new("success #{message}")
Expand Down Expand Up @@ -532,7 +543,7 @@ def call(message:, server_context: nil)

test "#resources_list_handler sets the resources/list handler" do
@server.resources_list_handler do
[{ uri: "https://test_resource.invalid", name: "Test resource", description: "Test resource" }]
[{ uri: "https://test_resource.invalid", name: "test-resource", title: "Test Resource", description: "Test resource" }]
end

request = {
Expand All @@ -543,7 +554,7 @@ def call(message:, server_context: nil)

response = @server.handle(request)
assert_equal(
{ resources: [{ uri: "https://test_resource.invalid", name: "Test resource", description: "Test resource" }] },
{ resources: [{ uri: "https://test_resource.invalid", name: "test-resource", title: "Test Resource", description: "Test resource" }] },
response[:result],
)
assert_instrumentation_data({ method: "resources/list" })
Expand Down
Loading