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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -1
- data/README.md +155 -12
- data/lib/model_context_protocol/server/cancellable.rb +54 -0
- data/lib/model_context_protocol/server/configuration.rb +4 -9
- data/lib/model_context_protocol/server/progressable.rb +72 -0
- data/lib/model_context_protocol/server/prompt.rb +3 -1
- data/lib/model_context_protocol/server/redis_client_proxy.rb +134 -0
- data/lib/model_context_protocol/server/redis_config.rb +108 -0
- data/lib/model_context_protocol/server/redis_pool_manager.rb +110 -0
- data/lib/model_context_protocol/server/resource.rb +3 -0
- data/lib/model_context_protocol/server/router.rb +36 -3
- data/lib/model_context_protocol/server/stdio_transport/request_store.rb +102 -0
- data/lib/model_context_protocol/server/stdio_transport.rb +31 -6
- data/lib/model_context_protocol/server/streamable_http_transport/event_counter.rb +35 -0
- data/lib/model_context_protocol/server/streamable_http_transport/message_poller.rb +101 -0
- data/lib/model_context_protocol/server/streamable_http_transport/notification_queue.rb +80 -0
- data/lib/model_context_protocol/server/streamable_http_transport/request_store.rb +224 -0
- data/lib/model_context_protocol/server/streamable_http_transport/session_message_queue.rb +120 -0
- data/lib/model_context_protocol/server/{session_store.rb → streamable_http_transport/session_store.rb} +30 -16
- data/lib/model_context_protocol/server/streamable_http_transport/stream_registry.rb +119 -0
- data/lib/model_context_protocol/server/streamable_http_transport.rb +162 -79
- data/lib/model_context_protocol/server/tool.rb +4 -0
- data/lib/model_context_protocol/server.rb +9 -3
- data/lib/model_context_protocol/version.rb +1 -1
- data/tasks/templates/dev-http.erb +58 -14
- 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
|
-
@
|
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 = {}
|
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 =
|
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
|
-
@
|
36
|
-
@notification_queue =
|
37
|
-
@
|
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
|
-
|
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
|
104
|
+
if @stream_registry.has_any_local_streams?
|
72
105
|
deliver_to_active_streams(notification)
|
73
106
|
else
|
74
|
-
@notification_queue
|
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
|
-
@
|
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
|
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
|
-
|
245
|
-
|
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
|
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:
|
312
|
+
stream_proc: create_progressive_request_sse_stream_proc(body, session_id)
|
261
313
|
}
|
262
314
|
else
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
366
|
-
rescue
|
367
|
-
|
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("
|
372
|
-
|
373
|
-
|
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 @
|
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(
|
468
|
+
send_to_stream(stream, data)
|
395
469
|
return true
|
396
470
|
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
397
|
-
|
471
|
+
@stream_registry.unregister_stream(session_id)
|
398
472
|
end
|
399
473
|
end
|
400
474
|
|
401
|
-
@session_store.
|
475
|
+
@session_store.queue_message_for_session(session_id, data)
|
402
476
|
end
|
403
477
|
|
404
478
|
def cleanup_session(session_id)
|
405
|
-
|
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
|
410
|
-
|
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
|
-
@
|
489
|
+
@stream_registry.has_any_local_streams?
|
432
490
|
end
|
433
491
|
|
434
492
|
def deliver_to_active_streams(notification)
|
435
|
-
@
|
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
|
-
|
496
|
+
@stream_registry.unregister_stream(session_id)
|
439
497
|
end
|
440
498
|
end
|
441
499
|
|
442
500
|
def flush_notifications_to_stream(stream)
|
443
|
-
|
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
|
@@ -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
|
102
|
-
|
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
|
-
|
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
|
-
|
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
|
+
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-
|
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
|