model-context-protocol-rb 0.6.0 → 0.7.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -2
  3. data/README.md +174 -978
  4. data/lib/model_context_protocol/rspec/helpers.rb +54 -0
  5. data/lib/model_context_protocol/rspec/matchers/be_mcp_error_response.rb +123 -0
  6. data/lib/model_context_protocol/rspec/matchers/be_valid_mcp_class.rb +103 -0
  7. data/lib/model_context_protocol/rspec/matchers/be_valid_mcp_prompt_response.rb +126 -0
  8. data/lib/model_context_protocol/rspec/matchers/be_valid_mcp_resource_response.rb +121 -0
  9. data/lib/model_context_protocol/rspec/matchers/be_valid_mcp_tool_response.rb +135 -0
  10. data/lib/model_context_protocol/rspec/matchers/have_audio_content.rb +109 -0
  11. data/lib/model_context_protocol/rspec/matchers/have_embedded_resource_content.rb +150 -0
  12. data/lib/model_context_protocol/rspec/matchers/have_image_content.rb +109 -0
  13. data/lib/model_context_protocol/rspec/matchers/have_message_count.rb +87 -0
  14. data/lib/model_context_protocol/rspec/matchers/have_message_with_role.rb +152 -0
  15. data/lib/model_context_protocol/rspec/matchers/have_resource_annotations.rb +135 -0
  16. data/lib/model_context_protocol/rspec/matchers/have_resource_blob.rb +108 -0
  17. data/lib/model_context_protocol/rspec/matchers/have_resource_link_content.rb +138 -0
  18. data/lib/model_context_protocol/rspec/matchers/have_resource_mime_type.rb +103 -0
  19. data/lib/model_context_protocol/rspec/matchers/have_resource_text.rb +112 -0
  20. data/lib/model_context_protocol/rspec/matchers/have_structured_content.rb +88 -0
  21. data/lib/model_context_protocol/rspec/matchers/have_text_content.rb +113 -0
  22. data/lib/model_context_protocol/rspec/matchers.rb +31 -0
  23. data/lib/model_context_protocol/rspec.rb +23 -0
  24. data/lib/model_context_protocol/server/client_logger.rb +1 -1
  25. data/lib/model_context_protocol/server/configuration.rb +195 -91
  26. data/lib/model_context_protocol/server/content_helpers.rb +1 -1
  27. data/lib/model_context_protocol/server/prompt.rb +0 -14
  28. data/lib/model_context_protocol/server/redis_client_proxy.rb +2 -14
  29. data/lib/model_context_protocol/server/redis_config.rb +5 -7
  30. data/lib/model_context_protocol/server/redis_pool_manager.rb +10 -13
  31. data/lib/model_context_protocol/server/registry.rb +8 -0
  32. data/lib/model_context_protocol/server/router.rb +279 -4
  33. data/lib/model_context_protocol/server/server_logger.rb +5 -2
  34. data/lib/model_context_protocol/server/stdio_configuration.rb +114 -0
  35. data/lib/model_context_protocol/server/stdio_transport/request_store.rb +0 -41
  36. data/lib/model_context_protocol/server/streamable_http_configuration.rb +218 -0
  37. data/lib/model_context_protocol/server/streamable_http_transport/event_counter.rb +0 -13
  38. data/lib/model_context_protocol/server/streamable_http_transport/notification_queue.rb +0 -41
  39. data/lib/model_context_protocol/server/streamable_http_transport/request_store.rb +0 -103
  40. data/lib/model_context_protocol/server/streamable_http_transport/server_request_store.rb +0 -64
  41. data/lib/model_context_protocol/server/streamable_http_transport/session_message_queue.rb +0 -58
  42. data/lib/model_context_protocol/server/streamable_http_transport/session_store.rb +17 -31
  43. data/lib/model_context_protocol/server/streamable_http_transport/stream_registry.rb +0 -34
  44. data/lib/model_context_protocol/server/streamable_http_transport.rb +192 -56
  45. data/lib/model_context_protocol/server/tool.rb +67 -1
  46. data/lib/model_context_protocol/server.rb +203 -262
  47. data/lib/model_context_protocol/version.rb +1 -1
  48. data/lib/model_context_protocol.rb +4 -1
  49. data/lib/puma/plugin/mcp.rb +39 -0
  50. data/tasks/mcp.rake +26 -0
  51. data/tasks/templates/dev-http-puma.erb +251 -0
  52. data/tasks/templates/dev-http.erb +166 -184
  53. data/tasks/templates/dev.erb +29 -7
  54. metadata +26 -2
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModelContextProtocol
4
+ module RSpec
5
+ module Matchers
6
+ # Matcher that validates a tool response contains specific text content.
7
+ #
8
+ # @example With exact string match
9
+ # expect(response).to have_text_content("Hello, World!")
10
+ #
11
+ # @example With regex match
12
+ # expect(response).to have_text_content(/doubled is \d+/)
13
+ #
14
+ def have_text_content(expected_text)
15
+ HaveTextContent.new(expected_text)
16
+ end
17
+
18
+ class HaveTextContent
19
+ def initialize(expected_text)
20
+ @expected_text = expected_text
21
+ @failure_reasons = []
22
+ end
23
+
24
+ def matches?(actual)
25
+ @actual = actual
26
+ @failure_reasons = []
27
+ @serialized = serialize_response(actual)
28
+
29
+ return false if @serialized.nil?
30
+
31
+ validate_has_content &&
32
+ validate_has_text_content
33
+ end
34
+
35
+ def failure_message
36
+ "expected response to have text content matching #{@expected_text.inspect}, but:\n" +
37
+ @failure_reasons.map { |reason| " - #{reason}" }.join("\n")
38
+ end
39
+
40
+ def failure_message_when_negated
41
+ "expected response not to have text content matching #{@expected_text.inspect}, but it did"
42
+ end
43
+
44
+ def description
45
+ "have text content matching #{@expected_text.inspect}"
46
+ end
47
+
48
+ private
49
+
50
+ def serialize_response(response)
51
+ if response.respond_to?(:serialized)
52
+ response.serialized
53
+ elsif response.is_a?(Hash)
54
+ response
55
+ else
56
+ @failure_reasons << "response must respond to :serialized or be a Hash"
57
+ nil
58
+ end
59
+ end
60
+
61
+ def validate_has_content
62
+ @content = @serialized[:content] || @serialized["content"]
63
+
64
+ unless @content
65
+ @failure_reasons << "response does not have :content key"
66
+ return false
67
+ end
68
+
69
+ unless @content.is_a?(Array)
70
+ @failure_reasons << "content must be an Array"
71
+ return false
72
+ end
73
+
74
+ true
75
+ end
76
+
77
+ def validate_has_text_content
78
+ text_items = @content.select do |item|
79
+ type = item[:type] || item["type"]
80
+ type == "text"
81
+ end
82
+
83
+ if text_items.empty?
84
+ @failure_reasons << "no text content found in response"
85
+ return false
86
+ end
87
+
88
+ matching_item = text_items.find do |item|
89
+ text = item[:text] || item["text"]
90
+ text_matches?(text)
91
+ end
92
+
93
+ unless matching_item
94
+ actual_texts = text_items.map { |item| item[:text] || item["text"] }
95
+ @failure_reasons << "no text content matched, found: #{actual_texts.inspect}"
96
+ return false
97
+ end
98
+
99
+ true
100
+ end
101
+
102
+ def text_matches?(text)
103
+ case @expected_text
104
+ when Regexp
105
+ @expected_text.match?(text)
106
+ else
107
+ text.include?(@expected_text.to_s)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,31 @@
1
+ require_relative "matchers/be_valid_mcp_class"
2
+
3
+ # Tool response matchers
4
+ require_relative "matchers/be_valid_mcp_tool_response"
5
+ require_relative "matchers/have_structured_content"
6
+ require_relative "matchers/have_text_content"
7
+ require_relative "matchers/have_image_content"
8
+ require_relative "matchers/have_audio_content"
9
+ require_relative "matchers/have_embedded_resource_content"
10
+ require_relative "matchers/have_resource_link_content"
11
+ require_relative "matchers/be_mcp_error_response"
12
+
13
+ # Prompt response matchers
14
+ require_relative "matchers/be_valid_mcp_prompt_response"
15
+ require_relative "matchers/have_message_with_role"
16
+ require_relative "matchers/have_message_count"
17
+
18
+ # Resource response matchers
19
+ require_relative "matchers/be_valid_mcp_resource_response"
20
+ require_relative "matchers/have_resource_text"
21
+ require_relative "matchers/have_resource_blob"
22
+ require_relative "matchers/have_resource_mime_type"
23
+ require_relative "matchers/have_resource_annotations"
24
+
25
+ module ModelContextProtocol
26
+ module RSpec
27
+ module Matchers
28
+ # Include all matchers when this module is included
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ require "model_context_protocol"
2
+ require_relative "rspec/matchers"
3
+ require_relative "rspec/helpers"
4
+
5
+ module ModelContextProtocol
6
+ module RSpec
7
+ # Convenience method to configure RSpec with MCP matchers and helpers.
8
+ #
9
+ # @example
10
+ # # In spec_helper.rb
11
+ # require "model_context_protocol/rspec"
12
+ # ModelContextProtocol::RSpec.configure!
13
+ #
14
+ # @return [void]
15
+ def self.configure!
16
+ ::RSpec.configure do |config|
17
+ config.include ModelContextProtocol::RSpec::Matchers
18
+ config.include ModelContextProtocol::RSpec::Helpers, type: :mcp
19
+ config.include ModelContextProtocol::RSpec::Helpers, file_path: %r{spec/mcp/}
20
+ end
21
+ end
22
+ end
23
+ end
@@ -30,7 +30,7 @@ module ModelContextProtocol
30
30
  Logger::UNKNOWN => "emergency"
