vector_mcp 0.3.4 → 0.4.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 +59 -0
- data/README.md +132 -342
- data/lib/vector_mcp/handlers/core.rb +82 -27
- data/lib/vector_mcp/image_util.rb +34 -11
- data/lib/vector_mcp/middleware/base.rb +1 -5
- data/lib/vector_mcp/middleware/context.rb +11 -1
- data/lib/vector_mcp/rails/tool.rb +85 -0
- data/lib/vector_mcp/request_context.rb +1 -1
- data/lib/vector_mcp/security/middleware.rb +2 -2
- data/lib/vector_mcp/server/capabilities.rb +4 -10
- data/lib/vector_mcp/server/registry.rb +36 -4
- data/lib/vector_mcp/server.rb +45 -38
- data/lib/vector_mcp/session.rb +5 -3
- data/lib/vector_mcp/tool.rb +221 -0
- data/lib/vector_mcp/transport/base_session_manager.rb +1 -17
- data/lib/vector_mcp/transport/http_stream/event_store.rb +18 -4
- data/lib/vector_mcp/transport/http_stream/session_manager.rb +34 -11
- data/lib/vector_mcp/transport/http_stream/stream_handler.rb +132 -47
- data/lib/vector_mcp/transport/http_stream.rb +161 -82
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +6 -8
- metadata +4 -10
- data/lib/vector_mcp/transport/sse/client_connection.rb +0 -113
- data/lib/vector_mcp/transport/sse/message_handler.rb +0 -166
- data/lib/vector_mcp/transport/sse/puma_config.rb +0 -77
- data/lib/vector_mcp/transport/sse/stream_manager.rb +0 -92
- data/lib/vector_mcp/transport/sse.rb +0 -377
- data/lib/vector_mcp/transport/sse_session_manager.rb +0 -188
- data/lib/vector_mcp/transport/stdio.rb +0 -473
- data/lib/vector_mcp/transport/stdio_session_manager.rb +0 -181
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "securerandom"
|
|
5
|
-
require "puma"
|
|
6
|
-
require "rack"
|
|
7
|
-
require "concurrent-ruby"
|
|
8
|
-
|
|
9
|
-
require_relative "../errors"
|
|
10
|
-
require_relative "../util"
|
|
11
|
-
require_relative "../session"
|
|
12
|
-
require_relative "sse_session_manager"
|
|
13
|
-
require_relative "sse/client_connection"
|
|
14
|
-
require_relative "sse/stream_manager"
|
|
15
|
-
require_relative "sse/message_handler"
|
|
16
|
-
require_relative "sse/puma_config"
|
|
17
|
-
|
|
18
|
-
module VectorMCP
|
|
19
|
-
module Transport
|
|
20
|
-
# Implements the Model Context Protocol transport over HTTP using Server-Sent Events (SSE)
|
|
21
|
-
# for server-to-client messages and HTTP POST for client-to-server messages.
|
|
22
|
-
# This transport uses Puma as the HTTP server with Ruby threading for concurrency.
|
|
23
|
-
#
|
|
24
|
-
# It provides two main HTTP endpoints:
|
|
25
|
-
# 1. SSE Endpoint (`<path_prefix>/sse`): Clients connect here via GET to establish an SSE stream.
|
|
26
|
-
# The server sends an initial `event: endpoint` with a unique URL for the client to POST messages back.
|
|
27
|
-
# Subsequent messages from the server (responses, notifications) are sent as `event: message`.
|
|
28
|
-
# 2. Message Endpoint (`<path_prefix>/message`): Clients POST JSON-RPC messages here.
|
|
29
|
-
# The `session_id` (obtained from the SSE endpoint event) must be included as a query parameter.
|
|
30
|
-
# The server responds with a 202 Accepted and then sends the actual JSON-RPC response/error
|
|
31
|
-
# asynchronously over the client's established SSE stream.
|
|
32
|
-
#
|
|
33
|
-
# @example Basic Usage with a Server
|
|
34
|
-
# server = VectorMCP::Server.new("my-sse-server")
|
|
35
|
-
# # ... register tools, resources, prompts ...
|
|
36
|
-
# transport = VectorMCP::Transport::SSE.new(server, port: 8080)
|
|
37
|
-
# server.run(transport: transport)
|
|
38
|
-
#
|
|
39
|
-
# @attr_reader logger [Logger] The logger instance, shared with the server.
|
|
40
|
-
# @attr_reader server [VectorMCP::Server] The server instance this transport is bound to.
|
|
41
|
-
# @attr_reader host [String] The hostname or IP address the server will bind to.
|
|
42
|
-
# @attr_reader port [Integer] The port number the server will listen on.
|
|
43
|
-
# @attr_reader path_prefix [String] The base URL path for MCP endpoints (e.g., "/mcp").
|
|
44
|
-
class SSE
|
|
45
|
-
attr_reader :logger, :server, :host, :port, :path_prefix, :session_manager
|
|
46
|
-
|
|
47
|
-
# Initializes a new SSE transport.
|
|
48
|
-
#
|
|
49
|
-
# @param server [VectorMCP::Server] The server instance that will handle messages.
|
|
50
|
-
# @param options [Hash] Configuration options for the transport.
|
|
51
|
-
# @option options [String] :host ("localhost") The hostname or IP to bind to.
|
|
52
|
-
# @option options [Integer] :port (8000) The port to listen on.
|
|
53
|
-
# @option options [String] :path_prefix ("/mcp") The base path for HTTP endpoints.
|
|
54
|
-
# @option options [Boolean] :disable_session_manager (false) **DEPRECATED**: Whether to disable secure session isolation.
|
|
55
|
-
# When false (default), each client gets isolated sessions. When true, all clients share a global session (security risk).
|
|
56
|
-
def initialize(server, options = {})
|
|
57
|
-
@server = server
|
|
58
|
-
@logger = server.logger
|
|
59
|
-
@host = options[:host] || "localhost"
|
|
60
|
-
@port = options[:port] || 8000
|
|
61
|
-
prefix = options[:path_prefix] || "/mcp"
|
|
62
|
-
@path_prefix = prefix.start_with?("/") ? prefix : "/#{prefix}"
|
|
63
|
-
@path_prefix = @path_prefix.delete_suffix("/")
|
|
64
|
-
@sse_path = "#{@path_prefix}/sse"
|
|
65
|
-
@message_path = "#{@path_prefix}/message"
|
|
66
|
-
|
|
67
|
-
# Thread-safe client storage using concurrent-ruby (legacy approach)
|
|
68
|
-
@clients = Concurrent::Hash.new
|
|
69
|
-
@session = nil # Global session for this transport instance, initialized in run
|
|
70
|
-
|
|
71
|
-
# Initialize session manager for secure multi-client session isolation (default behavior)
|
|
72
|
-
# Legacy shared session behavior can be enabled with disable_session_manager: true (deprecated)
|
|
73
|
-
if options[:disable_session_manager]
|
|
74
|
-
logger.warn("[DEPRECATED] SSE shared session mode is deprecated and poses security risks in multi-client scenarios. " \
|
|
75
|
-
"Consider removing disable_session_manager: true to use secure per-client sessions.")
|
|
76
|
-
@session_manager = nil
|
|
77
|
-
else
|
|
78
|
-
@session_manager = SseSessionManager.new(self)
|
|
79
|
-
end
|
|
80
|
-
@puma_server = nil
|
|
81
|
-
@running = false
|
|
82
|
-
|
|
83
|
-
logger.debug { "SSE Transport initialized with prefix: #{@path_prefix}, SSE path: #{@sse_path}, Message path: #{@message_path}" }
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Starts the SSE transport, creating a shared session and launching the Puma server.
|
|
87
|
-
# This method will block until the server is stopped (e.g., via SIGINT/SIGTERM).
|
|
88
|
-
#
|
|
89
|
-
# @return [void]
|
|
90
|
-
# @raise [StandardError] if there's a fatal error during server startup.
|
|
91
|
-
def run
|
|
92
|
-
logger.info("Starting server with Puma SSE transport on #{@host}:#{@port}")
|
|
93
|
-
# Only create shared session if explicitly using legacy mode (deprecated)
|
|
94
|
-
create_session unless @session_manager
|
|
95
|
-
start_puma_server
|
|
96
|
-
rescue StandardError => e
|
|
97
|
-
handle_fatal_error(e)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# --- Rack-compatible #call method ---
|
|
101
|
-
|
|
102
|
-
# Handles incoming HTTP requests. This is the entry point for the Rack application.
|
|
103
|
-
# It routes requests to the appropriate handler based on the path.
|
|
104
|
-
#
|
|
105
|
-
# @param env [Hash] The Rack environment hash.
|
|
106
|
-
# @return [Array(Integer, Hash, Object)] A standard Rack response triplet: [status, headers, body].
|
|
107
|
-
def call(env)
|
|
108
|
-
start_time = Time.now
|
|
109
|
-
path = env["PATH_INFO"]
|
|
110
|
-
http_method = env["REQUEST_METHOD"]
|
|
111
|
-
logger.info "Received #{http_method} request for #{path}"
|
|
112
|
-
|
|
113
|
-
status, headers, body = route_request(path, env)
|
|
114
|
-
|
|
115
|
-
log_response(http_method, path, start_time, status)
|
|
116
|
-
[status, headers, body]
|
|
117
|
-
rescue StandardError => e
|
|
118
|
-
handle_call_error(http_method, path, e)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# --- Public methods for Server to send notifications ---
|
|
122
|
-
|
|
123
|
-
# Sends a JSON-RPC notification to the first available client session.
|
|
124
|
-
# If no clients are connected, returns false.
|
|
125
|
-
#
|
|
126
|
-
# @param method [String] The method name of the notification.
|
|
127
|
-
# @param params [Hash, Array, nil] The parameters for the notification (optional).
|
|
128
|
-
# @return [Boolean] True if the message was sent successfully, false otherwise.
|
|
129
|
-
def send_notification(method, params = nil)
|
|
130
|
-
return false if @clients.empty?
|
|
131
|
-
|
|
132
|
-
# Send to first available client
|
|
133
|
-
first_client = @clients.values.first
|
|
134
|
-
return false unless first_client
|
|
135
|
-
|
|
136
|
-
message = { jsonrpc: "2.0", method: method }
|
|
137
|
-
message[:params] = params if params
|
|
138
|
-
|
|
139
|
-
StreamManager.enqueue_message(first_client, message)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Sends a JSON-RPC notification to a specific client session via its SSE stream.
|
|
143
|
-
#
|
|
144
|
-
# @param session_id [String] The ID of the client session to send the notification to.
|
|
145
|
-
# @param method [String] The method name of the notification.
|
|
146
|
-
# @param params [Hash, Array, nil] The parameters for the notification (optional).
|
|
147
|
-
# @return [Boolean] True if the message was successfully enqueued, false otherwise (e.g., client not found).
|
|
148
|
-
def send_notification_to_session(session_id, method, params = nil)
|
|
149
|
-
message = { jsonrpc: "2.0", method: method }
|
|
150
|
-
message[:params] = params if params
|
|
151
|
-
|
|
152
|
-
client_conn = @clients[session_id]
|
|
153
|
-
return false unless client_conn
|
|
154
|
-
|
|
155
|
-
StreamManager.enqueue_message(client_conn, message)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# Broadcasts a JSON-RPC notification to all currently connected client sessions.
|
|
159
|
-
#
|
|
160
|
-
# @param method [String] The method name of the notification.
|
|
161
|
-
# @param params [Hash, Array, nil] The parameters for the notification (optional).
|
|
162
|
-
# @return [void]
|
|
163
|
-
def broadcast_notification(method, params = nil)
|
|
164
|
-
# Broadcasting notification to clients
|
|
165
|
-
message = { jsonrpc: "2.0", method: method }
|
|
166
|
-
message[:params] = params if params
|
|
167
|
-
|
|
168
|
-
@clients.each_value do |client_conn|
|
|
169
|
-
StreamManager.enqueue_message(client_conn, message)
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Provides compatibility for tests that expect a `build_rack_app` helper.
|
|
174
|
-
# Since the transport itself is a Rack app (defines `#call`), it returns `self`.
|
|
175
|
-
#
|
|
176
|
-
# @param session [VectorMCP::Session, nil] An optional session to persist for testing.
|
|
177
|
-
# @return [self] The transport instance itself.
|
|
178
|
-
def build_rack_app(session = nil)
|
|
179
|
-
@session = session if session
|
|
180
|
-
self
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Stops the transport and cleans up resources
|
|
184
|
-
def stop
|
|
185
|
-
@running = false
|
|
186
|
-
if @session_manager
|
|
187
|
-
@session_manager.cleanup_all_sessions
|
|
188
|
-
else
|
|
189
|
-
cleanup_clients
|
|
190
|
-
end
|
|
191
|
-
@puma_server&.stop
|
|
192
|
-
logger.info("SSE transport stopped")
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
# Cleans up all client connections (legacy mode)
|
|
196
|
-
def cleanup_clients
|
|
197
|
-
logger.info("Cleaning up #{@clients.size} client connection(s)")
|
|
198
|
-
@clients.each_value do |client_conn|
|
|
199
|
-
client_conn.close if client_conn.respond_to?(:close)
|
|
200
|
-
rescue StandardError => e
|
|
201
|
-
logger.warn("Error closing client connection: #{e.message}")
|
|
202
|
-
end
|
|
203
|
-
@clients.clear
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# --- Private methods ---
|
|
207
|
-
private
|
|
208
|
-
|
|
209
|
-
# Creates a single, shared {VectorMCP::Session} instance for this transport run.
|
|
210
|
-
# All client interactions will use this session context.
|
|
211
|
-
def create_session
|
|
212
|
-
@session = VectorMCP::Session.new(server, self, id: SecureRandom.uuid)
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# Starts the Puma HTTP server.
|
|
216
|
-
def start_puma_server
|
|
217
|
-
@puma_server = Puma::Server.new(build_rack_app)
|
|
218
|
-
puma_config = PumaConfig.new(@host, @port, logger)
|
|
219
|
-
puma_config.configure(@puma_server)
|
|
220
|
-
|
|
221
|
-
@running = true
|
|
222
|
-
setup_signal_traps
|
|
223
|
-
|
|
224
|
-
logger.info("Puma server starting on #{@host}:#{@port}")
|
|
225
|
-
@puma_server.run.join # This blocks until server stops
|
|
226
|
-
logger.info("Puma server stopped.")
|
|
227
|
-
ensure
|
|
228
|
-
# Only cleanup if session manager is enabled
|
|
229
|
-
@session_manager&.cleanup_all_sessions
|
|
230
|
-
logger.info("SSE transport and resources shut down.")
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# Sets up POSIX signal traps for graceful server shutdown (INT, TERM).
|
|
234
|
-
def setup_signal_traps
|
|
235
|
-
Signal.trap("INT") do
|
|
236
|
-
logger.info("SIGINT received, stopping server...")
|
|
237
|
-
stop
|
|
238
|
-
end
|
|
239
|
-
Signal.trap("TERM") do
|
|
240
|
-
logger.info("SIGTERM received, stopping server...")
|
|
241
|
-
stop
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
# Handles fatal errors during server startup or main run loop.
|
|
246
|
-
def handle_fatal_error(error)
|
|
247
|
-
logger.fatal("Fatal error in SSE transport: #{error.message}\n#{error.backtrace.join("\n")}")
|
|
248
|
-
exit(1)
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
# Routes an incoming request to the appropriate handler based on its path.
|
|
252
|
-
def route_request(path, env)
|
|
253
|
-
case path
|
|
254
|
-
when @sse_path
|
|
255
|
-
handle_sse_connection(env)
|
|
256
|
-
when @message_path
|
|
257
|
-
handle_message_post(env)
|
|
258
|
-
when "/"
|
|
259
|
-
[200, { "Content-Type" => "text/plain" }, ["VectorMCP Server OK"]]
|
|
260
|
-
else
|
|
261
|
-
[404, { "Content-Type" => "text/plain" }, ["Not Found"]]
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# Logs the response details including status, method, path, and duration.
|
|
266
|
-
def log_response(method, path, start_time, status)
|
|
267
|
-
duration = format("%.4f", Time.now - start_time)
|
|
268
|
-
logger.info "Responded #{status} to #{method} #{path} in #{duration}s"
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
# Generic error handler for exceptions occurring within the `#call` method's request processing.
|
|
272
|
-
def handle_call_error(method, path, error)
|
|
273
|
-
error_context = method || "UNKNOWN_METHOD"
|
|
274
|
-
path_context = path || "UNKNOWN_PATH"
|
|
275
|
-
backtrace = error.backtrace.join("\n")
|
|
276
|
-
logger.error("Error during SSE request processing for #{error_context} #{path_context}: #{error.message}\n#{backtrace}")
|
|
277
|
-
[500, { "Content-Type" => "text/plain", "connection" => "close" }, ["Internal Server Error"]]
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
# Handles a new client connection to the SSE endpoint.
|
|
281
|
-
def handle_sse_connection(env)
|
|
282
|
-
return invalid_method_response(env) unless env["REQUEST_METHOD"] == "GET"
|
|
283
|
-
|
|
284
|
-
session_id = SecureRandom.uuid
|
|
285
|
-
logger.info("New SSE client connected: #{session_id}")
|
|
286
|
-
|
|
287
|
-
# Create client connection
|
|
288
|
-
client_conn = ClientConnection.new(session_id, logger)
|
|
289
|
-
|
|
290
|
-
# Store client connection
|
|
291
|
-
if @session_manager
|
|
292
|
-
@session_manager.register_client(session_id, client_conn)
|
|
293
|
-
else
|
|
294
|
-
@clients[session_id] = client_conn
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
# Build message POST URL for this client
|
|
298
|
-
message_post_url = build_post_url(session_id)
|
|
299
|
-
# Client message POST URL configured
|
|
300
|
-
|
|
301
|
-
# Set up SSE stream
|
|
302
|
-
headers = sse_headers
|
|
303
|
-
body = StreamManager.create_sse_stream(client_conn, message_post_url, logger)
|
|
304
|
-
|
|
305
|
-
[200, headers, body]
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
# Handles incoming POST requests containing JSON-RPC messages from clients.
|
|
309
|
-
def handle_message_post(env)
|
|
310
|
-
return invalid_post_method_response(env) unless env["REQUEST_METHOD"] == "POST"
|
|
311
|
-
|
|
312
|
-
session_id = extract_session_id(env["QUERY_STRING"])
|
|
313
|
-
unless session_id
|
|
314
|
-
return error_response(nil, VectorMCP::InvalidRequestError.new("Missing session_id parameter").code,
|
|
315
|
-
"Missing session_id parameter")
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
# Get client connection and session
|
|
319
|
-
if @session_manager
|
|
320
|
-
client_conn = @session_manager.clients[session_id]
|
|
321
|
-
shared_session = @session_manager.shared_session
|
|
322
|
-
else
|
|
323
|
-
client_conn = @clients[session_id]
|
|
324
|
-
shared_session = @session
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
return error_response(nil, VectorMCP::NotFoundError.new("Invalid session_id").code, "Invalid session_id") unless client_conn
|
|
328
|
-
|
|
329
|
-
MessageHandler.new(@server, shared_session, logger).handle_post_message(env, client_conn)
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
# Helper methods
|
|
333
|
-
def invalid_method_response(env)
|
|
334
|
-
method = env["REQUEST_METHOD"]
|
|
335
|
-
logger.warn("Received non-GET request on SSE endpoint: #{method}")
|
|
336
|
-
[405, { "Content-Type" => "text/plain", "Allow" => "GET" }, ["Method Not Allowed. Only GET is supported for SSE endpoint."]]
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
def invalid_post_method_response(env)
|
|
340
|
-
method = env["REQUEST_METHOD"]
|
|
341
|
-
logger.warn("Received non-POST request on message endpoint: #{method}")
|
|
342
|
-
[405, { "Content-Type" => "text/plain", "Allow" => "POST" }, ["Method Not Allowed"]]
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
def sse_headers
|
|
346
|
-
{
|
|
347
|
-
"Content-Type" => "text/event-stream",
|
|
348
|
-
"Cache-Control" => "no-cache",
|
|
349
|
-
"Connection" => "keep-alive",
|
|
350
|
-
"X-Accel-Buffering" => "no"
|
|
351
|
-
}
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
def build_post_url(session_id)
|
|
355
|
-
"#{@message_path}?session_id=#{session_id}"
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
def extract_session_id(query_string)
|
|
359
|
-
return nil unless query_string
|
|
360
|
-
|
|
361
|
-
URI.decode_www_form(query_string).to_h["session_id"]
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
def error_response(id, code, message, data = nil)
|
|
365
|
-
status = case code
|
|
366
|
-
when -32_700, -32_600, -32_602 then 400
|
|
367
|
-
when -32_601, -32_001 then 404
|
|
368
|
-
else 500
|
|
369
|
-
end
|
|
370
|
-
error_payload = { code: code, message: message }
|
|
371
|
-
error_payload[:data] = data if data
|
|
372
|
-
body = { jsonrpc: "2.0", id: id, error: error_payload }.to_json
|
|
373
|
-
[status, { "Content-Type" => "application/json" }, [body]]
|
|
374
|
-
end
|
|
375
|
-
end
|
|
376
|
-
end
|
|
377
|
-
end
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "base_session_manager"
|
|
4
|
-
|
|
5
|
-
module VectorMCP
|
|
6
|
-
module Transport
|
|
7
|
-
# Session manager for SSE transport with single shared session and client connection management.
|
|
8
|
-
# Extends BaseSessionManager with SSE-specific functionality.
|
|
9
|
-
#
|
|
10
|
-
# The SSE transport uses a single shared session for all client connections,
|
|
11
|
-
# but manages multiple client connections separately.
|
|
12
|
-
class SseSessionManager < BaseSessionManager
|
|
13
|
-
attr_reader :clients
|
|
14
|
-
|
|
15
|
-
# Initializes a new SSE session manager.
|
|
16
|
-
#
|
|
17
|
-
# @param transport [SSE] The parent transport instance
|
|
18
|
-
# @param session_timeout [Integer] Session timeout in seconds
|
|
19
|
-
def initialize(transport, session_timeout = 300)
|
|
20
|
-
@clients = Concurrent::Hash.new
|
|
21
|
-
super
|
|
22
|
-
|
|
23
|
-
# Create the single shared session for SSE transport
|
|
24
|
-
@shared_session = create_shared_session
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Gets the shared session for SSE transport.
|
|
28
|
-
# SSE uses a single session shared across all client connections.
|
|
29
|
-
#
|
|
30
|
-
# @return [Session] The shared session
|
|
31
|
-
def shared_session
|
|
32
|
-
@shared_session.touch!
|
|
33
|
-
@shared_session
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Registers a client connection with the session manager.
|
|
37
|
-
#
|
|
38
|
-
# @param client_id [String] The client connection ID
|
|
39
|
-
# @param client_connection [Object] The client connection object
|
|
40
|
-
# @return [void]
|
|
41
|
-
def register_client(client_id, client_connection)
|
|
42
|
-
@clients[client_id] = client_connection
|
|
43
|
-
session_metadata_updated?(@shared_session.id, clients_count: @clients.size)
|
|
44
|
-
logger.debug { "Client registered: #{client_id}" }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Unregisters a client connection from the session manager.
|
|
48
|
-
#
|
|
49
|
-
# @param client_id [String] The client connection ID
|
|
50
|
-
# @return [Boolean] True if client was found and removed
|
|
51
|
-
def client_unregistered?(client_id)
|
|
52
|
-
client = @clients.delete(client_id)
|
|
53
|
-
return false unless client
|
|
54
|
-
|
|
55
|
-
session_metadata_updated?(@shared_session.id, clients_count: @clients.size)
|
|
56
|
-
logger.debug { "Client unregistered: #{client_id}" }
|
|
57
|
-
true
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Gets all client connections.
|
|
61
|
-
#
|
|
62
|
-
# @return [Hash] Hash of client_id => client_connection
|
|
63
|
-
def all_clients
|
|
64
|
-
@clients.dup
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Gets the number of connected clients.
|
|
68
|
-
#
|
|
69
|
-
# @return [Integer] Number of connected clients
|
|
70
|
-
def client_count
|
|
71
|
-
@clients.size
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Cleans up all clients and the shared session.
|
|
75
|
-
#
|
|
76
|
-
# @return [void]
|
|
77
|
-
def cleanup_all_sessions
|
|
78
|
-
logger.info { "Cleaning up #{@clients.size} client connection(s)" }
|
|
79
|
-
|
|
80
|
-
@clients.each_value do |client_conn|
|
|
81
|
-
close_client_connection(client_conn)
|
|
82
|
-
end
|
|
83
|
-
@clients.clear
|
|
84
|
-
|
|
85
|
-
super
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
protected
|
|
89
|
-
|
|
90
|
-
# Override: SSE doesn't need automatic cleanup since it has a single shared session.
|
|
91
|
-
def auto_cleanup_enabled?
|
|
92
|
-
false
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Override: Called when the shared session is terminated.
|
|
96
|
-
def on_session_terminated(_session)
|
|
97
|
-
# Clean up all client connections when session is terminated
|
|
98
|
-
@clients.each_value do |client_conn|
|
|
99
|
-
close_client_connection(client_conn)
|
|
100
|
-
end
|
|
101
|
-
@clients.clear
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Override: Returns metadata for SSE sessions.
|
|
105
|
-
def create_session_metadata
|
|
106
|
-
{ clients_count: 0, session_type: :sse_shared }
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Override: Checks if any clients are connected to receive messages.
|
|
110
|
-
def can_send_message_to_session?(_session)
|
|
111
|
-
!@clients.empty?
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Override: Sends a message to the first available client.
|
|
115
|
-
def send_message_to_session(_session, message)
|
|
116
|
-
return false if @clients.empty?
|
|
117
|
-
|
|
118
|
-
first_client = @clients.values.first
|
|
119
|
-
return false unless first_client
|
|
120
|
-
|
|
121
|
-
@transport.class::StreamManager.enqueue_message(first_client, message)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Override: Broadcasts messages to all connected clients.
|
|
125
|
-
def broadcast_message(message)
|
|
126
|
-
count = 0
|
|
127
|
-
@clients.each_value do |client_conn|
|
|
128
|
-
count += 1 if @transport.class::StreamManager.enqueue_message(client_conn, message)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
logger.debug { "Message broadcasted to #{count} client(s)" }
|
|
132
|
-
count
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
private
|
|
136
|
-
|
|
137
|
-
# Creates the single shared session for SSE transport.
|
|
138
|
-
#
|
|
139
|
-
# @return [BaseSessionManager::Session] The shared session
|
|
140
|
-
def create_shared_session(rack_env = nil)
|
|
141
|
-
session_id = "sse_shared_session_#{SecureRandom.uuid}"
|
|
142
|
-
now = Time.now
|
|
143
|
-
|
|
144
|
-
# Create VectorMCP session context with request context
|
|
145
|
-
session_context = create_session_with_context(session_id, rack_env)
|
|
146
|
-
|
|
147
|
-
# Create internal session record using base session manager struct
|
|
148
|
-
session = BaseSessionManager::Session.new(
|
|
149
|
-
session_id,
|
|
150
|
-
session_context,
|
|
151
|
-
now,
|
|
152
|
-
now,
|
|
153
|
-
create_session_metadata
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
@sessions[session_id] = session
|
|
157
|
-
logger.info { "Shared SSE session created: #{session_id}" }
|
|
158
|
-
session
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Creates a VectorMCP::Session with proper request context from Rack environment
|
|
162
|
-
def create_session_with_context(session_id, rack_env)
|
|
163
|
-
request_context = if rack_env
|
|
164
|
-
# Create request context from Rack environment
|
|
165
|
-
VectorMCP::RequestContext.from_rack_env(rack_env, "sse")
|
|
166
|
-
else
|
|
167
|
-
# Fallback to minimal context for cases where rack_env is not available
|
|
168
|
-
VectorMCP::RequestContext.minimal("sse")
|
|
169
|
-
end
|
|
170
|
-
VectorMCP::Session.new(@transport.server, @transport, id: session_id, request_context: request_context)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Closes a client connection safely.
|
|
174
|
-
#
|
|
175
|
-
# @param client_conn [Object] The client connection to close
|
|
176
|
-
# @return [void]
|
|
177
|
-
def close_client_connection(client_conn)
|
|
178
|
-
return unless client_conn
|
|
179
|
-
|
|
180
|
-
begin
|
|
181
|
-
client_conn.close if client_conn.respond_to?(:close)
|
|
182
|
-
rescue StandardError => e
|
|
183
|
-
logger.warn { "Error closing client connection: #{e.message}" }
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
end
|