model-context-protocol-rb 0.5.1 → 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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -1
  3. data/README.md +181 -950
  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/cancellable.rb +5 -5
  25. data/lib/model_context_protocol/server/{mcp_logger.rb → client_logger.rb} +8 -11
  26. data/lib/model_context_protocol/server/configuration.rb +196 -109
  27. data/lib/model_context_protocol/server/content_helpers.rb +1 -1
  28. data/lib/model_context_protocol/server/global_config/server_logging.rb +78 -0
  29. data/lib/model_context_protocol/server/progressable.rb +43 -21
  30. data/lib/model_context_protocol/server/prompt.rb +12 -21
  31. data/lib/model_context_protocol/server/redis_client_proxy.rb +2 -14
  32. data/lib/model_context_protocol/server/redis_config.rb +5 -7
  33. data/lib/model_context_protocol/server/redis_pool_manager.rb +11 -14
  34. data/lib/model_context_protocol/server/registry.rb +8 -0
  35. data/lib/model_context_protocol/server/resource.rb +7 -4
  36. data/lib/model_context_protocol/server/router.rb +285 -9
  37. data/lib/model_context_protocol/server/server_logger.rb +31 -0
  38. data/lib/model_context_protocol/server/stdio_configuration.rb +114 -0
  39. data/lib/model_context_protocol/server/stdio_transport/request_store.rb +12 -53
  40. data/lib/model_context_protocol/server/stdio_transport.rb +18 -12
  41. data/lib/model_context_protocol/server/streamable_http_configuration.rb +218 -0
  42. data/lib/model_context_protocol/server/streamable_http_transport/event_counter.rb +0 -13
  43. data/lib/model_context_protocol/server/streamable_http_transport/message_poller.rb +9 -9
  44. data/lib/model_context_protocol/server/streamable_http_transport/notification_queue.rb +0 -41
  45. data/lib/model_context_protocol/server/streamable_http_transport/request_store.rb +21 -124
  46. data/lib/model_context_protocol/server/streamable_http_transport/server_request_store.rb +167 -0
  47. data/lib/model_context_protocol/server/streamable_http_transport/session_message_queue.rb +0 -58
  48. data/lib/model_context_protocol/server/streamable_http_transport/session_store.rb +17 -31
  49. data/lib/model_context_protocol/server/streamable_http_transport/stream_registry.rb +0 -34
  50. data/lib/model_context_protocol/server/streamable_http_transport.rb +589 -215
  51. data/lib/model_context_protocol/server/tool.rb +73 -6
  52. data/lib/model_context_protocol/server.rb +204 -261
  53. data/lib/model_context_protocol/version.rb +1 -1
  54. data/lib/model_context_protocol.rb +4 -1
  55. data/lib/puma/plugin/mcp.rb +39 -0
  56. data/tasks/mcp.rake +26 -0
  57. data/tasks/templates/dev-http-puma.erb +251 -0
  58. data/tasks/templates/dev-http.erb +166 -184
  59. data/tasks/templates/dev.erb +29 -7
  60. metadata +33 -6
