vector_mcp 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -1
  3. data/README.md +39 -0
  4. data/lib/vector_mcp/definitions.rb +30 -0
  5. data/lib/vector_mcp/handlers/core.rb +32 -89
  6. data/lib/vector_mcp/invocation.rb +106 -0
  7. data/lib/vector_mcp/middleware/anonymizer.rb +186 -0
  8. data/lib/vector_mcp/middleware/hook.rb +7 -24
  9. data/lib/vector_mcp/middleware.rb +26 -9
  10. data/lib/vector_mcp/request_context.rb +97 -14
  11. data/lib/vector_mcp/security/auth_manager.rb +15 -14
  12. data/lib/vector_mcp/security/auth_result.rb +33 -0
  13. data/lib/vector_mcp/security/authorization.rb +5 -9
  14. data/lib/vector_mcp/security/middleware.rb +0 -47
  15. data/lib/vector_mcp/security/session_context.rb +11 -27
  16. data/lib/vector_mcp/security/strategies/api_key.rb +7 -52
  17. data/lib/vector_mcp/security/strategies/custom.rb +15 -39
  18. data/lib/vector_mcp/security/strategies/jwt_token.rb +7 -17
  19. data/lib/vector_mcp/server/capabilities.rb +22 -26
  20. data/lib/vector_mcp/server/message_handling.rb +24 -17
  21. data/lib/vector_mcp/server/registry.rb +70 -120
  22. data/lib/vector_mcp/server.rb +53 -19
  23. data/lib/vector_mcp/session.rb +55 -27
  24. data/lib/vector_mcp/token_store.rb +80 -0
  25. data/lib/vector_mcp/transport/base_session_manager.rb +12 -38
  26. data/lib/vector_mcp/transport/http_stream/event_store.rb +14 -16
  27. data/lib/vector_mcp/transport/http_stream/session_manager.rb +20 -61
  28. data/lib/vector_mcp/transport/http_stream/stream_handler.rb +7 -7
  29. data/lib/vector_mcp/transport/http_stream.rb +95 -55
  30. data/lib/vector_mcp/util/token_sweeper.rb +74 -0
  31. data/lib/vector_mcp/version.rb +1 -1
  32. data/lib/vector_mcp.rb +11 -0
  33. metadata +6 -1
@@ -72,7 +72,7 @@ module VectorMCP
72
72
  SUPPORTED_PROTOCOL_VERSIONS = %w[2025-11-25 2025-03-26 2024-11-05].freeze
73
73
 
74
74
  attr_reader :logger, :name, :version, :protocol_version, :tools, :resources, :prompts, :roots, :in_flight_requests,
75
- :auth_manager, :authorization, :security_middleware, :middleware_manager
75
+ :auth_manager, :authorization, :security_middleware, :middleware_manager, :oauth_resource_metadata_url
76
76
  attr_accessor :transport
77
77
 
78
78
  # Initializes a new VectorMCP server.
@@ -122,6 +122,7 @@ module VectorMCP
122
122
  @auth_manager = Security::AuthManager.new
123
123
  @authorization = Security::Authorization.new
124
124
  @security_middleware = Security::Middleware.new(@auth_manager, @authorization)
125
+ @oauth_resource_metadata_url = nil
125
126
 
126
127
  # Initialize middleware manager
127
128
  @middleware_manager = Middleware::Manager.new
@@ -188,28 +189,18 @@ module VectorMCP
188
189
  # Enable authentication with specified strategy and configuration
189
190
  # @param strategy [Symbol] the authentication strategy (:api_key, :jwt, :custom)
190
191
  # @param options [Hash] strategy-specific configuration options
192
+ # @option options [String] :resource_metadata_url OAuth 2.1 protected resource metadata URL
193
+ # (RFC 9728). When provided, unauthenticated requests to the HTTP Stream transport's MCP
194
+ # endpoint return HTTP 401 with a +WWW-Authenticate: Bearer+ header pointing at this URL,
195
+ # enabling MCP clients (e.g. Claude Desktop) to discover the authorization server and
196
+ # initiate an OAuth 2.1 flow. When omitted (default), auth failures continue to surface
197
+ # as JSON-RPC +-32401+ errors — existing behavior is preserved for non-OAuth deployments.
191
198
  # @return [void]
192
199
  def enable_authentication!(strategy: :api_key, **options, &block)
