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
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "base"
6
+ require_relative "../token_store"
7
+ require_relative "../util/token_sweeper"
8
+
9
+ module VectorMCP
10
+ module Middleware
11
+ # Middleware that rewrites selected string fields in outbound tool
12
+ # results into opaque tokens and restores them on inbound tool
13
+ # invocations. All domain knowledge (which keys to match, token
14
+ # prefixes, which keys to treat as atomic blobs) is supplied by the
15
+ # application via the constructor.
16
+ #
17
+ # @example Wiring on a server
18
+ # anonymizer = VectorMCP::Middleware::Anonymizer.new(
19
+ # store: VectorMCP::TokenStore.new,
20
+ # field_rules: [
21
+ # { pattern: /\bname\b/i, prefix: "NAME" },
22
+ # { pattern: /email/i, prefix: "EMAIL" }
23
+ # ],
24
+ # atomic_keys: /address/i
25
+ # )
26
+ # anonymizer.install_on(server)
27
+ class Anonymizer
28
+ # @param store [VectorMCP::TokenStore] the backing token store.
29
+ # @param field_rules [Array<Hash>] an array of +{ pattern: Regexp, prefix: String }+
30
+ # hashes. The pattern is matched against each Hash key whose value is a String.
31
+ # @param atomic_keys [Regexp, nil] optional pattern; Hash values whose parent
32
+ # key matches are serialized and tokenized as a single opaque unit instead
33
+ # of recursed into.
34
+ def initialize(store:, field_rules:, atomic_keys: nil)
35
+ raise ArgumentError, "store is required" if store.nil?
36
+ raise ArgumentError, "field_rules is required" if field_rules.nil?
37
+
38
+ @store = store
39
+ @field_rules = field_rules.map { |rule| validate_rule!(rule) }.freeze
40
+ @atomic_keys = atomic_keys
41
+ end
42
+
43
+ # Tokenize sensitive string fields in an outbound payload.
44
+ #
45
+ # @param obj [Object] a parsed JSON-like Ruby structure.
46
+ # @return [Object] a new structure with matched values replaced by tokens.
47
+ def sweep_outbound(obj)
48
+ replace_atomic_nodes(obj).then do |shaped|
49
+ VectorMCP::Util::TokenSweeper.sweep(shaped) do |value, parent_key|
50
+ rule = rule_for(parent_key)
51
+ rule ? @store.tokenize(value, prefix: rule[:prefix]) : value
52
+ end
53
+ end
54
+ end
55
+
56
+ # Resolve tokens in an inbound payload back to their original values.
57
+ # Unknown token-shaped strings pass through unchanged.
58
+ #
59
+ # @param obj [Object] a parsed JSON-like Ruby structure.
60
+ # @return [Object] a new structure with tokens resolved to original values.
61
+ def sweep_inbound(obj)
62
+ VectorMCP::Util::TokenSweeper.sweep(obj) do |value, _parent_key|
63
+ if @store.token?(value)
64
+ resolved = @store.resolve(value)
65
+ resolved.nil? ? value : resolved
66
+ else
67
+ value
68
+ end
69
+ end
70
+ end
71
+
72
+ # Middleware hook: rewrite tool arguments before the handler runs.
73
+ # @param context [VectorMCP::Middleware::Context]
74
+ def before_tool_call(context)
75
+ return unless context.params.is_a?(Hash)
76
+
77
+ arguments = context.params["arguments"]
78
+ return unless arguments.is_a?(Hash) || arguments.is_a?(Array)
79
+
80
+ context.params = context.params.merge("arguments" => sweep_inbound(arguments))
81
+ end
82
+
83
+ # Middleware hook: tokenize matched fields in the tool result.
84
+ # @param context [VectorMCP::Middleware::Context]
85
+ def after_tool_call(context)
86
+ return if context.result.nil?
87
+
88
+ context.result = sweep_outbound(context.result)
89
+ end
90
+
91
+ # Register this anonymizer instance on +server+ for the tool call
92
+ # lifecycle. Creates a thin adapter class so the middleware manager's
93
+ # argumentless instantiation can still deliver the configured instance.
94
+ #
95
+ # @param server [VectorMCP::Server] the server instance.
96
+ # @param priority [Integer] middleware priority.
97
+ # @return [Class] the adapter class registered with the server.
98
+ def install_on(server, priority: Hook::DEFAULT_PRIORITY)
99
+ instance = self
100
+ adapter = Class.new(Base) do
101
+ define_method(:before_tool_call) { |context| instance.before_tool_call(context) }
102
+ define_method(:after_tool_call) { |context| instance.after_tool_call(context) }
103
+ end
104
+ server.use_middleware(adapter, %i[before_tool_call after_tool_call], priority: priority)
105
+ adapter
106
+ end
107
+
108
+ private
109
+
110
+ def validate_rule!(rule)
111
+ unless rule.is_a?(Hash) && rule[:pattern].is_a?(Regexp) && rule[:prefix].is_a?(String)
112
+ raise ArgumentError,
113
+ "each field_rule must be a Hash with :pattern (Regexp) and :prefix (String)"
114
+ end
115
+
116
+ rule
117
+ end
118
+
119
+ def rule_for(parent_key)
120
+ return nil if parent_key.nil?
121
+
122
+ key_string = parent_key.to_s
123
+ @field_rules.find { |rule| rule[:pattern].match?(key_string) }
124
+ end
125
+
126
+ def atomic_match?(parent_key)
127
+ return false if @atomic_keys.nil? || parent_key.nil?
128
+
129
+ @atomic_keys.match?(parent_key.to_s)
130
+ end
131
+
132
+ # First pass: collapse Hash nodes whose parent key matches +atomic_keys+
133
+ # into a single tokenized string. A fresh traversal avoids entanglement
134
+ # with the field-rule sweep that runs afterwards.
135
+ def replace_atomic_nodes(obj, parent_key = nil, visited = {}.compare_by_identity)
136
+ case obj
137
+ when Hash
138
+ return obj if visited[obj]
139
+
140
+ if atomic_match?(parent_key)
141
+ @store.tokenize(canonical_json(obj), prefix: atomic_prefix_for(parent_key))
142
+ else
143
+ visited[obj] = true
144
+ begin
145
+ obj.each_with_object({}) do |(key, value), out|
146
+ out[key] = replace_atomic_nodes(value, key, visited)
147
+ end
148
+ ensure
149
+ visited.delete(obj)
150
+ end
151
+ end
152
+ when Array
153
+ return obj if visited[obj]
154
+
155
+ visited[obj] = true
156
+ begin
157
+ obj.map { |element| replace_atomic_nodes(element, parent_key, visited) }
158
+ ensure
159
+ visited.delete(obj)
160
+ end
161
+ else
162
+ obj
163
+ end
164
+ end
165
+
166
+ # Atomic nodes use the prefix of the first field rule whose pattern
167
+ # matches the enclosing key. If no field rule matches, fall back to a
168
+ # neutral default so the token is still well-formed.
169
+ def atomic_prefix_for(parent_key)
170
+ rule_for(parent_key)&.dig(:prefix) || "ATOM"
171
+ end
172
+
173
+ def canonical_json(hash)
174
+ JSON.generate(canonicalize(hash))
175
+ end
176
+
177
+ def canonicalize(obj)
178
+ case obj
179
+ when Hash then obj.keys.map(&:to_s).sort.to_h { |k| [k, canonicalize(obj[k] || obj[k.to_sym])] }
180
+ when Array then obj.map { |element| canonicalize(element) }
181
+ else obj
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -4,7 +4,7 @@ module VectorMCP
4
4
  module Middleware
