vector_mcp 0.2.0 → 0.3.1

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +281 -0
  3. data/README.md +302 -373
  4. data/lib/vector_mcp/definitions.rb +3 -1
  5. data/lib/vector_mcp/errors.rb +24 -0
  6. data/lib/vector_mcp/handlers/core.rb +132 -6
  7. data/lib/vector_mcp/logging/component.rb +131 -0
  8. data/lib/vector_mcp/logging/configuration.rb +156 -0
  9. data/lib/vector_mcp/logging/constants.rb +21 -0
  10. data/lib/vector_mcp/logging/core.rb +175 -0
  11. data/lib/vector_mcp/logging/filters/component.rb +69 -0
  12. data/lib/vector_mcp/logging/filters/level.rb +23 -0
  13. data/lib/vector_mcp/logging/formatters/base.rb +52 -0
  14. data/lib/vector_mcp/logging/formatters/json.rb +83 -0
  15. data/lib/vector_mcp/logging/formatters/text.rb +72 -0
  16. data/lib/vector_mcp/logging/outputs/base.rb +64 -0
  17. data/lib/vector_mcp/logging/outputs/console.rb +35 -0
  18. data/lib/vector_mcp/logging/outputs/file.rb +157 -0
  19. data/lib/vector_mcp/logging.rb +71 -0
  20. data/lib/vector_mcp/security/auth_manager.rb +79 -0
  21. data/lib/vector_mcp/security/authorization.rb +96 -0
  22. data/lib/vector_mcp/security/middleware.rb +172 -0
  23. data/lib/vector_mcp/security/session_context.rb +147 -0
  24. data/lib/vector_mcp/security/strategies/api_key.rb +167 -0
  25. data/lib/vector_mcp/security/strategies/custom.rb +71 -0
  26. data/lib/vector_mcp/security/strategies/jwt_token.rb +118 -0
  27. data/lib/vector_mcp/security.rb +46 -0
  28. data/lib/vector_mcp/server/registry.rb +24 -0
  29. data/lib/vector_mcp/server.rb +141 -1
  30. data/lib/vector_mcp/transport/sse/client_connection.rb +113 -0
  31. data/lib/vector_mcp/transport/sse/message_handler.rb +166 -0
  32. data/lib/vector_mcp/transport/sse/puma_config.rb +77 -0
  33. data/lib/vector_mcp/transport/sse/stream_manager.rb +92 -0
  34. data/lib/vector_mcp/transport/sse.rb +119 -460
  35. data/lib/vector_mcp/version.rb +1 -1
  36. data/lib/vector_mcp.rb +35 -2
  37. metadata +63 -21
@@ -12,6 +12,13 @@ require_relative "util" # Needed if not using Handlers::Core
12
12
  require_relative "server/registry"
13
13
  require_relative "server/capabilities"
14
14
  require_relative "server/message_handling"
15
+ require_relative "security/auth_manager"
16
+ require_relative "security/authorization"
17
+ require_relative "security/middleware"
18
+ require_relative "security/session_context"
19
+ require_relative "security/strategies/api_key"
20
+ require_relative "security/strategies/jwt_token"
21
+ require_relative "security/strategies/custom"
15
22
 
16
23
  module VectorMCP
17
24
  # The `Server` class is the central component for an MCP server implementation.
@@ -62,7 +69,8 @@ module VectorMCP
62
69
  # The specific version of the Model Context Protocol this server implements.
63
70
  PROTOCOL_VERSION = "2024-11-05"
64
71
 
65
- attr_reader :logger, :name, :version, :protocol_version, :tools, :resources, :prompts, :roots, :in_flight_requests
72
+ attr_reader :logger, :name, :version, :protocol_version, :tools, :resources, :prompts, :roots, :in_flight_requests,
73
+ :auth_manager, :authorization, :security_middleware
66
74
  attr_accessor :transport
67
75
 
68
76
  # Initializes a new VectorMCP server.
@@ -108,6 +116,11 @@ module VectorMCP
108
116
  # Configure sampling capabilities
109
117
  @sampling_config = configure_sampling_capabilities(options[:sampling_config] || {})
110
118
 
119
+ # Initialize security components
120
+ @auth_manager = Security::AuthManager.new
121
+ @authorization = Security::Authorization.new
122
+ @security_middleware = Security::Middleware.new(@auth_manager, @authorization)
123
+
111
124
  setup_default_handlers