193
- # Clear existing strategies when switching to a new configuration
194
200
  clear_auth_strategies unless @auth_manager.strategies.empty?
195
-
201
+ extract_oauth_metadata!(options)
196
202
  @auth_manager.enable!(default_strategy: strategy)
197
-
198
- case strategy
199
- when :api_key
200
- add_api_key_auth(options[:keys] || [], allow_query_params: options[:allow_query_params] || false)
201
- when :jwt
202
- add_jwt_auth(options)
203
- when :custom
204
- handler = block || options[:handler]
205
- raise ArgumentError, "Custom authentication strategy requires a handler block" unless handler
206
-
207
- add_custom_auth(&handler)
208
-
209
- else
210
- raise ArgumentError, "Unknown authentication strategy: #{strategy}"
211
- end
212
-
203
+ register_auth_strategy(strategy, options, block || options.delete(:handler))
213
204
  @logger.info("Authentication enabled with strategy: #{strategy}")
214
205
  end
215
206
 
@@ -217,6 +208,7 @@ module VectorMCP
217
208
  # @return [void]
218
209
  def disable_authentication!
219
210
  @auth_manager.disable!
211
+ @oauth_resource_metadata_url = nil
220
212
  @logger.info("Authentication disabled")
221
213
  end
222
214
 
@@ -318,6 +310,34 @@ module VectorMCP
318
310
 
319
311
  private
320
312
 
313
+ # Extract OAuth resource metadata URL from options before they reach strategy constructors
314
+ # @param options [Hash] the options hash (mutated — :resource_metadata_url is removed)
315
+ # @return [void]
316
+ def extract_oauth_metadata!(options)
317
+ @oauth_resource_metadata_url = options.delete(:resource_metadata_url)
318
+ warn_on_insecure_oauth_metadata_url(@oauth_resource_metadata_url)
319
+ end
320
+
321
+ # Register the appropriate auth strategy based on the strategy name
322
+ # @param strategy [Symbol] the strategy type
323
+ # @param options [Hash] strategy-specific options
324
+ # @param handler [Proc, nil] custom handler block (for :custom strategy)
325
+ # @return [void]
326
+ def register_auth_strategy(strategy, options, handler)
327
+ case strategy
328
+ when :api_key
329
+ add_api_key_auth(options[:keys] || [], allow_query_params: options[:allow_query_params] || false)
330
+ when :jwt
331
+ add_jwt_auth(options)
332
+ when :custom
333
+ raise ArgumentError, "Custom authentication strategy requires a handler block" unless handler
334
+
335
+ add_custom_auth(&handler)
336
+ else
337
+ raise ArgumentError, "Unknown authentication strategy: #{strategy}"
338
+ end
339
+ end
340
+
321
341
  # Add API key authentication strategy
322
342
  # @param keys [Array<String>] array of valid API keys
323
343
  # @param allow_query_params [Boolean] whether to accept API keys from query parameters
@@ -350,6 +370,20 @@ module VectorMCP
350
370
  @auth_manager.remove_strategy(strategy_name)
351
371
  end
352
372
  end
373
+
374
+ # Emit a warning when the OAuth resource metadata URL is not HTTPS.
375
+ # We don't raise because local development against http://localhost is a valid use case.
376
+ # @param url [String, nil] the configured metadata URL
377
+ # @return [void]
378
+ def warn_on_insecure_oauth_metadata_url(url)
379
+ return if url.nil?
380
+ return if url.start_with?("https://")
381
+
382
+ @logger.warn do
383
+ "[SECURITY] resource_metadata_url is not HTTPS (#{url}). " \
384
+ "Use HTTPS in production; plaintext is only acceptable for local development."
385
+ end
386
+ end
353
387
  end
354
388
 
355
389
  module Transport
@@ -17,7 +17,8 @@ module VectorMCP
17
17
  # @attr_reader client_capabilities [Hash, nil] Capabilities supported by the client, received during initialization.
18
18
  # @attr_reader request_context [RequestContext] The request context for this session.
19
19
  class Session
20
- attr_reader :server_info, :server_capabilities, :protocol_version, :client_info, :client_capabilities, :server, :transport, :id, :request_context
20
+ attr_reader :server_info, :server_capabilities, :protocol_version, :client_info, :client_capabilities, :server, :transport, :id,
21
+ :request_context, :created_at, :last_accessed_at, :metadata
21
22
  attr_accessor :data, :security_context # For user-defined session-specific storage and resolved auth context
