model-context-protocol-rb 0.4.0 → 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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -1
  3. data/README.md +155 -12
  4. data/lib/model_context_protocol/server/cancellable.rb +54 -0
  5. data/lib/model_context_protocol/server/configuration.rb +4 -9
  6. data/lib/model_context_protocol/server/progressable.rb +72 -0
  7. data/lib/model_context_protocol/server/prompt.rb +3 -1
  8. data/lib/model_context_protocol/server/redis_client_proxy.rb +134 -0
  9. data/lib/model_context_protocol/server/redis_config.rb +108 -0
  10. data/lib/model_context_protocol/server/redis_pool_manager.rb +110 -0
  11. data/lib/model_context_protocol/server/resource.rb +3 -0
  12. data/lib/model_context_protocol/server/router.rb +36 -3
  13. data/lib/model_context_protocol/server/stdio_transport/request_store.rb +102 -0
  14. data/lib/model_context_protocol/server/stdio_transport.rb +31 -6
  15. data/lib/model_context_protocol/server/streamable_http_transport/event_counter.rb +35 -0
  16. data/lib/model_context_protocol/server/streamable_http_transport/message_poller.rb +101 -0
  17. data/lib/model_context_protocol/server/streamable_http_transport/notification_queue.rb +80 -0
  18. data/lib/model_context_protocol/server/streamable_http_transport/request_store.rb +224 -0
  19. data/lib/model_context_protocol/server/streamable_http_transport/session_message_queue.rb +120 -0
  20. data/lib/model_context_protocol/server/{session_store.rb → streamable_http_transport/session_store.rb} +30 -16
  21. data/lib/model_context_protocol/server/streamable_http_transport/stream_registry.rb +119 -0
  22. data/lib/model_context_protocol/server/streamable_http_transport.rb +162 -79
  23. data/lib/model_context_protocol/server/tool.rb +4 -0
  24. data/lib/model_context_protocol/server.rb +9 -3
  25. data/lib/model_context_protocol/version.rb +1 -1
  26. data/tasks/templates/dev-http.erb +58 -14
  27. metadata +57 -3
@@ -14,29 +14,62 @@ module ModelContextProtocol
14
14
  {jsonrpc: "2.0", id:, error:}
15
15
  end
16
16
  end
17
+
17
18
  def initialize(router:, configuration:)
18
19
  @router = router
19
20
  @configuration = configuration
20
21
 
21
22
  transport_options = @configuration.transport_options
22
- @redis = transport_options[:redis_client]
23
+ @redis_pool = ModelContextProtocol::Server::RedisConfig.pool
23
24
  @require_sessions = transport_options.fetch(:require_sessions, false)
24
25
  @default_protocol_version = transport_options.fetch(:default_protocol_version, "2025-03-26")
25
- @session_protocol_versions = {} # Track protocol versions per session
26
+ @session_protocol_versions = {}
26
27
  @validate_origin = transport_options.fetch(:validate_origin, true)
27
28
  @allowed_origins = transport_options.fetch(:allowed_origins, ["http://localhost", "https://localhost", "http://127.0.0.1", "https://127.0.0.1"])
29
+ @redis = ModelContextProtocol::Server::RedisClientProxy.new(@redis_pool)
28
30
 