@@ -0,0 +1,218 @@
1
+ require "uri"
2
+
3
+ module ModelContextProtocol
4
+ # Settings for servers that communicate via the MCP streamable HTTP transport, typically
5
+ # used by Rack applications serving multiple clients concurrently with Redis-backed coordination.
6
+ #
7
+ # Created by Server.with_streamable_http_transport, which yields an instance to a configuration
8
+ # block before passing it to Router. Adds session management (require_sessions), CORS control
9
+ # (validate_origin, allowed_origins), connection timeouts (session_ttl, ping_timeout), and
10
+ # Redis connection pool settings (redis_url, redis_pool_size, etc.) on top of the base class.
11
+ # validate_transport! verifies that redis_url is a valid Redis URL, and setup_transport!
12
+ # configures the Redis connection pool via RedisConfig before the server starts.
13
+ #
14
+ # Router queries supports_list_changed? (true for this subclass, false for stdio) to advertise
15
+ # listChanged capabilities in the initialize response. StreamableHttpTransport reads require_sessions,
16
+ # validate_origin, allowed_origins, session_ttl, and ping_timeout directly from this configuration.
17
+ class Server::StreamableHttpConfiguration < Server::Configuration
18
+ # @return [Symbol] :streamable_http (used by Server.start to instantiate StreamableHttpTransport)
19
+ def transport_type = :streamable_http
20
+
21
+ # @return [Boolean] true (Router advertises listChanged in initialize response capabilities;
22
+ # StreamableHttpTransport can push unsolicited notifications to clients)
23
+ def supports_list_changed? = true
24
+
25
+ # @!attribute [w] require_sessions
26
+ # Whether to enforce that clients send a session ID with each request.
27
+ # StreamableHttpTransport checks this and returns 400 Bad Request if session_id is missing.
28
+ # @see StreamableHttpTransport#handle
29
+ attr_writer :require_sessions
30
+
31
+ # @!attribute [w] validate_origin
32
+ # Whether to enforce CORS origin validation against allowed_origins.
33
+ # StreamableHttpTransport checks this in the OPTIONS preflight handler.
34
+ # @see StreamableHttpTransport#handle
35
+ attr_writer :validate_origin
36
+
37
+ # @!attribute [w] allowed_origins
38
+ # List of origins permitted in CORS requests; checked by StreamableHttpTransport
39
+ # when validate_origin is true. Supports exact strings or "*" wildcard.
40
+ # @see StreamableHttpTransport#handle
41
+ attr_writer :allowed_origins
42
+
43
+ # @!attribute [w] session_ttl
44
+ # How long (in seconds) Redis session entries persist after last activity.
45
+ # StreamableHttpTransport passes this to SessionStore.new to set key expiration.
46
+ # @see StreamableHttpTransport#initialize
47
+ attr_writer :session_ttl
48
+
49
+ # @!attribute [w] ping_timeout
50
+ # How long (in seconds) to wait for ping responses before considering a stream dead.
51
+ # StreamableHttpTransport passes this to StreamMonitor to detect stale connections.
52
+ # @see StreamableHttpTransport#initialize
53
+ attr_writer :ping_timeout
54
+
55
+ # @!attribute [w] redis_url
56
+ # The Redis connection URL (redis:// or rediss:// scheme). Required.
57
+ # Passed to RedisConfig during setup_transport! to create the connection pool.
58
+ attr_writer :redis_url
59
+
60
+ # @!attribute [w] redis_pool_size
61
+ # Number of Redis connections in the pool.
62
+ # Passed to RedisConfig during setup_transport!.
63
+ attr_writer :redis_pool_size
64
+
65
+ # @!attribute [w] redis_pool_timeout
66
+ # Seconds to wait for a connection from the pool before raising.
67
+ # Passed to RedisConfig during setup_transport!.
68
+ attr_writer :redis_pool_timeout
69
+
70
+ # @!attribute [w] redis_ssl_params
71
+ # SSL parameters for Redis connections (e.g., { verify_mode: OpenSSL::SSL::VERIFY_NONE }).
72
+ # Passed to RedisConfig during setup_transport!.
73
+ attr_writer :redis_ssl_params
74
+
75
+ # @!attribute [w] redis_enable_reaper
76
+ # Whether to enable the idle connection reaper thread.
77
+ # Passed to RedisConfig during setup_transport!.
78
+ attr_writer :redis_enable_reaper
79
+
80
+ # @!attribute [w] redis_reaper_interval
81
+ # How often (in seconds) the reaper checks for idle connections.
82
+ # Passed to RedisConfig during setup_transport!.
83
+ attr_writer :redis_reaper_interval
84
+
85
+ # @!attribute [w] redis_idle_timeout
86
+ # How long (in seconds) a connection can sit idle before the reaper closes it.
87
+ # Passed to RedisConfig during setup_transport!.
88
+ attr_writer :redis_idle_timeout
89
+
90
+ # Check whether session IDs are mandatory for incoming requests.
91
+ # StreamableHttpTransport reads this at request handling time to decide whether
92
+ # to reject requests without session_id query parameters.
93
+ #
94
+ # @return [Boolean] true by default (sessions are required)
95
+ def require_sessions
96
+ @require_sessions.nil? ? true : @require_sessions
97
+ end
98
+
99
+ # Check whether CORS origin validation is enforced.
100
+ # StreamableHttpTransport reads this during OPTIONS preflight handling to decide
101
+ # whether to reject requests with disallowed Origin headers.
102
+ #
103
+ # @return [Boolean] true by default (validate origins against allowed_origins list)
104
+ def validate_origin
105
+ @validate_origin.nil? ? true : @validate_origin
106
+ end
107
+
108
+ # Retrieve the list of permitted CORS origins.
109
+ # StreamableHttpTransport reads this during OPTIONS preflight handling to check
110
+ # the request's Origin header against allowed values.
111
+ #
112
+ # @return [Array<String>] list of allowed origins (localhost on standard ports by default);
113
+ # supports exact matches or "*" for any origin
114
+ def allowed_origins
115
+ @allowed_origins || DEFAULT_ALLOWED_ORIGINS
116
+ end
117
+
118
+ # Retrieve the session expiration time in seconds.
119
+ # StreamableHttpTransport passes this to SessionStore.new, which sets Redis key TTLs
120
+ # to automatically expire inactive sessions.
121
+ #
122
+ # @return [Integer] seconds until session data expires (3600 = 1 hour by default)
123
+ def session_ttl
124
+ @session_ttl || 3600
125
+ end
126
+
127
+ # Retrieve the ping response timeout in seconds.
128
+ # StreamableHttpTransport passes this to StreamMonitor, which closes streams that
129
+ # don't respond to ping messages within this window (indicating client disconnection
130
+ # or network failure).
131
+ #
132
+ # @return [Integer] seconds to wait for ping responses (10 seconds by default)
133
+ def ping_timeout
134
+ @ping_timeout || 10
135
+ end
136
+
137
+ # @return [String, nil] the Redis connection URL
138
+ attr_reader :redis_url
139
+
140
+ # @return [Integer] number of Redis connections in the pool (default: 20)
141
+ def redis_pool_size
142
+ @redis_pool_size || 20
143
+ end
144
+
145
+ # @return [Integer] seconds to wait for a pool connection (default: 5)
146
+ def redis_pool_timeout
147
+ @redis_pool_timeout || 5
148
+ end
149
+
150
+ # @return [Hash, nil] SSL parameters for Redis connections
151
+ attr_reader :redis_ssl_params
152
+
153
+ # @return [Boolean] whether the idle connection reaper is enabled (default: true)
154
+ def redis_enable_reaper
155
+ @redis_enable_reaper.nil? ? true : @redis_enable_reaper
156
+ end
157
+
158
+ # @return [Integer] seconds between reaper checks (default: 60)
159
+ def redis_reaper_interval
160
+ @redis_reaper_interval || 60
161
+ end
162
+
163
+ # @return [Integer] seconds before idle connections are reaped (default: 300)
164
+ def redis_idle_timeout
165
+ @redis_idle_timeout || 300
166
+ end
167
+
168
+ private
169
+
170
+ # Default CORS origins permitted for HTTP transport: localhost and 127.0.0.1 on both HTTP and HTTPS.
171
+ # StreamableHttpTransport uses this list when allowed_origins hasn't been explicitly set,
172
+ # allowing development and testing without additional configuration. Production deployments
173
+ # should override with actual client origins or ["*"] for unrestricted access.
174
+ #
175
+ # @return [Array<String>] four localhost variants with http/https and localhost/127.0.0.1
176
+ DEFAULT_ALLOWED_ORIGINS = [
177
+ "http://localhost", "https://localhost",
178
+ "http://127.0.0.1", "https://127.0.0.1"
179
+ ].freeze
180
+
181
+ # Verify that redis_url is a valid Redis URL before allowing HTTP transport.
182
+ # Overrides the base class template method; called by validate! after checking name/version/registry.
183
+ # Uses URI parsing ("parse, don't validate") to catch nil, empty, malformed, and non-Redis URLs.
184
+ #
185
+ # @raise [InvalidTransportError] if redis_url is nil, empty, malformed, or not a redis:// / rediss:// URL
186
+ # @return [void]
187
+ def validate_transport!
188
+ uri = URI.parse(redis_url.to_s)
189
+ unless %w[redis rediss].include?(uri.scheme)
190
+ raise InvalidTransportError,
191
+ "streamable_http transport requires a valid Redis URL (redis:// or rediss://). " \
192
+ "Set config.redis_url in the configuration block."
193
+ end
194
+ rescue URI::InvalidURIError
195
+ raise InvalidTransportError,
196
+ "streamable_http transport requires a valid Redis URL (redis:// or rediss://). " \
197
+ "Set config.redis_url in the configuration block."
198
+ end
199
+
200
+ # Configure the Redis connection pool via RedisConfig.
201
+ # Overrides the base class template method; called by Server.build_server after validate! passes.
202
+ # Maps the redis_* attributes on this configuration to the corresponding RedisConfig::Configuration
203
+ # attributes, then starts the pool manager.
204
+ #
205
+ # @return [void]
206
+ def setup_transport!
207
+ ModelContextProtocol::Server::RedisConfig.configure do |redis_config|
208
+ redis_config.redis_url = redis_url
209
+ redis_config.pool_size = redis_pool_size
210
+ redis_config.pool_timeout = redis_pool_timeout
211
+ redis_config.enable_reaper = redis_enable_reaper
212
+ redis_config.reaper_interval = redis_reaper_interval
213
+ redis_config.idle_timeout = redis_idle_timeout
214
+ redis_config.ssl_params = redis_ssl_params
215
+ end
216
+ end
217
+ end
218
+ end
@@ -17,19 +17,6 @@ module ModelContextProtocol
17
17
  count = @redis.incr(@counter_key)