22
23
 
23
24
  # Initializes a new session.
@@ -35,6 +36,9 @@ module VectorMCP
35
36
  @client_capabilities = nil
36
37
  @data = {} # Initialize user data hash
37
38
  @security_context = Security::SessionContext.anonymous
39
+ @created_at = Time.now
40
+ @last_accessed_at = @created_at
41
+ @metadata = {} # Transport-owned lifecycle/connection state
38
42
  @logger = server.logger
39
43
 
40
44
  # Initialize request context
@@ -90,14 +94,34 @@ module VectorMCP
90
94
  @initialized_state == :succeeded
91
95
  end
92
96
 
97
+ # --- Lifecycle (managed by the transport's session manager) ---
98
+
99
+ # Records activity on this session, deferring expiry.
100
+ # @return [void]
101
+ def touch!
102
+ @last_accessed_at = Time.now
103
+ end
104
+
105
+ # @param timeout [Numeric] Idle timeout in seconds.
106
+ # @return [Boolean] True when the session has been idle longer than the timeout.
107
+ def expired?(timeout)
108
+ Time.now - @last_accessed_at > timeout
109
+ end
110
+
111
+ # @return [Float] Seconds since the session was created.
112
+ def age
113
+ Time.now - @created_at
114
+ end
115
+
93
116
  # Sets the request context for this session.
94
- # This method should be called by transport layers to populate request-specific data.
95
117
  #
118
+ # @deprecated Will be removed in 0.7. Request-scoped data travels on the
119
+ # per-request {VectorMCP::Invocation} passed to handlers.
96
120
  # @param context [RequestContext, Hash] The request context to set.
97
- # Can be a RequestContext object or a hash of attributes.
98
121
  # @return [RequestContext] The newly set request context.
99
122
  # @raise [ArgumentError] If the context is not a RequestContext or Hash.
100
123
  def request_context=(context)
124
+ deprecated_request_api(:request_context=)
101
125
  @request_context = case context
102
126
  when RequestContext
103
127
  context
@@ -109,53 +133,47 @@ module VectorMCP
109
133
  end
110
134
 
111
135
  # Updates the request context with new data.
112
- # This merges the provided attributes with the existing context.
113
136
  #
137
+ # @deprecated Will be removed in 0.7. Use the per-request invocation's
138
+ # {VectorMCP::Invocation#update_request_context} instead.
114
139
  # @param attributes [Hash] The attributes to merge into the request context.
115
140
  # @return [RequestContext] The updated request context.
116
141
  def update_request_context(**attributes)
117
- current_attrs = @request_context.to_h
118
-
119
- # Deep merge nested hashes like headers and params
120
- merged_attrs = current_attrs.dup
121
- attributes.each do |key, value|
122
- merged_attrs[key] = if value.is_a?(Hash) && current_attrs[key].is_a?(Hash)
123
- current_attrs[key].merge(value)
124
- else
125
- value
126
- end
127
- end
128
-
129
- @request_context = RequestContext.new(**merged_attrs)
142
+ deprecated_request_api(:update_request_context)
143
+ @request_context = @request_context.merge(attributes)
130
144
  end
131
145
 
132
- # Convenience method to check if the session has request headers.
133
- #
134
- # @return [Boolean] True if the request context has headers, false otherwise.
146
+ # @deprecated Will be removed in 0.7. Use the per-request invocation's
147
+ # {VectorMCP::Invocation#request_headers?} instead.
148
+ # @return [Boolean] True if the request context has headers.
135
149
  def request_headers?
150
+ deprecated_request_api(:request_headers?)
136
151
  @request_context.headers?
137
152
  end
138
153
 
139
- # Convenience method to check if the session has request parameters.
140
- #
141
- # @return [Boolean] True if the request context has parameters, false otherwise.
154
+ # @deprecated Will be removed in 0.7. Use the per-request invocation's
155
+ # {VectorMCP::Invocation#request_params?} instead.
156
+ # @return [Boolean] True if the request context has parameters.
142
157
  def request_params?
158
+ deprecated_request_api(:request_params?)
143
159
  @request_context.params?
144
160
  end
145
161
 
146
- # Convenience method to get a request header value.
147
- #
162
+ # @deprecated Will be removed in 0.7. Use the per-request invocation's
163
+ # {VectorMCP::Invocation#request_header} instead.
148
164
  # @param name [String] The header name.