31
31
  }.freeze
32
32
 
33
- attr_accessor :transport
33
+ attr_reader :transport
34
34
  attr_reader :logger_name
35
35
 
36
36
  def initialize(logger_name: "server", level: "info")
@@ -1,65 +1,149 @@
1
1
  module ModelContextProtocol
2
+ # Base settings container for MCP servers with two concrete subclasses:
3
+ # - {StdioConfiguration} for standalone scripts using stdin/stdout
4
+ # - {StreamableHttpConfiguration} for Rack applications using streamable HTTP with Redis
5
+ #
6
+ # Server.rb factory methods (with_stdio_transport, with_streamable_http_transport)
7
+ # instantiate the appropriate subclass, yield it to a block for population, validate
8
+ # it, then pass it to Router.new. Router reads pagination settings via pagination_options
9
+ # and queries transport capabilities via supports_list_changed? and apply_environment_variables?.
10
+ #
11
+ # The base class provides shared attributes (name, version, registry, pagination) and
12
+ # validation logic, while subclasses override transport_type and validate_transport! to
13
+ # enforce transport-specific constraints.
2
14
  class Server::Configuration
3
- # Raised when configured with invalid name.
15
+ # Signals that the server's identifying name is missing or not a String.
16
+ # Raised by validate! when name is nil or non-String.
4
17
  class InvalidServerNameError < StandardError; end