18
18
  "#{@server_instance}-#{count}"
19
19
  end
20
-
21
- def current_count
22
- count = @redis.get(@counter_key)
23
- count ? count.to_i : 0
24
- end
25
-
26
- def reset
27
- @redis.set(@counter_key, 0)
28
- end
29
-
30
- def set_count(value)
31
- @redis.set(@counter_key, value.to_i)
32
- end
33
20
  end
34
21
  end
35
22
  end
@@ -6,10 +6,10 @@ module ModelContextProtocol
6
6
  POLL_INTERVAL = 0.1 # 100ms
7
7
  BATCH_SIZE = 100
8
8
 
9
- def initialize(redis_client, stream_registry, logger, &message_delivery_block)
9
+ def initialize(redis_client, stream_registry, client_logger, &message_delivery_block)
10
10
  @redis = redis_client
11
11
  @stream_registry = stream_registry
12
- @logger = logger
12
+ @client_logger = client_logger
13
13
  @message_delivery_block = message_delivery_block
14
14
  @running = false
15
15
  @poll_thread = nil
@@ -22,14 +22,14 @@ module ModelContextProtocol
22
22
  @poll_thread = Thread.new do
23
23
  poll_loop
24
24
  rescue => e
25
- @logger.error("Message poller thread error", error: e.message, backtrace: e.backtrace.first(5))
25
+ @client_logger.error("Message poller thread error", error: e.message, backtrace: e.backtrace.first(5))
26
26
  sleep 1