149
165
  # @return [String, nil] The header value or nil if not found.
150
166
  def request_header(name)
167
+ deprecated_request_api(:request_header)
151
168
  @request_context.header(name)
152
169
  end
153
170
 
154
- # Convenience method to get a request parameter value.
155
- #
171
+ # @deprecated Will be removed in 0.7. Use the per-request invocation's
172
+ # {VectorMCP::Invocation#request_param} instead.
156
173
  # @param name [String] The parameter name.
157
174
  # @return [String, nil] The parameter value or nil if not found.
158
175
  def request_param(name)
176
+ deprecated_request_api(:request_param)
159
177
  @request_context.param(name)
160
178
  end
161
179
 
@@ -220,6 +238,16 @@ module VectorMCP
220
238
 
221
239
  private
222
240
 
241
+ # Emit the standard deprecation warning for the request-scoped Session API.
242
+ # @api private
243
+ def deprecated_request_api(method_name)
244
+ VectorMCP.deprecation_warning(
245
+ "Session##{method_name} is deprecated and will be removed in 0.7; " \
246
+ "request-scoped data lives on the per-request invocation passed to handlers " \
247
+ "(e.g. invocation.request_header)"
248
+ )
249
+ end
250
+
223
251
  # Validates that sampling can be performed on this session.
224
252
  # @api private
225
253
  # @raise [StandardError, InitializationError] if preconditions are not met.
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent-ruby"
4
+ require "securerandom"
5
+
6
+ module VectorMCP
7
+ # Thread-safe bidirectional store mapping arbitrary string values to opaque
8
+ # tokens and back. The store has no knowledge of domain semantics: callers
9
+ # supply the prefix, and the store guarantees that the same (value, prefix)
10
+ # pair always yields the same token within its lifetime.
11
+ class TokenStore
12
+ # Regexp describing the token format emitted by {#tokenize}.
13
+ TOKEN_PATTERN = /\A[A-Z]+_[0-9A-F]{8}\z/
14
+
15
+ def initialize
16
+ @forward = Concurrent::Hash.new
17
+ @reverse = Concurrent::Hash.new
18
+ @mutex = Mutex.new
19
+ end
20
+
21
+ # Return an opaque token for +value+. Calling this repeatedly with the
22
+ # same +value+ and +prefix+ returns the same token.
23
+ #
24
+ # @param value [String] the value to tokenize.
25
+ # @param prefix [String] the token prefix (uppercase recommended).
26
+ # @return [String] a token of the form +"PREFIX_XXXXXXXX"+.
27
+ def tokenize(value, prefix:)
28
+ key = [prefix, value]
29
+ existing = @forward[key]
30
+ return existing if existing
31
+
32
+ @mutex.synchronize do
33
+ existing = @forward[key]
34
+ return existing if existing
35
+
36
+ token = generate_token(prefix)
37
+ # Populate the reverse map first so any thread that observes the
38
+ # token in @forward can always resolve it.
39
+ @reverse[token] = value
40
+ @forward[key] = token
41
+ token
42
+ end
43
+ end
44
+
45
+ # Resolve a token back to its original value.
46
+ #
47
+ # @param token [String] a token previously returned by {#tokenize}.
48
+ # @return [String, nil] the original value, or +nil+ if unknown.
49
+ def resolve(token)
50
+ @reverse[token]
51
+ end
52
+
53
+ # Predicate: does +string+ look like a token issued by this class?
54
+ # This check is purely structural and does not consult the store.
55
+ #
56
+ # @param string [Object] the value to test.
57
+ # @return [Boolean]
58
+ def token?(string)
59
+ string.is_a?(String) && TOKEN_PATTERN.match?(string)
60
+ end
61
+
62
+ # Remove all mappings. Intended for test teardown.
63
+ # @return [void]
64
+ def clear
65
+ @mutex.synchronize do
66
+ @forward.clear
67
+ @reverse.clear
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def generate_token(prefix)
74
+ loop do
75
+ candidate = "#{prefix}_#{SecureRandom.hex(4).upcase}"
76
+ return candidate unless @reverse.key?(candidate)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -11,21 +11,6 @@ module VectorMCP
11
11
  #
12
12
  # @abstract Subclass and implement transport-specific methods
13
13
  class BaseSessionManager
