vector_mcp 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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +147 -337
  4. data/lib/vector_mcp/definitions.rb +30 -0
  5. data/lib/vector_mcp/handlers/core.rb +78 -81
  6. data/lib/vector_mcp/image_util.rb +34 -11
  7. data/lib/vector_mcp/middleware/anonymizer.rb +186 -0
  8. data/lib/vector_mcp/middleware/base.rb +1 -5
  9. data/lib/vector_mcp/middleware/context.rb +11 -1
  10. data/lib/vector_mcp/middleware/hook.rb +7 -24
  11. data/lib/vector_mcp/middleware.rb +26 -9
  12. data/lib/vector_mcp/rails/tool.rb +85 -0
  13. data/lib/vector_mcp/request_context.rb +1 -1
  14. data/lib/vector_mcp/security/auth_manager.rb +12 -13
  15. data/lib/vector_mcp/security/auth_result.rb +33 -0
  16. data/lib/vector_mcp/security/authorization.rb +5 -9
  17. data/lib/vector_mcp/security/middleware.rb +2 -2
  18. data/lib/vector_mcp/security/session_context.rb +11 -27
  19. data/lib/vector_mcp/security/strategies/api_key.rb +1 -5
  20. data/lib/vector_mcp/security/strategies/custom.rb +10 -37
  21. data/lib/vector_mcp/security/strategies/jwt_token.rb +1 -10
  22. data/lib/vector_mcp/server/capabilities.rb +22 -32
  23. data/lib/vector_mcp/server/message_handling.rb +21 -14
  24. data/lib/vector_mcp/server/registry.rb +102 -120
  25. data/lib/vector_mcp/server.rb +98 -57
  26. data/lib/vector_mcp/session.rb +5 -3
  27. data/lib/vector_mcp/token_store.rb +80 -0
  28. data/lib/vector_mcp/tool.rb +221 -0
  29. data/lib/vector_mcp/transport/base_session_manager.rb +1 -17
  30. data/lib/vector_mcp/transport/http_stream/event_store.rb +29 -17
  31. data/lib/vector_mcp/transport/http_stream/session_manager.rb +41 -36
  32. data/lib/vector_mcp/transport/http_stream/stream_handler.rb +132 -47
  33. data/lib/vector_mcp/transport/http_stream.rb +242 -124
  34. data/lib/vector_mcp/util/token_sweeper.rb +74 -0
  35. data/lib/vector_mcp/version.rb +1 -1
  36. data/lib/vector_mcp.rb +8 -8
  37. metadata +8 -10
  38. data/lib/vector_mcp/transport/sse/client_connection.rb +0 -113
  39. data/lib/vector_mcp/transport/sse/message_handler.rb +0 -166
  40. data/lib/vector_mcp/transport/sse/puma_config.rb +0 -77
  41. data/lib/vector_mcp/transport/sse/stream_manager.rb +0 -92
  42. data/lib/vector_mcp/transport/sse.rb +0 -377
  43. data/lib/vector_mcp/transport/sse_session_manager.rb +0 -188
  44. data/lib/vector_mcp/transport/stdio.rb +0 -473
  45. data/lib/vector_mcp/transport/stdio_session_manager.rb +0 -181
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VectorMCP
4
+ module Util
5
+ # Stateless recursive traversal utility for parsed JSON-like structures.
6
+ #
7
+ # {.sweep} walks Hashes, Arrays, and String leaves, yielding each String
8
+ # value together with its parent Hash key (or the enclosing Hash key of
9
+ # the nearest containing Array). The block's return value replaces the
10
+ # String in the output. All other scalar types (Integer, Float, nil,
11
+ # Boolean, etc.) are returned unchanged and are not yielded.
12
+ #
13
+ # The method is purely functional: it never mutates the input structure
14
+ # and always returns a fresh Hash/Array spine when containers are
15
+ # encountered. Circular references are detected via an identity-compared
16
+ # visited set and the originally-referenced node is returned unchanged
17
+ # on cycles.
18
+ module TokenSweeper
19
+ # Traverse +obj+ and return a new structure with String leaves
20
+ # transformed by +block+.
21
+ #
22
+ # @param obj [Object] the object to sweep (typically Hash/Array/String/scalar).
23
+ # @yield [value, parent_key] invoked for each String leaf.
24
+ # @yieldparam value [String] the String value.
25
+ # @yieldparam parent_key [Object, nil] the Hash key under which +value+
26
+ # lives, or +nil+ when the String is a top-level scalar; propagated
27
+ # from the nearest containing Hash when inside Arrays.
28
+ # @yieldreturn [Object] the replacement value.
29
+ # @return [Object] the transformed structure.
30
+ def self.sweep(obj, &block)
31
+ raise ArgumentError, "TokenSweeper.sweep requires a block" unless block
32
+
33
+ walk(obj, nil, {}.compare_by_identity, &block)
34
+ end
35
+
36
+ class << self
37
+ private
38
+
39
+ def walk(obj, parent_key, visited, &)
40
+ case obj
41
+ when Hash then walk_hash(obj, visited, &)
42
+ when Array then walk_array(obj, parent_key, visited, &)
43
+ when String then yield(obj, parent_key)
44
+ else obj
45
+ end
46
+ end
47
+
48
+ def walk_hash(hash, visited, &)
49
+ return hash if visited[hash]
50
+
51
+ visited[hash] = true
52
+ begin
53
+ hash.each_with_object({}) do |(key, value), out|
54
+ out[key] = walk(value, key, visited, &)
55
+ end
56
+ ensure
57
+ visited.delete(hash)
58
+ end
59
+ end
60
+
61
+ def walk_array(array, parent_key, visited, &)
62
+ return array if visited[array]
63
+
64
+ visited[array] = true
65
+ begin
66
+ array.map { |element| walk(element, parent_key, visited, &) }
67
+ ensure
68
+ visited.delete(array)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module VectorMCP
4
4
  # The current version of the VectorMCP gem.