27
27
  retry if @running
28
28
  end
29
29
 
30
30
  @poll_thread.name = "MCP-MessagePoller" if @poll_thread.respond_to?(:name=)
31
31
 
32
- @logger.debug("Message poller started")
32
+ @client_logger.debug("Message poller started")
33
33
  end
34
34
 
35
35
  def stop
@@ -37,11 +37,11 @@ module ModelContextProtocol
37
37
 
38
38
  if @poll_thread&.alive?
39
39
  @poll_thread.kill
40
- @poll_thread.join(timeout: 5)
40
+ @poll_thread.join(5)
41
41
  end
42
42
 
43
43
  @poll_thread = nil
44
- @logger.debug("Message poller stopped")
44
+ @client_logger.debug("Message poller stopped")
45
45
  end
46
46
 
47
47
  def running?
@@ -55,7 +55,7 @@ module ModelContextProtocol
55
55
  begin
56
56
  poll_and_deliver_messages
57
57
  rescue => e
58
- @logger.error("Error in message polling", error: e.message)
58
+ @client_logger.error("Error in message polling", error: e.message)
59
59
  end
60
60
 
61
61
  sleep POLL_INTERVAL
@@ -91,9 +91,9 @@ module ModelContextProtocol
91
91
  @message_delivery_block&.call(stream, message)