14
- # Session data structure for unified session management
15
- Session = Struct.new(:id, :context, :created_at, :last_accessed_at, :metadata) do
16
- def touch!
17
- self.last_accessed_at = Time.now
18
- end
19
-
20
- def expired?(timeout)
21
- Time.now - last_accessed_at > timeout
22
- end
23
-
24
- def age
25
- Time.now - created_at
26
- end
27
- end
28
-
29
14
  attr_reader :transport, :session_timeout, :logger
30
15
 
31
16
  # Initializes a new session manager.
@@ -46,7 +31,7 @@ module VectorMCP
46
31
  # Gets an existing session by ID.
47
32
  #
48
33
  # @param session_id [String] The session ID
49
- # @return [Session, nil] The session if found and valid
34
+ # @return [VectorMCP::Session, nil] The session if found and valid
50
35
  def get_session(session_id)
51
36
  return nil unless session_id
52
37
 
@@ -58,40 +43,29 @@ module VectorMCP
58
43
  end
59
44
 
60
45
  # Gets an existing session or creates a new one.
46
+ # When a session_id is provided but not found (expired or unknown), returns
47
+ # nil instead of creating a session with the client-supplied ID — session
48
+ # IDs are always server-generated (prevents session fixation).
61
49
  #
62
50
  # @param session_id [String, nil] The session ID (optional)
63
- # @return [Session] The existing or newly created session
51
+ # @return [VectorMCP::Session, nil] The existing session, a newly created one, or nil
64
52
  def get_or_create_session(session_id = nil)
65
53
  if session_id
66
- session = get_session(session_id)
67
- return session if session
68
-
69
- # If session_id was provided but not found, create with that ID
70
- return create_session(session_id)
54
+ get_session(session_id)
55
+ else
56
+ create_session
71
57
  end
72
-
73
- create_session
74
58
  end
75
59
 
76
60
  # Creates a new session.
77
61
  #
78
62
  # @param session_id [String, nil] Optional specific session ID to use
79
- # @return [Session] The newly created session
63
+ # @return [VectorMCP::Session] The newly created session
80
64
  def create_session(session_id = nil)
81
65
  session_id ||= generate_session_id
82
- now = Time.now
83
-
84
- # Create VectorMCP session context
85
- session_context = VectorMCP::Session.new(@transport.server, @transport, id: session_id)
86
-
87
- # Create internal session record with transport-specific metadata
88
- session = Session.new(
89
- session_id,
90
- session_context,
91
- now,
92
- now,
93
- create_session_metadata
94
- )
66
+
67
+ session = VectorMCP::Session.new(@transport.server, @transport, id: session_id)
68
+ session.metadata.merge!(create_session_metadata)
95
69
 
96
70
  @sessions[session_id] = session
97
71
 
@@ -36,7 +36,8 @@ module VectorMCP
36
36
  def initialize(max_events)
37
37
  @max_events = max_events
38
38
  @events = Concurrent::Array.new
39
- @event_index = Concurrent::Hash.new # event_id -> index for fast lookup
39
+ @event_index = Concurrent::Hash.new # event_id -> logical position
40
+ @offset = 0 # number of events shifted off the front
40
41
  @current_sequence = Concurrent::AtomicFixnum.new(0)
41
42
  end
42
43
 
@@ -53,19 +54,15 @@ module VectorMCP
53
54
 
54
55
  event = Event.new(event_id, data, type, timestamp, session_id, stream_id)
55
56
 
56
- # Add to events array
57
+ # Add to events array and record logical position
57
58
  @events.push(event)
58
-
59
- # Update index
60
- @event_index[event_id] = @events.length - 1
59
+ @event_index[event_id] = @offset + @events.length - 1
61
60
 
62
61
  # Maintain circular buffer
63
62
  if @events.length > @max_events
64
63
  removed_event = @events.shift
65
64
  @event_index.delete(removed_event.id)
66
-
67
- # Update all indices after removal
68
- @event_index.transform_values! { |index| index - 1 }
65
+ @offset += 1
69
66
  end
70
67
 
71
68
  event_id
@@ -81,13 +78,13 @@ module VectorMCP
81
78
  events = if last_event_id.nil?
82
79
  @events.to_a
83
80
  else
84
- last_index = @event_index[last_event_id]
85
- return [] if last_index.nil?
81
+ logical = @event_index[last_event_id]
82
+ return [] if logical.nil?
86
83
 
