vector_mcp 0.3.1 → 0.3.3
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 +122 -0
- data/lib/vector_mcp/definitions.rb +25 -9
- data/lib/vector_mcp/errors.rb +2 -3
- data/lib/vector_mcp/handlers/core.rb +206 -50
- data/lib/vector_mcp/logger.rb +148 -0
- data/lib/vector_mcp/middleware/base.rb +171 -0
- data/lib/vector_mcp/middleware/context.rb +76 -0
- data/lib/vector_mcp/middleware/hook.rb +169 -0
- data/lib/vector_mcp/middleware/manager.rb +179 -0
- data/lib/vector_mcp/middleware.rb +43 -0
- data/lib/vector_mcp/request_context.rb +182 -0
- data/lib/vector_mcp/sampling/result.rb +11 -1
- data/lib/vector_mcp/security/middleware.rb +2 -28
- data/lib/vector_mcp/security/strategies/api_key.rb +2 -24
- data/lib/vector_mcp/security/strategies/jwt_token.rb +6 -3
- data/lib/vector_mcp/server/capabilities.rb +5 -7
- data/lib/vector_mcp/server/message_handling.rb +11 -5
- data/lib/vector_mcp/server.rb +74 -20
- data/lib/vector_mcp/session.rb +131 -8
- data/lib/vector_mcp/transport/base_session_manager.rb +320 -0
- data/lib/vector_mcp/transport/http_stream/event_store.rb +151 -0
- data/lib/vector_mcp/transport/http_stream/session_manager.rb +189 -0
- data/lib/vector_mcp/transport/http_stream/stream_handler.rb +269 -0
- data/lib/vector_mcp/transport/http_stream.rb +779 -0
- data/lib/vector_mcp/transport/sse.rb +74 -19
- data/lib/vector_mcp/transport/sse_session_manager.rb +188 -0
- data/lib/vector_mcp/transport/stdio.rb +70 -13
- data/lib/vector_mcp/transport/stdio_session_manager.rb +181 -0
- data/lib/vector_mcp/util.rb +39 -1
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +10 -35
- metadata +25 -24
- data/lib/vector_mcp/logging/component.rb +0 -131
- data/lib/vector_mcp/logging/configuration.rb +0 -156
- data/lib/vector_mcp/logging/constants.rb +0 -21
- data/lib/vector_mcp/logging/core.rb +0 -175
- data/lib/vector_mcp/logging/filters/component.rb +0 -69
- data/lib/vector_mcp/logging/filters/level.rb +0 -23
- data/lib/vector_mcp/logging/formatters/base.rb +0 -52
- data/lib/vector_mcp/logging/formatters/json.rb +0 -83
- data/lib/vector_mcp/logging/formatters/text.rb +0 -72
- data/lib/vector_mcp/logging/outputs/base.rb +0 -64
- data/lib/vector_mcp/logging/outputs/console.rb +0 -35
- data/lib/vector_mcp/logging/outputs/file.rb +0 -157
- data/lib/vector_mcp/logging.rb +0 -71
@@ -9,6 +9,7 @@ require "concurrent-ruby"
|
|
9
9
|
require_relative "../errors"
|
10
10
|
require_relative "../util"
|
11
11
|
require_relative "../session"
|
12
|
+
require_relative "sse_session_manager"
|
12
13
|
require_relative "sse/client_connection"
|
13
14
|
require_relative "sse/stream_manager"
|
14
15
|
require_relative "sse/message_handler"
|
@@ -41,7 +42,7 @@ module VectorMCP
|
|
41
42
|
# @attr_reader port [Integer] The port number the server will listen on.
|
42
43
|
# @attr_reader path_prefix [String] The base URL path for MCP endpoints (e.g., "/mcp").
|
43
44
|
class SSE
|
44
|
-
attr_reader :logger, :server, :host, :port, :path_prefix
|
45
|
+
attr_reader :logger, :server, :host, :port, :path_prefix, :session_manager
|
45
46
|
|
46
47
|
# Initializes a new SSE transport.
|
47
48
|
#
|
@@ -50,6 +51,8 @@ module VectorMCP
|
|
50
51
|
# @option options [String] :host ("localhost") The hostname or IP to bind to.
|
51
52
|
# @option options [Integer] :port (8000) The port to listen on.
|
52
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).
|
53
56
|
def initialize(server, options = {})
|
54
57
|
@server = server
|
55
58
|
@logger = server.logger
|
@@ -61,9 +64,19 @@ module VectorMCP
|
|
61
64
|
@sse_path = "#{@path_prefix}/sse"
|
62
65
|
@message_path = "#{@path_prefix}/message"
|
63
66
|
|
64
|
-
# Thread-safe client storage using concurrent-ruby
|
67
|
+
# Thread-safe client storage using concurrent-ruby (legacy approach)
|
65
68
|
@clients = Concurrent::Hash.new
|
66
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
|
67
80
|
@puma_server = nil
|
68
81
|
@running = false
|
69
82
|
|
@@ -77,7 +90,8 @@ module VectorMCP
|
|
77
90
|
# @raise [StandardError] if there's a fatal error during server startup.
|
78
91
|
def run
|
79
92
|
logger.info("Starting server with Puma SSE transport on #{@host}:#{@port}")
|
80
|
-
|
93
|
+
# Only create shared session if explicitly using legacy mode (deprecated)
|
94
|
+
create_session unless @session_manager
|
81
95
|
start_puma_server
|
82
96
|
rescue StandardError => e
|
83
97
|
handle_fatal_error(e)
|
@@ -106,13 +120,32 @@ module VectorMCP
|
|
106
120
|
|
107
121
|
# --- Public methods for Server to send notifications ---
|
108
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
|
+
|
109
142
|
# Sends a JSON-RPC notification to a specific client session via its SSE stream.
|
110
143
|
#
|
111
144
|
# @param session_id [String] The ID of the client session to send the notification to.
|
112
145
|
# @param method [String] The method name of the notification.
|
113
146
|
# @param params [Hash, Array, nil] The parameters for the notification (optional).
|
114
147
|
# @return [Boolean] True if the message was successfully enqueued, false otherwise (e.g., client not found).
|
115
|
-
def
|
148
|
+
def send_notification_to_session(session_id, method, params = nil)
|
116
149
|
message = { jsonrpc: "2.0", method: method }
|
117
150
|
message[:params] = params if params
|
118
151
|
|
@@ -128,7 +161,7 @@ module VectorMCP
|
|
128
161
|
# @param params [Hash, Array, nil] The parameters for the notification (optional).
|
129
162
|
# @return [void]
|
130
163
|
def broadcast_notification(method, params = nil)
|
131
|
-
|
164
|
+
# Broadcasting notification to clients
|
132
165
|
message = { jsonrpc: "2.0", method: method }
|
133
166
|
message[:params] = params if params
|
134
167
|
|
@@ -150,11 +183,26 @@ module VectorMCP
|
|
150
183
|
# Stops the transport and cleans up resources
|
151
184
|
def stop
|
152
185
|
@running = false
|
153
|
-
|
186
|
+
if @session_manager
|
187
|
+
@session_manager.cleanup_all_sessions
|
188
|
+
else
|
189
|
+
cleanup_clients
|
190
|
+
end
|
154
191
|
@puma_server&.stop
|
155
192
|
logger.info("SSE transport stopped")
|
156
193
|
end
|
157
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
|
+
|
158
206
|
# --- Private methods ---
|
159
207
|
private
|
160
208
|
|
@@ -177,8 +225,8 @@ module VectorMCP
|
|
177
225
|
@puma_server.run.join # This blocks until server stops
|
178
226
|
logger.info("Puma server stopped.")
|
179
227
|
ensure
|
180
|
-
|
181
|
-
@
|
228
|
+
# Only cleanup if session manager is enabled
|
229
|
+
@session_manager&.cleanup_all_sessions
|
182
230
|
logger.info("SSE transport and resources shut down.")
|
183
231
|
end
|
184
232
|
|
@@ -194,13 +242,6 @@ module VectorMCP
|
|
194
242
|
end
|
195
243
|
end
|
196
244
|
|
197
|
-
# Cleans up resources for all connected clients on server shutdown.
|
198
|
-
def cleanup_clients
|
199
|
-
logger.info("Cleaning up #{@clients.size} client connection(s)...")
|
200
|
-
@clients.each_value(&:close)
|
201
|
-
@clients.clear
|
202
|
-
end
|
203
|
-
|
204
245
|
# Handles fatal errors during server startup or main run loop.
|
205
246
|
def handle_fatal_error(error)
|
206
247
|
logger.fatal("Fatal error in SSE transport: #{error.message}\n#{error.backtrace.join("\n")}")
|
@@ -245,11 +286,17 @@ module VectorMCP
|
|
245
286
|
|
246
287
|
# Create client connection
|
247
288
|
client_conn = ClientConnection.new(session_id, logger)
|
248
|
-
|
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
|
249
296
|
|
250
297
|
# Build message POST URL for this client
|
251
298
|
message_post_url = build_post_url(session_id)
|
252
|
-
|
299
|
+
# Client message POST URL configured
|
253
300
|
|
254
301
|
# Set up SSE stream
|
255
302
|
headers = sse_headers
|
@@ -268,10 +315,18 @@ module VectorMCP
|
|
268
315
|
"Missing session_id parameter")
|
269
316
|
end
|
270
317
|
|
271
|
-
|
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
|
+
|
272
327
|
return error_response(nil, VectorMCP::NotFoundError.new("Invalid session_id").code, "Invalid session_id") unless client_conn
|
273
328
|
|
274
|
-
MessageHandler.new(@server,
|
329
|
+
MessageHandler.new(@server, shared_session, logger).handle_post_message(env, client_conn)
|
275
330
|
end
|
276
331
|
|
277
332
|
# Helper methods
|
@@ -0,0 +1,188 @@
|
|
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
|
@@ -4,6 +4,7 @@
|
|
4
4
|
require "json"
|
5
5
|
require_relative "../errors"
|
6
6
|
require_relative "../util"
|
7
|
+
require_relative "stdio_session_manager"
|
7
8
|
require "securerandom" # For generating unique request IDs
|
8
9
|
require "timeout" # For request timeouts
|
9
10
|
|
@@ -20,6 +21,8 @@ module VectorMCP
|
|
20
21
|
attr_reader :server
|
21
22
|
# @return [Logger] The logger instance, shared with the server.
|
22
23
|
attr_reader :logger
|
24
|
+
# @return [StdioSessionManager] The session manager for this transport.
|
25
|
+
attr_reader :session_manager
|
23
26
|
|
24
27
|
# Timeout for waiting for a response to a server-initiated request (in seconds)
|
25
28
|
DEFAULT_REQUEST_TIMEOUT = 30 # Configurable if needed
|
@@ -27,9 +30,12 @@ module VectorMCP
|
|
27
30
|
# Initializes a new Stdio transport.
|
28
31
|
#
|
29
32
|
# @param server [VectorMCP::Server] The server instance that will handle messages.
|
30
|
-
|
33
|
+
# @param options [Hash] Optional configuration options.
|
34
|
+
# @option options [Boolean] :enable_session_manager (false) Whether to enable the unified session manager.
|
35
|
+
def initialize(server, options = {})
|
31
36
|
@server = server
|
32
37
|
@logger = server.logger
|
38
|
+
@session_manager = options[:enable_session_manager] ? StdioSessionManager.new(self) : nil
|
33
39
|
@input_mutex = Mutex.new
|
34
40
|
@output_mutex = Mutex.new
|
35
41
|
@running = false
|
@@ -109,6 +115,43 @@ module VectorMCP
|
|
109
115
|
write_message(notification)
|
110
116
|
end
|
111
117
|
|
118
|
+
# Sends a JSON-RPC notification message to a specific session.
|
119
|
+
# For stdio transport, this behaves the same as send_notification since there's only one session.
|
120
|
+
#
|
121
|
+
# @param _session_id [String] The session ID (ignored for stdio transport).
|
122
|
+
# @param method [String] The method name of the notification.
|
123
|
+
# @param params [Hash, Array, nil] The parameters for the notification (optional).
|
124
|
+
# @return [Boolean] True if the notification was sent successfully.
|
125
|
+
# rubocop:disable Naming/PredicateMethod
|
126
|
+
def send_notification_to_session(_session_id, method, params = nil)
|
127
|
+
send_notification(method, params)
|
128
|
+
true
|
129
|
+
end
|
130
|
+
# rubocop:enable Naming/PredicateMethod
|
131
|
+
|
132
|
+
# Sends a JSON-RPC notification message to a specific session.
|
133
|
+
# For stdio transport, this behaves the same as send_notification since there's only one session.
|
134
|
+
#
|
135
|
+
# @param _session_id [String] The session ID (ignored for stdio transport).
|
136
|
+
# @param method [String] The method name of the notification.
|
137
|
+
# @param params [Hash, Array, nil] The parameters for the notification (optional).
|
138
|
+
# @return [Boolean] True if the notification was sent successfully.
|
139
|
+
def notification_sent_to_session?(_session_id, method, params = nil)
|
140
|
+
send_notification(method, params)
|
141
|
+
true
|
142
|
+
end
|
143
|
+
|
144
|
+
# Broadcasts a JSON-RPC notification message to all sessions.
|
145
|
+
# For stdio transport, this behaves the same as send_notification since there's only one session.
|
146
|
+
#
|
147
|
+
# @param method [String] The method name of the notification.
|
148
|
+
# @param params [Hash, Array, nil] The parameters for the notification (optional).
|
149
|
+
# @return [Integer] Number of sessions the notification was sent to (always 1 for stdio).
|
150
|
+
def broadcast_notification(method, params = nil)
|
151
|
+
send_notification(method, params)
|
152
|
+
1
|
153
|
+
end
|
154
|
+
|
112
155
|
# Sends a server-initiated JSON-RPC request to the client and waits for a response.
|
113
156
|
# This is a blocking call.
|
114
157
|
#
|
@@ -126,7 +169,7 @@ module VectorMCP
|
|
126
169
|
request_payload[:params] = params if params
|
127
170
|
|
128
171
|
setup_request_tracking(request_id)
|
129
|
-
|
172
|
+
# Sending request to client
|
130
173
|
write_message(request_payload)
|
131
174
|
|
132
175
|
response = wait_for_response(request_id, method, timeout)
|
@@ -184,7 +227,6 @@ module VectorMCP
|
|
184
227
|
|
185
228
|
return handle_outgoing_response(message) if outgoing_response?(message)
|
186
229
|
|
187
|
-
ensure_session_exists
|
188
230
|
handle_server_message(message)
|
189
231
|
end
|
190
232
|
|
@@ -196,7 +238,21 @@ module VectorMCP
|
|
196
238
|
message["id"] && !message["method"] && (message.key?("result") || message.key?("error"))
|
197
239
|
end
|
198
240
|
|
199
|
-
#
|
241
|
+
# Gets the global session for this stdio transport.
|
242
|
+
# @api private
|
243
|
+
# @return [VectorMCP::Session] The current session.
|
244
|
+
def session
|
245
|
+
# Try session manager first, fallback to old method for backward compatibility
|
246
|
+
if @session_manager
|
247
|
+
session_wrapper = @session_manager.global_session
|
248
|
+
return session_wrapper.context if session_wrapper
|
249
|
+
end
|
250
|
+
|
251
|
+
# Fallback to old session creation for backward compatibility
|
252
|
+
ensure_session_exists
|
253
|
+
end
|
254
|
+
|
255
|
+
# Ensures a global session exists for this stdio transport (legacy method).
|
200
256
|
# @api private
|
201
257
|
# @return [VectorMCP::Session] The current session.
|
202
258
|
def ensure_session_exists
|
@@ -208,11 +264,11 @@ module VectorMCP
|
|
208
264
|
# @param message [Hash] The parsed message.
|
209
265
|
# @return [void]
|
210
266
|
def handle_server_message(message)
|
211
|
-
|
212
|
-
session_id =
|
267
|
+
current_session = session
|
268
|
+
session_id = current_session.id
|
213
269
|
|
214
270
|
begin
|
215
|
-
result = @server.handle_message(message,
|
271
|
+
result = @server.handle_message(message, current_session, session_id)
|
216
272
|
send_response(message["id"], result) if message["id"] && result
|
217
273
|
rescue VectorMCP::ProtocolError => e
|
218
274
|
handle_protocol_error(e, message)
|
@@ -223,11 +279,11 @@ module VectorMCP
|
|
223
279
|
|
224
280
|
# --- Run helpers (private) ---
|
225
281
|
|
226
|
-
#
|
282
|
+
# Gets the session for the stdio connection.
|
227
283
|
# @api private
|
228
|
-
# @return [VectorMCP::Session] The
|
284
|
+
# @return [VectorMCP::Session] The session.
|
229
285
|
def create_session
|
230
|
-
|
286
|
+
session
|
231
287
|
end
|
232
288
|
|
233
289
|
# Launches the input reading loop in a new thread.
|
@@ -250,6 +306,7 @@ module VectorMCP
|
|
250
306
|
def shutdown_transport
|
251
307
|
@running = false
|
252
308
|
@input_thread&.kill if @input_thread&.alive?
|
309
|
+
@session_manager&.cleanup_all_sessions
|
253
310
|
logger.info("Stdio transport shut down")
|
254
311
|
end
|
255
312
|
|
@@ -261,7 +318,7 @@ module VectorMCP
|
|
261
318
|
# @return [void]
|
262
319
|
def handle_outgoing_response(message)
|
263
320
|
request_id = message["id"]
|
264
|
-
|
321
|
+
# Received response for outgoing request
|
265
322
|
|
266
323
|
@mutex.synchronize do
|
267
324
|
# Store the response (convert keys to symbols for consistency)
|
@@ -272,7 +329,7 @@ module VectorMCP
|
|
272
329
|
condition = @outgoing_request_conditions[request_id]
|
273
330
|
if condition
|
274
331
|
condition.signal
|
275
|
-
|
332
|
+
# Signaled condition for request
|
276
333
|
else
|
277
334
|
logger.warn "[Stdio Transport] Received response for request ID #{request_id} but no thread is waiting"
|
278
335
|
end
|
@@ -342,7 +399,7 @@ module VectorMCP
|
|
342
399
|
# @return [void]
|
343
400
|
def write_message(message)
|
344
401
|
json_msg = message.to_json
|
345
|
-
|
402
|
+
# Sending stdio message
|
346
403
|
|
347
404
|
begin
|
348
405
|
@output_mutex.synchronize do
|