model-context-protocol-rb 0.3.4 → 0.5.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -1
  3. data/README.md +886 -196
  4. data/lib/model_context_protocol/server/cancellable.rb +54 -0
  5. data/lib/model_context_protocol/server/configuration.rb +80 -8
  6. data/lib/model_context_protocol/server/content.rb +321 -0
  7. data/lib/model_context_protocol/server/content_helpers.rb +84 -0
  8. data/lib/model_context_protocol/server/pagination.rb +71 -0
  9. data/lib/model_context_protocol/server/progressable.rb +72 -0
  10. data/lib/model_context_protocol/server/prompt.rb +108 -14
  11. data/lib/model_context_protocol/server/redis_client_proxy.rb +134 -0
  12. data/lib/model_context_protocol/server/redis_config.rb +108 -0
  13. data/lib/model_context_protocol/server/redis_pool_manager.rb +110 -0
  14. data/lib/model_context_protocol/server/registry.rb +94 -18
  15. data/lib/model_context_protocol/server/resource.rb +98 -25
  16. data/lib/model_context_protocol/server/resource_template.rb +26 -13
  17. data/lib/model_context_protocol/server/router.rb +36 -3
  18. data/lib/model_context_protocol/server/stdio_transport/request_store.rb +102 -0
  19. data/lib/model_context_protocol/server/stdio_transport.rb +31 -6
  20. data/lib/model_context_protocol/server/streamable_http_transport/event_counter.rb +35 -0
  21. data/lib/model_context_protocol/server/streamable_http_transport/message_poller.rb +101 -0
  22. data/lib/model_context_protocol/server/streamable_http_transport/notification_queue.rb +80 -0
  23. data/lib/model_context_protocol/server/streamable_http_transport/request_store.rb +224 -0
  24. data/lib/model_context_protocol/server/streamable_http_transport/session_message_queue.rb +120 -0
  25. data/lib/model_context_protocol/server/{session_store.rb → streamable_http_transport/session_store.rb} +30 -16
  26. data/lib/model_context_protocol/server/streamable_http_transport/stream_registry.rb +119 -0
  27. data/lib/model_context_protocol/server/streamable_http_transport.rb +352 -112
  28. data/lib/model_context_protocol/server/tool.rb +79 -53
  29. data/lib/model_context_protocol/server.rb +124 -21
  30. data/lib/model_context_protocol/version.rb +1 -1
  31. data/tasks/mcp.rake +28 -2
  32. data/tasks/templates/dev-http.erb +288 -0
  33. data/tasks/templates/dev.erb +7 -1
  34. metadata +61 -3
@@ -2,6 +2,13 @@ require "json-schema"
2
2
 
3
3
  module ModelContextProtocol
4
4
  class Server::Tool
5
+ # Raised when output schema validation fails.
6
+ class OutputSchemaValidationError < StandardError; end
7
+
8
+ include ModelContextProtocol::Server::Cancellable
9
+ include ModelContextProtocol::Server::ContentHelpers
10
+ include ModelContextProtocol::Server::Progressable
11
+
5
12
  attr_reader :arguments, :context, :logger
6
13
 
7
14
  def initialize(arguments, logger, context = {})
@@ -15,58 +22,58 @@ module ModelContextProtocol
15
22
  raise NotImplementedError, "Subclasses must implement the call method"
16
23
  end
17
24
 
18
- TextResponse = Data.define(:text) do
19
- def serialized
20
- {content: [{type: "text", text:}], isError: false}
21
- end
22
- end
23
- private_constant :TextResponse
24
-
25
- ImageResponse = Data.define(:data, :mime_type) do
26
- def initialize(data:, mime_type: "image/png")
27
- super
28
- end
29
-
25
+ Response = Data.define(:content) do
30
26
  def serialized
31
- {content: [{type: "image", data:, mimeType: mime_type}], isError: false}
27
+ serialized_contents = content.map(&:serialized)
28
+ {content: serialized_contents, isError: false}
32
29
  end
33
30
  end
