Skip to content

Commit e2a8f27

Browse files
committed
implement BaseMetadata module mixin
1 parent 390e6b8 commit e2a8f27

File tree

8 files changed

+186
-18
lines changed

8 files changed

+186
-18
lines changed

lib/mcp.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require_relative "mcp/base_metadata"
34
require_relative "mcp/configuration"
45
require_relative "mcp/content"
56
require_relative "mcp/instrumentation"

lib/mcp/base_metadata.rb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# frozen_string_literal: true
2+
3+
module MCP
4+
# Provides shared functionality for classes implementing the BaseMetadata interface from the MCP spec.
5+
#
6+
# BaseMetadata defines:
7+
# - name: Intended for programmatic or logical use
8+
# - title: Intended for UI and end-user contexts (optional)
9+
#
10+
# This module provides display_name logic that follows the spec:
11+
# - Generally: use title if present, otherwise fall back to name
12+
# - For Tool: use annotations.title first, then title, then name
13+
module BaseMetadata
14+
# Returns the appropriate display name according to spec priority.
15+
#
16+
# For most classes: title (if present) or name
17+
# For Tool: annotations.title, title, or name
18+
#
19+
# @return [String] the display name
20+
def display_name
21+
if respond_to?(:annotations_value) && annotations_value&.title
22+
annotations_value.title
23+
elsif respond_to?(:title_value)
24+
title_value || name_value
25+
elsif respond_to?(:title)
26+
title || name
27+
else
28+
name
29+
end
30+
end
31+
32+
# Module containing class-level methods for BaseMetadata.
33+
# Use by including BaseMetadata and extending BaseMetadata::ClassMethods.
34+
module ClassMethods
35+
# Returns the appropriate display name according to spec priority.
36+
#
37+
# For Tool: annotations.title, title, or name
38+
# For others: title or name
39+
#
40+
# @return [String] the display name
41+
def display_name
42+
if respond_to?(:annotations_value) && annotations_value&.title
43+
annotations_value.title
44+
elsif title_value
45+
title_value
46+
else
47+
name_value
48+
end
49+
end
50+
51+
# Accessor for name value, must be implemented by including class
52+
def name_value
53+
raise NotImplementedError, "#{self} must implement name_value"
54+
end
55+
56+
# Accessor for title value
57+
def title_value
58+
@title_value
59+
end
60+
61+
# Class method to set or get title
62+
# Note: Classes using this module should define their own NOT_SET constant
63+
# which is used as a sentinel value to distinguish between getter and setter calls
64+
#
65+
# @param value [Object] the title value or NOT_SET to read
66+
# @return [String, nil] the title value when reading
67+
def title(value = NOT_SET)
68+
if value == NOT_SET
69+
@title_value
70+
else
71+
@title_value = value
72+
end
73+
end
74+
end
75+
end
76+
end

lib/mcp/prompt.rb

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
module MCP
44
class Prompt
55
class << self
6+
include BaseMetadata::ClassMethods
7+
68
NOT_SET = Object.new
79

8-
attr_reader :title_value
910
attr_reader :description_value
1011
attr_reader :arguments_value
1112
attr_reader :meta_value
@@ -45,14 +46,6 @@ def name_value
4546
@name_value || StringUtils.handle_from_class_name(name)
4647
end
4748

48-
def title(value = NOT_SET)
49-
if value == NOT_SET
50-
@title_value
51-
else
52-
@title_value = value
53-
end
54-
end
55-
5649
def description(value = NOT_SET)
5750
if value == NOT_SET
5851
@description_value

lib/mcp/prompt/argument.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
module MCP
44
class Prompt
55
class Argument
6+
include BaseMetadata
7+
68
attr_reader :name, :title, :description, :required
79

810
def initialize(name:, title: nil, description: nil, required: false)

lib/mcp/resource.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
module MCP
44
class Resource
5+
include BaseMetadata
6+
57
attr_reader :uri, :name, :title, :description, :mime_type
68

79
def initialize(uri:, name:, title: nil, description: nil, mime_type: nil)

lib/mcp/resource_template.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
module MCP
44
class ResourceTemplate
5+
include BaseMetadata
6+
57
attr_reader :uri_template, :name, :title, :description, :mime_type
68

79
def initialize(uri_template:, name:, title: nil, description: nil, mime_type: nil)

