diff --git a/README.md b/README.md index 59e297b..53ab515 100644 --- a/README.md +++ b/README.md @@ -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: { @@ -381,7 +382,6 @@ class MyTool < MCP::Tool required: ["message"] ) annotations( - title: "My Tool", read_only_hint: true, destructive_hint: false, idempotent_hint: true, @@ -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| @@ -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( @@ -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( @@ -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", ) diff --git a/examples/http_server.rb b/examples/http_server.rb index f59ee50..c94bb72 100644 --- a/examples/http_server.rb +++ b/examples/http_server.rb @@ -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", ), diff --git a/examples/stdio_server.rb b/examples/stdio_server.rb index 1c058ba..e446dcc 100644 --- a/examples/stdio_server.rb +++ b/examples/stdio_server.rb @@ -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", ), diff --git a/lib/mcp/prompt.rb b/lib/mcp/prompt.rb index 2b77c6e..10dd2e6 100644 --- a/lib/mcp/prompt.rb +++ b/lib/mcp/prompt.rb @@ -6,6 +6,7 @@ class Prompt class << self NOT_SET = Object.new + attr_reader :title_value attr_reader :description_value attr_reader :arguments_value @@ -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 @@ -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 @@ -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| diff --git a/lib/mcp/resource.rb b/lib/mcp/resource.rb index 95c7db8..b9d7d59 100644 --- a/lib/mcp/resource.rb +++ b/lib/mcp/resource.rb @@ -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 diff --git a/lib/mcp/resource_template.rb b/lib/mcp/resource_template.rb index e2cc6f2..c30059a 100644 --- a/lib/mcp/resource_template.rb +++ b/lib/mcp/resource_template.rb @@ -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 diff --git a/lib/mcp/server.rb b/lib/mcp/server.rb index e6c2684..15af0b7 100644 --- a/lib/mcp/server.rb +++ b/lib/mcp/server.rb @@ -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 diff --git a/lib/mcp/tool.rb b/lib/mcp/tool.rb index 32dd619..024704d 100644 --- a/lib/mcp/tool.rb +++ b/lib/mcp/tool.rb @@ -5,6 +5,7 @@ class Tool class << self NOT_SET = Object.new + attr_reader :title_value attr_reader :description_value attr_reader :annotations_value @@ -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, } @@ -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) @@ -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 @@ -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 diff --git a/test/mcp/prompt_test.rb b/test/mcp/prompt_test.rb index 7200d21..1ad5f20 100644 --- a/test/mcp/prompt_test.rb +++ b/test/mcp/prompt_test.rb @@ -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), diff --git a/test/mcp/server/transports/stdio_notification_integration_test.rb b/test/mcp/server/transports/stdio_notification_integration_test.rb index eb4947c..745aa4a 100644 --- a/test/mcp/server/transports/stdio_notification_integration_test.rb +++ b/test/mcp/server/transports/stdio_notification_integration_test.rb @@ -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", ), diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index 2346bd7..f098fbd 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -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 @@ -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), @@ -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", ) @@ -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 @@ -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 @@ -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}") @@ -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 = { @@ -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" }) diff --git a/test/mcp/tool_test.rb b/test/mcp/tool_test.rb index abd3fcb..83996c7 100644 --- a/test/mcp/tool_test.rb +++ b/test/mcp/tool_test.rb @@ -25,8 +25,12 @@ def call(message:, server_context: nil) end test "#to_h returns a hash with name, description, and inputSchema" do - tool = Tool.define(name: "mock_tool", description: "a mock tool for testing") - assert_equal tool.to_h, { name: "mock_tool", description: "a mock tool for testing", inputSchema: { type: "object" } } + tool = Tool.define( + name: "mock_tool", + title: "Mock Tool", + description: "a mock tool for testing", + ) + assert_equal tool.to_h, { name: "mock_tool", title: "Mock Tool", description: "a mock tool for testing", inputSchema: { type: "object" } } end test "#to_h includes annotations when present" do @@ -109,7 +113,11 @@ class InputSchemaTool < Tool end test ".define allows definition of simple tools with a block" do - tool = Tool.define(name: "mock_tool", description: "a mock tool for testing") do |_| + tool = Tool.define( + name: "mock_tool", + title: "Mock Tool", + description: "a mock tool for testing", + ) do |_| Tool::Response.new([{ type: "text", content: "OK" }]) end @@ -121,9 +129,9 @@ class InputSchemaTool < Tool test ".define allows definition of tools with annotations" do tool = Tool.define( name: "mock_tool", + title: "Mock Tool", description: "a mock tool for testing", annotations: { - title: "Mock Tool", read_only_hint: true, }, ) do |_| @@ -131,9 +139,10 @@ class InputSchemaTool < Tool end assert_equal tool.name_value, "mock_tool" + assert_equal tool.title, "Mock Tool" assert_equal tool.description, "a mock tool for testing" assert_equal tool.input_schema, Tool::InputSchema.new - assert_equal tool.annotations_value.to_h, { title: "Mock Tool", readOnlyHint: true } + assert_equal tool.annotations_value.to_h, { readOnlyHint: true } end # Tests for Tool::Annotations class