5
- VERSION = "0.3.4"
5
+ VERSION = "0.5.0"
6
6
  end
data/lib/vector_mcp.rb CHANGED
@@ -8,20 +8,21 @@ require_relative "vector_mcp/errors"
8
8
  require_relative "vector_mcp/definitions"
9
9
  require_relative "vector_mcp/session"
10
10
  require_relative "vector_mcp/util"
11
+ require_relative "vector_mcp/util/token_sweeper"
12
+ require_relative "vector_mcp/token_store"
11
13
  require_relative "vector_mcp/log_filter"
12
14
  require_relative "vector_mcp/image_util"
13
15
  require_relative "vector_mcp/handlers/core"
14
- require_relative "vector_mcp/transport/stdio"
15
- # require_relative "vector_mcp/transport/sse" # Load on demand
16
16
  require_relative "vector_mcp/logger"
17
17
  require_relative "vector_mcp/middleware"
18
18
  require_relative "vector_mcp/server"
19
+ require_relative "vector_mcp/tool"
19
20
 
20
21
  # The VectorMCP module provides a full-featured, opinionated Ruby implementation
21
22
  # of the **Model Context Protocol (MCP)**. It gives developers everything needed
22
23
  # to spin up an MCP-compatible server—including:
23
24
  #
24
- # * **Transport adapters** (synchronous `stdio` or HTTP + SSE)
25
+ # * **Transport adapters** (streamable HTTP)
25
26
  # * **High-level abstractions** for *tools*, *resources*, and *prompts*
26
27
  # * **JSON-RPC 2.0** message handling with sensible defaults and detailed
27
28
  # error reporting helpers
@@ -40,11 +41,10 @@ require_relative "vector_mcp/server"
40
41
  # input_schema: {type: "object", properties: {text: {type: "string"}}}
41
42
  # ) { |args| args["text"] }
42
43
  #
43
- # server.run # => starts the stdio transport and begins processing JSON-RPC messages
44
+ # server.run # => starts the HTTP stream transport and begins processing JSON-RPC messages
44
45
  # ```
45
46
  #