34
- private_constant :ImageResponse
35
-
36
- ResourceResponse = Data.define(:uri, :text, :mime_type) do
37
- def initialize(uri:, text:, mime_type: "text/plain")
38
- super
39
- end
31
+ private_constant :Response
40
32
 
33
+ StructuredContentResponse = Data.define(:structured_content, :tool) do
41
34
  def serialized
42
- {content: [{type: "resource", resource: {uri:, mimeType: mime_type, text:}}], isError: false}
35
+ json_text = JSON.generate(structured_content)
36
+ text_content = ModelContextProtocol::Server::Content::Text[
37
+ meta: nil,
38
+ annotations: nil,
39
+ text: json_text
40
+ ]
41
+
42
+ validation_errors = JSON::Validator.fully_validate(
43
+ tool.class.definition[:outputSchema], structured_content
44
+ )
45
+
46
+ if validation_errors.empty?
47
+ {
48
+ structuredContent: structured_content,
49
+ content: [text_content.serialized],
50
+ isError: false
51
+ }
52
+ else
53
+ raise OutputSchemaValidationError, validation_errors.join(", ")
54
+ end
43
55
  end
44
56
  end
45
- private_constant :ResourceResponse
57
+ private_constant :StructuredContentResponse
46
58
 
47
- ToolErrorResponse = Data.define(:text) do
59
+ ErrorResponse = Data.define(:error) do
48
60
  def serialized
49
- {content: [{type: "text", text:}], isError: true}
61
+ {content: [{type: "text", text: error}], isError: true}
50
62
  end
51
63
  end
52
- private_constant :ToolErrorResponse
53
-
54
- private def respond_with(type, **options)
55
- case [type, options]
56
- in [:text, {text:}]
57
- TextResponse[text:]
58
- in [:image, {data:, mime_type:}]
59
- ImageResponse[data:, mime_type:]
60
- in [:image, {data:}]
61
- ImageResponse[data:]
62
- in [:resource, {mime_type:, text:, uri:}]
63
- ResourceResponse[mime_type:, text:, uri:]
64
- in [:resource, {text:, uri:}]
65
- ResourceResponse[text:, uri:]
66
- in [:error, {text:}]
67
- ToolErrorResponse[text:]
64
+ private_constant :ErrorResponse
65
+
66
+ private def respond_with(**kwargs)
67
+ case [kwargs]
68
+ in [{content:}]
69
+ content_array = content.is_a?(Array) ? content : [content]
70
+ Response[content: content_array]
71
+ in [{structured_content:}]
72
+ StructuredContentResponse[structured_content:, tool: self]
73
+ in [{error:}]
74
+ ErrorResponse[error:]
68
75
  else
69
- raise ModelContextProtocol::Server::ResponseArgumentsError, "Invalid arguments: #{type}, #{options}"
76
+ raise ModelContextProtocol::Server::ResponseArgumentsError, "Invalid arguments: #{kwargs.inspect}"
70
77
  end
71
78
  end
72
79
 
@@ -75,39 +82,48 @@ module ModelContextProtocol
75
82
  end
76
83
 
77
84
  class << self
78
- attr_reader :name, :description, :input_schema
85
+ attr_reader :name, :description, :title, :input_schema, :output_schema
79
86
 
80
- def with_metadata(&block)
81
- metadata_dsl = MetadataDSL.new
82
- metadata_dsl.instance_eval(&block)
87
+ def define(&block)
88
+ definition_dsl = DefinitionDSL.new
89
+ definition_dsl.instance_eval(&block)
83
90
 
84
- @name = metadata_dsl.name
85
- @description = metadata_dsl.description
86
- @input_schema = metadata_dsl.input_schema
91
+ @name = definition_dsl.name
92
+ @description = definition_dsl.description
93
+ @title = definition_dsl.title
94
+ @input_schema = definition_dsl.input_schema
95
+ @output_schema = definition_dsl.output_schema
87
96
  end
88
97
 
89
98
  def inherited(subclass)
90
99
  subclass.instance_variable_set(:@name, @name)
91
100
  subclass.instance_variable_set(:@description, @description)
