model-context-protocol-rb 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,7 @@
1
1
  module ModelContextProtocol
2
2
  class Server::Prompt
3
+ include Server::ContentHelpers
4
+
3
5
  attr_reader :arguments, :context, :logger
4
6
 
5
7
  def initialize(arguments, logger, context = {})
@@ -13,15 +15,23 @@ module ModelContextProtocol
13
15
  raise NotImplementedError, "Subclasses must implement the call method"
14
16
  end
15
17
 
16
- Response = Data.define(:messages, :description) do
18
+ Response = Data.define(:messages, :description, :title) do
17
19
  def serialized
18
- {description:, messages:}
20
+ result = {description:, messages:}
21
+ result[:title] = title if title
22
+ result
19
23
  end
20
24
  end
21
25
  private_constant :Response
22
26
 
27
+ def message_history(&block)
28
+ builder = MessageHistoryBuilder.new(self)
29
+ builder.instance_eval(&block)
30
+ builder.messages
31
+ end
32
+
23
33
  private def respond_with(messages:)
24
- Response[messages:, description: self.class.description]
34
+ Response[messages:, description: self.class.description, title: self.class.title]
25
35
  end
26
36
 
27
37
  private def validate!(arguments = {})
@@ -43,16 +53,18 @@ module ModelContextProtocol
43
53
  end
44
54
 
45
55
  class << self
46
- attr_reader :name, :description, :defined_arguments
56
+ attr_reader :name, :description, :title, :defined_arguments
47
57
 
48
- def with_metadata(&block)
58
+ def define(&block)
49
59
  @defined_arguments ||= []
50
60
 
51
- metadata_dsl = MetadataDSL.new
52
- metadata_dsl.instance_eval(&block)
61
+ definition_dsl = DefinitionDSL.new
62
+ definition_dsl.instance_eval(&block)
53
63
 
54
- @name = metadata_dsl.name
55
- @description = metadata_dsl.description
64
+ @name = definition_dsl.name
65
+ @description = definition_dsl.description
66
+ @title = definition_dsl.title
67
+ @defined_arguments.concat(definition_dsl.arguments)
56
68
  end
57
69
 
58
70
  def with_argument(&block)
@@ -72,6 +84,7 @@ module ModelContextProtocol
72
84
  def inherited(subclass)
73
85
  subclass.instance_variable_set(:@name, @name)
74
86
  subclass.instance_variable_set(:@description, @description)
87
+ subclass.instance_variable_set(:@title, @title)
75
88
  subclass.instance_variable_set(:@defined_arguments, @defined_arguments&.dup)
76
89
  end
77
90
 
@@ -81,8 +94,10 @@ module ModelContextProtocol
81
94
  raise ModelContextProtocol::Server::ParameterValidationError, error.message
82
95
  end
83
96
 
84
- def metadata
85
- {name: @name, description: @description, arguments: @defined_arguments}
97
+ def definition
98
+ result = {name: @name, description: @description, arguments: @defined_arguments}
99
+ result[:title] = @title if @title
100
+ result
86
101
  end
87
102
 
88
103
  def complete_for(arg_name, value)
@@ -92,7 +107,52 @@ module ModelContextProtocol
92
107
  end
93
108
  end
94
109
 
95
- class MetadataDSL
110
+ class MessageHistoryBuilder
111
+ include Server::ContentHelpers
112
+
113
+ attr_reader :messages
114
+
115
+ def initialize(prompt_instance)
116
+ @messages = []
117
+ @prompt_instance = prompt_instance
118
+ end
119
+
120
+ def arguments
121
+ @prompt_instance.arguments
122
+ end
123
+
124
+ def context
125
+ @prompt_instance.context
126
+ end
127
+
128
+ def logger
129
+ @prompt_instance.logger
130
+ end
131
+
132
+ def user_message(&block)
133
+ content = instance_eval(&block).serialized
134
+ @messages << {
135
+ role: "user",
136
+ content: content
137
+ }
138
+ end
139
+
140
+ def assistant_message(&block)
141
+ content = instance_eval(&block).serialized
142
+ @messages << {
143
+ role: "assistant",
144
+ content: content
145
+ }
146
+ end
147
+ end
148
+
149
+ class DefinitionDSL
150
+ attr_reader :arguments
151
+
152
+ def initialize
153
+ @arguments = []
154
+ end
155
+
96
156
  def name(value = nil)