46
- # For production you could instead pass an `SSE` transport instance to `run` in
47
- # order to serve multiple concurrent clients over HTTP.
47
+ # The default HTTP stream transport supports multiple concurrent clients over HTTP.
48
48
  #
49
49
  module VectorMCP
50
50
  class << self
@@ -68,8 +68,8 @@ module VectorMCP
68
68
  # Any positional or keyword arguments are forwarded verbatim to the underlying
69
69
  # constructor, so refer to {VectorMCP::Server#initialize} for the full list of
70
70
  # accepted parameters.
71
- def new(*args, **kwargs)
72
- Server.new(*args, **kwargs)
71
+ def new(*, **)
72
+ Server.new(*, **)
73
73
  end
74
74
  end
75
75
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vector_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergio Bayona
@@ -132,15 +132,18 @@ files:
132
132
  - lib/vector_mcp/log_filter.rb
133
133
  - lib/vector_mcp/logger.rb
134
134
  - lib/vector_mcp/middleware.rb
135
+ - lib/vector_mcp/middleware/anonymizer.rb
135
136
  - lib/vector_mcp/middleware/base.rb
136
137
  - lib/vector_mcp/middleware/context.rb
137
138
  - lib/vector_mcp/middleware/hook.rb
138
139
  - lib/vector_mcp/middleware/manager.rb
140
+ - lib/vector_mcp/rails/tool.rb
139
141
  - lib/vector_mcp/request_context.rb
140
142
  - lib/vector_mcp/sampling/request.rb
141
143
  - lib/vector_mcp/sampling/result.rb
142
144
  - lib/vector_mcp/security.rb
143
145
  - lib/vector_mcp/security/auth_manager.rb
146
+ - lib/vector_mcp/security/auth_result.rb
144
147
  - lib/vector_mcp/security/authorization.rb
145
148
  - lib/vector_mcp/security/middleware.rb
146
149
  - lib/vector_mcp/security/session_context.rb
@@ -152,20 +155,15 @@ files:
152
155
  - lib/vector_mcp/server/message_handling.rb
153
156
  - lib/vector_mcp/server/registry.rb
154
157
  - lib/vector_mcp/session.rb
158
+ - lib/vector_mcp/token_store.rb
159
+ - lib/vector_mcp/tool.rb
155
160
  - lib/vector_mcp/transport/base_session_manager.rb
156
161
  - lib/vector_mcp/transport/http_stream.rb
157
162
  - lib/vector_mcp/transport/http_stream/event_store.rb
158
163
  - lib/vector_mcp/transport/http_stream/session_manager.rb
159
164
  - lib/vector_mcp/transport/http_stream/stream_handler.rb
160
- - lib/vector_mcp/transport/sse.rb
161
- - lib/vector_mcp/transport/sse/client_connection.rb
162
- - lib/vector_mcp/transport/sse/message_handler.rb
163
- - lib/vector_mcp/transport/sse/puma_config.rb
164
- - lib/vector_mcp/transport/sse/stream_manager.rb
165
- - lib/vector_mcp/transport/sse_session_manager.rb
166
- - lib/vector_mcp/transport/stdio.rb
167
- - lib/vector_mcp/transport/stdio_session_manager.rb
168
165
  - lib/vector_mcp/util.rb
166
+ - lib/vector_mcp/util/token_sweeper.rb
169
167
  - lib/vector_mcp/version.rb
170
168
  homepage: https://github.com/sergiobayona/vector_mcp
171
169
  licenses:
@@ -182,7 +180,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
182
180
  requirements:
183
181
  - - ">="
184
182
  - !ruby/object:Gem::Version
185
- version: 3.0.6
183
+ version: '3.2'
186
184
  required_rubygems_version: !ruby/object:Gem::Requirement
187
185
  requirements:
188
186
  - - ">="
@@ -1,113 +0,0 @@
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}: #{VectorMCP::LogFilter.filter_hash(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
@@ -1,166 +0,0 @@
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
@@ -1,77 +0,0 @@
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
@@ -1,92 +0,0 @@
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}: #{VectorMCP::LogFilter.filter_string(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