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,473 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# lib/vector_mcp/transport/stdio.rb
|
|
4
|
-
require "json"
|
|
5
|
-
require_relative "../errors"
|
|
6
|
-
require_relative "../util"
|
|
7
|
-
require_relative "stdio_session_manager"
|
|
8
|
-
require "securerandom" # For generating unique request IDs
|
|
9
|
-
require "timeout" # For request timeouts
|
|
10
|
-
|
|
11
|
-
module VectorMCP
|
|
12
|
-
module Transport
|
|
13
|
-
# Implements the Model Context Protocol transport over standard input/output (stdio).
|
|
14
|
-
# This transport reads JSON-RPC messages line-by-line from `$stdin` and writes
|
|
15
|
-
# responses/notifications line-by-line to `$stdout`.
|
|
16
|
-
#
|
|
17
|
-
# It is suitable for inter-process communication on the same machine where a parent
|
|
18
|
-
# process spawns an MCP server and communicates with it via its stdio streams.
|
|
19
|
-
class Stdio
|
|
20
|
-
# @return [VectorMCP::Server] The server instance this transport is bound to.
|
|
21
|
-
attr_reader :server
|
|
22
|
-
# @return [Logger] The logger instance, shared with the server.
|
|
23
|
-
attr_reader :logger
|
|
24
|
-
# @return [StdioSessionManager] The session manager for this transport.
|
|
25
|
-
attr_reader :session_manager
|
|
26
|
-
|
|
27
|
-
# Timeout for waiting for a response to a server-initiated request (in seconds)
|
|
28
|
-
DEFAULT_REQUEST_TIMEOUT = 30 # Configurable if needed
|
|
29
|
-
|
|
30
|
-
# Initializes a new Stdio transport.
|
|
31
|
-
#
|
|
32
|
-
# @param server [VectorMCP::Server] The server instance that will handle messages.
|
|
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 = {})
|
|
36
|
-
@server = server
|
|
37
|
-
@logger = server.logger
|
|
38
|
-
@session_manager = options[:enable_session_manager] ? StdioSessionManager.new(self) : nil
|
|
39
|
-
@input_mutex = Mutex.new
|
|
40
|
-
@output_mutex = Mutex.new
|
|
41
|
-
@running = false
|
|
42
|
-
@input_thread = nil
|
|
43
|
-
@shutdown_requested = false
|
|
44
|
-
@outgoing_request_responses = {} # To store responses for server-initiated requests
|
|
45
|
-
@outgoing_request_conditions = {} # ConditionVariables for server-initiated requests
|
|
46
|
-
@mutex = Mutex.new # To synchronize access to shared response data
|
|
47
|
-
@request_id_generator = Enumerator.new do |y|
|
|
48
|
-
i = 0
|
|
49
|
-
loop { y << "vecmcp_stdio_#{i += 1}_#{SecureRandom.hex(4)}" }
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Starts the stdio transport, listening for input and processing messages.
|
|
54
|
-
# This method will block until the input stream is closed or an interrupt is received.
|
|
55
|
-
#
|
|
56
|
-
# @return [void]
|
|
57
|
-
def run
|
|
58
|
-
session = create_session
|
|
59
|
-
logger.info("Starting stdio transport")
|
|
60
|
-
@running = true
|
|
61
|
-
|
|
62
|
-
begin
|
|
63
|
-
launch_input_thread(session)
|
|
64
|
-
@input_thread.join
|
|
65
|
-
rescue Interrupt
|
|
66
|
-
logger.info("Interrupted. Shutting down...")
|
|
67
|
-
ensure
|
|
68
|
-
shutdown_transport
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Sends a JSON-RPC response message for a given request ID.
|
|
73
|
-
#
|
|
74
|
-
# @param id [String, Integer, nil] The ID of the request being responded to.
|
|
75
|
-
# @param result [Object] The result data for the successful request.
|
|
76
|
-
# @return [void]
|
|
77
|
-
def send_response(id, result)
|
|
78
|
-
response = {
|
|
79
|
-
jsonrpc: "2.0",
|
|
80
|
-
id: id,
|
|
81
|
-
result: result
|
|
82
|
-
}
|
|
83
|
-
write_message(response)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Sends a JSON-RPC error response message.
|
|
87
|
-
#
|
|
88
|
-
# @param id [String, Integer, nil] The ID of the request that caused the error.
|
|
89
|
-
# @param code [Integer] The JSON-RPC error code.
|
|
90
|
-
# @param message [String] A short description of the error.
|
|
91
|
-
# @param data [Object, nil] Additional error data (optional).
|
|
92
|
-
# @return [void]
|
|
93
|
-
def send_error(id, code, message, data = nil)
|
|
94
|
-
error_obj = { code: code, message: message }
|
|
95
|
-
error_obj[:data] = data if data
|
|
96
|
-
response = {
|
|
97
|
-
jsonrpc: "2.0",
|
|
98
|
-
id: id,
|
|
99
|
-
error: error_obj
|
|
100
|
-
}
|
|
101
|
-
write_message(response)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Sends a JSON-RPC notification message (a request without an ID).
|
|
105
|
-
#
|
|
106
|
-
# @param method [String] The method name of the notification.
|
|
107
|
-
# @param params [Hash, Array, nil] The parameters for the notification (optional).
|
|
108
|
-
# @return [void]
|
|
109
|
-
def send_notification(method, params = nil)
|
|
110
|
-
notification = {
|
|
111
|
-
jsonrpc: "2.0",
|
|
112
|
-
method: method
|
|
113
|
-
}
|
|
114
|
-
notification[:params] = params if params
|
|
115
|
-
write_message(notification)
|
|
116
|
-
end
|
|
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
|
-
|
|
155
|
-
# Sends a server-initiated JSON-RPC request to the client and waits for a response.
|
|
156
|
-
# This is a blocking call.
|
|
157
|
-
#
|
|
158
|
-
# @param method [String] The request method name.
|
|
159
|
-
# @param params [Hash, Array, nil] The request parameters.
|
|
160
|
-
# @param timeout [Numeric] How long to wait for a response, in seconds.
|
|
161
|
-
# @return [Object] The result part of the client's response.
|
|
162
|
-
# @raise [VectorMCP::SamplingError, VectorMCP::SamplingTimeoutError] if the client returns an error or times out.
|
|
163
|
-
# @raise [ArgumentError] if method is blank.
|
|
164
|
-
def send_request(method, params = nil, timeout: DEFAULT_REQUEST_TIMEOUT)
|
|
165
|
-
raise ArgumentError, "Method cannot be blank" if method.to_s.strip.empty?
|
|
166
|
-
|
|
167
|
-
request_id = @request_id_generator.next
|
|
168
|
-
request_payload = { jsonrpc: "2.0", id: request_id, method: method }
|
|
169
|
-
request_payload[:params] = params if params
|
|
170
|
-
|
|
171
|
-
setup_request_tracking(request_id)
|
|
172
|
-
# Sending request to client
|
|
173
|
-
write_message(request_payload)
|
|
174
|
-
|
|
175
|
-
response = wait_for_response(request_id, method, timeout)
|
|
176
|
-
process_response(response, request_id, method)
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Initiates an immediate shutdown of the transport.
|
|
180
|
-
# Sets the running flag to false and attempts to kill the input reading thread.
|
|
181
|
-
#
|
|
182
|
-
# @return [void]
|
|
183
|
-
def shutdown
|
|
184
|
-
logger.info("Shutdown requested for stdio transport.")
|
|
185
|
-
@running = false
|
|
186
|
-
@input_thread&.kill if @input_thread&.alive?
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
private
|
|
190
|
-
|
|
191
|
-
# The main loop for reading and processing lines from `$stdin`.
|
|
192
|
-
# @api private
|
|
193
|
-
# @param session [VectorMCP::Session] The session object for this connection.
|
|
194
|
-
# @return [void]
|
|
195
|
-
# Constant identifier for stdio sessions
|
|
196
|
-
def read_input_loop(session)
|
|
197
|
-
while @running
|
|
198
|
-
line = read_input_line
|
|
199
|
-
if line.nil?
|
|
200
|
-
logger.info("End of input ($stdin closed). Shutting down stdio transport.")
|
|
201
|
-
break
|
|
202
|
-
end
|
|
203
|
-
next if line.strip.empty?
|
|
204
|
-
|
|
205
|
-
handle_input_line(line, session)
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# Reads a single line from `$stdin` in a thread-safe manner.
|
|
210
|
-
# @api private
|
|
211
|
-
# @return [String, nil] The line read from stdin, or nil if EOF is reached.
|
|
212
|
-
def read_input_line
|
|
213
|
-
@input_mutex.synchronize do
|
|
214
|
-
$stdin.gets
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
# Parses a line of input as JSON and dispatches it to the server for handling.
|
|
219
|
-
# Sends back any response data or errors.
|
|
220
|
-
# @api private
|
|
221
|
-
# @param line [String] The line of text read from stdin.
|
|
222
|
-
# @param session [VectorMCP::Session] The current session.
|
|
223
|
-
# @return [void]
|
|
224
|
-
def handle_input_line(line, _session)
|
|
225
|
-
message = parse_json(line)
|
|
226
|
-
return if message.is_a?(Array) && message.empty? # Error handled in parse_json, indicated by empty array
|
|
227
|
-
|
|
228
|
-
return handle_outgoing_response(message) if outgoing_response?(message)
|
|
229
|
-
|
|
230
|
-
handle_server_message(message)
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# Checks if a message is a response to an outgoing request.
|
|
234
|
-
# @api private
|
|
235
|
-
# @param message [Hash] The parsed message.
|
|
236
|
-
# @return [Boolean] True if this is an outgoing response.
|
|
237
|
-
def outgoing_response?(message)
|
|
238
|
-
message["id"] && !message["method"] && (message.key?("result") || message.key?("error"))
|
|
239
|
-
end
|
|
240
|
-
|
|
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).
|
|
256
|
-
# @api private
|
|
257
|
-
# @return [VectorMCP::Session] The current session.
|
|
258
|
-
def ensure_session_exists
|
|
259
|
-
@ensure_session_exists ||= VectorMCP::Session.new(@server, self, id: "stdio_global_session")
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
# Handles a server message with proper error handling.
|
|
263
|
-
# @api private
|
|
264
|
-
# @param message [Hash] The parsed message.
|
|
265
|
-
# @return [void]
|
|
266
|
-
def handle_server_message(message)
|
|
267
|
-
current_session = session
|
|
268
|
-
session_id = current_session.id
|
|
269
|
-
|
|
270
|
-
begin
|
|
271
|
-
result = @server.handle_message(message, current_session, session_id)
|
|
272
|
-
send_response(message["id"], result) if message["id"] && result
|
|
273
|
-
rescue VectorMCP::ProtocolError => e
|
|
274
|
-
handle_protocol_error(e, message)
|
|
275
|
-
rescue StandardError => e
|
|
276
|
-
handle_unexpected_error(e, message)
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
# --- Run helpers (private) ---
|
|
281
|
-
|
|
282
|
-
# Gets the session for the stdio connection.
|
|
283
|
-
# @api private
|
|
284
|
-
# @return [VectorMCP::Session] The session.
|
|
285
|
-
def create_session
|
|
286
|
-
session
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
# Launches the input reading loop in a new thread.
|
|
290
|
-
# Exits the process on fatal errors within this thread.
|
|
291
|
-
# @api private
|
|
292
|
-
# @param session [VectorMCP::Session] The session to pass to the input loop.
|
|
293
|
-
# @return [void]
|
|
294
|
-
def launch_input_thread(session)
|
|
295
|
-
@input_thread = Thread.new do
|
|
296
|
-
read_input_loop(session)
|
|
297
|
-
rescue StandardError => e
|
|
298
|
-
logger.error("Fatal error in input thread: #{e.message}")
|
|
299
|
-
exit(1) # Critical failure, exit the server process
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
# Cleans up transport resources, ensuring the input thread is stopped.
|
|
304
|
-
# @api private
|
|
305
|
-
# @return [void]
|
|
306
|
-
def shutdown_transport
|
|
307
|
-
@running = false
|
|
308
|
-
@input_thread&.kill if @input_thread&.alive?
|
|
309
|
-
@session_manager&.cleanup_all_sessions
|
|
310
|
-
logger.info("Stdio transport shut down")
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
# --- Input helpers (private) ---
|
|
314
|
-
|
|
315
|
-
# Handles responses to outgoing requests (like sampling requests).
|
|
316
|
-
# @api private
|
|
317
|
-
# @param message [Hash] The parsed response message.
|
|
318
|
-
# @return [void]
|
|
319
|
-
def handle_outgoing_response(message)
|
|
320
|
-
request_id = message["id"]
|
|
321
|
-
# Received response for outgoing request
|
|
322
|
-
|
|
323
|
-
@mutex.synchronize do
|
|
324
|
-
# Store the response (convert keys to symbols for consistency)
|
|
325
|
-
response_data = deep_transform_keys(message, &:to_sym)
|
|
326
|
-
@outgoing_request_responses[request_id] = response_data
|
|
327
|
-
|
|
328
|
-
# Signal any thread waiting for this response
|
|
329
|
-
condition = @outgoing_request_conditions[request_id]
|
|
330
|
-
if condition
|
|
331
|
-
condition.signal
|
|
332
|
-
# Signaled condition for request
|
|
333
|
-
else
|
|
334
|
-
logger.warn "[Stdio Transport] Received response for request ID #{request_id} but no thread is waiting"
|
|
335
|
-
end
|
|
336
|
-
end
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
# Parses a line of text as JSON.
|
|
340
|
-
# If parsing fails, sends a JSON-RPC ParseError and returns an empty array
|
|
341
|
-
# to signal that the error has been handled.
|
|
342
|
-
# @api private
|
|
343
|
-
# @param line [String] The line to parse.
|
|
344
|
-
# @return [Hash, Array] The parsed JSON message as a Hash, or an empty Array if a parse error occurred and was handled.
|
|
345
|
-
def parse_json(line)
|
|
346
|
-
JSON.parse(line.strip)
|
|
347
|
-
rescue JSON::ParserError => e
|
|
348
|
-
logger.error("Failed to parse message as JSON: #{line.strip.inspect} - #{e.message}")
|
|
349
|
-
id = begin
|
|
350
|
-
VectorMCP::Util.extract_id_from_invalid_json(line)
|
|
351
|
-
rescue StandardError
|
|
352
|
-
nil # Best effort, don't let ID extraction fail fatally
|
|
353
|
-
end
|
|
354
|
-
send_error(id, -32_700, "Parse error")
|
|
355
|
-
[] # Signal that error was handled
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
# Handles known VectorMCP::ProtocolError exceptions during message processing.
|
|
359
|
-
# @api private
|
|
360
|
-
# @param error [VectorMCP::ProtocolError] The protocol error instance.
|
|
361
|
-
# @param message [Hash, nil] The original parsed message, if available.
|
|
362
|
-
# @return [void]
|
|
363
|
-
def handle_protocol_error(error, message)
|
|
364
|
-
logger.error("Protocol error processing message: #{error.message} (code: #{error.code}), Details: #{error.details.inspect}")
|
|
365
|
-
request_id = error.request_id || message&.fetch("id", nil)
|
|
366
|
-
send_error(request_id, error.code, error.message, error.details)
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
# Handles unexpected StandardError exceptions during message processing.
|
|
370
|
-
# @api private
|
|
371
|
-
# @param error [StandardError] The unexpected error instance.
|
|
372
|
-
# @param message [Hash, nil] The original parsed message, if available.
|
|
373
|
-
# @return [void]
|
|
374
|
-
def handle_unexpected_error(error, message)
|
|
375
|
-
logger.error("Unexpected error handling message: #{error.message}\n#{error.backtrace.join("\n")}")
|
|
376
|
-
request_id = message&.fetch("id", nil)
|
|
377
|
-
send_error(request_id, -32_603, "Internal error", { details: error.message })
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
# Recursively transforms hash keys using the given block.
|
|
381
|
-
# @api private
|
|
382
|
-
# @param obj [Object] The object to transform (Hash, Array, or other).
|
|
383
|
-
# @return [Object] The transformed object.
|
|
384
|
-
def deep_transform_keys(obj, &block)
|
|
385
|
-
case obj
|
|
386
|
-
when Hash
|
|
387
|
-
obj.transform_keys(&block).transform_values { |v| deep_transform_keys(v, &block) }
|
|
388
|
-
when Array
|
|
389
|
-
obj.map { |v| deep_transform_keys(v, &block) }
|
|
390
|
-
else
|
|
391
|
-
obj
|
|
392
|
-
end
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
# Writes a message hash to `$stdout` as a JSON string, followed by a newline.
|
|
396
|
-
# Ensures the output is flushed. Handles EPIPE errors if stdout closes.
|
|
397
|
-
# @api private
|
|
398
|
-
# @param message [Hash] The message hash to send.
|
|
399
|
-
# @return [void]
|
|
400
|
-
def write_message(message)
|
|
401
|
-
json_msg = message.to_json
|
|
402
|
-
# Sending stdio message
|
|
403
|
-
|
|
404
|
-
begin
|
|
405
|
-
@output_mutex.synchronize do
|
|
406
|
-
$stdout.puts(json_msg)
|
|
407
|
-
$stdout.flush
|
|
408
|
-
end
|
|
409
|
-
rescue Errno::EPIPE
|
|
410
|
-
logger.error("Output pipe closed. Cannot send message. Shutting down stdio transport.")
|
|
411
|
-
shutdown # Initiate shutdown as we can no longer communicate
|
|
412
|
-
end
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
# Sets up tracking for an outgoing request.
|
|
416
|
-
# @api private
|
|
417
|
-
# @param request_id [String] The request ID to track.
|
|
418
|
-
# @return [void]
|
|
419
|
-
def setup_request_tracking(request_id)
|
|
420
|
-
condition = ConditionVariable.new
|
|
421
|
-
@mutex.synchronize do
|
|
422
|
-
@outgoing_request_conditions[request_id] = condition
|
|
423
|
-
end
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
# Waits for a response to an outgoing request.
|
|
427
|
-
# @api private
|
|
428
|
-
# @param request_id [String] The request ID to wait for.
|
|
429
|
-
# @param method [String] The request method name.
|
|
430
|
-
# @param timeout [Numeric] How long to wait.
|
|
431
|
-
# @return [Hash] The response data.
|
|
432
|
-
# @raise [VectorMCP::SamplingTimeoutError] if timeout occurs.
|
|
433
|
-
def wait_for_response(request_id, method, timeout)
|
|
434
|
-
condition = @outgoing_request_conditions[request_id]
|
|
435
|
-
|
|
436
|
-
@mutex.synchronize do
|
|
437
|
-
Timeout.timeout(timeout) do
|
|
438
|
-
condition.wait(@mutex) until @outgoing_request_responses.key?(request_id)
|
|
439
|
-
@outgoing_request_responses.delete(request_id)
|
|
440
|
-
end
|
|
441
|
-
rescue Timeout::Error
|
|
442
|
-
logger.warn "[Stdio Transport] Timeout waiting for response to request ID #{request_id} (#{method}) after #{timeout}s"
|
|
443
|
-
@outgoing_request_responses.delete(request_id)
|
|
444
|
-
@outgoing_request_conditions.delete(request_id)
|
|
445
|
-
raise VectorMCP::SamplingTimeoutError, "Timeout waiting for client response to '#{method}' request (ID: #{request_id})"
|
|
446
|
-
ensure
|
|
447
|
-
@outgoing_request_conditions.delete(request_id)
|
|
448
|
-
end
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
# Processes the response from an outgoing request.
|
|
452
|
-
# @api private
|
|
453
|
-
# @param response [Hash, nil] The response data.
|
|
454
|
-
# @param request_id [String] The request ID.
|
|
455
|
-
# @param method [String] The request method name.
|
|
456
|
-
# @return [Object] The result data.
|
|
457
|
-
# @raise [VectorMCP::SamplingError] if response contains an error or is nil.
|
|
458
|
-
def process_response(response, request_id, method)
|
|
459
|
-
if response.nil?
|
|
460
|
-
raise VectorMCP::SamplingError, "No response received for '#{method}' request (ID: #{request_id}) - this indicates a logic error."
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
if response.key?(:error)
|
|
464
|
-
err = response[:error]
|
|
465
|
-
logger.warn "[Stdio Transport] Client returned error for request ID #{request_id} (#{method}): #{err.inspect}"
|
|
466
|
-
raise VectorMCP::SamplingError, "Client returned an error for '#{method}' request (ID: #{request_id}): [#{err[:code]}] #{err[:message]}"
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
response[:result]
|
|
470
|
-
end
|
|
471
|
-
end
|
|
472
|
-
end
|
|
473
|
-
end
|
|
@@ -1,181 +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 Stdio transport with single global session.
|
|
8
|
-
# Extends BaseSessionManager with stdio-specific functionality.
|
|
9
|
-
#
|
|
10
|
-
# The Stdio transport uses a single global session for the entire transport lifetime.
|
|
11
|
-
class StdioSessionManager < BaseSessionManager
|
|
12
|
-
GLOBAL_SESSION_ID = "stdio_global_session"
|
|
13
|
-
|
|
14
|
-
# Initializes a new Stdio session manager.
|
|
15
|
-
#
|
|
16
|
-
# @param transport [Stdio] The parent transport instance
|
|
17
|
-
# @param session_timeout [Integer] Session timeout in seconds (ignored for stdio)
|
|
18
|
-
def initialize(transport, session_timeout = 300)
|
|
19
|
-
super
|
|
20
|
-
|
|
21
|
-
# Create the single global session for stdio transport
|
|
22
|
-
@global_session = create_global_session
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Gets the global session for stdio transport.
|
|
26
|
-
# Stdio uses a single global session for the entire transport lifetime.
|
|
27
|
-
#
|
|
28
|
-
# @return [Session] The global session
|
|
29
|
-
def global_session
|
|
30
|
-
@global_session&.touch!
|
|
31
|
-
@global_session
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Gets or creates the global session for stdio transport.
|
|
35
|
-
# This is an alias for global_session for stdio transport.
|
|
36
|
-
#
|
|
37
|
-
# @return [Session] The global session
|
|
38
|
-
def global_session_or_create
|
|
39
|
-
global_session
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Override: Gets session by ID, but always returns the global session for stdio.
|
|
43
|
-
#
|
|
44
|
-
# @param session_id [String] The session ID (ignored for stdio)
|
|
45
|
-
# @return [Session] The global session
|
|
46
|
-
def session(_session_id = nil)
|
|
47
|
-
global_session
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Override: Always returns the global session for stdio.
|
|
51
|
-
#
|
|
52
|
-
# @param session_id [String, nil] The session ID (ignored)
|
|
53
|
-
# @return [Session] The global session
|
|
54
|
-
def session_or_create(_session_id = nil)
|
|
55
|
-
global_session
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Override: Cannot create additional sessions in stdio transport.
|
|
59
|
-
#
|
|
60
|
-
# @param session_id [String, nil] The session ID (ignored)
|
|
61
|
-
# @return [Session] The global session
|
|
62
|
-
def create_session(_session_id = nil)
|
|
63
|
-
# For stdio, always return the existing global session
|
|
64
|
-
global_session
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Override: Cannot terminate the global session while transport is running.
|
|
68
|
-
#
|
|
69
|
-
# @param session_id [String] The session ID (ignored)
|
|
70
|
-
# @return [Boolean] Always false (session cannot be terminated individually)
|
|
71
|
-
def session_terminated?(_session_id)
|
|
72
|
-
# For stdio, the session is only terminated when the transport shuts down
|
|
73
|
-
false
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Override: Always returns 1 for the single global session.
|
|
77
|
-
#
|
|
78
|
-
# @return [Integer] Always 1
|
|
79
|
-
def session_count
|
|
80
|
-
1
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Override: Always returns the global session ID.
|
|
84
|
-
#
|
|
85
|
-
# @return [Array<String>] Array containing the global session ID
|
|
86
|
-
def active_session_ids
|
|
87
|
-
[GLOBAL_SESSION_ID]
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Override: Always returns true for the single session.
|
|
91
|
-
#
|
|
92
|
-
# @return [Boolean] Always true
|
|
93
|
-
def sessions?
|
|
94
|
-
true
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Gets all sessions for stdio transport (just the one global session).
|
|
98
|
-
#
|
|
99
|
-
# @return [Array<Session>] Array containing the global session
|
|
100
|
-
def all_sessions
|
|
101
|
-
[@global_session].compact
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Alias for global_session for compatibility with tests.
|
|
105
|
-
#
|
|
106
|
-
# @return [Session] The global session
|
|
107
|
-
def current_global_session
|
|
108
|
-
global_session
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Alias for global_session_or_create for compatibility with tests.
|
|
112
|
-
#
|
|
113
|
-
# @return [Session] The global session
|
|
114
|
-
def global_session_or_create_current
|
|
115
|
-
global_session_or_create
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Alias for all_sessions for compatibility with tests.
|
|
119
|
-
#
|
|
120
|
-
# @return [Array<Session>] Array containing the global session
|
|
121
|
-
def current_all_sessions
|
|
122
|
-
all_sessions
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
protected
|
|
126
|
-
|
|
127
|
-
# Override: Stdio doesn't need automatic cleanup since it has a single persistent session.
|
|
128
|
-
def auto_cleanup_enabled?
|
|
129
|
-
false
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Override: Returns metadata for stdio sessions.
|
|
133
|
-
def create_session_metadata
|
|
134
|
-
{ session_type: :stdio_global, created_via: :transport_startup }
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Override: Stdio can always send messages (single session assumption).
|
|
138
|
-
def can_send_message_to_session?(_session)
|
|
139
|
-
true
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Override: Sends messages via the transport's notification mechanism.
|
|
143
|
-
def message_sent_to_session?(_session, message)
|
|
144
|
-
# For stdio, we send notifications directly via the transport
|
|
145
|
-
@transport.send_notification(message["method"], message["params"])
|
|
146
|
-
true
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Override: Stdio broadcasts to the single session (same as regular send).
|
|
150
|
-
def broadcast_message(message)
|
|
151
|
-
message_sent_to_session?(@global_session, message) ? 1 : 0
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
private
|
|
155
|
-
|
|
156
|
-
# Creates the single global session for stdio transport.
|
|
157
|
-
#
|
|
158
|
-
# @return [BaseSessionManager::Session] The global session
|
|
159
|
-
def create_global_session
|
|
160
|
-
now = Time.now
|
|
161
|
-
|
|
162
|
-
# Create VectorMCP session context with minimal request context
|
|
163
|
-
request_context = VectorMCP::RequestContext.minimal("stdio")
|
|
164
|
-
session_context = VectorMCP::Session.new(@transport.server, @transport, id: GLOBAL_SESSION_ID, request_context: request_context)
|
|
165
|
-
|
|
166
|
-
# Create internal session record using base session manager struct
|
|
167
|
-
session = BaseSessionManager::Session.new(
|
|
168
|
-
GLOBAL_SESSION_ID,
|
|
169
|
-
session_context,
|
|
170
|
-
now,
|
|
171
|
-
now,
|
|
172
|
-
create_session_metadata
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
@sessions[GLOBAL_SESSION_ID] = session
|
|
176
|
-
logger.info { "Global stdio session created: #{GLOBAL_SESSION_ID}" }
|
|
177
|
-
session
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
end
|