29
- @session_store = ModelContextProtocol::Server::SessionStore.new(
31
+ @session_store = SessionStore.new(
30
32
  @redis,
31
33
  ttl: transport_options[:session_ttl] || 3600
32
34
  )
33
35
 
34
36
  @server_instance = "#{Socket.gethostname}-#{Process.pid}-#{SecureRandom.hex(4)}"
35
- @local_streams = {}
36
- @notification_queue = []
37
- @sse_event_counter = 0
37
+ @stream_registry = StreamRegistry.new(@redis, @server_instance)
38
+ @notification_queue = NotificationQueue.new(@redis, @server_instance)
39
+ @event_counter = EventCounter.new(@redis, @server_instance)
40
+ @request_store = RequestStore.new(@redis, @server_instance)
41
+ @stream_monitor_thread = nil
42
+ @message_poller = MessagePoller.new(@redis, @stream_registry, @configuration.logger) do |stream, message|
43
+ send_to_stream(stream, message)
44
+ end
38
45
 
39
- setup_redis_subscriber
46
+ start_message_poller
47
+ start_stream_monitor
48
+ end
49
+
50
+ def shutdown
51
+ @configuration.logger.info("Shutting down StreamableHttpTransport")
52
+
53
+ # Stop the message poller
54
+ @message_poller&.stop
55
+
56
+ # Stop the stream monitor thread
57
+ if @stream_monitor_thread&.alive?
58
+ @stream_monitor_thread.kill
59
+ @stream_monitor_thread.join(timeout: 5)
60
+ end
61
+
62
+ # Unregister all local streams
63
+ @stream_registry.get_all_local_streams.each do |session_id, stream|
64
+ @stream_registry.unregister_stream(session_id)
65
+ @session_store.mark_stream_inactive(session_id)
66
+ rescue => e
67
+ @configuration.logger.error("Error during stream cleanup", session_id: session_id, error: e.message)
68
+ end
69
+
70
+ @redis_pool.checkin(@redis) if @redis_pool && @redis
71
+
72
+ @configuration.logger.info("StreamableHttpTransport shutdown complete")
40
73
  end
41
74
 
42
75
  def handle
@@ -68,10 +101,10 @@ module ModelContextProtocol
68
101
  params: params
69
102
  }
70
103
 
71
- if has_active_streams?
104
+ if @stream_registry.has_any_local_streams?
72
105
  deliver_to_active_streams(notification)
73
106
  else
74
- @notification_queue << notification
107
+ @notification_queue.push(notification)
75
108
  end
76
109
  end
77
110
 
@@ -96,7 +129,6 @@ module ModelContextProtocol
96
129
 
97
130
  protocol_version = env["HTTP_MCP_PROTOCOL_VERSION"]
98
131
  if protocol_version
99
- # Check if this matches a known negotiated version
100
132
  valid_versions = @session_protocol_versions.values.compact.uniq
101
133
  unless valid_versions.empty? || valid_versions.include?(protocol_version)
102
134
  error_response = ErrorResponse[id: nil, error: {code: -32600, message: "Invalid MCP protocol version: #{protocol_version}. Expected one of: #{valid_versions.join(", ")}"}]
@@ -133,9 +165,31 @@ module ModelContextProtocol
133
165
  end
134
166
  end
135
167
 
168
+ def create_progressive_request_sse_stream_proc(request_body, session_id)
169
+ proc do |stream|
170
+ temp_stream_id = session_id || "temp-#{SecureRandom.hex(8)}"
171
+ @stream_registry.register_stream(temp_stream_id, stream)
172
+
173
+ begin
174
+ result = @router.route(request_body, request_store: @request_store, session_id: session_id, transport: self)
175
+
176
+ if result
177
+ response = Response[id: request_body["id"], result: result.serialized]
178
+
179
+ event_id = next_event_id
180
+ send_sse_event(stream, response.serialized, event_id)
181
+ else
182
+ event_id = next_event_id
183
+ send_sse_event(stream, {}, event_id)
184
+ end
185
+ ensure
186
+ @stream_registry.unregister_stream(temp_stream_id)
187
+ end
188
+ end
189
+ end
190
+
136
191
  def next_event_id
137
- @sse_event_counter += 1
138
- "#{@server_instance}-#{@sse_event_counter}"
192
+ @event_counter.next_event_id
139
193
  end
140
194
 
141
195
  def send_sse_event(stream, data, event_id = nil)
@@ -176,7 +230,7 @@ module ModelContextProtocol
176
230
  end
177
231
 
178
232
  def handle_initialization(body, accept_header)
179
- result = @router.route(body)
233
+ result = @router.route(body, transport: self)
180
234
  response = Response[id: body["id"], result: result.serialized]
181
235
  response_headers = {}