lib/mcp/tool.rb

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
module MCP
44
class Tool
55
class << self
6+
include BaseMetadata::ClassMethods
7+
68
NOT_SET = Object.new
79

8-
attr_reader :title_value
910
attr_reader :description_value
1011
attr_reader :annotations_value
1112
attr_reader :meta_value
@@ -55,14 +56,6 @@ def input_schema_value
5556

5657
attr_reader :output_schema_value
5758

58-
def title(value = NOT_SET)
59-
if value == NOT_SET
60-
@title_value
61-
else
62-
@title_value = value
63-
end
64-
end
65-
6659
def description(value = NOT_SET)
6760
if value == NOT_SET
6861
@description_value

test/mcp/base_metadata_test.rb

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class BaseMetadataTest < Minitest::Test
6+
# Test instance-level BaseMetadata (Resource, ResourceTemplate, Prompt::Argument)
7+
def test_resource_display_name_with_title
8+
resource = MCP::Resource.new(uri: "file:///test", name: "test_resource", title: "Test Resource")
9+
assert_equal("Test Resource", resource.display_name)
10+
end
11+
12+
def test_resource_display_name_without_title
13+
resource = MCP::Resource.new(uri: "file:///test", name: "test_resource")
14+
assert_equal("test_resource", resource.display_name)
15+
end
16+
17+
def test_resource_template_display_name_with_title
18+
template = MCP::ResourceTemplate.new(uri_template: "file:///{name}", name: "test_template", title: "Test Template")
19+
assert_equal("Test Template", template.display_name)
20+
end
21+
22+
def test_resource_template_display_name_without_title
23+
template = MCP::ResourceTemplate.new(uri_template: "file:///{name}", name: "test_template")
24+
assert_equal("test_template", template.display_name)
25+
end
26+
27+
def test_prompt_argument_display_name_with_title
28+
argument = MCP::Prompt::Argument.new(name: "test_arg", title: "Test Argument")
29+
assert_equal("Test Argument", argument.display_name)
30+
end
31+
32+
def test_prompt_argument_display_name_without_title
33+
argument = MCP::Prompt::Argument.new(name: "test_arg")
34+
assert_equal("test_arg", argument.display_name)
35+
end
36+
37+
# Test class-level BaseMetadata (Tool, Prompt)
38+
def test_tool_display_name_with_annotations_title
39+
tool_class = Class.new(MCP::Tool) do
40+
tool_name "test_tool"
41+
title "Tool Title"
42+
annotations(title: "Annotations Title")
43+
end
44+
45+
assert_equal("Annotations Title", tool_class.display_name)
46+
end
47+
48+
def test_tool_display_name_with_title_only
49+
tool_class = Class.new(MCP::Tool) do
50+
tool_name "test_tool"
51+
title "Tool Title"
52+
end
53+
54+
assert_equal("Tool Title", tool_class.display_name)
55+
end
56+
57+
def test_tool_display_name_with_name_only
58+
tool_class = Class.new(MCP::Tool) do
59+
tool_name "test_tool"
60+
end
61+
62+
assert_equal("test_tool", tool_class.display_name)
63+
end
64+
65+
def test_prompt_display_name_with_title
66+
prompt_class = Class.new(MCP::Prompt) do
67+
prompt_name "test_prompt"
68+
title "Prompt Title"
69+
end
70+
71+
assert_equal("Prompt Title", prompt_class.display_name)
72+
end
73+
74+
def test_prompt_display_name_without_title
75+
prompt_class = Class.new(MCP::Prompt) do
76+
prompt_name "test_prompt"
77+
end
78+
79+
assert_equal("test_prompt", prompt_class.display_name)
80+
end
81+
82+
def test_title_method_still_works_on_tool
83+
tool_class = Class.new(MCP::Tool) do
84+
tool_name "test_tool"
85+
title "My Tool"
86+
end
87+
88+
assert_equal("My Tool", tool_class.title)
89+
end
90+
91+
def test_title_method_still_works_on_prompt
92+
prompt_class = Class.new(MCP::Prompt) do
93+
prompt_name "test_prompt"
94+
title "My Prompt"
95+
end
96+
97+
assert_equal("My Prompt", prompt_class.title)
98+
end
99+
end

0 commit comments

Comments
 (0)