5
5
  # Represents a single middleware hook with priority and execution logic
6
6
  class Hook
7
- attr_reader :middleware_class, :hook_type, :priority, :conditions
7
+ attr_reader :middleware_class, :hook_type, :operation_type, :priority, :conditions
8
8
 
9
9
  # Default priority for middleware (lower numbers execute first)
10
10
  DEFAULT_PRIORITY = 100
@@ -21,6 +21,8 @@ module VectorMCP
21
21
 
22
22
  validate_hook_type!
23
23
  validate_middleware_class!
24
+
25
+ @operation_type = HOOK_OPERATION_TYPES[@hook_type]
24
26
  end
25
27
 
26
28
  # Execute this hook with the given context
@@ -72,12 +74,12 @@ module VectorMCP
72
74
  raise ArgumentError, "middleware_class must inherit from VectorMCP::Middleware::Base"
73
75
  end
74
76
 
77
+ # Whether this hook's operation_type matches the context's. Transport- and
78
+ # auth-level hooks (operation_type == nil) match any context.
75
79
  def matches_operation_type?(context)
76
- operation_prefix = @hook_type.split("_")[1..].join("_")
77
-
78
- return true if transport_or_auth_hook?(operation_prefix)
80
+ return true if @operation_type.nil?
79
81
 