182
236
 
@@ -235,21 +289,19 @@ module ModelContextProtocol
235
289
 
236
290
  case message_type
237
291
  when :notification, :response
238
- if session_id && @session_store.session_has_active_stream?(session_id)
292
+ if body["method"] == "notifications/cancelled"
293
+ handle_cancellation(body, session_id)
294
+ elsif session_id && @session_store.session_has_active_stream?(session_id)
239
295
  deliver_to_session_stream(session_id, body)
240
296
  end
241
297
  {json: {}, status: 202}
242
298
 
243
299
  when :request
244
- result = @router.route(body)
245
- response = Response[id: body["id"], result: result.serialized]
246
-
247
- if session_id && @session_store.session_has_active_stream?(session_id)
248
- deliver_to_session_stream(session_id, response.serialized)
249
- return {json: {accepted: true}, status: 200}
250
- end
300
+ has_progress_token = body.dig("params", "_meta", "progressToken")
301
+ should_stream = (accept_header.include?("text/event-stream") && !accept_header.include?("application/json")) ||
302
+ has_progress_token
251
303
 
252
- if accept_header.include?("text/event-stream") && !accept_header.include?("application/json")
304
+ if should_stream
253
305
  {
254
306
  stream: true,
255
307
  headers: {
@@ -257,14 +309,27 @@ module ModelContextProtocol
257
309
  "Cache-Control" => "no-cache",
258
310
  "Connection" => "keep-alive"
259
311
  },
260
- stream_proc: create_request_sse_stream_proc(response.serialized)
312
+ stream_proc: create_progressive_request_sse_stream_proc(body, session_id)
261
313
  }
262
314
  else
263
- {
264
- json: response.serialized,
265
- status: 200,
266
- headers: {"Content-Type" => "application/json"}
267
- }
315
+ result = @router.route(body, request_store: @request_store, session_id: session_id, transport: self)
316
+
317
+ if result
318
+ response = Response[id: body["id"], result: result.serialized]
319
+
320
+ if session_id && @session_store.session_has_active_stream?(session_id)
321
+ deliver_to_session_stream(session_id, response.serialized)
322
+ return {json: {accepted: true}, status: 200}
323
+ end
324
+
325
+ {
326
+ json: response.serialized,
327
+ status: 200,
328
+ headers: {"Content-Type" => "application/json"}
329
+ }
330
+ else
331
+ {json: {}, status: 204}
332
+ end
268
333
  end
269
334
  end
270
335
  end
@@ -315,7 +380,7 @@ module ModelContextProtocol
315
380
 
316
381
  def create_sse_stream_proc(session_id, last_event_id = nil)
317
382
  proc do |stream|
318
- register_local_stream(session_id, stream) if session_id
383
+ @stream_registry.register_stream(session_id, stream) if session_id
319
384
 
320
385
  if last_event_id
321
386
  replay_messages_after_event_id(stream, session_id, last_event_id)
@@ -323,26 +388,15 @@ module ModelContextProtocol
323
388
  flush_notifications_to_stream(stream)
324
389
  end
325
390
 
326
- start_keepalive_thread(session_id, stream)
327
-
328
391
  loop do
329
392
  break unless stream_connected?(stream)
330
393
  sleep 0.1
331
394
  end
332
395
  ensure
333
- cleanup_local_stream(session_id) if session_id
396
+ @stream_registry.unregister_stream(session_id) if session_id
334
397
  end
335
398
  end
336
399
 
337
- def register_local_stream(session_id, stream)
338
- @local_streams[session_id] = stream
339
- end
340
-
341
- def cleanup_local_stream(session_id)
342
- @local_streams.delete(session_id)
343
- @session_store.mark_stream_inactive(session_id)
344
- end
345
-
346
400
  def stream_connected?(stream)
347
401
  return false unless stream
348
402
 
@@ -355,22 +409,41 @@ module ModelContextProtocol
355
409
  end
356
410
  end
357
411
 