92
92
  rescue IOError, Errno::EPIPE, Errno::ECONNRESET
93
93
  @stream_registry.unregister_stream(session_id)
94
- @logger.debug("Unregistered disconnected stream", session_id: session_id)
94
+ @client_logger.debug("Unregistered disconnected stream", session_id: session_id)
95
95
  rescue => e
96
- @logger.error("Error delivering message to stream",
96
+ @client_logger.error("Error delivering message to stream",
97
97
  session_id: session_id, error: e.message)
98
98
  end
99
99
  end
@@ -22,13 +22,6 @@ module ModelContextProtocol
22
22
  end
23
23
  end
24
24
 
25
- def pop
26
- notification_json = @redis.rpop(@queue_key)
27
- return nil unless notification_json
28
-
29
- JSON.parse(notification_json)
30
- end
31
-
32
25
  def pop_all
33
26
  notification_jsons = @redis.multi do |multi|
34
27
  multi.lrange(@queue_key, 0, -1)
@@ -41,40 +34,6 @@ module ModelContextProtocol
41
34
  JSON.parse(notification_json)
42
35
  end
43
36
  end
44
-
45
- def peek_all
46
- notification_jsons = @redis.lrange(@queue_key, 0, -1)
47
- return [] if notification_jsons.empty?
48
-
49
- notification_jsons.reverse.map do |notification_json|
50
- JSON.parse(notification_json)
51
- end
52
- end
53
-
54
- def size
55
- @redis.llen(@queue_key)
56
- end
57
-
58
- def empty?
59
- size == 0
60
- end
61
-
62
- def clear
63
- @redis.del(@queue_key)
64
- end
65
-
66
- def push_bulk(notifications)
67
- return if notifications.empty?
68
-
69
- notification_jsons = notifications.map(&:to_json)
70
-
71
- @redis.multi do |multi|
72
- notification_jsons.each do |json|
73
- multi.lpush(@queue_key, json)
74
- end
75
- multi.ltrim(@queue_key, 0, @max_size - 1)
76
- end
77
- end
78
37
  end
79
38
  end
80
39
  end
@@ -19,10 +19,10 @@ module ModelContextProtocol
19
19
 
20
20
  # Register a new request with its associated session
21
21
  #
22
- # @param request_id [String] the unique request identifier
22
+ # @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
23
23
  # @param session_id [String] the session identifier (can be nil for sessionless requests)
24
24
  # @return [void]
25
- def register_request(request_id, session_id = nil)
25
+ def register_request(jsonrpc_request_id, session_id = nil)
26
26
  request_data = {
27
27
  session_id: session_id,
28
28
  server_instance: @server_instance,
@@ -30,11 +30,11 @@ module ModelContextProtocol
30
30
  }
31
31
 
32
32
  @redis.multi do |multi|
33
- multi.set("#{REQUEST_KEY_PREFIX}#{request_id}",
33
+ multi.set("#{REQUEST_KEY_PREFIX}#{jsonrpc_request_id}",
34
34
  request_data.to_json, ex: @ttl)
35
35
 
36
36
  if session_id
37
- multi.set("#{SESSION_KEY_PREFIX}#{session_id}:#{request_id}",
37
+ multi.set("#{SESSION_KEY_PREFIX}#{session_id}:#{jsonrpc_request_id}",
38
38
  true, ex: @ttl)
39
39
  end
40
40
  end
@@ -42,48 +42,37 @@ module ModelContextProtocol
42
42
 
43
43
  # Mark a request as cancelled
44
44
  #
45
- # @param request_id [String] the unique request identifier
45
+ # @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
46
46
  # @param reason [String] optional reason for cancellation
47
47
  # @return [Boolean] true if cancellation was recorded
48
- def mark_cancelled(request_id, reason = nil)
48
+ def mark_cancelled(jsonrpc_request_id, reason = nil)
49
49
  cancellation_data = {
50
50
  cancelled_at: Time.now.to_f,
51
51
  reason: reason
52
52
  }
53
53
 
54
- result = @redis.set("#{CANCELLED_KEY_PREFIX}#{request_id}",
54
+ result = @redis.set("#{CANCELLED_KEY_PREFIX}#{jsonrpc_request_id}",
55
55
  cancellation_data.to_json, ex: @ttl)
56
56
  result == "OK"
57
57
  end
58
58
 
59
59
  # Check if a request has been cancelled
60
60
  #
61
- # @param request_id [String] the unique request identifier
61
+ # @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
62
62
  # @return [Boolean] true if the request is cancelled, false otherwise
63
- def cancelled?(request_id)
64
- @redis.exists("#{CANCELLED_KEY_PREFIX}#{request_id}") == 1
65
- end
66
-
67
- # Get cancellation information for a request
68
- #
69
- # @param request_id [String] the unique request identifier
70
- # @return [Hash, nil] cancellation data or nil if not cancelled
71
- def get_cancellation_info(request_id)
72
- data = @redis.get("#{CANCELLED_KEY_PREFIX}#{request_id}")
73
- data ? JSON.parse(data) : nil
74
- rescue JSON::ParserError
75
- nil
63
+ def cancelled?(jsonrpc_request_id)
64
+ @redis.exists("#{CANCELLED_KEY_PREFIX}#{jsonrpc_request_id}") == 1
76
65
  end
77
66
 
78
67
  # Unregister a request (typically called when request completes)
79
68
  #
80
- # @param request_id [String] the unique request identifier
69
+ # @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
81
70
  # @return [void]
82
- def unregister_request(request_id)
83
- request_data = @redis.get("#{REQUEST_KEY_PREFIX}#{request_id}")
71
+ def unregister_request(jsonrpc_request_id)
72
+ request_data = @redis.get("#{REQUEST_KEY_PREFIX}#{jsonrpc_request_id}")
84
73
 
85
- keys_to_delete = ["#{REQUEST_KEY_PREFIX}#{request_id}",
86
- "#{CANCELLED_KEY_PREFIX}#{request_id}"]
74
+ keys_to_delete = ["#{REQUEST_KEY_PREFIX}#{jsonrpc_request_id}",
75
+ "#{CANCELLED_KEY_PREFIX}#{jsonrpc_request_id}"]
87
76
 
88
77
  if request_data
89
78
  begin
@@ -91,7 +80,7 @@ module ModelContextProtocol
91
80
  session_id = data["session_id"]
92
81
 
93
82
  if session_id
94
- keys_to_delete << "#{SESSION_KEY_PREFIX}#{session_id}:#{request_id}"
83
+ keys_to_delete << "#{SESSION_KEY_PREFIX}#{session_id}:#{jsonrpc_request_id}"
95
84
  end
96
85
  rescue JSON::ParserError
97
86
  nil
@@ -101,25 +90,6 @@ module ModelContextProtocol
101
90
  @redis.del(*keys_to_delete) unless keys_to_delete.empty?
102
91
  end
103
92
 
104
- # Get information about a specific request
105
- #
106
- # @param request_id [String] the unique request identifier
107
- # @return [Hash, nil] request information or nil if not found
108
- def get_request(request_id)
109
- data = @redis.get("#{REQUEST_KEY_PREFIX}#{request_id}")
110
- data ? JSON.parse(data) : nil
111
- rescue JSON::ParserError
112
- nil
113
- end
114
-
115
- # Check if a request is currently active
116
- #
117
- # @param request_id [String] the unique request identifier
118
- # @return [Boolean] true if the request is active, false otherwise
119
- def active?(request_id)
120
- @redis.exists("#{REQUEST_KEY_PREFIX}#{request_id}") == 1
121
- end
122
-
123
93
  # Clean up all requests associated with a session
124
94
  # This is typically called when a session is terminated
125
95
  #
@@ -131,93 +101,20 @@ module ModelContextProtocol
131
101
  return [] if request_keys.empty?
132
102
 
133
103
  # Extract request IDs from the keys
134
- request_ids = request_keys.map do |key|
104
+ jsonrpc_request_ids = request_keys.map do |key|
135
105
  key.sub("#{SESSION_KEY_PREFIX}#{session_id}:", "")
136
106
  end
137
107
 
138
108
  # Delete all related keys
139
109
  all_keys = []
140
- request_ids.each do |request_id|
141
- all_keys << "#{REQUEST_KEY_PREFIX}#{request_id}"
142
- all_keys << "#{CANCELLED_KEY_PREFIX}#{request_id}"
110
+ jsonrpc_request_ids.each do |jsonrpc_request_id|
111
+ all_keys << "#{REQUEST_KEY_PREFIX}#{jsonrpc_request_id}"
112
+ all_keys << "#{CANCELLED_KEY_PREFIX}#{jsonrpc_request_id}"
143
113
  end
144
114
  all_keys.concat(request_keys)
145
115
 
146
116
  @redis.del(*all_keys) unless all_keys.empty?
147
- request_ids
148
- end
149
-
150
- # Get all active request IDs for a specific session
151
- #
152
- # @param session_id [String] the session identifier
153
- # @return [Array<String>] list of active request IDs for the session
154
- def get_session_requests(session_id)
155
- pattern = "#{SESSION_KEY_PREFIX}#{session_id}:*"
156
- request_keys = @redis.keys(pattern)
157
-
158
- request_keys.map do |key|
159
- key.sub("#{SESSION_KEY_PREFIX}#{session_id}:", "")
160
- end
161
- end
162
-
163
- # Get all active request IDs across all sessions
164
- #
165
- # @return [Array<String>] list of all active request IDs
166
- def get_all_active_requests
167
- pattern = "#{REQUEST_KEY_PREFIX}*"
168
- request_keys = @redis.keys(pattern)
169
-
170
- request_keys.map do |key|
171
- key.sub(REQUEST_KEY_PREFIX, "")
172
- end
173
- end
174
-
175
- # Clean up expired requests based on TTL
176
- # This method can be called periodically to ensure cleanup
177
- #
178
- # @return [Integer] number of expired requests cleaned up
179
- def cleanup_expired_requests
180
- active_keys = @redis.keys("#{REQUEST_KEY_PREFIX}*")
181
- expired_count = 0
182
- key_exists_without_expiration = -1
183
- key_does_not_exist = -2
184
-
185
- active_keys.each do |key|
186
- ttl = @redis.ttl(key)
187
- if ttl == key_exists_without_expiration
188
- @redis.expire(key, @ttl)
189
- elsif ttl == key_does_not_exist
190
- expired_count += 1
191
- end
192
- end
193
-
194
- expired_count
195
- end
196
-
197
- # Refresh the TTL for an active request
198
- #
199
- # @param request_id [String] the unique request identifier
200
- # @return [Boolean] true if TTL was refreshed, false if request doesn't exist
201
- def refresh_request_ttl(request_id)
202
- request_data = @redis.get("#{REQUEST_KEY_PREFIX}#{request_id}")
203
- return false unless request_data
204
-
205
- @redis.multi do |multi|
206
- multi.expire("#{REQUEST_KEY_PREFIX}#{request_id}", @ttl)
207
- multi.expire("#{CANCELLED_KEY_PREFIX}#{request_id}", @ttl)
208
-
209
- begin
210
- data = JSON.parse(request_data)
211
- session_id = data["session_id"]
212
- if session_id
213
- multi.expire("#{SESSION_KEY_PREFIX}#{session_id}:#{request_id}", @ttl)
214
- end
215
- rescue JSON::ParserError
216
- nil
217
- end
218
- end
219
-
220
- true
117
+ jsonrpc_request_ids
221
118
  end
222
119
  end
223
120
  end