97
157
  @name = value if value
98
158
  @name
@@ -102,6 +162,23 @@ module ModelContextProtocol
102
162
  @description = value if value
103
163
  @description
104
164
  end
165
+
166
+ def title(value = nil)
167
+ @title = value if value
168
+ @title
169
+ end
170
+
171
+ def argument(&block)
172
+ argument_dsl = ArgumentDSL.new
173
+ argument_dsl.instance_eval(&block)
174
+
175
+ @arguments << {
176
+ name: argument_dsl.name,
177
+ description: argument_dsl.description,
178
+ required: argument_dsl.required,
179
+ completion: argument_dsl.completion
180
+ }
181
+ end
105
182
  end
106
183
 
107
184
  class ArgumentDSL
@@ -120,10 +197,25 @@ module ModelContextProtocol
120
197
  @required
121
198
  end
122
199
 
123
- def completion(klass = nil)
124
- @completion = klass unless klass.nil?
200
+ def completion(klass_or_values = nil)
201
+ unless klass_or_values.nil?
202
+ @completion = if klass_or_values.is_a?(Array)
203
+ create_array_completion(klass_or_values)
204
+ else
205
+ klass_or_values
206
+ end
207
+ end
125
208
  @completion
126
209
  end
210
+
211
+ private
212
+
213
+ def create_array_completion(values)
214
+ ModelContextProtocol::Server::Completion.define do
215
+ filtered_values = values.grep(/#{argument_value}/)
216
+ respond_with values: filtered_values
217
+ end
218
+ end
127
219
  end
128
220
  end
129
221
  end
@@ -39,8 +39,8 @@ module ModelContextProtocol
39
39
  end
40
40
 
41
41
  def register(klass)
42
- metadata = klass.metadata
43
- entry = {klass: klass}.merge(metadata)
42
+ definition = klass.definition
43
+ entry = {klass: klass}.merge(definition)
44
44
 
45
45
  case klass.ancestors
46
46
  when ->(ancestors) { ancestors.include?(ModelContextProtocol::Server::Prompt) }
@@ -74,45 +74,121 @@ module ModelContextProtocol
74
74
  find_by_name(@tools, name)
75
75
  end
76
76
 
77
- def prompts_data
78
- PromptsData[prompts: @prompts.map { |entry| entry.except(:klass) }]
77
+ def prompts_data(cursor: nil, page_size: nil, cursor_ttl: nil)
78
+ items = @prompts.map { |entry| entry.except(:klass) }
79
+
80
+ if cursor || page_size
81
+ paginated = Server::Pagination.paginate(
82
+ items,
83
+ cursor: cursor,
84
+ page_size: page_size || 100,
85
+ cursor_ttl: cursor_ttl
86
+ )
87
+
88
+ PromptsData[prompts: paginated.items, next_cursor: paginated.next_cursor]
89
+ else
90
+ PromptsData[prompts: items]
91
+ end
79
92
  end
80
93
 
81
- def resources_data
82
- ResourcesData[resources: @resources.map { |entry| entry.except(:klass) }]
94
+ def resources_data(cursor: nil, page_size: nil, cursor_ttl: nil)
95
+ items = @resources.map { |entry| entry.except(:klass) }
96
+
97
+ if cursor || page_size
98
+ paginated = Server::Pagination.paginate(
99
+ items,
100
+ cursor: cursor,
101
+ page_size: page_size || 100,
102
+ cursor_ttl: cursor_ttl
103
+ )
104
+
105
+ ResourcesData[resources: paginated.items, next_cursor: paginated.next_cursor]
106
+ else
107
+ ResourcesData[resources: items]
108
+ end
83
109
  end
84
110
 
85
- def resource_templates_data
86
- ResourceTemplatesData[resource_templates: @resource_templates.map { |entry| entry.except(:klass, :completions) }]
111
+ def resource_templates_data(cursor: nil, page_size: nil, cursor_ttl: nil)
112
+ items = @resource_templates.map { |entry| entry.except(:klass, :completions) }
113
+
114
+ if cursor || page_size
115
+ paginated = Server::Pagination.paginate(
116
+ items,
117
+ cursor: cursor,
118
+ page_size: page_size || 100,
119
+ cursor_ttl: cursor_ttl
120
+ )
121
+
122
+ ResourceTemplatesData[resource_templates: paginated.items, next_cursor: paginated.next_cursor]
123
+ else
124
+ ResourceTemplatesData[resource_templates: items]
125
+ end
87
126
  end
88
127
 
89
- def tools_data
90
- ToolsData[tools: @tools.map { |entry| entry.except(:klass) }]
128
+ def tools_data(cursor: nil, page_size: nil, cursor_ttl: nil)
129
+ items = @tools.map { |entry| entry.except(:klass) }
130
+
131
+ if cursor || page_size
132
+ paginated = Server::Pagination.paginate(
133
+ items,
134
+ cursor: cursor,
135
+ page_size: page_size || 100,
136
+ cursor_ttl: cursor_ttl
137
+ )
138
+
139
+ ToolsData[tools: paginated.items, next_cursor: paginated.next_cursor]
140
+ else
141
+ ToolsData[tools: items]
142
+ end
91
143
  end
92
144
 
93
145
  private
94
146
 
95
- PromptsData = Data.define(:prompts) do
147
+ PromptsData = Data.define(:prompts, :next_cursor) do
148
+ def initialize(prompts:, next_cursor: nil)
149
+ super
150
+ end
151
+
96
152
  def serialized
97
- {prompts:}
153
+ result = {prompts:}
154
+ result[:nextCursor] = next_cursor if next_cursor
155
+ result
98
156
  end
99
157
  end
100
158
 
101
- ResourcesData = Data.define(:resources) do
159
+ ResourcesData = Data.define(:resources, :next_cursor) do
160
+ def initialize(resources:, next_cursor: nil)
161
+ super
162
+ end
163
+
102
164
  def serialized
103
- {resources:}
165
+ result = {resources:}
166
+ result[:nextCursor] = next_cursor if next_cursor
167
+ result
104
168
  end
105
169
  end
106
170
 
107
- ResourceTemplatesData = Data.define(:resource_templates) do
171
+ ResourceTemplatesData = Data.define(:resource_templates, :next_cursor) do
172
+ def initialize(resource_templates:, next_cursor: nil)
173
+ super
174
+ end
175
+
108
176
  def serialized
109
- {resourceTemplates: resource_templates}
177
+ result = {resourceTemplates: resource_templates}
178
+ result[:nextCursor] = next_cursor if next_cursor
179
+ result
110
180
  end
111
181
  end
112
182
 
113
- ToolsData = Data.define(:tools) do
183
+ ToolsData = Data.define(:tools, :next_cursor) do
184
+ def initialize(tools:, next_cursor: nil)
185
+ super
186
+ end
187
+
114
188
  def serialized
115
- {tools:}
189
+ result = {tools:}
190
+ result[:nextCursor] = next_cursor if next_cursor
191
+ result
116
192
  end
117
193
  end
118
194
 
@@ -1,12 +1,10 @@
1
1
  module ModelContextProtocol
2
2
  class Server::Resource
3
- attr_reader :mime_type, :uri, :context, :logger
3
+ attr_reader :mime_type, :uri
4
4
 
5
- def initialize(logger, context = {})
5
+ def initialize
6
6
  @mime_type = self.class.mime_type
7
7
  @uri = self.class.uri
8
- @context = context
9
- @logger = logger
10
8
  end
11
9
 
12
10
  def call
@@ -15,59 +13,76 @@ module ModelContextProtocol
15
13
 
16
14
  TextResponse = Data.define(:resource, :text) do
17
15
  def serialized
18
- {contents: [{mimeType: resource.mime_type, text:, uri: resource.uri}]}
16
+ content = {mimeType: resource.mime_type, text:, uri: resource.uri}
17
+ content[:title] = resource.class.title if resource.class.title
18
+ annotations = resource.class.annotations&.serialized
19
+ content[:annotations] = annotations if annotations
20
+ {contents: [content]}
19
21
  end
20
22
  end
21
23
  private_constant :TextResponse
22
24
 
23
25
  BinaryResponse = Data.define(:blob, :resource) do
24
26
  def serialized
25
- {contents: [{blob:, mimeType: resource.mime_type, uri: resource.uri}]}
27
+ content = {blob:, mimeType: resource.mime_type, uri: resource.uri}
28
+ content[:title] = resource.class.title if resource.class.title
29
+ annotations = resource.class.annotations&.serialized
30
+ content[:annotations] = annotations if annotations
31
+ {contents: [content]}
26
32
  end
27
33
  end
28
34
  private_constant :BinaryResponse
29
35
 
30
- private def respond_with(type, **options)
31
- case [type, options]
32
- in [:text, {text:}]
36
+ private def respond_with(**kwargs)
37
+ case [kwargs]
38
+ in [{text:}]
33
39
  TextResponse[resource: self, text:]
34
- in [:binary, {blob:}]
35
- BinaryResponse[blob:, resource: self]
40
+ in [{binary:}]
41
+ BinaryResponse[blob: binary, resource: self]
36
42
  else
37
- raise ModelContextProtocol::Server::ResponseArgumentsError, "Invalid arguments: #{type}, #{options}"
43
+ raise ModelContextProtocol::Server::ResponseArgumentsError, "Invalid arguments: #{options}"
38
44
  end
39
45
  end
40
46
 
41
47
  class << self
42
- attr_reader :name, :description, :mime_type, :uri
48
+ attr_reader :name, :description, :title, :mime_type, :uri, :annotations
43
49
 
44
- def with_metadata(&block)
45
- metadata_dsl = MetadataDSL.new
46
- metadata_dsl.instance_eval(&block)
50
+ def define(&block)
51
+ definition_dsl = DefinitionDSL.new
52
+ definition_dsl.instance_eval(&block)
47
53
 
48
- @name = metadata_dsl.name
49
- @description = metadata_dsl.description
50
- @mime_type = metadata_dsl.mime_type
51
- @uri = metadata_dsl.uri
54
+ @name = definition_dsl.name
55
+ @description = definition_dsl.description
56
+ @title = definition_dsl.title
57
+ @mime_type = definition_dsl.mime_type
58
+ @uri = definition_dsl.uri
59
+ @annotations = definition_dsl.defined_annotations
52
60
  end
53
61
 
54
62
  def inherited(subclass)
55
63
  subclass.instance_variable_set(:@name, @name)
56
64
  subclass.instance_variable_set(:@description, @description)
65
+ subclass.instance_variable_set(:@title, @title)
57
66
  subclass.instance_variable_set(:@mime_type, @mime_type)
58
67
  subclass.instance_variable_set(:@uri, @uri)
68
+ subclass.instance_variable_set(:@annotations, @annotations&.dup)
59
69
  end
60
70
 
61
- def call(logger, context = {})
62
- new(logger, context).call
71
+ def call
72
+ new.call
63
73
  end
64
74
 
65
- def metadata
66
- {name: @name, description: @description, mimeType: @mime_type, uri: @uri}
75
+ def definition
76
+ result = {name: @name, description: @description, mimeType: @mime_type, uri: @uri}
77
+ result[:title] = @title if @title
78
+ result[:annotations] = @annotations.serialized if @annotations
79
+ result
67
80
  end
68
81
  end
69
82
 
70
- class MetadataDSL
83
+ class DefinitionDSL
84
+ attr_reader :defined_annotations
85
+
71
86
  def name(value = nil)
72
87
  @name = value if value
73
88
  @name
@@ -78,6 +93,11 @@ module ModelContextProtocol
78
93
  @description
79
94
  end
80
95
 
96
+ def title(value = nil)
97
+ @title = value if value
98
+ @title
99
+ end
100
+
81
101
  def mime_type(value = nil)
82
102
  @mime_type = value if value
83
103
  @mime_type
@@ -87,6 +107,56 @@ module ModelContextProtocol
87
107
  @uri = value if value
88
108
  @uri
89
109
  end
110
+
111
+ def annotations(&block)
112
+ @defined_annotations = AnnotationsDSL.new
113
+ @defined_annotations.instance_eval(&block)
114
+ @defined_annotations
115
+ end
116
+ end
117
+
118
+ class AnnotationsDSL
119
+ VALID_AUDIENCE_VALUES = [:user, :assistant].freeze
120
+
121
+ def initialize
122
+ @audience = nil
123
+ @priority = nil
124
+ @last_modified = nil
125
+ end
126
+
127
+ def audience(value)
128
+ normalized_value = Array(value).map(&:to_sym)
129
+ invalid_values = normalized_value - VALID_AUDIENCE_VALUES
130
+ unless invalid_values.empty?
131
+ raise ArgumentError, "Invalid audience values: #{invalid_values.join(", ")}. Valid values are: #{VALID_AUDIENCE_VALUES.join(", ")}"
132
+ end
133
+ @audience = normalized_value.map(&:to_s)
134
+ end
135
+
136
+ def priority(value)
137
+ unless value.is_a?(Numeric) && value >= 0.0 && value <= 1.0
138
+ raise ArgumentError, "Priority must be a number between 0.0 and 1.0, got: #{value}"
139
+ end
140
+ @priority = value.to_f
141
+ end
142
+
143
+ def last_modified(value)
144
+ # Validate ISO 8601 format
145
+ begin
146
+ Time.iso8601(value)
147
+ rescue ArgumentError
148
+ raise ArgumentError, "lastModified must be in ISO 8601 format (e.g., '2025-01-12T15:00:58Z'), got: #{value}"
149
+ end
150
+ @last_modified = value
151
+ end
152
+
153
+ def serialized
154
+ result = {}
155
+ result[:audience] = @audience if @audience
156
+ result[:priority] = @priority if @priority
157
+ result[:lastModified] = @last_modified if @last_modified
158
+ result.empty? ? nil : result
159
+ end
90
160
  end
91
161
  end
92
162
  end
@@ -3,15 +3,15 @@ module ModelContextProtocol
3
3
  class << self
4
4
  attr_reader :name, :description, :mime_type, :uri_template, :completions
5
5
 
6
- def with_metadata(&block)
7
- metadata_dsl = MetadataDSL.new
8
- metadata_dsl.instance_eval(&block)
9
-
10
- @name = metadata_dsl.name
11
- @description = metadata_dsl.description
12
- @mime_type = metadata_dsl.mime_type
13
- @uri_template = metadata_dsl.uri_template
14
- @completions = metadata_dsl.completions
6
+ def define(&block)
7
+ definition_dsl = DefinitionDSL.new
8
+ definition_dsl.instance_eval(&block)
9
+
10
+ @name = definition_dsl.name
11
+ @description = definition_dsl.description
12
+ @mime_type = definition_dsl.mime_type
13
+ @uri_template = definition_dsl.uri_template
14
+ @completions = definition_dsl.completions
15
15
  end
16
16
 
17
17
  def inherited(subclass)
@@ -32,7 +32,7 @@ module ModelContextProtocol
32
32
  completion.call(param_name.to_s, value)
33
33
  end
34
34
 
35
- def metadata
35
+ def definition
36
36
  {
37
37
  name: @name,
38
38
  description: @description,
@@ -43,7 +43,7 @@ module ModelContextProtocol
43
43
  end
44
44
  end
45
45
 
46
- class MetadataDSL
46
+ class DefinitionDSL
47
47
  attr_reader :completions
48
48
 
49
49
  def initialize
@@ -85,8 +85,21 @@ module ModelContextProtocol
85
85
  @completions = {}
86
86
  end
87
87
 
88
- def completion(param_name, completion_class)
89
- @completions[param_name.to_s] = completion_class
88
+ def completion(param_name, completion_class_or_values)
89
+ @completions[param_name.to_s] = if completion_class_or_values.is_a?(Array)
90
+ create_array_completion(completion_class_or_values)
91
+ else
92
+ completion_class_or_values
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def create_array_completion(values)
99
+ ModelContextProtocol::Server::Completion.define do
100
+ filtered_values = values.grep(/#{argument_value}/)
101
+ respond_with values: filtered_values
102
+ end
90
103
  end
91
104
  end
92
105
  end