5
18
 
6
- # Raised when configured with invalid version.
19
+ # Signals that the server's version string is missing or not a String.
20
+ # Raised by validate! when version is nil or non-String.
7
21
  class InvalidServerVersionError < StandardError; end
8
22
 
9
- # Raised when configured with invalid title.
23
+ # Signals that the optional UI title is non-nil but not a String.
24
+ # Raised by validate_title! when title is set to a non-String value.
10
25
  class InvalidServerTitleError < StandardError; end
11
26
 
12
- # Raised when configured with invalid instructions.
27
+ # Signals that the optional LLM instructions are non-nil but not a String.
28
+ # Raised by validate_instructions! when instructions is set to a non-String value.
13
29
  class InvalidServerInstructionsError < StandardError; end
14
30
 
15
- # Raised when configured with invalid registry.
31
+ # Signals that the registry is missing or not a Registry instance.
32
+ # Raised by validate! when registry is nil or not a ModelContextProtocol::Server::Registry.
16
33
  class InvalidRegistryError < StandardError; end
17
34
 
18
- # Raised when a required environment variable is not set
35
+ # Signals that a required environment variable is absent (stdio transport only).
36
+ # Raised by StdioConfiguration#validate_environment_variables! when a variable
37
+ # declared via require_environment_variable is not set.
19
38
  class MissingRequiredEnvironmentVariable < StandardError; end
