model-context-protocol-rb 0.3.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -1
- data/README.md +886 -196
- data/lib/model_context_protocol/server/cancellable.rb +54 -0
- data/lib/model_context_protocol/server/configuration.rb +80 -8
- data/lib/model_context_protocol/server/content.rb +321 -0
- data/lib/model_context_protocol/server/content_helpers.rb +84 -0
- data/lib/model_context_protocol/server/pagination.rb +71 -0
- data/lib/model_context_protocol/server/progressable.rb +72 -0
- data/lib/model_context_protocol/server/prompt.rb +108 -14
- 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/registry.rb +94 -18
- data/lib/model_context_protocol/server/resource.rb +98 -25
- data/lib/model_context_protocol/server/resource_template.rb +26 -13
- 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 +352 -112
- data/lib/model_context_protocol/server/tool.rb +79 -53
- data/lib/model_context_protocol/server.rb +124 -21
- data/lib/model_context_protocol/version.rb +1 -1
- data/tasks/mcp.rake +28 -2
- data/tasks/templates/dev-http.erb +288 -0
- data/tasks/templates/dev.erb +7 -1
- metadata +61 -3
@@ -14,42 +14,80 @@ 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
|
-
|
24
|
-
@
|
23
|
+
@redis_pool = ModelContextProtocol::Server::RedisConfig.pool
|
24
|
+
@require_sessions = transport_options.fetch(:require_sessions, false)
|
25
|
+
@default_protocol_version = transport_options.fetch(:default_protocol_version, "2025-03-26")
|
26
|
+
@session_protocol_versions = {}
|
27
|
+
@validate_origin = transport_options.fetch(:validate_origin, true)
|
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)
|
30
|
+
|
31
|
+
@session_store = SessionStore.new(
|
25
32
|
@redis,
|
26
33
|
ttl: transport_options[:session_ttl] || 3600
|
27
34
|
)
|
28
35
|
|
29
36
|
@server_instance = "#{Socket.gethostname}-#{Process.pid}-#{SecureRandom.hex(4)}"
|
30
|
-
@
|
31
|
-
@notification_queue =
|
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
|
32
45
|
|
33
|
-
|
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")
|
34
73
|
end
|
35
74
|
|
36
75
|
def handle
|
37
76
|
@configuration.logger.connect_transport(self)
|
38
77
|
|
39
|
-
|
40
|
-
response = @configuration.transport_options[:response]
|
78
|
+
env = @configuration.transport_options[:env]
|
41
79
|
|
42
|
-
unless
|
43
|
-
raise ArgumentError, "StreamableHTTP transport requires
|
80
|
+
unless env
|
81
|
+
raise ArgumentError, "StreamableHTTP transport requires Rack env hash in transport_options"
|
44
82
|
end
|
45
83
|
|
46
|
-
case
|
84
|
+
case env["REQUEST_METHOD"]
|
47
85
|
when "POST"
|
48
|
-
handle_post_request(
|
86
|
+
handle_post_request(env)
|
49
87
|
when "GET"
|
50
|
-
handle_sse_request(
|
88
|
+
handle_sse_request(env)
|
51
89
|
when "DELETE"
|
52
|
-
handle_delete_request(
|
90
|
+
handle_delete_request(env)
|
53
91
|
else
|
54
92
|
error_response = ErrorResponse[id: nil, error: {code: -32601, message: "Method not allowed"}]
|
55
93
|
{json: error_response.serialized, status: 405}
|
@@ -63,25 +101,120 @@ module ModelContextProtocol
|
|
63
101
|
params: params
|
64
102
|
}
|
65
103
|
|
66
|
-
if
|
104
|
+
if @stream_registry.has_any_local_streams?
|
67
105
|
deliver_to_active_streams(notification)
|
68
106
|
else
|
69
|
-
@notification_queue
|
107
|
+
@notification_queue.push(notification)
|
70
108
|
end
|
71
109
|
end
|
72
110
|
|
73
111
|
private
|
74
112
|
|
75
|
-
def
|
76
|
-
|
113
|
+
def validate_headers(env)
|
114
|
+
if @validate_origin
|
115
|
+
origin = env["HTTP_ORIGIN"]
|
116
|
+
if origin && !@allowed_origins.any? { |allowed| origin.start_with?(allowed) }
|
117
|
+
error_response = ErrorResponse[id: nil, error: {code: -32600, message: "Origin not allowed"}]
|
118
|
+
return {json: error_response.serialized, status: 403}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
accept_header = env["HTTP_ACCEPT"]
|
123
|
+
if accept_header
|
124
|
+
unless accept_header.include?("application/json") || accept_header.include?("text/event-stream")
|
125
|
+
error_response = ErrorResponse[id: nil, error: {code: -32600, message: "Invalid Accept header. Must include application/json or text/event-stream"}]
|
126
|
+
return {json: error_response.serialized, status: 400}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
protocol_version = env["HTTP_MCP_PROTOCOL_VERSION"]
|
131
|
+
if protocol_version
|
132
|
+
valid_versions = @session_protocol_versions.values.compact.uniq
|
133
|
+
unless valid_versions.empty? || valid_versions.include?(protocol_version)
|
134
|
+
error_response = ErrorResponse[id: nil, error: {code: -32600, message: "Invalid MCP protocol version: #{protocol_version}. Expected one of: #{valid_versions.join(", ")}"}]
|
135
|
+
return {json: error_response.serialized, status: 400}
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
nil
|
140
|
+
end
|
141
|
+
|
142
|
+
def determine_message_type(body)
|
143
|
+
if body.key?("method") && body.key?("id")
|
144
|
+
:request
|
145
|
+
elsif body.key?("method") && !body.key?("id")
|
146
|
+
:notification
|
147
|
+
elsif body.key?("id") && body.key?("result") || body.key?("error")
|
148
|
+
:response
|
149
|
+
else
|
150
|
+
:unknown
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def create_initialization_sse_stream_proc(response_data)
|
155
|
+
proc do |stream|
|
156
|
+
event_id = next_event_id
|
157
|
+
send_sse_event(stream, response_data, event_id)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def create_request_sse_stream_proc(response_data)
|
162
|
+
proc do |stream|
|
163
|
+
event_id = next_event_id
|
164
|
+
send_sse_event(stream, response_data, event_id)
|
165
|
+
end
|
166
|
+
end
|
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
|
+
|
191
|
+
def next_event_id
|
192
|
+
@event_counter.next_event_id
|
193
|
+
end
|
194
|
+
|
195
|
+
def send_sse_event(stream, data, event_id = nil)
|
196
|
+
if event_id
|
197
|
+
stream.write("id: #{event_id}\n")
|
198
|
+
end
|
199
|
+
message = data.is_a?(String) ? data : data.to_json
|
200
|
+
stream.write("data: #{message}\n\n")
|
201
|
+
stream.flush if stream.respond_to?(:flush)
|
202
|
+
end
|
203
|
+
|
204
|
+
def handle_post_request(env)
|
205
|
+
validation_error = validate_headers(env)
|
206
|
+
return validation_error if validation_error
|
207
|
+
|
208
|
+
body_string = env["rack.input"].read
|
77
209
|
body = JSON.parse(body_string)
|
78
|
-
session_id =
|
210
|
+
session_id = env["HTTP_MCP_SESSION_ID"]
|
211
|
+
accept_header = env["HTTP_ACCEPT"] || ""
|
79
212
|
|
80
213
|
case body["method"]
|
81
214
|
when "initialize"
|
82
|
-
handle_initialization(body)
|
215
|
+
handle_initialization(body, accept_header)
|
83
216
|
else
|
84
|
-
handle_regular_request(body, session_id)
|
217
|
+
handle_regular_request(body, session_id, accept_header)
|
85
218
|
end
|
86
219
|
rescue JSON::ParserError
|
87
220
|
error_response = ErrorResponse[id: "", error: {code: -32700, message: "Parse error"}]
|
@@ -96,51 +229,133 @@ module ModelContextProtocol
|
|
96
229
|
{json: error_response.serialized, status: 500}
|
97
230
|
end
|
98
231
|
|
99
|
-
def handle_initialization(body)
|
100
|
-
|
101
|
-
|
102
|
-
@session_store.create_session(session_id, {
|
103
|
-
server_instance: @server_instance,
|
104
|
-
context: @configuration.context || {},
|
105
|
-
created_at: Time.now.to_f
|
106
|
-
})
|
107
|
-
|
108
|
-
result = @router.route(body)
|
232
|
+
def handle_initialization(body, accept_header)
|
233
|
+
result = @router.route(body, transport: self)
|
109
234
|
response = Response[id: body["id"], result: result.serialized]
|
235
|
+
response_headers = {}
|
236
|
+
|
237
|
+
negotiated_protocol_version = result.serialized[:protocolVersion] || result.serialized["protocolVersion"]
|
238
|
+
|
239
|
+
if @require_sessions
|
240
|
+
session_id = SecureRandom.uuid
|
241
|
+
@session_store.create_session(session_id, {
|
242
|
+
server_instance: @server_instance,
|
243
|
+
context: @configuration.context || {},
|
244
|
+
created_at: Time.now.to_f,
|
245
|
+
negotiated_protocol_version: negotiated_protocol_version
|
246
|
+
})
|
247
|
+
response_headers["Mcp-Session-Id"] = session_id
|
248
|
+
@session_protocol_versions[session_id] = negotiated_protocol_version
|
249
|
+
else
|
250
|
+
@session_protocol_versions[:default] = negotiated_protocol_version
|
251
|
+
end
|
110
252
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
253
|
+
if accept_header.include?("text/event-stream") && !accept_header.include?("application/json")
|
254
|
+
response_headers.merge!({
|
255
|
+
"Content-Type" => "text/event-stream",
|
256
|
+
"Cache-Control" => "no-cache",
|
257
|
+
"Connection" => "keep-alive"
|
258
|
+
})
|
259
|
+
|
260
|
+
{
|
261
|
+
stream: true,
|
262
|
+
headers: response_headers,
|
263
|
+
stream_proc: create_initialization_sse_stream_proc(response.serialized)
|
264
|
+
}
|
265
|
+
else
|
266
|
+
response_headers["Content-Type"] = "application/json"
|
267
|
+
{
|
268
|
+
json: response.serialized,
|
269
|
+
status: 200,
|
270
|
+
headers: response_headers
|
271
|
+
}
|
272
|
+
end
|
116
273
|
end
|
117
274
|
|
118
|
-
def handle_regular_request(body, session_id)
|
119
|
-
|
120
|
-
|
121
|
-
|
275
|
+
def handle_regular_request(body, session_id, accept_header)
|
276
|
+
if @require_sessions
|
277
|
+
unless session_id && @session_store.session_exists?(session_id)
|
278
|
+
if session_id && !@session_store.session_exists?(session_id)
|
279
|
+
error_response = ErrorResponse[id: body["id"], error: {code: -32600, message: "Session terminated"}]
|
280
|
+
return {json: error_response.serialized, status: 404}
|
281
|
+
else
|
282
|
+
error_response = ErrorResponse[id: body["id"], error: {code: -32600, message: "Invalid or missing session ID"}]
|
283
|
+
return {json: error_response.serialized, status: 400}
|
284
|
+
end
|
285
|
+
end
|
122
286
|
end
|
123
287
|
|
124
|
-
|
125
|
-
response = Response[id: body["id"], result: result.serialized]
|
288
|
+
message_type = determine_message_type(body)
|
126
289
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
290
|
+
case message_type
|
291
|
+
when :notification, :response
|
292
|
+
if body["method"] == "notifications/cancelled"
|
293
|
+
handle_cancellation(body, session_id)
|
294
|
+
elsif session_id && @session_store.session_has_active_stream?(session_id)
|
295
|
+
deliver_to_session_stream(session_id, body)
|
296
|
+
end
|
297
|
+
{json: {}, status: 202}
|
298
|
+
|
299
|
+
when :request
|
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
|
303
|
+
|
304
|
+
if should_stream
|
305
|
+
{
|
306
|
+
stream: true,
|
307
|
+
headers: {
|
308
|
+
"Content-Type" => "text/event-stream",
|
309
|
+
"Cache-Control" => "no-cache",
|
310
|
+
"Connection" => "keep-alive"
|
311
|
+
},
|
312
|
+
stream_proc: create_progressive_request_sse_stream_proc(body, session_id)
|
313
|
+
}
|
314
|
+
else
|
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
|
333
|
+
end
|
132
334
|
end
|
133
335
|
end
|
134
336
|
|
135
|
-
def handle_sse_request(
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
error_response = ErrorResponse[id: nil, error: {code: -32600, message: "Invalid or missing session ID"}]
|
337
|
+
def handle_sse_request(env)
|
338
|
+
accept_header = env["HTTP_ACCEPT"] || ""
|
339
|
+
unless accept_header.include?("text/event-stream")
|
340
|
+
error_response = ErrorResponse[id: nil, error: {code: -32600, message: "Accept header must include text/event-stream"}]
|
140
341
|
return {json: error_response.serialized, status: 400}
|
141
342
|
end
|
142
343
|
|
143
|
-
|
344
|
+
session_id = env["HTTP_MCP_SESSION_ID"]
|
345
|
+
last_event_id = env["HTTP_LAST_EVENT_ID"]
|
346
|
+
|
347
|
+
if @require_sessions
|
348
|
+
unless session_id && @session_store.session_exists?(session_id)
|
349
|
+
if session_id && !@session_store.session_exists?(session_id)
|
350
|
+
error_response = ErrorResponse[id: nil, error: {code: -32600, message: "Session terminated"}]
|
351
|
+
return {json: error_response.serialized, status: 404}
|
352
|
+
else
|
353
|
+
error_response = ErrorResponse[id: nil, error: {code: -32600, message: "Invalid or missing session ID"}]
|
354
|
+
return {json: error_response.serialized, status: 400}
|
355
|
+
end
|
356
|
+
end
|
357
|
+
@session_store.mark_stream_active(session_id, @server_instance)
|
358
|
+
end
|
144
359
|
|
145
360
|
{
|
146
361
|
stream: true,
|
@@ -149,12 +364,12 @@ module ModelContextProtocol
|
|
149
364
|
"Cache-Control" => "no-cache",
|
150
365
|
"Connection" => "keep-alive"
|
151
366
|
},
|
152
|
-
stream_proc: create_sse_stream_proc(session_id)
|
367
|
+
stream_proc: create_sse_stream_proc(session_id, last_event_id)
|
153
368
|
}
|
154
369
|
end
|
155
370
|
|
156
|
-
def handle_delete_request(
|
157
|
-
session_id =
|
371
|
+
def handle_delete_request(env)
|
372
|
+
session_id = env["HTTP_MCP_SESSION_ID"]
|
158
373
|
|
159
374
|
if session_id
|
160
375
|
cleanup_session(session_id)
|
@@ -163,32 +378,25 @@ module ModelContextProtocol
|
|
163
378
|
{json: {success: true}, status: 200}
|
164
379
|
end
|
165
380
|
|
166
|
-
def create_sse_stream_proc(session_id)
|
381
|
+
def create_sse_stream_proc(session_id, last_event_id = nil)
|
167
382
|
proc do |stream|
|
168
|
-
|
169
|
-
|
170
|
-
flush_notifications_to_stream(stream)
|
383
|
+
@stream_registry.register_stream(session_id, stream) if session_id
|
171
384
|
|
172
|
-
|
385
|
+
if last_event_id
|
386
|
+
replay_messages_after_event_id(stream, session_id, last_event_id)
|
387
|
+
else
|
388
|
+
flush_notifications_to_stream(stream)
|
389
|
+
end
|
173
390
|
|
174
391
|
loop do
|
175
392
|
break unless stream_connected?(stream)
|
176
393
|
sleep 0.1
|
177
394
|
end
|
178
395
|
ensure
|
179
|
-
|
396
|
+
@stream_registry.unregister_stream(session_id) if session_id
|
180
397
|
end
|
181
398
|
end
|
182
399
|
|
183
|
-
def register_local_stream(session_id, stream)
|
184
|
-
@local_streams[session_id] = stream
|
185
|
-
end
|
186
|
-
|
187
|
-
def cleanup_local_stream(session_id)
|
188
|
-
@local_streams.delete(session_id)
|
189
|
-
@session_store.mark_stream_inactive(session_id)
|
190
|
-
end
|
191
|
-
|
192
400
|
def stream_connected?(stream)
|
193
401
|
return false unless stream
|
194
402
|
|
@@ -201,22 +409,41 @@ module ModelContextProtocol
|
|
201
409
|
end
|
202
410
|
end
|
203
411
|
|
204
|
-
def
|
205
|
-
Thread.new do
|
412
|
+
def start_stream_monitor
|
413
|
+
@stream_monitor_thread = Thread.new do
|
206
414
|
loop do
|
207
|
-
sleep 30
|
208
|
-
break unless stream_connected?(stream)
|
415
|
+
sleep 30 # Check every 30 seconds
|
209
416
|
|
210
417
|
begin
|
211
|
-
|
212
|
-
rescue
|
213
|
-
|
418
|
+
monitor_streams
|
419
|
+
rescue => e
|
420
|
+
@configuration.logger.error("Stream monitor error", error: e.message)
|
214
421
|
end
|
215
422
|
end
|
216
423
|
rescue => e
|
217
|
-
@configuration.logger.error("
|
218
|
-
|
219
|
-
|
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)
|
220
447
|
end
|
221
448
|
end
|
222
449
|
|
@@ -226,66 +453,79 @@ module ModelContextProtocol
|
|
226
453
|
end
|
227
454
|
|
228
455
|
def send_to_stream(stream, data)
|
229
|
-
|
230
|
-
stream
|
231
|
-
|
456
|
+
event_id = next_event_id
|
457
|
+
send_sse_event(stream, data, event_id)
|
458
|
+
end
|
459
|
+
|
460
|
+
def replay_messages_after_event_id(stream, session_id, last_event_id)
|
461
|
+
flush_notifications_to_stream(stream)
|
232
462
|
end
|
233
463
|
|
234
464
|
def deliver_to_session_stream(session_id, data)
|
235
|
-
if @
|
465
|
+
if @stream_registry.has_local_stream?(session_id)
|
466
|
+
stream = @stream_registry.get_local_stream(session_id)
|
236
467
|
begin
|
237
|
-
send_to_stream(
|
468
|
+
send_to_stream(stream, data)
|
238
469
|
return true
|
239
470
|
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
240
|
-
|
471
|
+
@stream_registry.unregister_stream(session_id)
|
241
472
|
end
|
242
473
|
end
|
243
474
|
|
244
|
-
@session_store.
|
475
|
+
@session_store.queue_message_for_session(session_id, data)
|
245
476
|
end
|
246
477
|
|
247
478
|
def cleanup_session(session_id)
|
248
|
-
|
479
|
+
@stream_registry.unregister_stream(session_id)
|
249
480
|
@session_store.cleanup_session(session_id)
|
481
|
+
@request_store.cleanup_session_requests(session_id)
|
250
482
|
end
|
251
483
|
|
252
|
-
def
|
253
|
-
|
254
|
-
@session_store.subscribe_to_server(@server_instance) do |data|
|
255
|
-
session_id = data["session_id"]
|
256
|
-
message = data["message"]
|
257
|
-
|
258
|
-
if @local_streams[session_id]
|
259
|
-
begin
|
260
|
-
send_to_stream(@local_streams[session_id], message)
|
261
|
-
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
262
|
-
cleanup_local_stream(session_id)
|
263
|
-
end
|
264
|
-
end
|
265
|
-
end
|
266
|
-
rescue => e
|
267
|
-
@configuration.logger.error("Redis subscriber error", error: e.message, backtrace: e.backtrace.first(5))
|
268
|
-
sleep 5
|
269
|
-
retry
|
270
|
-
end
|
484
|
+
def start_message_poller
|
485
|
+
@message_poller.start
|
271
486
|
end
|
272
487
|
|
273
488
|
def has_active_streams?
|
274
|
-
@
|
489
|
+
@stream_registry.has_any_local_streams?
|
275
490
|
end
|
276
491
|
|
277
492
|
def deliver_to_active_streams(notification)
|
278
|
-
@
|
493
|
+
@stream_registry.get_all_local_streams.each do |session_id, stream|
|
279
494
|
send_to_stream(stream, notification)
|
280
495
|
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
281
|
-
|
496
|
+
@stream_registry.unregister_stream(session_id)
|
282
497
|
end
|
283
498
|
end
|
284
499
|
|
285
500
|
def flush_notifications_to_stream(stream)
|
286
|
-
|
501
|
+
notifications = @notification_queue.pop_all
|
502
|
+
notifications.each do |notification|
|
287
503
|
send_to_stream(stream, notification)
|
288
504
|
end
|
289
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
|
290
530
|
end
|
291
531
|
end
|