87
- start_index = last_index + 1
88
- return [] if start_index >= @events.length
84
+ physical = logical - @offset + 1
85
+ return [] if physical >= @events.length
89
86
 
90
- @events[start_index..]
87
+ @events[physical..]
91
88
  end
92
89
 
93
90
  events = events.select { |e| e.session_id == session_id } if session_id
@@ -100,10 +97,10 @@ module VectorMCP
100
97
  # @param event_id [String] The event ID to look up
101
98
  # @return [Event, nil] The stored event, or nil if it is no longer retained
102
99
  def get_event(event_id)
103
- index = @event_index[event_id]
104
- return nil if index.nil?
100
+ logical = @event_index[event_id]
101
+ return nil if logical.nil?
105
102
 
106
- @events[index]
103
+ @events[logical - @offset]
107
104
  end
108
105
 
109
106
  # Gets the total number of stored events.
@@ -141,6 +138,7 @@ module VectorMCP
141
138
  def clear
142
139
  @events.clear
143
140
  @event_index.clear
141
+ @offset = 0
144
142
  end
145
143
 
146
144
  # Gets statistics about the event store.
@@ -19,20 +19,9 @@ module VectorMCP
19
19
  #
20
20
  # @api private
21
21
  class SessionManager < BaseSessionManager
22
- # HTTP stream session data structure extending base session
23
- Session = Struct.new(:id, :context, :created_at, :last_accessed_at, :metadata) do
24
- def touch!
25
- self.last_accessed_at = Time.now
26
- end
27
-
28
- def expired?(timeout)
29
- Time.now - last_accessed_at > timeout
30
- end
31
-
32
- def age
33
- Time.now - created_at
34
- end
35
-
22
+ # HTTP-streaming connection state, kept in the session's transport
23
+ # metadata. New sessions are extended with this module at creation.
24
+ module SessionStreaming
36
25
  def streaming?
37
26
  !streaming_connection.nil? || !streaming_connections.empty?
38
27
  end
@@ -65,28 +54,20 @@ module VectorMCP
65
54
  end
66
55
  end
67
56
 
68
- # Initializes a new HTTP stream session manager.
57
+ # Creates a new session. RequestContext.from_rack_env handles nil
58
+ # rack_env by falling back to a minimal context, so we don't need to
59
+ # branch here.
69
60
  #
70
- # @param transport [HttpStream] The parent transport instance
71
- # @param session_timeout [Integer] Session timeout in seconds
72
-
73
- # Optimized session creation with reduced object allocation and faster context creation
61
+ # @return [VectorMCP::Session] The newly created session
74
62
  def create_session(session_id = nil, rack_env = nil)
75
63
  session_id ||= generate_session_id
76
- now = Time.now
77
-
78
- # Optimize session context creation - use cached minimal context when rack_env is nil
79
- session_context = if rack_env
80
- create_session_with_context(session_id, rack_env)
81
- else
82
- create_minimal_session_context(session_id)
83
- end
84
-
85
- # Pre-allocate metadata hash for better performance
86
- metadata = create_session_metadata
87
64
 
88
- # Create internal session record with streaming connection metadata
89
- session = Session.new(session_id, session_context, now, now, metadata)
65
+ request_context = VectorMCP::RequestContext.from_rack_env(rack_env, "http_stream")
66
+ session = VectorMCP::Session.new(
67
+ @transport.server, @transport, id: session_id, request_context: request_context
68
+ )
69
+ session.extend(SessionStreaming)
70
+ session.metadata.merge!(create_session_metadata)
90
71
 
91
72
  @sessions[session_id] = session
92
73
 
@@ -95,38 +76,16 @@ module VectorMCP
95
76
  end
96
77
 
97
78
  # Override to add rack_env support.
98
- # Returns nil when a session_id is provided but not found (expired or unknown).
99
- # Callers are responsible for returning 404 in that case.
79
+ # Returns nil when a session_id is provided but not found (expired or
80
+ # unknown) — callers are responsible for returning 404 in that case.
81
+ # Request-scoped data is never written onto an existing session; each
82
+ # request carries its own context via the Invocation built at dispatch.
100
83
  def get_or_create_session(session_id = nil, rack_env = nil)
101
84
  if session_id