112
125
 
113
126
  @logger.info("Server instance '#{@name}' v#{@version} (MCP Protocol: #{@protocol_version}, Gem: v#{VectorMCP::VERSION}) initialized.")
@@ -148,6 +161,133 @@ module VectorMCP
148
161
  self.transport = active_transport
149
162
  active_transport.run
150
163
  end
164
+
165
+ # --- Security Configuration ---
166
+
167
+ # Enable authentication with specified strategy and configuration
168
+ # @param strategy [Symbol] the authentication strategy (:api_key, :jwt, :custom)
169
+ # @param options [Hash] strategy-specific configuration options
170
+ # @return [void]
171
+ def enable_authentication!(strategy: :api_key, **options, &block)
172
+ # Clear existing strategies when switching to a new configuration
173
+ clear_auth_strategies unless @auth_manager.strategies.empty?
174
+
175
+ @auth_manager.enable!(default_strategy: strategy)
176
+
177
+ case strategy
178
+ when :api_key
179
+ add_api_key_auth(options[:keys] || [])
180
+ when :jwt
181
+ add_jwt_auth(options)
182
+ when :custom
183
+ handler = block || options[:handler]
184
+ raise ArgumentError, "Custom authentication strategy requires a handler block" unless handler
185
+
186
+ add_custom_auth(&handler)
187
+
188
+ else
189
+ raise ArgumentError, "Unknown authentication strategy: #{strategy}"
190
+ end
191
+
192
+ @logger.info("Authentication enabled with strategy: #{strategy}")
193
+ end
194
+
195
+ # Disable authentication (return to pass-through mode)
196
+ # @return [void]
197
+ def disable_authentication!
198
+ @auth_manager.disable!
199
+ @logger.info("Authentication disabled")
200
+ end
201
+
202
+ # Enable authorization with optional policy configuration block
203
+ # @param block [Proc] optional block for configuring authorization policies
204
+ # @return [void]
205
+ def enable_authorization!(&)
206
+ @authorization.enable!
207
+ instance_eval(&) if block_given?
208
+ @logger.info("Authorization enabled")
209
+ end
210
+
211
+ # Disable authorization (return to pass-through mode)
212
+ # @return [void]
213
+ def disable_authorization!
214
+ @authorization.disable!
215
+ @logger.info("Authorization disabled")
216
+ end
217
+
218
+ # Add authorization policy for tools
219
+ # @param block [Proc] policy block that receives (user, action, tool)
220
+ # @return [void]
221
+ def authorize_tools(&)
222
+ @authorization.add_policy(:tool, &)
223
+ end
224
+
225
+ # Add authorization policy for resources
226
+ # @param block [Proc] policy block that receives (user, action, resource)
227
+ # @return [void]
228
+ def authorize_resources(&)
229
+ @authorization.add_policy(:resource, &)
230
+ end
231
+
232
+ # Add authorization policy for prompts
233
+ # @param block [Proc] policy block that receives (user, action, prompt)
234
+ # @return [void]
235
+ def authorize_prompts(&)
236
+ @authorization.add_policy(:prompt, &)
237
+ end
238
+
239
+ # Add authorization policy for roots
240
+ # @param block [Proc] policy block that receives (user, action, root)
241
+ # @return [void]
242
+ def authorize_roots(&)
243
+ @authorization.add_policy(:root, &)
244
+ end
245
+
246
+ # Check if security features are enabled
247
+ # @return [Boolean] true if authentication or authorization is enabled
248
+ def security_enabled?
249
+ @security_middleware.security_enabled?
250
+ end
251
+
252
+ # Get current security status for debugging/monitoring
253
+ # @return [Hash] security configuration status
254
+ def security_status
255
+ @security_middleware.security_status
256
+ end
257
+
258
+ private
259
+
260
+ # Add API key authentication strategy
261
+ # @param keys [Array<String>] array of valid API keys
262
+ # @return [void]
263
+ def add_api_key_auth(keys)
264
+ strategy = Security::Strategies::ApiKey.new(keys: keys)
265
+ @auth_manager.add_strategy(:api_key, strategy)
266
+ end
267
+
268
+ # Add JWT authentication strategy
269
+ # @param options [Hash] JWT configuration options
270
+ # @return [void]
271
+ def add_jwt_auth(options)
272
+ strategy = Security::Strategies::JwtToken.new(**options)
273
+ @auth_manager.add_strategy(:jwt, strategy)
274
+ end
275
+
276
+ # Add custom authentication strategy
277
+ # @param handler [Proc] custom authentication handler block
278
+ # @return [void]
279
+ def add_custom_auth(&)
280
+ strategy = Security::Strategies::Custom.new(&)
281
+ @auth_manager.add_strategy(:custom, strategy)
282
+ end
283
+
284
+ # Clear all authentication strategies
285
+ # @return [void]
286
+ def clear_auth_strategies
287
+ @auth_manager.strategies.each_key do |strategy_name|
288
+ @auth_manager.remove_strategy(strategy_name)
289
+ end
290
+ end
151
291
  end