358
- def start_keepalive_thread(session_id, stream)
359
- Thread.new do
412
+ def start_stream_monitor
413
+ @stream_monitor_thread = Thread.new do
360
414
  loop do
361
- sleep 30
362
- break unless stream_connected?(stream)
415
+ sleep 30 # Check every 30 seconds
363
416
 
364
417
  begin
365
- send_ping_to_stream(stream)
366
- rescue IOError, Errno::EPIPE, Errno::ECONNRESET
367
- break
418
+ monitor_streams
419
+ rescue => e
420
+ @configuration.logger.error("Stream monitor error", error: e.message)
368
421
  end
369
422
  end
370
423
  rescue => e
371
- @configuration.logger.error("Keepalive thread error", error: e.message)
372
- ensure
373
- cleanup_local_stream(session_id)
424
+ @configuration.logger.error("Stream monitor thread error", error: e.message)
425
+ sleep 5
426
+ retry
427
+ end
428
+ end
429
+
430
+ def monitor_streams
431
+ expired_sessions = @stream_registry.cleanup_expired_streams
432
+ expired_sessions.each do |session_id|
433
+ @session_store.mark_stream_inactive(session_id)
434
+ end
435
+
436
+ @stream_registry.get_all_local_streams.each do |session_id, stream|
437
+ if stream_connected?(stream)
438
+ send_ping_to_stream(stream)
439
+ @stream_registry.refresh_heartbeat(session_id)
440
+ else
441
+ @stream_registry.unregister_stream(session_id)
442
+ @session_store.mark_stream_inactive(session_id)
443
+ end
444
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET
445
+ @stream_registry.unregister_stream(session_id)
446
+ @session_store.mark_stream_inactive(session_id)
374
447
  end
375
448
  end
376
449
 
@@ -389,60 +462,70 @@ module ModelContextProtocol
389
462
  end
390
463
 
391
464
  def deliver_to_session_stream(session_id, data)
392
- if @local_streams[session_id]
465
+ if @stream_registry.has_local_stream?(session_id)
466
+ stream = @stream_registry.get_local_stream(session_id)
393
467
  begin
394
- send_to_stream(@local_streams[session_id], data)
468
+ send_to_stream(stream, data)
395
469
  return true
396
470
  rescue IOError, Errno::EPIPE, Errno::ECONNRESET
397
- cleanup_local_stream(session_id)
471
+ @stream_registry.unregister_stream(session_id)
398
472
  end
399
473
  end
400
474
 
401
- @session_store.route_message_to_session(session_id, data)
475
+ @session_store.queue_message_for_session(session_id, data)
402
476
  end
403
477
 
404
478
  def cleanup_session(session_id)
405
- cleanup_local_stream(session_id)
479
+ @stream_registry.unregister_stream(session_id)
406
480
  @session_store.cleanup_session(session_id)
481
+ @request_store.cleanup_session_requests(session_id)
407
482
  end
408
483
 
409
- def setup_redis_subscriber
410
- Thread.new do
411
- @session_store.subscribe_to_server(@server_instance) do |data|
412
- session_id = data["session_id"]
413
- message = data["message"]
414
-
415
- if @local_streams[session_id]
416
- begin
417
- send_to_stream(@local_streams[session_id], message)
418
- rescue IOError, Errno::EPIPE, Errno::ECONNRESET
419
- cleanup_local_stream(session_id)
420
- end
421
- end
422
- end
423
- rescue => e
424
- @configuration.logger.error("Redis subscriber error", error: e.message, backtrace: e.backtrace.first(5))
425
- sleep 5
426
- retry
427
- end
484
+ def start_message_poller
485
+ @message_poller.start
428
486
  end
429
487
 
430
488
  def has_active_streams?
431
- @local_streams.any?
489
+ @stream_registry.has_any_local_streams?
432
490
  end
433
491
 
434
492
  def deliver_to_active_streams(notification)
435
- @local_streams.each do |session_id, stream|
493
+ @stream_registry.get_all_local_streams.each do |session_id, stream|
436
494
  send_to_stream(stream, notification)