20
39
 
21
- # Raised when transport configuration is invalid
40
+ # Signals transport-specific validation failure (Redis missing for HTTP, stdout conflict for stdio).
41
+ # Raised by subclass implementations of validate_transport! when prerequisites are unmet.
22
42
  class InvalidTransportError < StandardError; end
23
43
 
24
- # Raised when pagination configuration is invalid
44
+ # Signals that pagination settings are invalid (negative sizes, out-of-range defaults).
45
+ # Raised by validate_pagination! when default_page_size exceeds max_page_size or values are non-positive.
25
46
  class InvalidPaginationError < StandardError; end
26
47
 
27
- attr_accessor :name, :registry, :version, :transport, :pagination, :title, :instructions
28
- attr_reader :client_logger, :server_logger
48
+ # @!attribute [rw] name
49
+ # @return [String] the server's identifying name (sent in initialize response serverInfo.name)
50
+ attr_accessor :name
51
+
52
+ # @!attribute [rw] version
53
+ # @return [String] the server's version string (sent in initialize response serverInfo.version)
54
+ attr_accessor :version
55
+
56
+ # @!attribute [rw] pagination
57
+ # @return [Hash, false, nil] pagination settings or false to disable;
58
+ # Router calls pagination_options to extract default_page_size, max_page_size, and cursor_ttl
59
+ # when handling list requests (resources/list, prompts/list, tools/list).
60
+ # Accepts Hash with :enabled, :default_page_size, :max_page_size, :cursor_ttl keys, or false to disable.
61
+ attr_accessor :pagination
62
+
63
+ # @!attribute [rw] title
64
+ # @return [String, nil] optional human-readable display title for Claude Desktop UI
65
+ # (sent in initialize response serverInfo.title if present)
66
+ attr_accessor :title
67
+
68
+ # @!attribute [rw] instructions
69
+ # @return [String, nil] optional guidance for LLMs on how to use the server
70
+ # (sent in initialize response instructions field if present)
71
+ attr_accessor :instructions
72
+
73
+ # @!attribute [r] client_logger
74
+ # @return [ClientLogger] logger for sending notifications/message to MCP clients via JSON-RPC;
75
+ # Router passes this to prompts, resources, and tools so they can log to the client
76
+ attr_reader :client_logger
77
+
78
+ # Lazily-built Ruby Logger for server-side diagnostics (not sent to clients).
79
+ # Reads from GlobalConfig::ServerLogging on first access so that
80
+ # Server.configure_server_logging can be called before or after the factory method.
81
+ #
82
+ # @return [ServerLogger] writes to stderr by default, or configured destination via configure_server_logging
83
+ def server_logger
84
+ @server_logger ||= begin
85
+ params = if ModelContextProtocol::Server::GlobalConfig::ServerLogging.configured?
86
+ ModelContextProtocol::Server::GlobalConfig::ServerLogging.logger_params
87
+ else
88
+ {}
89
+ end
90
+ ModelContextProtocol::Server::ServerLogger.new(**params)
91
+ end
92
+ end
29
93
 
94
+ # Initialize shared attributes and loggers for any configuration subclass.
95
+ # ClientLogger queues messages until a transport connects; ServerLogger is built
96
+ # lazily on first access via #server_logger.
30
97
  def initialize
31
98
  @client_logger = ModelContextProtocol::Server::ClientLogger.new(
32
99
  logger_name: "server",
33
100
  level: "info"
34
101
  )
