mcp 0.10.0 → 0.11.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/README.md +215 -1
- data/lib/json_rpc_handler.rb +1 -1
- data/lib/mcp/client.rb +62 -48
- data/lib/mcp/progress.rb +3 -1
- data/lib/mcp/server/transports/stdio_transport.rb +35 -0
- data/lib/mcp/server/transports/streamable_http_transport.rb +266 -75
- data/lib/mcp/server.rb +181 -23
- data/lib/mcp/server_context.rb +16 -2
- data/lib/mcp/server_session.rb +35 -7
- data/lib/mcp/tool/schema.rb +1 -14
- data/lib/mcp/transport.rb +13 -0
- data/lib/mcp/version.rb +1 -1
- metadata +5 -3
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require "securerandom"
|
|
5
4
|
require_relative "../../transport"
|
|
6
5
|
|
|
7
6
|
module MCP
|
|
8
7
|
class Server
|
|
9
8
|
module Transports
|
|
10
9
|
class StreamableHTTPTransport < Transport
|
|
10
|
+
SSE_HEADERS = {
|
|
11
|
+
"Content-Type" => "text/event-stream",
|
|
12
|
+
"Cache-Control" => "no-cache",
|
|
13
|
+
"Connection" => "keep-alive",
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
11
16
|
def initialize(server, stateless: false, session_idle_timeout: nil)
|
|
12
17
|
super(server)
|
|
13
18
|
# Maps `session_id` to `{ stream: stream_object, server_session: ServerSession, last_active_at: float_from_monotonic_clock }`.
|
|
@@ -16,6 +21,7 @@ module MCP
|
|
|
16
21
|
|
|
17
22
|
@stateless = stateless
|
|
18
23
|
@session_idle_timeout = session_idle_timeout
|
|
24
|
+
@pending_responses = {}
|
|
19
25
|
|
|
20
26
|
if @session_idle_timeout
|
|
21
27
|
if @stateless
|
|
@@ -50,12 +56,17 @@ module MCP
|
|
|
50
56
|
@reaper_thread&.kill
|
|
51
57
|
@reaper_thread = nil
|
|
52
58
|
|
|
53
|
-
@mutex.synchronize do
|
|
54
|
-
@sessions.each_key { |session_id| cleanup_session_unsafe(session_id) }
|
|
59
|
+
removed_sessions = @mutex.synchronize do
|
|
60
|
+
@sessions.each_key.filter_map { |session_id| cleanup_session_unsafe(session_id) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
removed_sessions.each do |session|
|
|
64
|
+
close_stream_safely(session[:stream])
|
|
65
|
+
close_post_request_streams(session)
|
|
55
66
|
end
|
|
56
67
|
end
|
|
57
68
|
|
|
58
|
-
def send_notification(method, params = nil, session_id: nil)
|
|
69
|
+
def send_notification(method, params = nil, session_id: nil, related_request_id: nil)
|
|
59
70
|
# Stateless mode doesn't support notifications
|
|
60
71
|
raise "Stateless mode does not support notifications" if @stateless
|
|
61
72
|
|
|
@@ -65,26 +76,35 @@ module MCP
|
|
|
65
76
|
}
|
|
66
77
|
notification[:params] = params if params
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
streams_to_close = []
|
|
80
|
+
|
|
81
|
+
result = @mutex.synchronize do
|
|
69
82
|
if session_id
|
|
70
83
|
# Send to specific session
|
|
71
|
-
session = @sessions[session_id]
|
|
72
|
-
|
|
84
|
+
if (session = @sessions[session_id])
|
|
85
|
+
stream = active_stream(session, related_request_id: related_request_id)
|
|
86
|
+
end
|
|
87
|
+
next false unless stream
|
|
73
88
|
|
|
74
89
|
if session_expired?(session)
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
cleanup_and_collect_stream(session_id, streams_to_close)
|
|
91
|
+
next false
|
|
77
92
|
end
|
|
78
93
|
|
|
79
94
|
begin
|
|
80
|
-
send_to_stream(
|
|
95
|
+
send_to_stream(stream, notification)
|
|
81
96
|
true
|
|
82
97
|
rescue *STREAM_WRITE_ERRORS => e
|
|
83
98
|
MCP.configuration.exception_reporter.call(
|
|
84
99
|
e,
|
|
85
100
|
{ session_id: session_id, error: "Failed to send notification" },
|
|
86
101
|
)
|
|
87
|
-
|
|
102
|
+
if related_request_id && session[:post_request_streams]&.key?(related_request_id)
|
|
103
|
+
session[:post_request_streams].delete(related_request_id)
|
|
104
|
+
streams_to_close << stream
|
|
105
|
+
else
|
|
106
|
+
cleanup_and_collect_stream(session_id, streams_to_close)
|
|
107
|
+
end
|
|
88
108
|
false
|
|
89
109
|
end
|
|
90
110
|
else
|
|
@@ -93,7 +113,7 @@ module MCP
|
|
|
93
113
|
failed_sessions = []
|
|
94
114
|
|
|
95
115
|
@sessions.each do |sid, session|
|
|
96
|
-
next unless session[:stream]
|
|
116
|
+
next unless (stream = session[:stream])
|
|
97
117
|
|
|
98
118
|
if session_expired?(session)
|
|
99
119
|
failed_sessions << sid
|
|
@@ -101,7 +121,7 @@ module MCP
|
|
|
101
121
|
end
|
|
102
122
|
|
|
103
123
|
begin
|
|
104
|
-
send_to_stream(
|
|
124
|
+
send_to_stream(stream, notification)
|
|
105
125
|
sent_count += 1
|
|
106
126
|
rescue *STREAM_WRITE_ERRORS => e
|
|
107
127
|
MCP.configuration.exception_reporter.call(
|
|
@@ -113,11 +133,93 @@ module MCP
|
|
|
113
133
|
end
|
|
114
134
|
|
|
115
135
|
# Clean up failed sessions
|
|
116
|
-
failed_sessions.each { |sid|
|
|
136
|
+
failed_sessions.each { |sid| cleanup_and_collect_stream(sid, streams_to_close) }
|
|
117
137
|
|
|
118
138
|
sent_count
|
|
119
139
|
end
|
|
120
140
|
end
|
|
141
|
+
|
|
142
|
+
streams_to_close.each do |stream|
|
|
143
|
+
close_stream_safely(stream)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
result
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Sends a server-to-client JSON-RPC request (e.g., `sampling/createMessage`) and
|
|
150
|
+
# blocks until the client responds.
|
|
151
|
+
#
|
|
152
|
+
# Uses a `Queue` for cross-thread synchronization. This method creates a `Queue`,
|
|
153
|
+
# sends the request via SSE stream, then blocks on `queue.pop`.
|
|
154
|
+
# When the client POSTs a response, `handle_response` matches it by `request_id`
|
|
155
|
+
# and pushes the result onto the queue, unblocking this thread.
|
|
156
|
+
def send_request(method, params = nil, session_id: nil, related_request_id: nil)
|
|
157
|
+
if @stateless
|
|
158
|
+
raise "Stateless mode does not support server-to-client requests."
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
unless session_id
|
|
162
|
+
raise "session_id is required for server-to-client requests."
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
request_id = generate_request_id
|
|
166
|
+
queue = Queue.new
|
|
167
|
+
|
|
168
|
+
request = { jsonrpc: "2.0", id: request_id, method: method }
|
|
169
|
+
request[:params] = params if params
|
|
170
|
+
|
|
171
|
+
sent = false
|
|
172
|
+
|
|
173
|
+
@mutex.synchronize do
|
|
174
|
+
unless (session = @sessions[session_id])
|
|
175
|
+
raise "Session not found: #{session_id}."
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
@pending_responses[request_id] = { queue: queue, session_id: session_id }
|
|
179
|
+
|
|
180
|
+
if (stream = active_stream(session, related_request_id: related_request_id))
|
|
181
|
+
begin
|
|
182
|
+
send_to_stream(stream, request)
|
|
183
|
+
sent = true
|
|
184
|
+
rescue *STREAM_WRITE_ERRORS
|
|
185
|
+
if related_request_id && session[:post_request_streams]&.key?(related_request_id)
|
|
186
|
+
session[:post_request_streams].delete(related_request_id)
|
|
187
|
+
close_stream_safely(stream)
|
|
188
|
+
else
|
|
189
|
+
cleanup_session_unsafe(session_id)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# TODO: Replace with event store + replay when resumability is implemented.
|
|
196
|
+
# Resumability is a separate MCP specification feature (SSE event IDs, Last-Event-ID replay,
|
|
197
|
+
# event store management) independent of sampling.
|
|
198
|
+
# See: https://modelcontextprotocol.io/specification/latest/basic/transports#resumability-and-redelivery
|
|
199
|
+
#
|
|
200
|
+
# The TypeScript and Python SDKs buffer messages and replay on reconnect.
|
|
201
|
+
# Until then, raise to prevent queue.pop from blocking indefinitely.
|
|
202
|
+
unless sent
|
|
203
|
+
raise "No active stream for #{method} request."
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
response = queue.pop
|
|
207
|
+
|
|
208
|
+
if response.is_a?(Hash) && response.key?(:error)
|
|
209
|
+
raise StandardError, "Client returned an error for #{method} request (code: #{response[:error][:code]}): #{response[:error][:message]}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if response == :session_closed
|
|
213
|
+
raise "SSE session closed while waiting for #{method} response."
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
response
|
|
217
|
+
ensure
|
|
218
|
+
if request_id
|
|
219
|
+
@mutex.synchronize do
|
|
220
|
+
@pending_responses.delete(request_id)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
121
223
|
end
|
|
122
224
|
|
|
123
225
|
private
|
|
@@ -136,22 +238,17 @@ module MCP
|
|
|
136
238
|
def reap_expired_sessions
|
|
137
239
|
return unless @session_idle_timeout
|
|
138
240
|
|
|
139
|
-
|
|
140
|
-
@sessions.
|
|
141
|
-
next unless session_expired?(
|
|
241
|
+
removed_sessions = @mutex.synchronize do
|
|
242
|
+
@sessions.each_key.filter_map do |session_id|
|
|
243
|
+
next unless session_expired?(@sessions[session_id])
|
|
142
244
|
|
|
143
|
-
|
|
144
|
-
@sessions.delete(session_id)
|
|
245
|
+
cleanup_session_unsafe(session_id)
|
|
145
246
|
end
|
|
146
247
|
end
|
|
147
248
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
# and will not attempt to close the same stream.
|
|
152
|
-
stream.close
|
|
153
|
-
rescue
|
|
154
|
-
nil
|
|
249
|
+
removed_sessions.each do |session|
|
|
250
|
+
close_stream_safely(session[:stream])
|
|
251
|
+
close_post_request_streams(session)
|
|
155
252
|
end
|
|
156
253
|
end
|
|
157
254
|
|
|
@@ -176,15 +273,19 @@ module MCP
|
|
|
176
273
|
body = parse_request_body(body_string)
|
|
177
274
|
return body unless body.is_a?(Hash) # Error response
|
|
178
275
|
|
|
179
|
-
if body[
|
|
276
|
+
if body[:method] == "initialize"
|
|
180
277
|
handle_initialization(body_string, body)
|
|
181
278
|
else
|
|
182
279
|
return missing_session_id_response if !@stateless && !session_id
|
|
183
280
|
|
|
184
|
-
if notification?(body)
|
|
281
|
+
if notification?(body)
|
|
185
282
|
handle_accepted
|
|
283
|
+
elsif response?(body)
|
|
284
|
+
return session_not_found_response if !@stateless && !session_exists?(session_id)
|
|
285
|
+
|
|
286
|
+
handle_response(body, session_id: session_id)
|
|
186
287
|
else
|
|
187
|
-
handle_regular_request(body_string, session_id)
|
|
288
|
+
handle_regular_request(body_string, session_id, related_request_id: body[:id])
|
|
188
289
|
end
|
|
189
290
|
end
|
|
190
291
|
rescue StandardError => e
|
|
@@ -228,21 +329,51 @@ module MCP
|
|
|
228
329
|
end
|
|
229
330
|
|
|
230
331
|
def cleanup_session(session_id)
|
|
231
|
-
@mutex.synchronize do
|
|
332
|
+
session = @mutex.synchronize do
|
|
232
333
|
cleanup_session_unsafe(session_id)
|
|
233
334
|
end
|
|
335
|
+
|
|
336
|
+
if session
|
|
337
|
+
close_stream_safely(session[:stream])
|
|
338
|
+
close_post_request_streams(session)
|
|
339
|
+
end
|
|
234
340
|
end
|
|
235
341
|
|
|
342
|
+
# Removes a session from `@sessions` and returns it. Does not close the stream.
|
|
343
|
+
# Callers must close the stream outside the mutex to avoid holding the lock during
|
|
344
|
+
# potentially blocking I/O.
|
|
236
345
|
def cleanup_session_unsafe(session_id)
|
|
237
|
-
session = @sessions
|
|
238
|
-
return unless session
|
|
346
|
+
session = @sessions.delete(session_id)
|
|
239
347
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
348
|
+
# Unblock threads waiting on pending responses for this session.
|
|
349
|
+
@pending_responses.each_value do |pending_response|
|
|
350
|
+
if pending_response[:session_id] == session_id
|
|
351
|
+
pending_response[:queue].push(:session_closed)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
session
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def cleanup_and_collect_stream(session_id, streams_to_close)
|
|
359
|
+
return unless (removed = cleanup_session_unsafe(session_id))
|
|
360
|
+
|
|
361
|
+
streams_to_close << removed[:stream]
|
|
362
|
+
removed[:post_request_streams]&.each_value { |stream| streams_to_close << stream }
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def close_stream_safely(stream)
|
|
366
|
+
stream&.close
|
|
367
|
+
rescue StandardError
|
|
368
|
+
# Ignore close-related errors from already closed/broken streams.
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def close_post_request_streams(session)
|
|
372
|
+
return unless (post_request_streams = session[:post_request_streams])
|
|
373
|
+
|
|
374
|
+
post_request_streams.each_value do |stream|
|
|
375
|
+
close_stream_safely(stream)
|
|
244
376
|
end
|
|
245
|
-
@sessions.delete(session_id)
|
|
246
377
|
end
|
|
247
378
|
|
|
248
379
|
def extract_session_id(request)
|
|
@@ -277,17 +408,35 @@ module MCP
|
|
|
277
408
|
end
|
|
278
409
|
|
|
279
410
|
def parse_request_body(body_string)
|
|
280
|
-
JSON.parse(body_string)
|
|
411
|
+
JSON.parse(body_string, symbolize_names: true)
|
|
281
412
|
rescue JSON::ParserError, TypeError
|
|
282
413
|
[400, { "Content-Type" => "application/json" }, [{ error: "Invalid JSON" }.to_json]]
|
|
283
414
|
end
|
|
284
415
|
|
|
285
416
|
def notification?(body)
|
|
286
|
-
!body[
|
|
417
|
+
!body[:id] && !!body[:method]
|
|
287
418
|
end
|
|
288
419
|
|
|
289
420
|
def response?(body)
|
|
290
|
-
!!body[
|
|
421
|
+
!!body[:id] && !body[:method]
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Verifies that the response came from the expected session to prevent
|
|
425
|
+
# cross-session response injection if request IDs are ever leaked.
|
|
426
|
+
def handle_response(body, session_id:)
|
|
427
|
+
request_id = body[:id]
|
|
428
|
+
@mutex.synchronize do
|
|
429
|
+
if (pending_response = @pending_responses[request_id]) && pending_response[:session_id] == session_id
|
|
430
|
+
if body.key?(:error)
|
|
431
|
+
error = body[:error]
|
|
432
|
+
pending_response[:queue].push(error: { code: error[:code], message: error[:message] })
|
|
433
|
+
else
|
|
434
|
+
pending_response[:queue].push(body[:result])
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
handle_accepted
|
|
291
440
|
end
|
|
292
441
|
|
|
293
442
|
def handle_initialization(body_string, body)
|
|
@@ -326,9 +475,8 @@ module MCP
|
|
|
326
475
|
[202, {}, []]
|
|
327
476
|
end
|
|
328
477
|
|
|
329
|
-
def handle_regular_request(body_string, session_id)
|
|
478
|
+
def handle_regular_request(body_string, session_id, related_request_id: nil)
|
|
330
479
|
server_session = nil
|
|
331
|
-
stream = nil
|
|
332
480
|
|
|
333
481
|
unless @stateless
|
|
334
482
|
if session_id
|
|
@@ -338,57 +486,106 @@ module MCP
|
|
|
338
486
|
@mutex.synchronize do
|
|
339
487
|
session = @sessions[session_id]
|
|
340
488
|
server_session = session[:server_session] if session
|
|
341
|
-
stream = session[:stream] if session
|
|
342
489
|
end
|
|
343
490
|
end
|
|
344
491
|
end
|
|
345
492
|
|
|
346
|
-
|
|
347
|
-
|
|
493
|
+
if session_id && !@stateless
|
|
494
|
+
handle_request_with_sse_response(body_string, session_id, server_session, related_request_id: related_request_id)
|
|
348
495
|
else
|
|
349
|
-
|
|
496
|
+
response = dispatch_handle_json(body_string, server_session)
|
|
497
|
+
[200, { "Content-Type" => "application/json" }, [response]]
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Returns the POST response as an SSE stream so the server can send
|
|
502
|
+
# JSON-RPC requests and notifications during request processing.
|
|
503
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server
|
|
504
|
+
def handle_request_with_sse_response(body_string, session_id, server_session, related_request_id: nil)
|
|
505
|
+
body = proc do |stream|
|
|
506
|
+
@mutex.synchronize do
|
|
507
|
+
session = @sessions[session_id]
|
|
508
|
+
if session && related_request_id
|
|
509
|
+
session[:post_request_streams] ||= {}
|
|
510
|
+
session[:post_request_streams][related_request_id] = stream
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
begin
|
|
515
|
+
response = dispatch_handle_json(body_string, server_session)
|
|
516
|
+
|
|
517
|
+
send_to_stream(stream, response) if response
|
|
518
|
+
ensure
|
|
519
|
+
if related_request_id
|
|
520
|
+
@mutex.synchronize do
|
|
521
|
+
session = @sessions[session_id]
|
|
522
|
+
session[:post_request_streams]&.delete(related_request_id) if session
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
begin
|
|
527
|
+
stream.close
|
|
528
|
+
rescue StandardError
|
|
529
|
+
# Ignore close-related errors from already closed/broken streams.
|
|
530
|
+
end
|
|
531
|
+
end
|
|
350
532
|
end
|
|
351
533
|
|
|
352
|
-
|
|
353
|
-
|
|
534
|
+
[200, SSE_HEADERS, body]
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Returns the SSE stream available for server-to-client messages.
|
|
538
|
+
# When `related_request_id` is given, returns only the POST response
|
|
539
|
+
# stream for that request (no fallback to GET SSE). This prevents
|
|
540
|
+
# request-scoped messages from leaking to the wrong stream.
|
|
541
|
+
# When `related_request_id` is nil, returns the GET SSE stream.
|
|
542
|
+
def active_stream(session, related_request_id: nil)
|
|
543
|
+
if related_request_id
|
|
544
|
+
session.dig(:post_request_streams, related_request_id)
|
|
354
545
|
else
|
|
355
|
-
[
|
|
546
|
+
session[:stream]
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def dispatch_handle_json(body_string, server_session)
|
|
551
|
+
if server_session
|
|
552
|
+
server_session.handle_json(body_string)
|
|
553
|
+
else
|
|
554
|
+
@server.handle_json(body_string)
|
|
356
555
|
end
|
|
357
556
|
end
|
|
358
557
|
|
|
359
558
|
def validate_and_touch_session(session_id)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
559
|
+
removed = nil
|
|
560
|
+
|
|
561
|
+
response = @mutex.synchronize do
|
|
562
|
+
next session_not_found_response unless (session = @sessions[session_id])
|
|
563
|
+
next unless @session_idle_timeout
|
|
363
564
|
|
|
364
565
|
if session_expired?(session)
|
|
365
|
-
cleanup_session_unsafe(session_id)
|
|
366
|
-
|
|
566
|
+
removed = cleanup_session_unsafe(session_id)
|
|
567
|
+
next session_not_found_response
|
|
367
568
|
end
|
|
368
569
|
|
|
369
570
|
session[:last_active_at] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
571
|
+
nil
|
|
370
572
|
end
|
|
371
573
|
|
|
372
|
-
|
|
574
|
+
if removed
|
|
575
|
+
close_stream_safely(removed[:stream])
|
|
576
|
+
|
|
577
|
+
removed[:post_request_streams]&.each_value do |stream|
|
|
578
|
+
close_stream_safely(stream)
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
response
|
|
373
583
|
end
|
|
374
584
|
|
|
375
585
|
def get_session_stream(session_id)
|
|
376
586
|
@mutex.synchronize { @sessions[session_id]&.fetch(:stream, nil) }
|
|
377
587
|
end
|
|
378
588
|
|
|
379
|
-
def send_response_to_stream(stream, response, session_id)
|
|
380
|
-
message = JSON.parse(response)
|
|
381
|
-
send_to_stream(stream, message)
|
|
382
|
-
handle_accepted
|
|
383
|
-
rescue *STREAM_WRITE_ERRORS => e
|
|
384
|
-
MCP.configuration.exception_reporter.call(
|
|
385
|
-
e,
|
|
386
|
-
{ session_id: session_id, error: "Stream closed during response" },
|
|
387
|
-
)
|
|
388
|
-
cleanup_session(session_id)
|
|
389
|
-
[200, { "Content-Type" => "application/json" }, [response]]
|
|
390
|
-
end
|
|
391
|
-
|
|
392
589
|
def session_exists?(session_id)
|
|
393
590
|
@mutex.synchronize { @sessions.key?(session_id) }
|
|
394
591
|
end
|
|
@@ -416,13 +613,7 @@ module MCP
|
|
|
416
613
|
def setup_sse_stream(session_id)
|
|
417
614
|
body = create_sse_body(session_id)
|
|
418
615
|
|
|
419
|
-
|
|
420
|
-
"Content-Type" => "text/event-stream",
|
|
421
|
-
"Cache-Control" => "no-cache",
|
|
422
|
-
"Connection" => "keep-alive",
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
[200, headers, body]
|
|
616
|
+
[200, SSE_HEADERS, body]
|
|
426
617
|
end
|
|
427
618
|
|
|
428
619
|
def create_sse_body(session_id)
|