vector_mcp 0.3.2 → 0.3.4

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -0
  3. data/lib/vector_mcp/definitions.rb +25 -9
  4. data/lib/vector_mcp/errors.rb +2 -6
  5. data/lib/vector_mcp/handlers/core.rb +12 -10
  6. data/lib/vector_mcp/image_util.rb +27 -2
  7. data/lib/vector_mcp/log_filter.rb +48 -0
  8. data/lib/vector_mcp/middleware/base.rb +1 -7
  9. data/lib/vector_mcp/middleware/manager.rb +3 -15
  10. data/lib/vector_mcp/request_context.rb +182 -0
  11. data/lib/vector_mcp/sampling/result.rb +11 -1
  12. data/lib/vector_mcp/security/middleware.rb +2 -28
  13. data/lib/vector_mcp/security/strategies/api_key.rb +29 -28
  14. data/lib/vector_mcp/security/strategies/jwt_token.rb +10 -5
  15. data/lib/vector_mcp/server/capabilities.rb +5 -7
  16. data/lib/vector_mcp/server/message_handling.rb +11 -5
  17. data/lib/vector_mcp/server.rb +21 -10
  18. data/lib/vector_mcp/session.rb +96 -6
  19. data/lib/vector_mcp/transport/base_session_manager.rb +320 -0
  20. data/lib/vector_mcp/transport/http_stream/event_store.rb +157 -0
  21. data/lib/vector_mcp/transport/http_stream/session_manager.rb +191 -0
  22. data/lib/vector_mcp/transport/http_stream/stream_handler.rb +270 -0
  23. data/lib/vector_mcp/transport/http_stream.rb +961 -0
  24. data/lib/vector_mcp/transport/sse/client_connection.rb +1 -1
  25. data/lib/vector_mcp/transport/sse/stream_manager.rb +1 -1
  26. data/lib/vector_mcp/transport/sse.rb +74 -19
  27. data/lib/vector_mcp/transport/sse_session_manager.rb +188 -0
  28. data/lib/vector_mcp/transport/stdio.rb +70 -13
  29. data/lib/vector_mcp/transport/stdio_session_manager.rb +181 -0
  30. data/lib/vector_mcp/util.rb +39 -1
  31. data/lib/vector_mcp/version.rb +1 -1
  32. data/lib/vector_mcp.rb +1 -0
  33. metadata +10 -1
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent-ruby"
4
+
5
+ module VectorMCP
6
+ module Transport
7
+ class HttpStream
8
+ # Handles Server-Sent Events streaming for HTTP transport.
9
+ #
10
+ # Manages:
11
+ # - SSE connection lifecycle
12
+ # - Event streaming with resumability
13
+ # - Last-Event-ID header processing
14
+ # - Connection health monitoring
15
+ #
16
+ # @api private
17
+ class StreamHandler
18
+ attr_reader :transport, :logger
19
+
20
+ # Streaming connection data structure
21
+ StreamingConnection = Struct.new(:session, :yielder, :thread, :closed) do
22
+ def close
23
+ self.closed = true
24
+ thread&.kill
25
+ end
26
+
27
+ def closed?
28
+ closed
29
+ end
30
+ end
31
+
32
+ # Initializes a new stream handler.
33
+ #
34
+ # @param transport [HttpStream] The parent transport instance
35
+ def initialize(transport)
36
+ @transport = transport
37
+ @logger = transport.logger
38
+ @active_connections = Concurrent::Hash.new
39
+ end
40
+
41
+ # Handles a streaming request (GET request for SSE).
42
+ #
43
+ # @param env [Hash] The Rack environment
44
+ # @param session [SessionManager::Session] The session for this request
45
+ # @return [Array] Rack response triplet for SSE
46
+ def handle_streaming_request(env, session)
47
+ last_event_id = extract_last_event_id(env)
48
+
49
+ logger.info("Starting SSE stream for session #{session.id}")
50
+
51
+ headers = build_sse_headers
52
+ body = create_sse_stream(session, last_event_id)
53
+
54
+ [200, headers, body]
55
+ end
56
+
57
+ # Sends a message to a specific session.
58
+ #
59
+ # @param session [SessionManager::Session] The target session
60
+ # @param message [Hash] The message to send
61
+ # @return [Boolean] True if message was sent successfully
62
+ def send_message_to_session(session, message)
63
+ return false unless session.streaming?
64
+
65
+ connection = @active_connections[session.id]
66
+ return false unless connection && !connection.closed?
67
+
68
+ begin
69
+ # Store event for resumability
70
+ event_data = message.to_json
71
+ event_id = @transport.event_store.store_event(event_data, "message", session_id: session.id)
72
+
73
+ # Send via SSE
74
+ sse_event = format_sse_event(event_data, "message", event_id)
75
+ connection.yielder << sse_event
76
+
77
+ logger.debug("Message sent to session #{session.id}")
78
+
79
+ true
80
+ rescue StandardError => e
81
+ logger.error("Error sending message to session #{session.id}: #{e.message}")
82
+
83
+ # Mark connection as closed and clean up
84
+ cleanup_connection(session)
85
+ false
86
+ end
87
+ end
88
+
89
+ # Gets the number of active streaming connections.
90
+ #
91
+ # @return [Integer] Number of active connections
92
+ def active_connection_count
93
+ @active_connections.size
94
+ end
95
+
96
+ # Cleans up all active connections.
97
+ #
98
+ # @return [void]
99
+ def cleanup_all_connections
100
+ logger.info("Cleaning up all streaming connections: #{@active_connections.size}")
101
+
102
+ @active_connections.each_value(&:close)
103
+
104
+ @active_connections.clear
105
+ end
106
+
107
+ private
108
+
109
+ # Extracts Last-Event-ID from request headers.
110
+ #
111
+ # @param env [Hash] The Rack environment
112
+ # @return [String, nil] The last event ID or nil
113
+ def extract_last_event_id(env)
114
+ env["HTTP_LAST_EVENT_ID"]
115
+ end
116
+
117
+ # Builds SSE response headers.
118
+ #
119
+ # @return [Hash] SSE headers
120
+ def build_sse_headers
121
+ {
122
+ "Content-Type" => "text/event-stream",
123
+ "Cache-Control" => "no-cache",
124
+ "Connection" => "keep-alive",
125
+ "X-Accel-Buffering" => "no",
126
+ "Access-Control-Allow-Origin" => "*",
127
+ "Access-Control-Allow-Headers" => "Last-Event-ID"
128
+ }
129
+ end
130
+
131
+ # Creates an SSE stream for a session.
132
+ #
133
+ # @param session [SessionManager::Session] The session
134
+ # @param last_event_id [String, nil] The last event ID for resumability
135
+ # @return [Enumerator] SSE stream enumerator
136
+ def create_sse_stream(session, last_event_id)
137
+ Enumerator.new do |yielder|
138
+ connection = StreamingConnection.new(session, yielder, nil, false)
139
+
140
+ # Register connection
141
+ @active_connections[session.id] = connection
142
+ @transport.session_manager.set_streaming_connection(session, connection)
143
+
144
+ # Start streaming thread
145
+ connection.thread = Thread.new do
146
+ stream_to_client(session, yielder, last_event_id)
147
+ rescue StandardError => e
148
+ logger.error("Error in streaming thread for #{session.id}: #{e.message}")
149
+ ensure
150
+ cleanup_connection(session)
151
+ end
152
+
153
+ # Keep connection alive until thread completes
154
+ connection.thread.join
155
+ end
156
+ end
157
+
158
+ # Streams events to a client.
159
+ #
160
+ # @param session [SessionManager::Session] The session
161
+ # @param yielder [Enumerator::Yielder] The SSE yielder
162
+ # @param last_event_id [String, nil] The last event ID for resumability
163
+ # @return [void]
164
+ def stream_to_client(session, yielder, last_event_id)
165
+ # Send initial connection event
166
+ connection_event = {
167
+ jsonrpc: "2.0",
168
+ method: "connection/established",
169
+ params: {
170
+ session_id: session.id,
171
+ timestamp: Time.now.iso8601
172
+ }
173
+ }
174
+
175
+ event_id = @transport.event_store.store_event(connection_event.to_json, "connection", session_id: session.id)
176
+ yielder << format_sse_event(connection_event.to_json, "connection", event_id)
177
+
178
+ # Replay missed events if resuming
179
+ replay_events(yielder, last_event_id, session) if last_event_id
180
+
181
+ # Send periodic keep-alive events
182
+ keep_alive_loop(session, yielder)
183
+ end
184
+
185
+ # Replays events after a specific event ID, scoped to the session.
186
+ #
187
+ # @param yielder [Enumerator::Yielder] The SSE yielder
188
+ # @param last_event_id [String] The last event ID received by client
189
+ # @param session [SessionManager::Session] The session to filter events for
190
+ # @return [void]
191
+ def replay_events(yielder, last_event_id, session)
192
+ missed_events = @transport.event_store.get_events_after(last_event_id, session_id: session.id)
193
+
194
+ logger.info("Replaying #{missed_events.length} missed events from #{last_event_id}")
195
+
196
+ missed_events.each do |event|
197
+ yielder << event.to_sse_format
198
+ end
199
+ end
200
+
201
+ # Keeps the connection alive with periodic heartbeat events.
202
+ #
203
+ # @param session [SessionManager::Session] The session
204
+ # @param yielder [Enumerator::Yielder] The SSE yielder
205
+ # @return [void]
206
+ def keep_alive_loop(session, yielder)
207
+ start_time = Time.now
208
+ max_duration = 300 # 5 minutes maximum connection time
209
+
210
+ loop do
211
+ sleep(30) # Send heartbeat every 30 seconds
212
+
213
+ connection = @active_connections[session.id]
214
+ break if connection.nil? || connection.closed?
215
+
216
+ # Check if connection has been alive too long
217
+ if Time.now - start_time > max_duration
218
+ logger.debug("Connection for #{session.id} reached maximum duration, closing")
219
+ break
220
+ end
221
+
222
+ # Send heartbeat
223
+ heartbeat_event = {
224
+ jsonrpc: "2.0",
225
+ method: "heartbeat",
226
+ params: { timestamp: Time.now.iso8601 }
227
+ }
228
+
229
+ begin
230
+ event_id = @transport.event_store.store_event(heartbeat_event.to_json, "heartbeat", session_id: session.id)
231
+ yielder << format_sse_event(heartbeat_event.to_json, "heartbeat", event_id)
232
+ rescue StandardError
233
+ logger.debug("Heartbeat failed for #{session.id}, connection likely closed")
234
+ break
235
+ end
236
+ end
237
+ end
238
+
239
+ # Formats data as an SSE event.
240
+ #
241
+ # @param data [String] The event data
242
+ # @param type [String] The event type
243
+ # @param event_id [String] The event ID
244
+ # @return [String] Formatted SSE event
245
+ def format_sse_event(data, type, event_id)
246
+ lines = []
247
+ lines << "id: #{event_id}"
248
+ lines << "event: #{type}" if type
249
+ lines << "data: #{data}"
250
+ lines << ""
251
+ "#{lines.join("\n")}\n"
252
+ end
253
+
254
+ # Cleans up a specific connection.
255
+ #
256
+ # @param session [SessionManager::Session] The session to clean up
257
+ # @return [void]
258
+ def cleanup_connection(session)
259
+ connection = @active_connections.delete(session.id)
260
+ return unless connection
261
+
262
+ connection.close
263
+ @transport.session_manager.remove_streaming_connection(session)
264
+
265
+ logger.debug("Streaming connection cleaned up for #{session.id}")
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end