437
495
  rescue IOError, Errno::EPIPE, Errno::ECONNRESET
438
- cleanup_local_stream(session_id)
496
+ @stream_registry.unregister_stream(session_id)
439
497
  end
440
498
  end
441
499
 
442
500
  def flush_notifications_to_stream(stream)
443
- while (notification = @notification_queue.shift)
501
+ notifications = @notification_queue.pop_all
502
+ notifications.each do |notification|
444
503
  send_to_stream(stream, notification)
445
504
  end
446
505
  end
506
+
507
+ # Handle a cancellation notification from the client
508
+ #
509
+ # @param message [Hash] the cancellation notification message
510
+ # @param session_id [String, nil] the session ID if available
511
+ def handle_cancellation(message, session_id = nil)
512
+ params = message["params"]
513
+ return unless params
514
+
515
+ request_id = params["requestId"]
516
+ reason = params["reason"]
517
+
518
+ return unless request_id
519
+
520
+ @request_store.mark_cancelled(request_id, reason)
521
+ rescue
522
+ nil
523
+ end
524
+
525
+ def cleanup
526
+ @message_poller&.stop
527
+ @stream_monitor_thread&.kill
528
+ @redis = nil
529
+ end
447
530
  end
448
531
  end
@@ -5,7 +5,9 @@ module ModelContextProtocol
5
5
  # Raised when output schema validation fails.
6
6
  class OutputSchemaValidationError < StandardError; end
7
7
 
8
+ include ModelContextProtocol::Server::Cancellable
8
9
  include ModelContextProtocol::Server::ContentHelpers
10
+ include ModelContextProtocol::Server::Progressable
9
11
 
10
12
  attr_reader :arguments, :context, :logger
11
13
 
@@ -107,6 +109,8 @@ module ModelContextProtocol
107
109
  raise ModelContextProtocol::Server::ParameterValidationError, validation_error.message
108
110
  rescue OutputSchemaValidationError, ModelContextProtocol::Server::ResponseArgumentsError => tool_error
109
111
  raise tool_error, tool_error.message
112
+ rescue Server::Cancellable::CancellationError
113
+ raise
110
114
  rescue => error
111
115
  ErrorResponse[error: error.message]
112
116
  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,7 +32,7 @@ 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
@@ -281,5 +281,11 @@ module ModelContextProtocol
281
281
  end
282
282
  end
283
283
  end
284
+
285
+ class << self
286
+ def configure_redis(&block)
287
+ RedisConfig.configure(&block)
288
+ end
289
+ end
284
290
  end
285
291
  end
@@ -1,3 +1,3 @@
1
1
  module ModelContextProtocol
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -4,6 +4,8 @@ require "bundler/setup"
4
4
  require "rack"
5
5
  require 'rackup/handler/webrick'
6
6
  require "webrick"
7
+ require "webrick/https"
8
+ require "openssl"
7
9
  require "securerandom"
8
10
  require "redis"
9
11
  require "logger"
@@ -12,6 +14,14 @@ require 'stringio'
12
14
 
13
15
  require_relative "../lib/model_context_protocol"
14
16
 
17
+ ModelContextProtocol::Server.configure_redis do |config|
18
+ config.redis_url = "redis://localhost:6379/0"
19
+ config.pool_size = 10
20
+ config.enable_reaper = true
21
+ config.reaper_interval = 10
22
+ config.idle_timeout = 15
23
+ end
24
+
15
25
  Dir[File.join(__dir__, "../spec/support/**/*.rb")].each { |file| require file }
16
26
 
17
27
  logger = Logger.new(STDOUT)
@@ -73,6 +83,11 @@ class MCPHttpApp
73
83
  @logger.info(" Request: #{body_content}") unless body_content.empty?
74
84
  end
75
85
 
86
+ if ModelContextProtocol::Server::RedisConfig.configured?
87
+ pool_stats = ModelContextProtocol::Server::RedisConfig.stats
88
+ @logger.info(" Redis Pool: #{pool_stats}")
89
+ end
90
+
76
91
  env['rack.input'] = StringIO.new(body_content)