80
- operation_matches?(operation_prefix, context.operation_type)
82
+ context.operation_type == @operation_type
81
83
  end
82
84
 
83
85
  def condition_matches?(key, value, context)
@@ -91,25 +93,6 @@ module VectorMCP
91
93
  end
92
94
  end
93
95
 
94
- def transport_or_auth_hook?(operation_prefix)
95
- %w[request response transport_error auth auth_error].include?(operation_prefix)
96
- end
97
-
98
- def operation_matches?(operation_prefix, operation_type)
99
- case operation_prefix
100
- when "tool_call", "tool_error"
101
- operation_type == :tool_call
102
- when "resource_read", "resource_error"
103
- operation_type == :resource_read
104
- when "prompt_get", "prompt_error"
105
- operation_type == :prompt_get
106
- when "sampling_request", "sampling_response", "sampling_error"
107
- operation_type == :sampling
108
- else
109
- true
110
- end
111
- end
112
-
113
96
  def operation_condition_matches?(key, value, context)
114
97
  included = Array(value).include?(context.operation_name)
115
98
  key == :only_operations ? included : !included
@@ -12,15 +12,32 @@ module VectorMCP
12
12
  # Middleware system for pluggable hooks around MCP operations
13
13
  # Allows developers to add custom behavior without modifying core code
14
14
  module Middleware
15
- # Hook types available in the system
16
- HOOK_TYPES = %w[
17
- before_tool_call after_tool_call on_tool_error
18
- before_resource_read after_resource_read on_resource_error
19
- before_prompt_get after_prompt_get on_prompt_error
20
- before_sampling_request after_sampling_response on_sampling_error
21
- before_request after_response on_transport_error
22
- before_auth after_auth on_auth_error
23
- ].freeze
15
+ # Maps each hook type to the operation_type it matches. `nil` means the
16
+ # hook is transport- or auth-level and matches any operation_type.
17
+ # This is the single source of truth — HOOK_TYPES is derived from it.
18
+ HOOK_OPERATION_TYPES = {
19
+ "before_tool_call" => :tool_call,
20
+ "after_tool_call" => :tool_call,
21
+ "on_tool_error" => :tool_call,
22
+ "before_resource_read" => :resource_read,
23
+ "after_resource_read" => :resource_read,
24
+ "on_resource_error" => :resource_read,
25
+ "before_prompt_get" => :prompt_get,
26
+ "after_prompt_get" => :prompt_get,
27
+ "on_prompt_error" => :prompt_get,
28
+ "before_sampling_request" => :sampling,
29
+ "after_sampling_response" => :sampling,
30
+ "on_sampling_error" => :sampling,
31
+ "before_request" => nil,
32
+ "after_response" => nil,
33
+ "on_transport_error" => nil,
34
+ "before_auth" => nil,
35
+ "after_auth" => nil,
36
+ "on_auth_error" => nil
37
+ }.freeze
38
+
39
+ # Hook types available in the system (derived from HOOK_OPERATION_TYPES)
40
+ HOOK_TYPES = HOOK_OPERATION_TYPES.keys.freeze
24
41
 
25
42
  # Error raised when invalid hook type is specified
26
43
  class InvalidHookTypeError < VectorMCP::Error
@@ -91,6 +91,25 @@ module VectorMCP
91
91
  http_methods.include?(@method.upcase)
92
92
  end
93
93
 
94
+ # Return a new context with the given attributes merged in. Nested hashes
95
+ # (headers, params, transport_metadata) are merged key-wise; scalar
96
+ # attributes are replaced.
97
+ #
98
+ # @param attributes [Hash] attributes to merge
99
+ # @return [RequestContext] a new, merged context
100
+ def merge(attributes)
101
+ current = to_h
102
+ merged = current.dup
103
+ attributes.each do |key, value|
104
+ merged[key] = if value.is_a?(Hash) && current[key].is_a?(Hash)
105
+ current[key].merge(value)
106
+ else
107
+ value
108
+ end
109
+ end
110
+ RequestContext.new(**merged)
111
+ end
112
+
94
113
  # Create a minimal request context for non-HTTP transports.