101
+ subclass.instance_variable_set(:@title, @title)
92
102
  subclass.instance_variable_set(:@input_schema, @input_schema)
103
+ subclass.instance_variable_set(:@output_schema, @output_schema)
93
104
  end
94
105
 
95
106
  def call(arguments, logger, context = {})
96
107
  new(arguments, logger, context).call
97
108
  rescue JSON::Schema::ValidationError => validation_error
98
109
  raise ModelContextProtocol::Server::ParameterValidationError, validation_error.message
99
- rescue ModelContextProtocol::Server::ResponseArgumentsError => response_arguments_error
100
- raise response_arguments_error
110
+ rescue OutputSchemaValidationError, ModelContextProtocol::Server::ResponseArgumentsError => tool_error
111
+ raise tool_error, tool_error.message
112
+ rescue Server::Cancellable::CancellationError
113
+ raise
101
114
  rescue => error
102
- ToolErrorResponse[text: error.message]
115
+ ErrorResponse[error: error.message]
103
116
  end
104
117
 
105
- def metadata
106
- {name: @name, description: @description, inputSchema: @input_schema}
118
+ def definition
119
+ result = {name: @name, description: @description, inputSchema: @input_schema}
120
+ result[:title] = @title if @title
121
+ result[:outputSchema] = @output_schema if @output_schema
122
+ result
107
123
  end
108
124
  end
109
125
 
110
- class MetadataDSL
126
+ class DefinitionDSL
111
127
  def name(value = nil)
112
128
  @name = value if value
113
129
  @name
@@ -118,10 +134,20 @@ module ModelContextProtocol
118
134
  @description
119
135
  end
120
136
 
137
+ def title(value = nil)
138
+ @title = value if value
139
+ @title
140
+ end
141
+
121
142
  def input_schema(&block)
122
143
  @input_schema = instance_eval(&block) if block_given?
123
144
  @input_schema
124
145
  end
146
+
147
+ def output_schema(&block)
148
+ @output_schema = instance_eval(&block) if block_given?
149
+ @output_schema
150
+ end
125
151
  end
126
152
  end
127
153
  end
@@ -8,7 +8,7 @@ module ModelContextProtocol
8
8
  # Raised when invalid parameters are provided.
9
9
  class ParameterValidationError < StandardError; end
10
10
 
11
- attr_reader :configuration, :router
11
+ attr_reader :configuration, :router, :transport
12
12
 
13
13
  def initialize
14
14
  @configuration = Configuration.new
@@ -20,7 +20,7 @@ module ModelContextProtocol
20
20
  def start
21
21
  configuration.validate!
22
22
 
23
- transport = case configuration.transport_type
23
+ @transport = case configuration.transport_type
24
24
  when :stdio, nil
25
25
  StdioTransport.new(router: @router, configuration: @configuration)
26
26
  when :streamable_http
@@ -32,21 +32,26 @@ module ModelContextProtocol
32
32
  raise ArgumentError, "Unknown transport: #{configuration.transport_type}"
33
33
  end
34
34
 
35
- transport.handle
35
+ @transport.handle
36
36
  end
37
37
 
38
38
  private
39
39
 
40
- PROTOCOL_VERSION = "2025-06-18".freeze
41
- private_constant :PROTOCOL_VERSION
40
+ SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18"].freeze
41
+ private_constant :SUPPORTED_PROTOCOL_VERSIONS
42
42
 
43
- InitializeResponse = Data.define(:protocol_version, :capabilities, :server_info) do
43
+ LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS.first
44
+ private_constant :LATEST_PROTOCOL_VERSION
45
+
46
+ InitializeResponse = Data.define(:protocol_version, :capabilities, :server_info, :instructions) do
44
47
  def serialized