77
92
  request = Rack::Request.new(env)
78
93
 
@@ -89,30 +104,26 @@ class MCPHttpApp
89
104
  }, [""]]
90
105
  end
91
106
 
92
- begin
93
- redis_client = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
94
- @logger.debug("Testing Redis connection...")
95
- redis_client.ping
96
- @logger.info("Redis connected successfully")
97
- end
98
-
99
107
  transport_config = {
100
108
  type: :streamable_http,
101
- env: env,
102
- redis_client: redis_client,
103
- require_sessions: false, # Optional sessions for easier testing
109
+ env:,
110
+ require_sessions: false,
104
111
  session_ttl: 3600,
105
- validate_origin: false, # Disable for testing
106
112
  allowed_origins: ["*"]
107
113
  }
108
114
 
109
115
  @logger.debug("Creating MCP server with transport config")
110
116
  server = create_mcp_server(transport_config)
117
+ transport = nil
111
118
 
112
119
  begin
113
120
  @logger.debug("Starting MCP server")
114
121
  result = server.start
115
122
 
123
+ if server.respond_to?(:transport)
124
+ transport = server.transport
125
+ end
126
+
116
127
  case result
117
128
  when Hash
118
129
  if result[:stream]
@@ -134,6 +145,10 @@ class MCPHttpApp
134
145
  @logger.info("← ERROR RESPONSE (code: #{response_json[:error][:code]})")
135
146
  elsif status == 202
136
147
  @logger.info("← NOTIFICATION ACCEPTED [NO RESPONSE REQUIRED]")
148
+ elsif response_json[:accepted] == true && status == 200
149
+ method = request_json['method'] rescue 'unknown'
150
+ id = request_json['id'] rescue 'unknown'
151
+ @logger.info("← #{method} RESPONSE (id: #{id}) [DELIVERED VIA SSE STREAM]")
137
152
  elsif response_json[:result]
138
153
  method = request_json['method'] rescue 'unknown'
139
154
  @logger.info("← #{method} RESPONSE (id: #{response_json[:id]})")
@@ -167,6 +182,7 @@ class MCPHttpApp
167
182
  @logger.debug("Full backtrace:\n#{e.backtrace.join("\n")}")
168
183
  [500, {"Content-Type" => "application/json"}, [%Q({"error": "Internal server error: #{e.message}"})]]
169
184
  ensure
185
+ transport&.cleanup if transport&.respond_to?(:cleanup)
170
186
  Thread.current[:request_id] = nil
171
187
  end
172
188
  end
@@ -217,13 +233,18 @@ class MCPHttpApp
217
233
  register TestToolWithMixedContentResponse
218
234
  register TestToolWithResourceResponse
219
235
  register TestToolWithToolErrorResponse
236
+ register TestToolWithCancellableSleep
220
237
  end
221
238
  end
222
239
  end
223
240
  end
224
241
  end
225
242
 
226
- logger.info("Starting MCP HTTP Development Server on http://localhost:9292/mcp")
243
+ use_ssl = ENV['SSL'] == 'true'
244
+ port = use_ssl ? 9293 : 9292
245
+ protocol = use_ssl ? 'https' : 'http'
246
+
247
+ logger.info("Starting MCP #{protocol.upcase} Development Server on #{protocol}://localhost:#{port}/mcp")
227
248
 
228
249
  app = Rack::Builder.new do
229
250
  map '/mcp' do
@@ -231,7 +252,30 @@ app = Rack::Builder.new do
231
252
  end
232
253
  end
233
254
 