95
114
  # This is useful for non-HTTP transports or testing contexts.
96
115
  #
@@ -106,6 +125,73 @@ module VectorMCP
106
125
  )
107
126
  end
108
127
 
128
+ # Coerce an arbitrary request representation into a RequestContext.
129
+ # This is the single boundary where legacy request shapes (raw Rack
130
+ # environments, `{headers:, params:}` hashes with symbol or string keys)
131
+ # are converted; everything downstream of authentication trusts this type.
132
+ #
133
+ # @param request [RequestContext, Hash] the request to coerce
134
+ # @param transport_type [String] transport identifier used when building from a Rack env
135
+ # @return [RequestContext]
136
+ # @raise [ArgumentError] if the input cannot be coerced
137
+ def self.coerce(request, transport_type = "http")
138
+ case request
139
+ when RequestContext
140
+ request
141
+ when Hash
142
+ return from_rack_env(request, transport_type) if request.key?("REQUEST_METHOD")
143
+
144
+ from_normalized_hash(request)
145
+ else
146
+ raise ArgumentError, "Cannot coerce #{request.class} into a VectorMCP::RequestContext"
147
+ end
148
+ end
149
+
150
+ # Build a context from a legacy `{headers:, params:, ...}` hash with either
151
+ # symbol or string keys.
152
+ # @api private
153
+ def self.from_normalized_hash(hash)
154
+ fetch = ->(key) { hash[key] || hash[key.to_s] }
155
+ new(
156
+ headers: fetch.call(:headers) || {},
157
+ params: fetch.call(:params) || {},
158
+ method: fetch.call(:method),
159
+ path: fetch.call(:path),
160
+ transport_metadata: fetch.call(:transport_metadata) || {}
161
+ )
162
+ end
163
+ private_class_method :from_normalized_hash
164
+
165
+ # Hash-style read access for compatibility with custom authentication
166
+ # handlers written against the legacy `{headers:, params:}` request hash.
167
+ #
168
+ # @param key [String, Symbol] one of :headers, :params, :method, :path, :transport_metadata
169
+ # @return [Object, nil]
170
+ def [](key)
171
+ hash_view[key.to_sym]
172
+ end
173
+
174
+ # @param key [String, Symbol] the top-level key to check
175
+ # @return [Boolean]
176
+ def key?(key)
177
+ hash_view.key?(key.to_sym)
178
+ end
179
+ alias has_key? key?
180
+ alias include? key?
181
+ alias member? key?
182
+
183
+ # Dig into the context like a nested hash, e.g. `context.dig(:headers, "X-API-Key")`.
184
+ #
185
+ # @param key [String, Symbol] the top-level key
186
+ # @param rest [Array] further keys passed to the nested value's #dig
187
+ # @return [Object, nil]
188
+ def dig(key, *rest)
189
+ value = self[key]
190
+ return value if rest.empty?
191
+
192
+ value&.dig(*rest)
193
+ end
194
+
109
195
  # Create a request context from a Rack environment.
110
196
  # This is a convenience method for HTTP-based transports.
111
197
  #
@@ -149,25 +235,22 @@ module VectorMCP
149
235
 
150
236
  private
151
237
 
152
- # Normalize headers to ensure consistent format.
153
- #
154
- # @param headers [Hash] Raw headers hash
155
- # @return [Hash] Normalized headers hash
156
- def normalize_headers(headers)
157
- return {} unless headers.is_a?(Hash)
158
-
159
- headers.transform_keys(&:to_s).transform_values { |v| v.nil? ? "" : v.to_s }
238
+ # Memoized nested-hash view backing the hash-style read accessors.
239
+ def hash_view
240
+ @hash_view ||= to_h.freeze
160
241
  end
161
242
 
162
- # Normalize parameters to ensure consistent format.
243
+ # Normalize a headers/params hash to string keys and string values.
163
244
  #