152
292
 
153
293
  module Transport
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VectorMCP
4
+ module Transport
5
+ class SSE
6
+ # Manages individual client connection state for SSE transport.
7
+ # Each client connection has a unique session ID, message queue, and streaming thread.
8
+ class ClientConnection
9
+ attr_reader :session_id, :message_queue, :logger
10
+ attr_accessor :stream_thread, :stream_io
11
+
12
+ # Initializes a new client connection.
13
+ #
14
+ # @param session_id [String] Unique identifier for this client session
15
+ # @param logger [Logger] Logger instance for debugging and error reporting
16
+ def initialize(session_id, logger)
17
+ @session_id = session_id
18
+ @logger = logger
19
+ @message_queue = Queue.new
20
+ @stream_thread = nil
21
+ @stream_io = nil
22
+ @closed = false
23
+ @mutex = Mutex.new
24
+
25
+ logger.debug { "Client connection created: #{session_id}" }
26
+ end
27
+
28
+ # Checks if the connection is closed
29
+ #
30
+ # @return [Boolean] true if connection is closed
31
+ def closed?
32
+ @mutex.synchronize { @closed }
33
+ end
34
+
35
+ # Closes the client connection and cleans up resources.
36
+ # This method is thread-safe and can be called multiple times.
37
+ def close
38
+ @mutex.synchronize do
39
+ return if @closed
40
+
41
+ @closed = true
42
+ logger.debug { "Closing client connection: #{session_id}" }
43
+
44
+ # Close the message queue to signal streaming thread to stop
45
+ @message_queue.close if @message_queue.respond_to?(:close)
46
+
47
+ # Close the stream I/O if it exists
48
+ begin
49
+ @stream_io&.close
50
+ rescue StandardError => e
51
+ logger.warn { "Error closing stream I/O for #{session_id}: #{e.message}" }
52
+ end
53
+
54
+ # Stop the streaming thread
55
+ if @stream_thread&.alive?
56
+ @stream_thread.kill
57
+ @stream_thread.join(1) # Wait up to 1 second for clean shutdown
58
+ end
59
+
60
+ logger.debug { "Client connection closed: #{session_id}" }
61
+ end
62
+ end
63
+
64
+ # Enqueues a message to be sent to this client.
65
+ # This method is thread-safe.
66
+ #
67
+ # @param message [Hash] The JSON-RPC message to send
68
+ # @return [Boolean] true if message was enqueued successfully
69
+ def enqueue_message(message)
70
+ return false if closed?
71
+
72
+ begin
73
+ @message_queue.push(message)
74
+ logger.debug { "Message enqueued for client #{session_id}: #{message.inspect}" }
75
+ true
76
+ rescue ClosedQueueError
77
+ logger.warn { "Attempted to enqueue message to closed queue for client #{session_id}" }
78
+ false
79
+ rescue StandardError => e
80
+ logger.error { "Error enqueuing message for client #{session_id}: #{e.message}" }
81
+ false
82
+ end
83
+ end
84
+
85
+ # Dequeues the next message from the client's message queue.
86
+ # This method blocks until a message is available or the queue is closed.
87
+ #
88
+ # @return [Hash, nil] The next message, or nil if queue is closed
89
+ def dequeue_message
90
+ return nil if closed?
91
+
92
+ begin
93
+ @message_queue.pop
94
+ rescue ClosedQueueError
95
+ nil
96
+ rescue StandardError => e
97
+ logger.error { "Error dequeuing message for client #{session_id}: #{e.message}" }
98
+ nil
99
+ end
100
+ end
101
+
102
+ # Gets the current queue size
103
+ #
104
+ # @return [Integer] Number of messages waiting in the queue
105
+ def queue_size
106
+ @message_queue.size
107
+ rescue StandardError
108
+ 0
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module VectorMCP
6
+ module Transport
7
+ class SSE
8
+ # Handles JSON-RPC message processing for POST requests.
9
+ # Processes incoming messages and sends responses via SSE streams.
10
+ class MessageHandler
11
+ # Initializes a new message handler.
12
+ #
13
+ # @param server [VectorMCP::Server] The MCP server instance
14
+ # @param session [VectorMCP::Session] The server session
15
+ # @param logger [Logger] Logger instance for debugging
16
+ def initialize(server, session, logger)
17
+ @server = server
18
+ @session = session
19
+ @logger = logger
20
+ end
21
+
22
+ # Handles a POST message request from a client.
23
+ #
24
+ # @param env [Hash] Rack environment hash
25
+ # @param client_conn [ClientConnection] The client connection
26
+ # @return [Array] Rack response triplet
27
+ def handle_post_message(env, client_conn)
28
+ request_body = read_request_body(env)
29
+ return error_response(nil, -32_600, "Request body is empty") if request_body.nil? || request_body.empty?
30
+
31
+ message = parse_json_message(request_body, client_conn)
32
+ return message if message.is_a?(Array) # Error response
33
+
34
+ process_message(message, client_conn)
35
+ rescue VectorMCP::ProtocolError => e
36
+ @logger.error { "Protocol error for client #{client_conn.session_id}: #{e.message}" }
37
+ request_id = e.request_id || message&.dig("id")
38
+ enqueue_error_response(client_conn, request_id, e.code, e.message, e.details)
39
+ error_response(request_id, e.code, e.message, e.details)
40
+ rescue StandardError => e
41
+ @logger.error { "Unexpected error for client #{client_conn.session_id}: #{e.message}\n#{e.backtrace.join("\n")}" }
42
+ request_id = message&.dig("id")
43
+ enqueue_error_response(client_conn, request_id, -32_603, "Internal server error")
44
+ error_response(request_id, -32_603, "Internal server error")
45
+ end
46
+
47
+ private
48
+
49
+ # Reads the request body from the Rack environment.
50
+ #
51
+ # @param env [Hash] Rack environment
52
+ # @return [String, nil] Request body as string
53
+ def read_request_body(env)
54
+ input = env["rack.input"]
55
+ return nil unless input
56
+
57
+ body = input.read
58
+ input.rewind if input.respond_to?(:rewind)
59
+ body
60
+ end
61
+
62
+ # Parses JSON message from request body.
63
+ #
64
+ # @param body_str [String] JSON string from request body
65
+ # @param client_conn [ClientConnection] Client connection for error handling
66
+ # @return [Hash, Array] Parsed message or error response triplet
67
+ def parse_json_message(body_str, client_conn)
68
+ JSON.parse(body_str)
69
+ rescue JSON::ParserError => e
70
+ @logger.error { "JSON parse error for client #{client_conn.session_id}: #{e.message}" }
71
+ malformed_id = VectorMCP::Util.extract_id_from_invalid_json(body_str)
72
+ enqueue_error_response(client_conn, malformed_id, -32_700, "Parse error")
73
+ error_response(malformed_id, -32_700, "Parse error")
74
+ end
75
+
76
+ # Processes a valid JSON-RPC message.
77
+ #
78
+ # @param message [Hash] Parsed JSON-RPC message
79
+ # @param client_conn [ClientConnection] Client connection
80
+ # @return [Array] Rack response triplet
81
+ def process_message(message, client_conn)
82
+ # Handle the message through the server
83
+ response_data = @server.handle_message(message, @session, client_conn.session_id)
84
+
85
+ # If it's a request (has id), send response via SSE
86
+ if message["id"]
87
+ enqueue_success_response(client_conn, message["id"], response_data)
88
+ else
89
+ @logger.debug { "Processed notification for client #{client_conn.session_id}" }
90
+ end
91
+
92
+ # Always return 202 Accepted for valid POST messages
93
+ success_response(message["id"])
94
+ end
95
+
96
+ # Enqueues a successful response to the client's SSE stream.
97
+ #
98
+ # @param client_conn [ClientConnection] Client connection
99
+ # @param request_id [String, Integer] Original request ID
100
+ # @param result [Object] Response result data
101
+ def enqueue_success_response(client_conn, request_id, result)
102
+ response = {
103
+ jsonrpc: "2.0",
104
+ id: request_id,
105
+ result: result
106
+ }
107
+ StreamManager.enqueue_message(client_conn, response)
108
+ end
109
+
110
+ # Enqueues an error response to the client's SSE stream.
111
+ #
112
+ # @param client_conn [ClientConnection] Client connection
113
+ # @param request_id [String, Integer, nil] Original request ID
114
+ # @param code [Integer] Error code
115
+ # @param message [String] Error message
116
+ # @param data [Object, nil] Additional error data
117
+ def enqueue_error_response(client_conn, request_id, code, message, data = nil)
118
+ error_payload = { code: code, message: message }
119
+ error_payload[:data] = data if data
120
+
121
+ error_response = {
122
+ jsonrpc: "2.0",
123
+ id: request_id,
124
+ error: error_payload
125
+ }
126
+ StreamManager.enqueue_message(client_conn, error_response)
127
+ end
128
+
129
+ # Creates a successful HTTP response for the POST request.
130
+ #
131
+ # @param request_id [String, Integer, nil] Request ID
132
+ # @return [Array] Rack response triplet
133
+ def success_response(request_id)
134
+ body = { status: "accepted", id: request_id }.to_json
135
+ [202, { "Content-Type" => "application/json" }, [body]]
136
+ end
137
+
138
+ # Creates an error HTTP response for the POST request.
139
+ #
140
+ # @param id [String, Integer, nil] Request ID
141
+ # @param code [Integer] Error code
142
+ # @param message [String] Error message
143
+ # @param data [Object, nil] Additional error data
144
+ # @return [Array] Rack response triplet
145
+ def error_response(id, code, message, data = nil)
146
+ status = case code
147
+ when -32_700, -32_600, -32_602 then 400 # Parse, Invalid Request, Invalid Params
148
+ when -32_601, -32_001 then 404 # Method Not Found, Not Found
149
+ else 500 # Internal Error, Server Error
150
+ end
151
+
152
+ error_payload = { code: code, message: message }
153
+ error_payload[:data] = data if data
154
+
155
+ body = {
156
+ jsonrpc: "2.0",
157
+ id: id,
158
+ error: error_payload
159
+ }.to_json
160
+
161
+ [status, { "Content-Type" => "application/json" }, [body]]
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VectorMCP
4
+ module Transport
5
+ class SSE
6
+ # Configures Puma server for production-ready SSE transport.
7
+ # Handles server setup, threading, and resource management.
8
+ class PumaConfig
9
+ attr_reader :host, :port, :logger
10
+
11
+ # Initializes Puma configuration.
12
+ #
13
+ # @param host [String] Host to bind to
14
+ # @param port [Integer] Port to listen on
15
+ # @param logger [Logger] Logger instance
16
+ def initialize(host, port, logger)
17
+ @host = host
18
+ @port = port
19
+ @logger = logger
20
+ end
21
+
22
+ # Configures a Puma server instance.
23
+ #
24
+ # @param server [Puma::Server] The Puma server to configure
25
+ def configure(server)
26
+ server.add_tcp_listener(host, port)
27
+
28
+ # Configure threading for production use
29
+ configure_threading(server)
30
+
31
+ # Set up server options
32
+ configure_server_options(server)
33
+
34
+ logger.debug { "Puma server configured for #{host}:#{port}" }
35
+ end
36
+
37
+ private
38
+
39
+ # Configures threading parameters for optimal performance.
40
+ #
41
+ # @param server [Puma::Server] The Puma server
42
+ def configure_threading(server)
43
+ # Set thread pool size based on CPU cores and expected load
44
+ min_threads = 2
45
+ max_threads = [4, Etc.nprocessors * 2].max
46
+
47
+ # Puma 6.x does not expose min_threads= and max_threads= as public API.
48
+ # Thread pool sizing should be set via Puma DSL/config before server creation.
49
+ # For legacy compatibility, set if possible, otherwise log a warning.
50
+ if server.respond_to?(:min_threads=) && server.respond_to?(:max_threads=)
51
+ server.min_threads = min_threads
52
+ server.max_threads = max_threads
53
+ logger.debug { "Puma configured with #{min_threads}-#{max_threads} threads" }
54
+ else
55
+ logger.warn { "Puma::Server does not support direct thread pool sizing; set threads via Puma config DSL before server creation." }
56
+ end
57
+ end
58
+
59
+ # Configures server-specific options.
60
+ #
61
+ # @param server [Puma::Server] The Puma server
62
+ def configure_server_options(server)
63
+ # Set server-specific options for SSE handling
64
+ server.leak_stack_on_error = false if server.respond_to?(:leak_stack_on_error=)
65
+
66
+ # Configure timeouts appropriate for SSE connections
67
+ # SSE connections should be long-lived, so we set generous timeouts
68
+ if server.respond_to?(:first_data_timeout=)
69
+ server.first_data_timeout = 30 # 30 seconds to send first data
70
+ end
71
+
72
+ logger.debug { "Puma server options configured for SSE transport" }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VectorMCP
4
+ module Transport
5
+ class SSE
6
+ # Manages Server-Sent Events streaming for client connections.
7
+ # Handles creation of streaming responses and message broadcasting.
8
+ class StreamManager
9
+ class << self
10
+ # Creates an SSE streaming response body for a client connection.
11
+ #
12
+ # @param client_conn [ClientConnection] The client connection to stream to
13
+ # @param endpoint_url [String] The URL for the client to POST messages to
14
+ # @param logger [Logger] Logger instance for debugging
15
+ # @return [Enumerator] Rack-compatible streaming response body
16
+ def create_sse_stream(client_conn, endpoint_url, logger)
17
+ Enumerator.new do |yielder|
18
+ # Send initial endpoint event
19
+ yielder << format_sse_event("endpoint", endpoint_url)
20
+ logger.debug { "Sent endpoint event to client #{client_conn.session_id}: #{endpoint_url}" }
21
+
22
+ # Start streaming thread for this client
23
+ client_conn.stream_thread = Thread.new do
24
+ stream_messages_to_client(client_conn, yielder, logger)
25
+ end
26
+
27
+ # Keep the connection alive by yielding from the streaming thread
28
+ client_conn.stream_thread.join
29
+ rescue StandardError => e
30
+ logger.error { "Error in SSE stream for client #{client_conn.session_id}: #{e.message}\n#{e.backtrace.join("\n")}" }
31
+ ensure
32
+ logger.debug { "SSE stream ended for client #{client_conn.session_id}" }
33
+ client_conn.close
34
+ end
35
+ end
36
+
37
+ # Enqueues a message to a specific client connection.
38
+ #
39
+ # @param client_conn [ClientConnection] The target client connection
40
+ # @param message [Hash] The JSON-RPC message to send
41
+ # @return [Boolean] true if message was enqueued successfully
42
+ def enqueue_message(client_conn, message)
43
+ return false unless client_conn && !client_conn.closed?
44
+
45
+ client_conn.enqueue_message(message)
46
+ end
47
+
48
+ private
49
+
50
+ # Streams messages from a client's queue to the SSE connection.
51
+ # This method runs in a dedicated thread per client.
52
+ #
53
+ # @param client_conn [ClientConnection] The client connection
54
+ # @param yielder [Enumerator::Yielder] The response yielder
55
+ # @param logger [Logger] Logger instance
56
+ def stream_messages_to_client(client_conn, yielder, logger)
57
+ logger.debug { "Starting message streaming thread for client #{client_conn.session_id}" }
58
+
59
+ loop do
60
+ message = client_conn.dequeue_message
61
+ break if message.nil? # Queue closed or connection closed
62
+
63
+ begin
64
+ json_message = message.to_json
65
+ sse_data = format_sse_event("message", json_message)
66
+ yielder << sse_data
67
+
68
+ logger.debug { "Streamed message to client #{client_conn.session_id}: #{json_message}" }
69
+ rescue StandardError => e
70
+ logger.error { "Error streaming message to client #{client_conn.session_id}: #{e.message}" }
71
+ break
72
+ end
73
+ end
74
+
75
+ logger.debug { "Message streaming thread ended for client #{client_conn.session_id}" }
76
+ rescue StandardError => e
77
+ logger.error { "Fatal error in streaming thread for client #{client_conn.session_id}: #{e.message}\n#{e.backtrace.join("\n")}" }
78
+ end
79
+
80
+ # Formats data as a Server-Sent Event.
81
+ #
82
+ # @param event [String] The event type
83
+ # @param data [String] The event data
84
+ # @return [String] Properly formatted SSE event
85
+ def format_sse_event(event, data)
86
+ "event: #{event}\ndata: #{data}\n\n"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end