234
- server = WEBrick::HTTPServer.new(Port: 9292, Host: '0.0.0.0')
255
+ server_options = {
256
+ Port: port,
257
+ Host: '0.0.0.0'
258
+ }
259
+
260
+ if use_ssl
261
+ cert_path = File.join(__dir__, '..', 'tmp', 'ssl', 'server.crt')
262
+ key_path = File.join(__dir__, '..', 'tmp', 'ssl', 'server.key')
263
+
264
+ unless File.exist?(cert_path) && File.exist?(key_path)
265
+ logger.error("SSL certificates not found at tmp/ssl/")
266
+ logger.error("Generate them with: openssl req -x509 -newkey rsa:4096 -keyout tmp/ssl/server.key -out tmp/ssl/server.crt -days 365 -nodes -subj \"/C=US/ST=Dev/L=Dev/O=Dev/CN=localhost\"")
267
+ exit(1)
268
+ end
269
+
270
+ server_options.merge!(
271
+ SSLEnable: true,
272
+ SSLCertificate: OpenSSL::X509::Certificate.new(File.read(cert_path)),
273
+ SSLPrivateKey: OpenSSL::PKey::RSA.new(File.read(key_path)),
274
+ SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE
275
+ )
276
+ end
277
+
278
+ server = WEBrick::HTTPServer.new(server_options)
235
279
  server.mount '/', Rackup::Handler::WEBrick, app
236
280
 
237
281
  ['INT', 'TERM'].each do |signal|
@@ -241,4 +285,4 @@ server.mount '/', Rackup::Handler::WEBrick, app
241
285
  end
242
286
  end
243
287
 
244
- server.start
288
+ server.start
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: model-context-protocol-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dick Davis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-09-08 00:00:00.000000000 Z
11
+ date: 2025-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json-schema
@@ -38,6 +38,48 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '2.8'
41
+ - !ruby/object:Gem::Dependency
42
+ name: redis
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: connection_pool
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.4'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: concurrent-ruby
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.3'
41
83
  description:
42
84
  email:
43
85
  - dick@hey.com
@@ -56,20 +98,32 @@ files:
56
98
  - Rakefile
57
99
  - lib/model_context_protocol.rb
58
100
  - lib/model_context_protocol/server.rb
101
+ - lib/model_context_protocol/server/cancellable.rb
59
102
  - lib/model_context_protocol/server/completion.rb
60
103
  - lib/model_context_protocol/server/configuration.rb
61
104
  - lib/model_context_protocol/server/content.rb
62
105
  - lib/model_context_protocol/server/content_helpers.rb
63
106
  - lib/model_context_protocol/server/mcp_logger.rb
64
107
  - lib/model_context_protocol/server/pagination.rb
108
+ - lib/model_context_protocol/server/progressable.rb
65
109
  - lib/model_context_protocol/server/prompt.rb
110
+ - lib/model_context_protocol/server/redis_client_proxy.rb
111
+ - lib/model_context_protocol/server/redis_config.rb
112
+ - lib/model_context_protocol/server/redis_pool_manager.rb
66
113
  - lib/model_context_protocol/server/registry.rb
67
114
  - lib/model_context_protocol/server/resource.rb
68
115
  - lib/model_context_protocol/server/resource_template.rb
69
116
  - lib/model_context_protocol/server/router.rb
70
- - lib/model_context_protocol/server/session_store.rb
71
117
  - lib/model_context_protocol/server/stdio_transport.rb
118
+ - lib/model_context_protocol/server/stdio_transport/request_store.rb
72
119
  - lib/model_context_protocol/server/streamable_http_transport.rb
120
+ - lib/model_context_protocol/server/streamable_http_transport/event_counter.rb
121
+ - lib/model_context_protocol/server/streamable_http_transport/message_poller.rb
122
+ - lib/model_context_protocol/server/streamable_http_transport/notification_queue.rb
123
+ - lib/model_context_protocol/server/streamable_http_transport/request_store.rb
124
+ - lib/model_context_protocol/server/streamable_http_transport/session_message_queue.rb
125
+ - lib/model_context_protocol/server/streamable_http_transport/session_store.rb
126
+ - lib/model_context_protocol/server/streamable_http_transport/stream_registry.rb
73
127
  - lib/model_context_protocol/server/tool.rb
74
128
  - lib/model_context_protocol/version.rb
75
129
  - tasks/mcp.rake