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
data/lib/vector_mcp/session.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "sampling/request"
|
|
|
4
4
|
require_relative "sampling/result"
|
|
5
5
|
require_relative "errors"
|
|
6
6
|
require_relative "request_context"
|
|
7
|
+
require_relative "security/session_context"
|
|
7
8
|
|
|
8
9
|
module VectorMCP
|
|
9
10
|
# Represents the state of a single client-server connection session in MCP.
|
|
@@ -17,7 +18,7 @@ module VectorMCP
|
|
|
17
18
|
# @attr_reader request_context [RequestContext] The request context for this session.
|
|
18
19
|
class Session
|
|
19
20
|
attr_reader :server_info, :server_capabilities, :protocol_version, :client_info, :client_capabilities, :server, :transport, :id, :request_context
|
|
20
|
-
attr_accessor :data # For user-defined session-specific storage
|
|
21
|
+
attr_accessor :data, :security_context # For user-defined session-specific storage and resolved auth context
|
|
21
22
|
|
|
22
23
|
# Initializes a new session.
|
|
23
24
|
#
|
|
@@ -33,6 +34,7 @@ module VectorMCP
|
|
|
33
34
|
@client_info = nil
|
|
34
35
|
@client_capabilities = nil
|
|
35
36
|
@data = {} # Initialize user data hash
|
|
37
|
+
@security_context = Security::SessionContext.anonymous
|
|
36
38
|
@logger = server.logger
|
|
37
39
|
|
|
38
40
|
# Initialize request context
|
|
@@ -243,11 +245,11 @@ module VectorMCP
|
|
|
243
245
|
send_request_kwargs = {}
|
|
244
246
|
send_request_kwargs[:timeout] = timeout if timeout
|
|
245
247
|
|
|
246
|
-
#
|
|
248
|
+
# Prefer session-targeted request sending when available
|
|
247
249
|
raw_result = if @transport.respond_to?(:send_request_to_session)
|
|
248
250
|
@transport.send_request_to_session(@id, *send_request_args, **send_request_kwargs)
|
|
249
251
|
else
|
|
250
|
-
# Fallback to generic send_request for
|
|
252
|
+
# Fallback to generic send_request for transports without session targeting
|
|
251
253
|
@transport.send_request(*send_request_args, **send_request_kwargs)
|
|
252
254
|
end
|
|
253
255
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module VectorMCP
|
|
8
|
+
# Abstract base class for declarative tool definitions.
|
|
9
|
+
#
|
|
10
|
+
# Subclass this to define tools using a class-level DSL instead of
|
|
11
|
+
# the block-based +register_tool+ API. The two styles are fully
|
|
12
|
+
# interchangeable -- both produce the same +VectorMCP::Definitions::Tool+
|
|
13
|
+
# struct that the rest of the system consumes.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# class ListProviders < VectorMCP::Tool
|
|
17
|
+
# tool_name "list_providers"
|
|
18
|
+
# description "List providers filtered by category or status"
|
|
19
|
+
#
|
|
20
|
+
# param :category, type: :string, desc: "Filter by category slug"
|
|
21
|
+
# param :active, type: :boolean, default: true
|
|
22
|
+
#
|
|
23
|
+
# def call(args, session)
|
|
24
|
+
# Provider.where(active: args.fetch("active", true))
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# server.register(ListProviders)
|
|
29
|
+
#
|
|
30
|
+
class Tool
|
|
31
|
+
# Maps Ruby symbol types to JSON Schema property fragments.
|
|
32
|
+
# Each value is merged into the generated property hash, so it may carry
|
|
33
|
+
# both +type+ and +format+ (or any other JSON Schema keyword).
|
|
34
|
+
TYPE_MAP = {
|
|
35
|
+
string: { "type" => "string" },
|
|
36
|
+
integer: { "type" => "integer" },
|
|
37
|
+
number: { "type" => "number" },
|
|
38
|
+
boolean: { "type" => "boolean" },
|
|
39
|
+
array: { "type" => "array" },
|
|
40
|
+
object: { "type" => "object" },
|
|
41
|
+
date: { "type" => "string", "format" => "date" },
|
|
42
|
+
datetime: { "type" => "string", "format" => "date-time" }
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
# Maps Ruby symbol types to a coercer lambda. Types not listed here
|
|
46
|
+
# pass their values through unchanged. Coercers receive the raw value
|
|
47
|
+
# (or nil) and return the coerced value. They must be total over the
|
|
48
|
+
# values JSON Schema validation would accept.
|
|
49
|
+
COERCERS = {
|
|
50
|
+
date: ->(v) { v.nil? || v.is_a?(Date) ? v : Date.parse(v.to_s) },
|
|
51
|
+
datetime: ->(v) { v.nil? || v.is_a?(Time) ? v : Time.parse(v.to_s) }
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# Ensures each subclass gets its own +@params+ array so sibling
|
|
55
|
+
# classes do not share mutable state.
|
|
56
|
+
def self.inherited(subclass)
|
|
57
|
+
super
|
|
58
|
+
subclass.instance_variable_set(:@params, [])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Sets or retrieves the tool name.
|
|
62
|
+
#
|
|
63
|
+
# When called with an argument, stores the name.
|
|
64
|
+
# When called without, returns the stored name or derives one from the class name.
|
|
65
|
+
#
|
|
66
|
+
# @param name [String, Symbol, nil] The tool name to set.
|
|
67
|
+
# @return [String] The tool name.
|
|
68
|
+
def self.tool_name(name = nil)
|
|
69
|
+
if name
|
|
70
|
+
@tool_name = name.to_s
|
|
71
|
+
else
|
|
72
|
+
@tool_name || derive_tool_name
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Sets or retrieves the tool description.
|
|
77
|
+
#
|
|
78
|
+
# @param text [String, nil] The description to set.
|
|
79
|
+
# @return [String, nil] The description.
|
|
80
|
+
def self.description(text = nil)
|
|
81
|
+
if text
|
|
82
|
+
@description = text
|
|
83
|
+
else
|
|
84
|
+
@description
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Declares a parameter for the tool's input schema.
|
|
89
|
+
#
|
|
90
|
+
# @param name [Symbol, String] The parameter name.
|
|
91
|
+
# @param type [Symbol] The parameter type (:string, :integer, :number, :boolean, :array, :object).
|
|
92
|
+
# @param desc [String, nil] A human-readable description.
|
|
93
|
+
# @param required [Boolean] Whether the parameter is required (default: false).
|
|
94
|
+
# @param options [Hash] Additional JSON Schema keywords (enum:, default:, format:, items:, etc.).
|
|
95
|
+
def self.param(name, type: :string, desc: nil, required: false, **options)
|
|
96
|
+
@params << {
|
|
97
|
+
name: name.to_s,
|
|
98
|
+
type: type,
|
|
99
|
+
desc: desc,
|
|
100
|
+
required: required,
|
|
101
|
+
options: options
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Builds a +VectorMCP::Definitions::Tool+ struct from the DSL metadata.
|
|
106
|
+
#
|
|
107
|
+
# @return [VectorMCP::Definitions::Tool]
|
|
108
|
+
# @raise [ArgumentError] If the subclass is missing a description or +#call+ method.
|
|
109
|
+
def self.to_definition
|
|
110
|
+
validate_tool_class!
|
|
111
|
+
|
|
112
|
+
VectorMCP::Definitions::Tool.new(
|
|
113
|
+
tool_name,
|
|
114
|
+
description,
|
|
115
|
+
build_input_schema,
|
|
116
|
+
build_handler
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The handler method that subclasses must implement.
|
|
121
|
+
#
|
|
122
|
+
# @param _args [Hash] The tool arguments (string keys).
|
|
123
|
+
# @param _session [VectorMCP::Session] The current session.
|
|
124
|
+
# @return [Object] The tool result.
|
|
125
|
+
def call(_args, _session)
|
|
126
|
+
raise NotImplementedError, "#{self.class.name} must implement #call(args, session)"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Derives a snake_case tool name from the class name.
|
|
130
|
+
def self.derive_tool_name
|
|
131
|
+
base = name&.split("::")&.last
|
|
132
|
+
return "unnamed_tool" unless base
|
|
133
|
+
|
|
134
|
+
base.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
135
|
+
end
|
|
136
|
+
private_class_method :derive_tool_name
|
|
137
|
+
|
|
138
|
+
# Validates that the subclass is properly configured.
|
|
139
|
+
def self.validate_tool_class!
|
|
140
|
+
raise ArgumentError, "#{name || self} must declare a description" unless description
|
|
141
|
+
return if method_defined?(:call) && instance_method(:call).owner != VectorMCP::Tool
|
|
142
|
+
|
|
143
|
+
raise ArgumentError, "#{name || self} must implement #call"
|
|
144
|
+
end
|
|
145
|
+
private_class_method :validate_tool_class!
|
|
146
|
+
|
|
147
|
+
# Builds a 2-arity handler lambda. A new instance is created per invocation.
|
|
148
|
+
# Arguments are coerced based on the declared param types before the
|
|
149
|
+
# handler sees them (e.g. :date param strings become Date objects).
|
|
150
|
+
def self.build_handler
|
|
151
|
+
klass = self
|
|
152
|
+
params = @params
|
|
153
|
+
->(args, session) { klass.new.call(klass.coerce_args(args, params), session) }
|
|
154
|
+
end
|
|
155
|
+
private_class_method :build_handler
|
|
156
|
+
|
|
157
|
+
# Applies coercers to the raw argument hash. Returns a new hash; does
|
|
158
|
+
# not mutate the original. Keys without a coercible type pass through.
|
|
159
|
+
# Keys that are absent from +args+ stay absent — coercion only fires
|
|
160
|
+
# for keys actually present.
|
|
161
|
+
#
|
|
162
|
+
# A parse failure on a client-supplied value is translated into
|
|
163
|
+
# +VectorMCP::InvalidParamsError+ (JSON-RPC -32602) so the client sees
|
|
164
|
+
# a "bad request" response instead of a generic internal error.
|
|
165
|
+
# This is needed because the +json-schema+ gem does not enforce
|
|
166
|
+
# +format: date+ upstream (it does enforce +format: date-time+), so
|
|
167
|
+
# malformed +:date+ values would otherwise crash inside +Date.parse+.
|
|
168
|
+
def self.coerce_args(args, params)
|
|
169
|
+
coerced = args.dup
|
|
170
|
+
params.each do |param|
|
|
171
|
+
name = param[:name]
|
|
172
|
+
next unless coerced.key?(name)
|
|
173
|
+
|
|
174
|
+
coercer = COERCERS[param[:type]]
|
|
175
|
+
next unless coercer
|
|
176
|
+
|
|
177
|
+
begin
|
|
178
|
+
coerced[name] = coercer.call(coerced[name])
|
|
179
|
+
rescue ArgumentError, TypeError => e
|
|
180
|
+
# Date::Error < ArgumentError in Ruby 3.2+, so ArgumentError alone covers Date.parse failures.
|
|
181
|
+
raise VectorMCP::InvalidParamsError.new(
|
|
182
|
+
"Invalid #{param[:type]} value for param '#{name}': #{e.message}",
|
|
183
|
+
details: { param: name, type: param[:type], message: e.message }
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
coerced
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Builds a JSON Schema hash from the declared params.
|
|
191
|
+
def self.build_input_schema
|
|
192
|
+
properties = {}
|
|
193
|
+
required = []
|
|
194
|
+
|
|
195
|
+
@params.each do |param|
|
|
196
|
+
type_fragment = TYPE_MAP.fetch(param[:type]) do
|
|
197
|
+
raise ArgumentError, "Unknown param type :#{param[:type]} for param '#{param[:name]}' in #{name}. " \
|
|
198
|
+
"Valid types: #{TYPE_MAP.keys.join(", ")}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
prop = type_fragment.dup
|
|
202
|
+
prop["description"] = param[:desc] if param[:desc]
|
|
203
|
+
|
|
204
|
+
param[:options].each do |key, value|
|
|
205
|
+
prop[key.to_s] = value
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
properties[param[:name]] = prop
|
|
209
|
+
required << param[:name] if param[:required]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
schema = {
|
|
213
|
+
"type" => "object",
|
|
214
|
+
"properties" => properties
|
|
215
|
+
}
|
|
216
|
+
schema["required"] = required unless required.empty?
|
|
217
|
+
schema
|
|
218
|
+
end
|
|
219
|
+
private_class_method :build_input_schema
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -183,22 +183,6 @@ module VectorMCP
|
|
|
183
183
|
end
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
-
# Broadcasts a message to all sessions that support messaging.
|
|
187
|
-
#
|
|
188
|
-
# @param message [Hash] The message to broadcast
|
|
189
|
-
# @return [Integer] Number of sessions the message was sent to
|
|
190
|
-
def broadcast_message(message)
|
|
191
|
-
count = 0
|
|
192
|
-
@sessions.each_value do |session|
|
|
193
|
-
next unless can_send_message_to_session?(session)
|
|
194
|
-
|
|
195
|
-
count += 1 if message_sent_to_session?(session, message)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Message broadcasted to recipients
|
|
199
|
-
count
|
|
200
|
-
end
|
|
201
|
-
|
|
202
186
|
protected
|
|
203
187
|
|
|
204
188
|
# Hook called when a session is created. Override in subclasses for transport-specific logic.
|
|
@@ -225,7 +209,7 @@ module VectorMCP
|
|
|
225
209
|
end
|
|
226
210
|
|
|
227
211
|
# Determines if this session manager should enable automatic cleanup.
|
|
228
|
-
# Override in subclasses that don't need automatic cleanup (e.g.,
|
|
212
|
+
# Override in subclasses that don't need automatic cleanup (e.g., transports with single persistent session).
|
|
229
213
|
#
|
|
230
214
|
# @return [Boolean] True if auto-cleanup should be enabled
|
|
231
215
|
def auto_cleanup_enabled?
|
|
@@ -17,7 +17,7 @@ module VectorMCP
|
|
|
17
17
|
# @api private
|
|
18
18
|
class EventStore
|
|
19
19
|
# Event data structure
|
|
20
|
-
Event = Struct.new(:id, :data, :type, :timestamp, :session_id) do
|
|
20
|
+
Event = Struct.new(:id, :data, :type, :timestamp, :session_id, :stream_id) do
|
|
21
21
|
def to_sse_format
|
|
22
22
|
lines = []
|
|
23
23
|
lines << "id: #{id}"
|
|
@@ -45,12 +45,13 @@ module VectorMCP
|
|
|
45
45
|
# @param data [String] The event data
|
|
46
46
|
# @param type [String] The event type (optional)
|
|
47
47
|
# @param session_id [String, nil] The session ID to scope this event to
|
|
48
|
+
# @param stream_id [String, nil] The stream ID to scope this event to
|
|
48
49
|
# @return [String] The generated event ID
|
|
49
|
-
def store_event(data, type = nil, session_id: nil)
|
|
50
|
+
def store_event(data, type = nil, session_id: nil, stream_id: nil)
|
|
50
51
|
event_id = generate_event_id
|
|
51
52
|
timestamp = Time.now
|
|
52
53
|
|
|
53
|
-
event = Event.new(event_id, data, type, timestamp, session_id)
|
|
54
|
+
event = Event.new(event_id, data, type, timestamp, session_id, stream_id)
|
|
54
55
|
|
|
55
56
|
# Add to events array
|
|
56
57
|
@events.push(event)
|
|
@@ -74,8 +75,9 @@ module VectorMCP
|
|
|
74
75
|
#
|
|
75
76
|
# @param last_event_id [String] The last event ID received by client
|
|
76
77
|
# @param session_id [String, nil] Filter events to this session only
|
|
78
|
+
# @param stream_id [String, nil] Filter events to this stream only
|
|
77
79
|
# @return [Array<Event>] Array of events after the specified ID
|
|
78
|
-
def get_events_after(last_event_id, session_id: nil)
|
|
80
|
+
def get_events_after(last_event_id, session_id: nil, stream_id: nil)
|
|
79
81
|
events = if last_event_id.nil?
|
|
80
82
|
@events.to_a
|
|
81
83
|
else
|
|
@@ -89,9 +91,21 @@ module VectorMCP
|
|
|
89
91
|
end
|
|
90
92
|
|
|
91
93
|
events = events.select { |e| e.session_id == session_id } if session_id
|
|
94
|
+
events = events.select { |e| e.stream_id == stream_id } if stream_id
|
|
92
95
|
events
|
|
93
96
|
end
|
|
94
97
|
|
|
98
|
+
# Retrieves a specific event by ID.
|
|
99
|
+
#
|
|
100
|
+
# @param event_id [String] The event ID to look up
|
|
101
|
+
# @return [Event, nil] The stored event, or nil if it is no longer retained
|
|
102
|
+
def get_event(event_id)
|
|
103
|
+
index = @event_index[event_id]
|
|
104
|
+
return nil if index.nil?
|
|
105
|
+
|
|
106
|
+
@events[index]
|
|
107
|
+
end
|
|
108
|
+
|
|
95
109
|
# Gets the total number of stored events.
|
|
96
110
|
#
|
|
97
111
|
# @return [Integer] Number of events currently stored
|
|
@@ -34,7 +34,7 @@ module VectorMCP
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def streaming?
|
|
37
|
-
|
|
37
|
+
!streaming_connection.nil? || !streaming_connections.empty?
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def streaming_connection
|
|
@@ -44,6 +44,25 @@ module VectorMCP
|
|
|
44
44
|
def streaming_connection=(connection)
|
|
45
45
|
metadata[:streaming_connection] = connection
|
|
46
46
|
end
|
|
47
|
+
|
|
48
|
+
def streaming_connections
|
|
49
|
+
metadata[:streaming_connections] ||= Concurrent::Hash.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def add_streaming_connection(connection)
|
|
53
|
+
streaming_connections[connection.stream_id] = connection
|
|
54
|
+
self.streaming_connection = connection
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def remove_streaming_connection(connection = nil)
|
|
58
|
+
if connection
|
|
59
|
+
streaming_connections.delete(connection.stream_id)
|
|
60
|
+
self.streaming_connection = streaming_connections.values.first if streaming_connection == connection
|
|
61
|
+
else
|
|
62
|
+
streaming_connections.clear
|
|
63
|
+
self.streaming_connection = nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
47
66
|
end
|
|
48
67
|
|
|
49
68
|
# Initializes a new HTTP stream session manager.
|
|
@@ -64,7 +83,7 @@ module VectorMCP
|
|
|
64
83
|
end
|
|
65
84
|
|
|
66
85
|
# Pre-allocate metadata hash for better performance
|
|
67
|
-
metadata =
|
|
86
|
+
metadata = create_session_metadata
|
|
68
87
|
|
|
69
88
|
# Create internal session record with streaming connection metadata
|
|
70
89
|
session = Session.new(session_id, session_context, now, now, metadata)
|
|
@@ -131,17 +150,18 @@ module VectorMCP
|
|
|
131
150
|
# @param connection [Object] The streaming connection object
|
|
132
151
|
# @return [void]
|
|
133
152
|
def set_streaming_connection(session, connection)
|
|
134
|
-
session.
|
|
153
|
+
session.add_streaming_connection(connection)
|
|
135
154
|
session.touch!
|
|
136
|
-
logger.debug { "Streaming connection associated: #{session.id}" }
|
|
155
|
+
logger.debug { "Streaming connection associated: #{session.id} (stream #{connection.stream_id})" }
|
|
137
156
|
end
|
|
138
157
|
|
|
139
158
|
# Removes streaming connection from a session.
|
|
140
159
|
#
|
|
141
160
|
# @param session [Session] The session to remove streaming from
|
|
161
|
+
# @param connection [Object, nil] The specific connection to remove, or nil to clear all
|
|
142
162
|
# @return [void]
|
|
143
|
-
def remove_streaming_connection(session)
|
|
144
|
-
session.
|
|
163
|
+
def remove_streaming_connection(session, connection = nil)
|
|
164
|
+
session.remove_streaming_connection(connection)
|
|
145
165
|
session.touch!
|
|
146
166
|
logger.debug { "Streaming connection removed: #{session.id}" }
|
|
147
167
|
end
|
|
@@ -155,7 +175,7 @@ module VectorMCP
|
|
|
155
175
|
|
|
156
176
|
# Override: Returns metadata for new HTTP stream sessions.
|
|
157
177
|
def create_session_metadata
|
|
158
|
-
{ streaming_connection: nil }
|
|
178
|
+
{ streaming_connection: nil, streaming_connections: Concurrent::Hash.new }
|
|
159
179
|
end
|
|
160
180
|
|
|
161
181
|
# Override: Checks if a session can receive messages (has streaming connection).
|
|
@@ -175,15 +195,18 @@ module VectorMCP
|
|
|
175
195
|
# @param session [Session] The session whose connection to close
|
|
176
196
|
# @return [void]
|
|
177
197
|
def close_streaming_connection(session)
|
|
178
|
-
return unless session&.
|
|
198
|
+
return unless session&.streaming?
|
|
199
|
+
|
|
200
|
+
connections = session.streaming_connections.values
|
|
201
|
+
connections = [session.streaming_connection] if connections.empty? && session.streaming_connection
|
|
179
202
|
|
|
180
|
-
|
|
181
|
-
|
|
203
|
+
connections.each do |connection|
|
|
204
|
+
connection.close
|
|
182
205
|
rescue StandardError => e
|
|
183
206
|
logger.warn { "Error closing streaming connection for #{session.id}: #{e.message}" }
|
|
184
207
|
end
|
|
185
208
|
|
|
186
|
-
session.
|
|
209
|
+
session.remove_streaming_connection
|
|
187
210
|
end
|
|
188
211
|
end
|
|
189
212
|
end
|