35
-
36
- server_logger_params = if ModelContextProtocol::Server::GlobalConfig::ServerLogging.configured?
37
- ModelContextProtocol::Server::GlobalConfig::ServerLogging.logger_params
38
- else
39
- {}
40
- end
41
-
42
- @server_logger = ModelContextProtocol::Server::ServerLogger.new(**server_logger_params)
43
102
  end
44
103
 
45
- def transport_type
46
- case transport
47
- when Hash
48
- transport[:type] || transport["type"]
49
- when Symbol, String
50
- transport.to_sym
51
- end
104
+ # Create and store a Registry from a block defining prompts, resources, and tools.
105
+ # Router queries the resulting registry to handle resources/list, tools/call, etc.
106
+ #
107
+ # @yieldparam (see Registry.new)
108
+ # @return [ModelContextProtocol::Server::Registry] the created registry
109
+ # @example
110
+ # config.registry do
111
+ # tools { register MyTool }
112
+ # end
113
+ def registry(&block)
114
+ return @registry unless block
115
+
116
+ @registry = ModelContextProtocol::Server::Registry.new(&block)
52
117
  end
53
118
 
54
- def transport_options
55
- case transport
56
- when Hash
57
- transport.except(:type, "type").transform_keys(&:to_sym)
58
- else
59
- {}
60
- end
61
- end
119
+ # Identify the transport layer for this configuration.
120
+ # Subclasses return :stdio or :streamable_http; Server.start uses this to
121
+ # instantiate the correct Transport class (StdioTransport or StreamableHttpTransport).
122
+ #
123
+ # @return [Symbol, nil] nil in the base class (never instantiated directly)
124
+ def transport_type = nil
125
+
126
+ # Determine whether the transport supports notifications/resources/list_changed
127
+ # and notifications/tools/list_changed. Router queries this when building the
128
+ # initialize response capabilities hash (adding listChanged: true to prompts/resources/tools).
129
+ # Only HTTP transport returns true (stdio can't push unsolicited notifications).
130
+ #
131
+ # @return [Boolean] false in base class and StdioConfiguration, true in StreamableHttpConfiguration
132
+ def supports_list_changed? = false
62
133
 
134
+ # Determine whether Router should modify ENV before executing handlers.
135
+ # StdioConfiguration returns true because stdin/stdout scripts run single-threaded
136
+ # and ENV mutation is safe. StreamableHttpConfiguration returns false because
137
+ # ENV is global and modifying it in a multi-threaded Rack server creates race conditions.
138
+ #
139
+ # @return [Boolean] false in base class, overridden by subclasses
140
+ def apply_environment_variables? = false
141
+
142
+ # Check whether pagination is active for list responses (resources/list, prompts/list, tools/list).
143
+ # Router calls this before extracting pagination params; if false, it returns unpaginated results.
144
+ # Enabled by default (nil or true), or when pagination Hash has enabled != false.
145
+ #
146
+ # @return [Boolean] true unless pagination is explicitly set to false
63
147
  def pagination_enabled?
64
148
  return true if pagination.nil?
65
149
 
@@ -73,6 +157,12 @@ module ModelContextProtocol
73
157
  end
74
158
  end
75
159
 
160
+ # Extract normalized pagination settings for Router to pass to Pagination.extract_pagination_params.
161
+ # Router uses default_page_size and max_page_size to validate cursor and page size params from the client,
162
+ # and cursor_ttl to configure how long the Pagination module stores cursor state in memory.
163
+ #
164
+ # @return [Hash] pagination parameters with keys :enabled, :default_page_size, :max_page_size, :cursor_ttl;
165
+ # defaults are 100, 1000, and 3600 (1 hour) respectively
76
166
  def pagination_options
77
167
  case pagination
78
168
  when Hash
@@ -94,6 +184,19 @@ module ModelContextProtocol
94
184
  end
95
185
  end
96
186
 