45
- {
48
+ response = {
46
49
  protocolVersion: protocol_version,
47
50
  capabilities: capabilities,
48
51
  serverInfo: server_info
49
52
  }
53
+ response[:instructions] = instructions if instructions
54
+ response
50
55
  end
51
56
  end
52
57
 
@@ -63,14 +68,26 @@ module ModelContextProtocol
63
68
  end
64
69
 
65
70
  def map_handlers
66
- router.map("initialize") do |_message|
71
+ router.map("initialize") do |message|
72
+ client_protocol_version = message["params"]&.dig("protocolVersion")
73
+
74
+ negotiated_version = if client_protocol_version && SUPPORTED_PROTOCOL_VERSIONS.include?(client_protocol_version)
75
+ client_protocol_version
76
+ else
77
+ LATEST_PROTOCOL_VERSION
78
+ end
79
+
80
+ server_info = {
81
+ name: configuration.name,
82
+ version: configuration.version
83
+ }
84
+ server_info[:title] = configuration.title if configuration.title
85
+
67
86
  InitializeResponse[
68
- protocol_version: PROTOCOL_VERSION,
87
+ protocol_version: negotiated_version,
69
88
  capabilities: build_capabilities,
70
- server_info: {
71
- name: configuration.name,
72
- version: configuration.version
73
- }
89
+ server_info: server_info,
90
+ instructions: configuration.instructions
74
91
  ]
75
92
  end
76
93
 
@@ -112,8 +129,28 @@ module ModelContextProtocol
112
129
  end
113
130
  end
114
131
 
115
- router.map("resources/list") do
116
- configuration.registry.resources_data
132
+ router.map("resources/list") do |message|
133
+ params = message["params"] || {}
134
+
135
+ if configuration.pagination_enabled? && Server::Pagination.pagination_requested?(params)
136
+ opts = configuration.pagination_options
137
+
138
+ pagination_params = Server::Pagination.extract_pagination_params(
139
+ params,
140
+ default_page_size: opts[:default_page_size],
141
+ max_page_size: opts[:max_page_size]
142
+ )
143
+
144
+ configuration.registry.resources_data(
145
+ cursor: pagination_params[:cursor],
146
+ page_size: pagination_params[:page_size],
147
+ cursor_ttl: opts[:cursor_ttl]
148
+ )
149
+ else
150
+ configuration.registry.resources_data
151
+ end
152
+ rescue Server::Pagination::InvalidCursorError => e
153
+ raise ParameterValidationError, e.message
117
154
  end
118
155
 
119
156
  router.map("resources/read") do |message|
@@ -123,15 +160,55 @@ module ModelContextProtocol
123
160
  raise ModelContextProtocol::Server::ParameterValidationError, "resource not found for #{uri}"
124
161
  end
125
162
 
126
- resource.call(configuration.logger, configuration.context)
163
+ resource.call
127
164
  end
128
165
 
129
166
  router.map("resources/templates/list") do |message|
130
- configuration.registry.resource_templates_data
167
+ params = message["params"] || {}
168
+
169
+ if configuration.pagination_enabled? && Server::Pagination.pagination_requested?(params)
170
+ opts = configuration.pagination_options
171
+
172
+ pagination_params = Server::Pagination.extract_pagination_params(
173
+ params,
174
+ default_page_size: opts[:default_page_size],
175
+ max_page_size: opts[:max_page_size]
176
+ )
177
+
178
+ configuration.registry.resource_templates_data(
179
+ cursor: pagination_params[:cursor],
180
+ page_size: pagination_params[:page_size],
181
+ cursor_ttl: opts[:cursor_ttl]
182
+ )
183
+ else
184
+ configuration.registry.resource_templates_data
185
+ end
186
+ rescue Server::Pagination::InvalidCursorError => e
187
+ raise ParameterValidationError, e.message
131
188
  end
132
189
 
133
- router.map("prompts/list") do
134
- configuration.registry.prompts_data
190
+ router.map("prompts/list") do |message|
191
+ params = message["params"] || {}
192
+
193
+ if configuration.pagination_enabled? && Server::Pagination.pagination_requested?(params)
194
+ opts = configuration.pagination_options
195
+
196
+ pagination_params = Server::Pagination.extract_pagination_params(
197
+ params,
198
+ default_page_size: opts[:default_page_size],
199
+ max_page_size: opts[:max_page_size]
200
+ )
201
+
202
+ configuration.registry.prompts_data(
203
+ cursor: pagination_params[:cursor],
204
+ page_size: pagination_params[:page_size],
205
+ cursor_ttl: opts[:cursor_ttl]
206
+ )
207
+ else
208
+ configuration.registry.prompts_data
209
+ end
210
+ rescue Server::Pagination::InvalidCursorError => e
211
+ raise ParameterValidationError, e.message
135
212
  end
136
213
 
137
214
  router.map("prompts/get") do |message|
@@ -143,8 +220,28 @@ module ModelContextProtocol
143
220
  .call(symbolized_arguments, configuration.logger, configuration.context)
144
221
  end
145
222
 
146
- router.map("tools/list") do
147
- configuration.registry.tools_data
223
+ router.map("tools/list") do |message|
224
+ params = message["params"] || {}
225
+
226
+ if configuration.pagination_enabled? && Server::Pagination.pagination_requested?(params)
227
+ opts = configuration.pagination_options
228
+
229
+ pagination_params = Server::Pagination.extract_pagination_params(
230
+ params,
231
+ default_page_size: opts[:default_page_size],
232
+ max_page_size: opts[:max_page_size]
233
+ )
234
+
235
+ configuration.registry.tools_data(
236
+ cursor: pagination_params[:cursor],
237
+ page_size: pagination_params[:page_size],
238
+ cursor_ttl: opts[:cursor_ttl]
239
+ )
240
+ else
241
+ configuration.registry.tools_data
242
+ end
243
+ rescue Server::Pagination::InvalidCursorError => e
244
+ raise ParameterValidationError, e.message
148
245
  end
149
246
 
150
247
  router.map("tools/call") do |message|
@@ -184,5 +281,11 @@ module ModelContextProtocol
184
281
  end
185
282
  end
186
283
  end
284
+
285
+ class << self
286
+ def configure_redis(&block)
287
+ RedisConfig.configure(&block)
288
+ end
289
+ end
187
290
  end
188
291
  end
@@ -1,3 +1,3 @@
1
1
  module ModelContextProtocol
2
- VERSION = "0.3.4"
2
+ VERSION = "0.5.0"
3
3
  end
data/tasks/mcp.rake CHANGED
@@ -1,8 +1,8 @@
1
1
  require "fileutils"
2
2
 
3
3
  namespace :mcp do
4
- desc "Generate the development server executable with the correct Ruby path"
5
- task :generate_executable do
4
+ desc "Generate the STDIO development server executable with the correct Ruby path"
5
+ task :generate_stdio_server do
6
6
  destination_path = "bin/dev"
7
7
  template_path = File.expand_path("templates/dev.erb", __dir__)
8
8
 
@@ -27,6 +27,32 @@ namespace :mcp do
27
27
  puts "Using Ruby path: #{ruby_path}"
28
28
  end
29
29
 
30
+ desc "Generate the streamable HTTP development server executable with the correct Ruby path"
31
+ task :generate_streamable_http_server do
32
+ destination_path = "bin/dev-http"
33
+ template_path = File.expand_path("templates/dev-http.erb", __dir__)
34
+
35
+ # Create directory if it doesn't exist
36
+ FileUtils.mkdir_p(File.dirname(destination_path))
37
+
38
+ # Get the Ruby path
39
+ ruby_path = detect_ruby_path
40
+
41
+ # Read and process the template
42
+ template = File.read(template_path)
43
+ content = template.gsub("<%= @ruby_path %>", ruby_path)
44
+
45
+ # Write the executable
46
+ File.write(destination_path, content)
47
+
48
+ # Set permissions
49
+ FileUtils.chmod(0o755, destination_path)
50
+
51
+ # Show success message
52
+ puts "\nCreated executable at: #{File.expand_path(destination_path)}"
53
+ puts "Using Ruby path: #{ruby_path}"
54
+ end
55
+
30
56
  def detect_ruby_path
31
57
  # Get Ruby version from project config
32
58
  ruby_version = get_project_ruby_version