164
- # @param params [Hash] Raw parameters hash
165
- # @return [Hash] Normalized parameters hash
166
- def normalize_params(params)
167
- return {} unless params.is_a?(Hash)
245
+ # @param hash [Hash] Raw hash
246
+ # @return [Hash] Normalized hash
247
+ def normalize_string_map(hash)
248
+ return {} unless hash.is_a?(Hash)
168
249
 
169
- params.transform_keys(&:to_s).transform_values { |v| v.nil? ? "" : v.to_s }
250
+ hash.transform_keys(&:to_s).transform_values { |v| v.nil? ? "" : v.to_s }
170
251
  end
252
+ alias normalize_headers normalize_string_map
253
+ alias normalize_params normalize_string_map
171
254
 
172
255
  # Normalize transport metadata to ensure consistent format.
173
256
  #
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "auth_result"
4
+
3
5
  module VectorMCP
4
6
  module Security
5
7
  # Manages authentication strategies for VectorMCP servers
@@ -40,27 +42,26 @@ module VectorMCP
40
42
  end
41
43
 
42
44
  # Authenticate a request using the specified or default strategy
43
- # @param request [Hash] the request object containing headers, params, etc.
45
+ # @param request [VectorMCP::RequestContext, Hash] the request. Legacy hash
46
+ # shapes (raw Rack env, {headers:, params:}) are coerced here — the single
47
+ # normalization boundary for the authentication pipeline.
44
48
  # @param strategy [Symbol] optional strategy override
45
- # @return [Object, false] authentication result or false if failed
49
+ # @return [AuthResult] the authentication outcome
46
50
  def authenticate(request, strategy: nil)
47
- return { authenticated: true, user: nil } unless @enabled
51
+ return AuthResult.passthrough unless @enabled
48
52
 
49
53
  strategy_name = strategy || @default_strategy
50
54
  auth_strategy = @strategies[strategy_name]
55
+ return AuthResult.failure unless auth_strategy
51
56
 
52
- return { authenticated: false, error: "Unknown strategy: #{strategy_name}" } unless auth_strategy
53
-
54
- begin
55
- result = auth_strategy.authenticate(request)
56
- if result
57
- { authenticated: true, user: result }
58
- else
59
- { authenticated: false, error: "Authentication failed" }
60
- end
61
- rescue StandardError => e
62
- { authenticated: false, error: "Authentication error: #{e.message}" }
57
+ result = auth_strategy.authenticate(VectorMCP::RequestContext.coerce(request))
58
+ if result == false
59
+ AuthResult.failure
60
+ else
61
+ AuthResult.success(user: result, strategy: strategy_name.to_s)
63
62
  end
63
+ rescue StandardError
64
+ AuthResult.failure
64
65
  end
65
66
 
66
67
  # Check if authentication is required
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VectorMCP
4
+ module Security
5
+ # Value object representing the outcome of an authentication attempt.
6
+ # Replaces the unstructured Hash that previously flowed through the auth pipeline.
7
+ class AuthResult
8
+ attr_reader :user, :strategy, :authenticated_at
9
+
10
+ def initialize(authenticated:, user: nil, strategy: nil, authenticated_at: nil)
11
+ @authenticated = authenticated
12
+ @user = user
13
+ @strategy = strategy
14
+ @authenticated_at = authenticated_at || (Time.now if authenticated)
15
+ freeze
16
+ end
17
+
18
+ def authenticated? = @authenticated
19
+
20
+ def self.success(user:, strategy:, authenticated_at: Time.now)
21
+ new(authenticated: true, user: user, strategy: strategy, authenticated_at: authenticated_at)
22
+ end
23
+
24
+ def self.failure
25
+ new(authenticated: false)
26
+ end
27
+
28
+ def self.passthrough
29
+ new(authenticated: true)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -10,6 +10,7 @@ module VectorMCP
10
10
  def initialize
11
11
  @policies = {}
12
12
  @enabled = false
13
+ @logger = VectorMCP.logger_for("authorization")
13
14
  end
14
15
 
15
16
  # Enable authorization system
@@ -45,17 +46,12 @@ module VectorMCP
45
46
 
46
47
  resource_type = determine_resource_type(resource)
47
48
  policy = @policies[resource_type]
48
-
49
- # If no policy is defined, allow access (opt-in authorization)
50
49
  return true unless policy