187
+ # Verify all required attributes and transport-specific constraints.
188
+ # Called by Server.build_server (the factory method's internal logic) after yielding
189
+ # the configuration block but before constructing the Router. Ensures the configuration
190
+ # is complete and internally consistent.
191
+ #
192
+ # @raise [InvalidServerNameError] if name is nil or non-String
193
+ # @raise [InvalidRegistryError] if registry is nil or not a Registry instance
194
+ # @raise [InvalidServerVersionError] if version is nil or non-String
195
+ # @raise [InvalidTransportError] if transport prerequisites fail (subclass-specific)
196
+ # @raise [InvalidPaginationError] if page sizes are negative or out of range
197
+ # @raise [InvalidServerTitleError] if title is non-nil but not a String
198
+ # @raise [InvalidServerInstructionsError] if instructions is non-nil but not a String
199
+ # @return [void]
97
200
  def validate!
98
201
  raise InvalidServerNameError unless valid_name?
99
202
  raise InvalidRegistryError unless valid_registry?
@@ -103,93 +206,84 @@ module ModelContextProtocol
103
206
  validate_pagination!
104
207
  validate_title!
105
208
  validate_instructions!
106
- validate_environment_variables!
107
- validate_server_logging_transport_constraints!
108
- end
109
-
110
- def environment_variables
111
- @environment_variables ||= {}
112
209
  end
113
210
 
114
- def environment_variable(key)
115
- environment_variables[key.to_s.upcase] || ENV[key.to_s.upcase] || nil
116
- end
117
-
118
- def require_environment_variable(key)
119
- required_environment_variables << key.to_s.upcase
120
- end
121
-
122
- # Programatically set an environment variable - useful if an alternative
123
- # to environment variables is used for security purposes. Despite being
124
- # more like 'configuration variables', these are called environment variables
125
- # to align with the Model Context Protocol terminology.
126
- #
127
- # see: https://modelcontextprotocol.io/docs/tools/debugging#environment-variables
211
+ # Access server-wide key-value storage merged with per-request session_context by Router.
212
+ # Router.effective_context merges this with Thread.current[:mcp_context][:session_context],
213
+ # then passes the result to prompts, resources, and tools so they can access both
214
+ # server-level (shared across all requests) and session-level (specific to HTTP session) data.
128
215
  #
129
- # @param key [String] The key to set the environment variable for
130
- # @param value [String] The value to set the environment variable to
131
- def set_environment_variable(key, value)
132
- environment_variables[key.to_s.upcase] = value
133
- end
134
-
216
+ # @return [Hash] the lazily-initialized context hash (defaults to {})
135
217
  def context
136
218
  @context ||= {}
137
219
  end
138
220
 
221
+ # Replace the server-wide context with a new hash.
222
+ # Router reads this via effective_context, merging it with session-level data before
223
+ # passing to handler implementations.
224
+ #
225
+ # @param context_hash [Hash] the new context to store (defaults to {})
226
+ # @return [Hash] the stored context
139
227
  def context=(context_hash = {})
140
228
  @context = context_hash
141
229
  end
142
230
 
143
231
  private
144
232
 
145
- def required_environment_variables
146
- @required_environment_variables ||= []
147
- end
148
-
149
- def validate_environment_variables!
150
- required_environment_variables.each do |key|
151
- raise MissingRequiredEnvironmentVariable, "#{key} is not set" unless environment_variable(key)
152
- end
233
+ # Template method for subclass-specific transport validation.
234
+ # StdioConfiguration checks that required environment variables are set and that
235
+ # server_logger isn't writing to stdout (which would corrupt the stdio protocol).
236
+ # StreamableHttpConfiguration validates that redis_url is a valid Redis URL.
237
+ #
238
+ # @raise [InvalidTransportError] when transport prerequisites are unmet
239
+ # @raise [MissingRequiredEnvironmentVariable] when stdio transport requires an unset variable
240
+ # @return [void]
241
+ def validate_transport!
242
+ # Template method — subclasses override to add transport-specific validations
153
243
  end
154
244
 