102
- session = get_session(session_id)
103
- if session
104
- # Update existing session context if rack_env is provided
105
- if rack_env
106
- request_context = VectorMCP::RequestContext.from_rack_env(rack_env, "http_stream")
107
- session.context.request_context = request_context
108
- end
109
- return session
110
- end
111
-
112
- # Session ID provided but not found — signal 404 to caller
113
- return nil
85
+ get_session(session_id)
86
+ else
87
+ create_session(nil, rack_env)
114
88
  end
115
-
116
- create_session(nil, rack_env)
117
- end
118
-
119
- # Creates a VectorMCP::Session with proper request context from Rack environment
120
- def create_session_with_context(session_id, rack_env)
121
- request_context = VectorMCP::RequestContext.from_rack_env(rack_env, "http_stream")
122
- VectorMCP::Session.new(@transport.server, @transport, id: session_id, request_context: request_context)
123
- end
124
-
125
- # Creates a minimal session context for each session (no caching to prevent contamination)
126
- def create_minimal_session_context(session_id)
127
- # Create a new minimal context for each session to prevent cross-session contamination
128
- minimal_context = VectorMCP::RequestContext.minimal("http_stream")
129
- VectorMCP::Session.new(@transport.server, @transport, id: session_id, request_context: minimal_context)
130
89
  end
131
90
 
132
91
  # Terminates a session by ID.
@@ -49,7 +49,7 @@ module VectorMCP
49
49
  # Handles a streaming request (GET request for SSE).
50
50
  #
51
51
  # @param env [Hash] The Rack environment
52
- # @param session [SessionManager::Session] The session for this request
52
+ # @param session [VectorMCP::Session] The session for this request
53
53
  # @return [Array] Rack response triplet for SSE
54
54
  def handle_streaming_request(env, session)
55
55
  last_event_id = extract_last_event_id(env)
@@ -65,7 +65,7 @@ module VectorMCP
65
65
 
66
66
  # Sends a message to a specific session.
67
67
  #
68
- # @param session [SessionManager::Session] The target session
68
+ # @param session [VectorMCP::Session] The target session
69
69
  # @param message [Hash] The message to send
70
70
  # @return [Boolean] True if message was sent successfully
71
71
  def send_message_to_session(session, message)
@@ -141,7 +141,7 @@ module VectorMCP
141
141
 
142
142
  # Creates an SSE stream for a session.
143
143
  #
144
- # @param session [SessionManager::Session] The session
144
+ # @param session [VectorMCP::Session] The session
145
145
  # @param last_event_id [String, nil] The last event ID for resumability
146
146
  # @param origin [Symbol] The stream origin (:get or :post)
147
147
  # @return [Enumerator] SSE stream enumerator
@@ -172,7 +172,7 @@ module VectorMCP
172
172
 
173
173
  # Streams events to a client.
174
174
  #
175
- # @param session [SessionManager::Session] The session
175
+ # @param session [VectorMCP::Session] The session
176
176
  # @param yielder [Enumerator::Yielder] The SSE yielder
177
177
  # @param last_event_id [String, nil] The last event ID for resumability
178
178
  # @param stream_id [String] The unique stream identifier
@@ -193,7 +193,7 @@ module VectorMCP
193
193
  #
194
194
  # @param yielder [Enumerator::Yielder] The SSE yielder
195
195
  # @param last_event_id [String] The last event ID received by client
196
- # @param session [SessionManager::Session] The session to filter events for
196
+ # @param session [VectorMCP::Session] The session to filter events for
197
197
  # @param stream_id [String] The logical stream ID being resumed
198
198
  # @return [void]
199
199
  def replay_events(yielder, last_event_id, session, stream_id)
@@ -212,7 +212,7 @@ module VectorMCP
212
212
 
213
213
  # Keeps the connection alive with periodic heartbeat events.
214
214
  #
215
- # @param session [SessionManager::Session] The session
215
+ # @param session [VectorMCP::Session] The session
216
216
  # @param yielder [Enumerator::Yielder] The SSE yielder
217
217
  # @param stream_id [String] The stream ID for event storage
218
218
  # @return [void]
@@ -274,7 +274,7 @@ module VectorMCP
274
274
 
275
275
  # Cleans up a specific connection.
276
276
  #
277
- # @param session [SessionManager::Session] The session to clean up
277
+ # @param session [VectorMCP::Session] The session to clean up
278
278
  # @return [void]
279
279
  def cleanup_connection(session, connection = nil)
280
280
  connection ||= session.streaming_connection