51
50
 
52
- begin
53
- policy_result = policy.call(user, action, resource)
54
- policy_result ? true : false
55
- rescue StandardError
56
- # Log error but deny access for safety
57
- false
58
- end
51
+ !!policy.call(user, action, resource)
52
+ rescue StandardError => e
53
+ @logger.error("Authorization policy error for #{resource_type}: #{e.message}")
54
+ false
59
55
  end
60
56
 
61
57
  # Check if authorization is required
@@ -73,25 +73,6 @@ module VectorMCP
73
73
  }
74
74
  end
75
75
 
76
- # Create a request object from different transport formats
77
- # @param transport_request [Object] the transport-specific request
78
- # @return [Hash] normalized request object
79
- def normalize_request(transport_request)
80
- case transport_request
81
- when Hash
82
- # Check if it's a Rack environment (has REQUEST_METHOD key)
83
- if transport_request.key?("REQUEST_METHOD")
84
- extract_from_rack_env(transport_request)
85
- else
86
- # Already normalized
87
- transport_request
88
- end
89
- else
90
- # Extract from transport-specific request (e.g., custom objects)
91
- extract_request_data(transport_request)
92
- end
93
- end
94
-
95
76
  # Check if security is enabled
96
77
  # @return [Boolean] true if any security features are enabled
97
78
  def security_enabled?
@@ -113,34 +94,6 @@ module VectorMCP
113
94
  }
114
95
  }
115
96
  end
116
-
117
- private
118
-
119
- # Extract request data from transport-specific formats
120
- # @param transport_request [Object] the transport request
121
- # @return [Hash] extracted request data
122
- def extract_request_data(transport_request)
123
- # Handle Rack environment (for HTTP transports)
124
- if transport_request.respond_to?(:[]) && transport_request["REQUEST_METHOD"]
125
- extract_from_rack_env(transport_request)
126
- else
127
- # Default fallback for non-HTTP request formats
128
- { headers: {}, params: {} }
129
- end
130
- end
131
-
132
- # Extract data from Rack environment
133
- # @param env [Hash] the Rack environment
134
- # @return [Hash] extracted request data
135
- def extract_from_rack_env(env)
136
- {
137
- headers: VectorMCP::Util.extract_headers_from_rack_env(env),
138
- params: VectorMCP::Util.extract_params_from_rack_env(env),
139
- method: env["REQUEST_METHOD"],
140
- path: env["PATH_INFO"],
141
- rack_env: env
142
- }
143
- end
144
97
  end
145
98
  end
146
99
  end
@@ -113,34 +113,18 @@ module VectorMCP
113
113
  new(authenticated: false)
114
114
  end
115
115
 
116
- # Create an authenticated session context from auth result
117
- # @param auth_result [Hash] the authentication result
118
- # @return [SessionContext] an authenticated session
116
+ # Create an authenticated session context from an AuthResult
117
+ # @param auth_result [VectorMCP::Security::AuthResult] the authentication outcome
118
+ # @return [SessionContext] an authenticated or anonymous session
119
119
  def self.from_auth_result(auth_result)
120
- return anonymous unless auth_result&.dig(:authenticated)
121
-
122
- user_data = auth_result[:user]
123
-
124
- # Handle special marker for authenticated nil user
125
- if user_data == :authenticated_nil_user
126
- new(
127
- user: nil,
128
- authenticated: true,
129
- auth_strategy: "custom",
130
- authenticated_at: Time.now
131
- )
132
- else
133
- # Extract strategy and authenticated_at only if user_data is a Hash
134
- strategy = user_data.is_a?(Hash) ? user_data[:strategy] : nil
135
- auth_time = user_data.is_a?(Hash) ? user_data[:authenticated_at] : nil
136
-
137
- new(
138
- user: user_data,
139
- authenticated: true,
140
- auth_strategy: strategy,
141
- authenticated_at: auth_time
142
- )
143
- end
120
+ return anonymous unless auth_result&.authenticated?
121
+
122
+ new(
123
+ user: auth_result.user,
124
+ authenticated: true,
125
+ auth_strategy: auth_result.strategy,
126
+ authenticated_at: auth_result.authenticated_at
127
+ )
144
128
  end
145
129
  end
146
130
  end