155
- def validate_server_logging_transport_constraints!
156
- return unless transport_type == :stdio && server_logger.logdev == $stdout
157
-
158
- raise ModelContextProtocol::Server::ServerLogger::StdoutNotAllowedError,
159
- "StdioTransport cannot log to stdout. Use stderr or a file instead."
245
+ # Template method for subclass-specific transport setup (side effects).
246
+ # Called by Server.build_server after validate! passes. Separated from validate_transport!
247
+ # because validation should be pure (no side effects), while setup performs actions like
248
+ # creating connection pools.
249
+ # StreamableHttpConfiguration overrides this to configure the Redis connection pool.
250
+ #
251
+ # @return [void]
252
+ def setup_transport!
253
+ # Template method — subclasses override to perform transport-specific setup
160
254
  end
161
255
 
256
+ # Check that name attribute is a non-nil String.
257
+ # Called by validate! to ensure the initialize response can be constructed.
258
+ #
259
+ # @return [Boolean] true if name is a String
162
260
  def valid_name?
163
261
  name&.is_a?(String)
164
262
  end
165
263
 
264
+ # Check that registry attribute is a Registry instance.
265
+ # Called by validate! to ensure Router can query tools, prompts, and resources.
266
+ #
267
+ # @return [Boolean] true if registry is a ModelContextProtocol::Server::Registry
166
268
  def valid_registry?
167
269
  registry&.is_a?(ModelContextProtocol::Server::Registry)
168
270
  end
169
271
 
272
+ # Check that version attribute is a non-nil String.
273
+ # Called by validate! to ensure the initialize response can be constructed.
274
+ #
275
+ # @return [Boolean] true if version is a String
170
276
  def valid_version?
171
277
  version&.is_a?(String)
172
278
  end
173
279
 
174
- def validate_transport!
175
- case transport_type
176
- when :streamable_http
177
- validate_streamable_http_transport!
178
- when :stdio, nil
179
- # stdio transport has no required options
180
- else
181
- raise InvalidTransportError, "Unknown transport type: #{transport_type}" if transport_type
182
- end
183
- end
184
-
185
- def validate_streamable_http_transport!
186
- unless ModelContextProtocol::Server::RedisConfig.configured?
187
- raise InvalidTransportError,
188
- "streamable_http transport requires Redis configuration. " \
189
- "Call ModelContextProtocol::Server.configure_redis in an initializer."
190
- end
191
- end
192
-
280
+ # Verify pagination settings are internally consistent (page sizes positive, defaults in range).
281
+ # Called by validate! only when pagination_enabled? is true; skipped if pagination is false.
282
+ # Ensures Router won't pass invalid params to Pagination.extract_pagination_params.
283
+ #
284
+ # @raise [InvalidPaginationError] if max_page_size <= 0, default_page_size <= 0,
285
+ # default_page_size > max_page_size, or cursor_ttl < 0
286
+ # @return [void]
193
287
  def validate_pagination!
194
288
  return unless pagination_enabled?
195
289
 
@@ -208,6 +302,11 @@ module ModelContextProtocol
208
302
  end
209
303
  end
210
304
 
305
+ # Check that the optional title attribute is nil or a String.
306
+ # Called by validate! to ensure the initialize response serverInfo.title is valid.
307
+ #
308
+ # @raise [InvalidServerTitleError] if title is non-nil and not a String
309
+ # @return [void]
211
310
  def validate_title!
212
311
  return if title.nil?
213
312
  return if title.is_a?(String)
@@ -215,6 +314,11 @@ module ModelContextProtocol
215
314
  raise InvalidServerTitleError, "Server title must be a string"
216
315
  end
217
316
 
317
+ # Check that the optional instructions attribute is nil or a String.
318
+ # Called by validate! to ensure the initialize response instructions field is valid.
319
+ #
320
+ # @raise [InvalidServerInstructionsError] if instructions is non-nil and not a String
321
+ # @return [void]
218
322
  def validate_instructions!
219
323
  return if instructions.nil?
220
324
  return if instructions.is_a?(String)
@@ -78,7 +78,7 @@ module ModelContextProtocol
78
78
  size:,
79
79
  title:,
80
80
  uri:
81
- ].serialized
81
+ ]
82
82
  end
83
83
  end
84
84
  end
@@ -70,20 +70,6 @@ module ModelContextProtocol
70
70
  @defined_arguments.concat(definition_dsl.arguments)
71
71
  end
72
72
 
73
- def with_argument(&block)
74
- @defined_arguments ||= []
75
-
76
- argument_dsl = ArgumentDSL.new
77
- argument_dsl.instance_eval(&block)
78
-
79
- @defined_arguments << {
80
- name: argument_dsl.name,
81
- description: argument_dsl.description,
82
- required: argument_dsl.required,
83
- completion: argument_dsl.completion
84
- }
85
- end
86
-
87
73
  def inherited(subclass)
88
74
  subclass.instance_variable_set(:@name, @name)
89
75
  subclass.instance_variable_set(:@description, @description)
@@ -39,8 +39,8 @@ module ModelContextProtocol
39
39
  with_connection { |redis| redis.hset(key, *args) }
40
40
  end
41
41
 
42
- def hgetall(key)
43
- with_connection { |redis| redis.hgetall(key) }
42
+ def hmget(key, *fields)
43
+ with_connection { |redis| redis.hmget(key, *fields) }
44
44
  end
45
45
 
46
46
  def lpush(key, *values)
@@ -67,10 +67,6 @@ module ModelContextProtocol
67
67
  with_connection { |redis| redis.incr(key) }
68
68
  end
69
69
 
70
- def decr(key)
71
- with_connection { |redis| redis.decr(key) }
72
- end
73
-
74
70
  def keys(pattern)
75
71
  with_connection { |redis| redis.keys(pattern) }
76
72
  end
@@ -101,14 +97,6 @@ module ModelContextProtocol
101
97
  with_connection { |redis| redis.eval(script, keys: keys, argv: argv) }
102
98
  end
103
99
 
104
- def ping
105
- with_connection { |redis| redis.ping }
106
- end
107
-
108
- def flushdb
109
- with_connection { |redis| redis.flushdb }
110
- end
111
-
112
100
  private
113
101
 
114
102
  def with_connection(&block)
@@ -6,7 +6,7 @@ module ModelContextProtocol
6
6
 
7
7
  class NotConfiguredError < StandardError
8
8
  def initialize
9
- super("Redis not configured. Call ModelContextProtocol::Server.configure_redis first")
9
+ super("Redis not configured. Set redis_url in the StreamableHttpConfiguration block.")
10
10
  end
11
11
  end
12
12
 
@@ -36,10 +36,6 @@ module ModelContextProtocol
36
36
  instance.stats
37
37
  end
38
38
 
39
- def self.pool_manager
40
- instance.manager
41
- end
42
-
43
39
  def initialize
44
40
  reset!
45
41
  end
@@ -53,7 +49,8 @@ module ModelContextProtocol
53
49
  @manager = Server::RedisPoolManager.new(
54
50
  redis_url: config.redis_url,
55
51
  pool_size: config.pool_size,
56
- pool_timeout: config.pool_timeout
52
+ pool_timeout: config.pool_timeout,
53
+ ssl_params: config.ssl_params
57
54
  )
58
55
 
59
56
  if config.enable_reaper
@@ -93,7 +90,7 @@ module ModelContextProtocol
93
90
 
94
91
  class Configuration
95
92
  attr_accessor :redis_url, :pool_size, :pool_timeout,
96
- :enable_reaper, :reaper_interval, :idle_timeout
93
+ :enable_reaper, :reaper_interval, :idle_timeout, :ssl_params
97
94
 
98
95
  def initialize
99
96
  @redis_url = nil
@@ -102,6 +99,7 @@ module ModelContextProtocol
102
99
  @enable_reaper = true
103
100
  @reaper_interval = 60
104
101
  @idle_timeout = 300
102
+ @ssl_params = nil
105
103
  end
106
104
  end
107
105
  end