vector_mcp 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edd8f7804cea74064b13b6432daa19149d4b2d6c34efbd7ccfbae393870b648a
4
- data.tar.gz: bd04d07372ef947498c39021adfba055d8030b94bf76b567fc8b5cb73aae994b
3
+ metadata.gz: 64b4dac9a0a1e9c782d3b5d693e7582803621ac81555f992031f9edbdd3f5b6b
4
+ data.tar.gz: 571e540ac540859b1fefaadc5e2b3cda39a1415c3496af38cee46cb839316c85
5
5
  SHA512:
6
- metadata.gz: 3a47a3ad028e39de9bd8d4973779040fd0c571067e4c3588ad406d26dd138cdfad06921d497a3922d1eb2406df3ede02379df7f24620539cb1170ac6fdb1bed5
7
- data.tar.gz: ad1d3d147c3b4ef8624651aeabb0809f689bbfb5c4ae702dcc0c5fa458cc500ff7ffd54e46a4ea02ae995d908af0122f5a88b7ab895eeaab8c1d780e6eec8043
6
+ metadata.gz: cc7c13562e31070ce7aef3d0d20801e4a7368a44b22ea58d01ec872f7f9191e7b70d96e72c3cdcd42b9ecc40dc9b7ea30e63ce12b5b54987734a933402269b71
7
+ data.tar.gz: 6f989302cbd97c00aa5f2dfe28950cffb5c2c53c204c3ab10eb886a9dc680e0db6a704fef54229488910c837ebe3857c7e493ae5aa8729d8d6677fe048c223e1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,57 @@
1
+ ## [0.3.3] – 2025-07-29
2
+
3
+ ### Fixed
4
+
5
+ * **Critical Security Fix - SSE Transport Session Isolation**: Fixed default behavior where SSE transport shared session state across all clients
6
+ - **BREAKING CHANGE**: SSE transport now defaults to secure session isolation mode
7
+ - Session manager is now enabled by default to prevent race conditions and data leakage
8
+ - Legacy shared session mode available via `disable_session_manager: true` option (deprecated with warning)
9
+ - Enhanced security tests to prevent regression of this critical vulnerability
10
+
11
+ * **Critical Security Fix - Path Traversal Vulnerability**: Replaced naive path traversal validation with robust canonicalization
12
+ - **Path Validation Enhancement**: Now uses `File.expand_path` for proper path canonicalization
13
+ - **Attack Prevention**: Eliminates false positives from simple string-based `.` checks
14
+ - **Security Monitoring**: Added warnings for potential path traversal attempts
15
+ - **Bypass Protection**: Prevents sophisticated path traversal attacks through encoding or complex patterns
16
+
17
+ * **Session Compatibility**: Fixed session object type detection across different transports
18
+ - **Transport Compatibility**: Added automatic detection between `BaseSessionManager::Session` and `VectorMCP::Session` types
19
+ - **Method Resolution**: Fixed `undefined method 'initialized?'` and `'initialize!'` errors
20
+ - **Transport Layer**: Enhanced stdio, SSE, and HTTP stream transports for consistent session handling
21
+
22
+ * **Race Condition Fix**: Resolved concurrent session creation test failures in SSE transport
23
+ - **Thread Safety**: Implemented `Concurrent::Array` for thread-safe session tracking
24
+ - **Test Stability**: Enhanced test reliability for concurrent operations
25
+
26
+ * **Code Quality**: Fixed RuboCop style and linting violations
27
+ - **Naming Conventions**: Updated method names to follow Ruby conventions (removed `get_` prefixes)
28
+ - **Style Compliance**: Fixed line length violations and predicate method naming
29
+ - **Consistency**: Applied consistent coding standards across the codebase
30
+
31
+ ### Security
32
+
33
+ * **Defense in Depth**: Major security improvements addressing critical vulnerabilities
34
+ - **Multi-Client Security**: Eliminated shared state vulnerabilities in SSE transport
35
+ - **Path Security**: Comprehensive path traversal protection using canonical path resolution
36
+ - **Session Isolation**: Proper session boundary enforcement across all transport types
37
+
38
+ * **Backward Compatibility**: Security fixes maintain API compatibility while improving defaults
39
+ - **Opt-in Legacy Mode**: Deprecated insecure modes available for gradual migration
40
+ - **Migration Path**: Clear deprecation warnings guide users to secure configurations
41
+
42
+ ### Testing
43
+
44
+ * **Enhanced Security Test Coverage**: Comprehensive test suites for critical security fixes
45
+ - **Path Traversal Tests**: 20+ test cases covering legitimate paths, attack vectors, and edge cases
46
+ - **SSE Security Tests**: Verification of default secure behavior and session isolation
47
+ - **Integration Tests**: Cross-transport compatibility and session handling validation
48
+
49
+ ### Technical Details
50
+
51
+ * **Session Architecture**: Improved session management layer for better transport compatibility
52
+ * **Security Monitoring**: Enhanced logging and warning systems for security events
53
+ * **Error Handling**: Better error messages and debugging information for session-related issues
54
+
1
55
  ## [0.3.2] – 2025-07-02
2
56
 
3
57
  ### Added
@@ -242,17 +242,33 @@ module VectorMCP
242
242
  # Currently, only file:// scheme is supported per MCP spec
243
243
  raise ArgumentError, "Only file:// URIs are supported for roots, got: #{parsed_uri.scheme}://" unless parsed_uri.scheme == "file"
244
244
 
245
- # Validate path exists and is a directory
246
- path = parsed_uri.path
247
- raise ArgumentError, "Root directory does not exist: #{path}" unless File.exist?(path)
248
-
249
- raise ArgumentError, "Root path is not a directory: #{path}" unless File.directory?(path)
245
+ # Validate and canonicalize path for security
246
+ raw_path = parsed_uri.path
247
+
248
+ # Canonicalize the path to resolve any relative components (., .., etc.)
249
+ # This prevents path traversal attacks and normalizes the path
250
+ begin
251
+ canonical_path = File.expand_path(raw_path)
252
+ rescue ArgumentError => e
253
+ raise ArgumentError, "Invalid path format: #{raw_path} (#{e.message})"
254
+ end
250
255
 
251
- # Security check: ensure we can read the directory
256
+ # Security check: Verify the canonical path exists and is a directory
257
+ raise ArgumentError, "Root directory does not exist: #{canonical_path}" unless File.exist?(canonical_path)
258
+ raise ArgumentError, "Root path is not a directory: #{canonical_path}" unless File.directory?(canonical_path)
259
+ raise ArgumentError, "Root directory is not readable: #{canonical_path}" unless File.readable?(canonical_path)
260
+
261
+ # Additional security: Check if the canonical path differs significantly from raw path
262
+ # This can indicate potential path traversal attempts
263
+ if raw_path != canonical_path && raw_path.include?("..")
264
+ # Log the canonicalization for security monitoring
265
+ # Note: This is informational - the canonical path is what we'll actually use
266
+ warn "[SECURITY] Path canonicalized from '#{raw_path}' to '#{canonical_path}'. " \
267
+ "This may indicate a path traversal attempt."
268
+ end
252
269
 
253
- raise ArgumentError, "Root directory is not readable: #{path}" unless File.readable?(path)
254
- # Validate against path traversal attempts in the URI itself
255
- raise ArgumentError, "Root path contains unsafe traversal patterns: #{path}" if path.include?("..") || path.include?("./")
270
+ # Update the URI to use the canonical path for consistency
271
+ self.uri = "file://#{canonical_path}"
256
272
 
257
273
  true
258
274
  end
@@ -28,8 +28,6 @@ module VectorMCP
28
28
  # @param details [Hash, nil] Additional details for the error (optional).
29
29
  # @param request_id [String, Integer, nil] The ID of the originating request.
30
30
  def initialize(message, code: -32_600, details: nil, request_id: nil)
31
- logger = VectorMCP.logger_for("errors")
32
- logger.debug("Initializing ProtocolError with code: #{code}")
33
31
  @code = code
34
32
  @message = message
35
33
  @details = details # NOTE: `data` in JSON-RPC is often used for this purpose.
@@ -105,8 +103,7 @@ module VectorMCP
105
103
  # @param details [Hash, nil] Additional details for the error (optional).
106
104
  # @param request_id [String, Integer, nil] The ID of the originating request.
107
105
  def initialize(message = "Server error", code: -32_000, details: nil, request_id: nil)
108
- logger = VectorMCP.logger_for("errors")
109
- logger.debug("Initializing ServerError with code: #{code}")
106
+ # ServerError initialization
110
107
  unless (-32_099..-32_000).cover?(code)
111
108
  warn "Server error code #{code} is outside of the reserved range (-32099 to -32000). Using -32000 instead."
112
109
  code = -32_000
@@ -133,8 +130,7 @@ module VectorMCP
133
130
  # @param details [Hash, nil] Additional details for the error (optional).
134
131
  # @param request_id [String, Integer, nil] The ID of the originating request.
135
132
  def initialize(message = "Not Found", details: nil, request_id: nil)
136
- logger = VectorMCP.logger_for("errors")
137
- logger.debug("Initializing NotFoundError with code: -32001")
133
+ # NotFoundError initialization
138
134
  super(message, code: -32_001, details: details, request_id: request_id)
139
135
  end
140
136
  end
@@ -22,8 +22,6 @@ module VectorMCP
22
22
  # @param _server [VectorMCP::Server] The server instance (ignored).
23
23
  # @return [Hash] An empty hash, as per MCP spec for ping.
24
24
  def self.ping(_params, _session, _server)
25
- logger = VectorMCP.logger_for("handlers.core")
26
- logger.debug("Handling ping request")
27
25
  {}
28
26
  end
29
27
 
@@ -65,7 +63,7 @@ module VectorMCP
65
63
  security_result = validate_tool_security!(session, tool, server)
66
64
  validate_input_arguments!(tool_name, tool, arguments)
67
65
 
68
- result = execute_tool_handler(tool, arguments, security_result)
66
+ result = execute_tool_handler(tool, arguments, security_result, session)
69
67
  context.result = build_tool_result(result)
70
68
 
71
69
  context = server.middleware_manager.execute_hooks(:after_tool_call, context)
@@ -426,12 +424,16 @@ module VectorMCP
426
424
  # @param session [VectorMCP::Session] The current session
427
425
  # @return [Hash] Request context for security middleware
428
426
  def self.extract_request_from_session(session)
429
- # Extract security context from session
430
- # This will be enhanced as we integrate with transport layers
427
+ # All sessions should have a request_context - this is enforced by Session initialization
428
+ unless session.respond_to?(:request_context) && session.request_context
429
+ raise VectorMCP::InternalError,
430
+ "Session missing request_context - transport layer integration error. Session ID: #{session.id}"
431
+ end
432
+
431
433
  {
432
- headers: session.instance_variable_get(:@request_headers) || {},
433
- params: session.instance_variable_get(:@request_params) || {},
434
- session_id: session.respond_to?(:id) ? session.id : "test-session"
434
+ headers: session.request_context.headers,
435
+ params: session.request_context.params,
436
+ session_id: session.id
435
437
  }
436
438
  end
437
439
  private_class_method :extract_request_from_session
@@ -497,11 +499,11 @@ module VectorMCP
497
499
  end
498
500
 
499
501
  # Execute tool handler with proper arity handling
500
- def self.execute_tool_handler(tool, arguments, security_result)
502
+ def self.execute_tool_handler(tool, arguments, _security_result, session)
501
503
  if [1, -1].include?(tool.handler.arity)
502
504
  tool.handler.call(arguments)
503
505
  else
504
- tool.handler.call(arguments, security_result[:session_context])
506
+ tool.handler.call(arguments, session)
505
507
  end
506
508
  end
507
509
 
@@ -16,13 +16,7 @@ module VectorMCP
16
16
  # @param hook_type [String] Type of hook being executed
17
17
  # @param context [VectorMCP::Middleware::Context] Execution context
18
18
  def call(hook_type, context)
19
- @logger.debug("Executing middleware hook") do
20
- {
21
- middleware: self.class.name,
22
- hook_type: hook_type,
23
- operation: context.operation_name
24
- }
25
- end
19
+ # Generic middleware hook execution
26
20
  end
27
21
 
28
22
  # Tool operation hooks
@@ -113,16 +113,10 @@ module VectorMCP
113
113
  @hooks[hook_type].to_a
114
114
  end
115
115
 
116
- def initialize_execution_state(hook_type_str, hooks, context)
116
+ def initialize_execution_state(hook_type_str, hooks, _context)
117
117
  start_time = Time.now
118
118
 
119
- @logger.debug("Executing middleware hooks") do
120
- {
121
- hook_type: hook_type_str,
122
- hook_count: hooks.size,
123
- operation: context.operation_name
124
- }
125
- end
119
+ # Executing middleware hooks
126
120
 
127
121
  {
128
122
  hook_type: hook_type_str,
@@ -157,13 +151,7 @@ module VectorMCP
157
151
  hooks_total: execution_state[:total_hooks]
158
152
  })
159
153
 
160
- @logger.debug("Completed middleware execution") do
161
- {
162
- hook_type: execution_state[:hook_type],
163
- execution_time: execution_time,
164
- hooks_executed: execution_state[:executed_count]
165
- }
166
- end
154
+ # Completed middleware execution
167
155
  end
168
156
 
169
157
  def handle_critical_error(error, hook_type, context)
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VectorMCP
4
+ # Encapsulates request-specific data for MCP sessions.
5
+ # This provides a formal interface for transports to populate request context
6
+ # and for handlers to access request data without coupling to session internals.
7
+ #
8
+ # @attr_reader headers [Hash] HTTP headers from the request
9
+ # @attr_reader params [Hash] Query parameters from the request
10
+ # @attr_reader method [String, nil] HTTP method (GET, POST, etc.) or transport-specific method
11
+ # @attr_reader path [String, nil] Request path or transport-specific path
12
+ # @attr_reader transport_metadata [Hash] Transport-specific metadata
13
+ class RequestContext
14
+ attr_reader :headers, :params, :method, :path, :transport_metadata
15
+
16
+ # Initialize a new request context with the provided data.
17
+ #
18
+ # @param headers [Hash] HTTP headers from the request (default: {})
19
+ # @param params [Hash] Query parameters from the request (default: {})
20
+ # @param method [String, nil] HTTP method or transport-specific method (default: nil)
21
+ # @param path [String, nil] Request path or transport-specific path (default: nil)
22
+ # @param transport_metadata [Hash] Transport-specific metadata (default: {})
23
+ def initialize(headers: {}, params: {}, method: nil, path: nil, transport_metadata: {})
24
+ @headers = normalize_headers(headers).freeze
25
+ @params = normalize_params(params).freeze
26
+ @method = method&.to_s&.freeze
27
+ @path = path&.to_s&.freeze
28
+ @transport_metadata = normalize_metadata(transport_metadata).freeze
29
+ end
30
+
31
+ # Convert the request context to a hash representation.
32
+ # This is useful for serialization and debugging.
33
+ #
34
+ # @return [Hash] Hash representation of the request context
35
+ def to_h
36
+ {
37
+ headers: @headers,
38
+ params: @params,
39
+ method: @method,
40
+ path: @path,
41
+ transport_metadata: @transport_metadata
42
+ }
43
+ end
44
+
45
+ # Check if the request context has any headers.
46
+ #
47
+ # @return [Boolean] True if headers are present, false otherwise
48
+ def headers?
49
+ !@headers.empty?
50
+ end
51
+
52
+ # Check if the request context has any parameters.
53
+ #
54
+ # @return [Boolean] True if parameters are present, false otherwise
55
+ def params?
56
+ !@params.empty?
57
+ end
58
+
59
+ # Get a specific header value.
60
+ #
61
+ # @param name [String] The header name
62
+ # @return [String, nil] The header value or nil if not found
63
+ def header(name)
64
+ @headers[name.to_s]
65
+ end
66
+
67
+ # Get a specific parameter value.
68
+ #
69
+ # @param name [String] The parameter name
70
+ # @return [String, nil] The parameter value or nil if not found
71
+ def param(name)
72
+ @params[name.to_s]
73
+ end
74
+
75
+ # Get transport-specific metadata.
76
+ #
77
+ # @param key [String, Symbol] The metadata key
78
+ # @return [Object, nil] The metadata value or nil if not found
79
+ def metadata(key)
80
+ @transport_metadata[key.to_s]
81
+ end
82
+
83
+ # Check if this is an HTTP-based transport.
84
+ #
85
+ # @return [Boolean] True if method is an HTTP method and path is present
86
+ def http_transport?
87
+ return false unless @method && @path
88
+
89
+ # Check if method is an HTTP method
90
+ http_methods = %w[GET POST PUT DELETE HEAD OPTIONS PATCH TRACE CONNECT]
91
+ http_methods.include?(@method.upcase)
92
+ end
93
+
94
+ # Create a minimal request context for non-HTTP transports.
95
+ # This is useful for stdio and other command-line transports.
96
+ #
97
+ # @param transport_type [String] The transport type identifier
98
+ # @return [RequestContext] A minimal request context
99
+ def self.minimal(transport_type)
100
+ new(
101
+ headers: {},
102
+ params: {},
103
+ method: transport_type.to_s.upcase,
104
+ path: "/",
105
+ transport_metadata: { transport_type: transport_type.to_s }
106
+ )
107
+ end
108
+
109
+ # Create a request context from a Rack environment.
110
+ # This is a convenience method for HTTP-based transports.
111
+ #
112
+ # @param rack_env [Hash] The Rack environment hash
113
+ # @param transport_type [String] The transport type identifier
114
+ # @return [RequestContext] A request context populated from the Rack environment
115
+ def self.from_rack_env(rack_env, transport_type)
116
+ # Handle nil rack_env by returning a minimal context
117
+ return minimal(transport_type) if rack_env.nil?
118
+
119
+ new(
120
+ headers: VectorMCP::Util.extract_headers_from_rack_env(rack_env),
121
+ params: VectorMCP::Util.extract_params_from_rack_env(rack_env),
122
+ method: rack_env["REQUEST_METHOD"],
123
+ path: rack_env["PATH_INFO"],
124
+ transport_metadata: {
125
+ transport_type: transport_type.to_s,
126
+ remote_addr: rack_env["REMOTE_ADDR"],
127
+ user_agent: rack_env["HTTP_USER_AGENT"],
128
+ content_type: rack_env["CONTENT_TYPE"]
129
+ }
130
+ )
131
+ end
132
+
133
+ # String representation of the request context.
134
+ #
135
+ # @return [String] String representation for debugging
136
+ def to_s
137
+ "<RequestContext method=#{@method} path=#{@path} headers=#{@headers.keys.size} params=#{@params.keys.size}>"
138
+ end
139
+
140
+ # Detailed string representation for debugging.
141
+ #
142
+ # @return [String] Detailed string representation
143
+ def inspect
144
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} " \
145
+ "method=#{@method.inspect} path=#{@path.inspect} " \
146
+ "headers=#{@headers.inspect} params=#{@params.inspect} " \
147
+ "transport_metadata=#{@transport_metadata.inspect}>"
148
+ end
149
+
150
+ private
151
+
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 }
160
+ end
161
+
162
+ # Normalize parameters to ensure consistent format.
163
+ #
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)
168
+
169
+ params.transform_keys(&:to_s).transform_values { |v| v.nil? ? "" : v.to_s }
170
+ end
171
+
172
+ # Normalize transport metadata to ensure consistent format.
173
+ #
174
+ # @param metadata [Hash] Raw metadata hash
175
+ # @return [Hash] Normalized metadata hash
176
+ def normalize_metadata(metadata)
177
+ return {} unless metadata.is_a?(Hash)
178
+
179
+ metadata.transform_keys(&:to_s)
180
+ end
181
+ end
182
+ end
@@ -19,12 +19,22 @@ module VectorMCP
19
19
  # - 'data' [String] (Optional) Base64 image data if type is "image".
20
20
  # - 'mimeType' [String] (Optional) Mime type if type is "image".
21
21
  def initialize(result_hash)
22
+ # Handle malformed or nil result_hash
23
+ raise ArgumentError, "Sampling result must be a Hash, got #{result_hash.class}: #{result_hash.inspect}" unless result_hash.is_a?(Hash)
24
+
22
25
  @raw_result = result_hash.transform_keys { |k| k.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase.to_sym }
23
26
 
24
27
  @model = @raw_result[:model]
25
28
  @stop_reason = @raw_result[:stop_reason]
26
29
  @role = @raw_result[:role]
27
- @content = (@raw_result[:content] || {}).transform_keys(&:to_sym)
30
+
31
+ # Safe content processing for malformed responses
32
+ content_raw = @raw_result[:content]
33
+ @content = if content_raw.is_a?(Hash)
34
+ content_raw.transform_keys(&:to_sym)
35
+ else
36
+ {}
37
+ end
28
38
 
29
39
  validate!
30
40
  end
@@ -133,35 +133,9 @@ module VectorMCP
133
133
  # @param env [Hash] the Rack environment
134
134
  # @return [Hash] extracted request data
135
135
  def extract_from_rack_env(env)
136
- # Extract headers (HTTP_ prefixed in Rack env)
137
- headers = {}
138
- env.each do |key, value|
139
- next unless key.start_with?("HTTP_")
140
-
141
- # Convert HTTP_X_API_KEY to X-API-Key format
142
- header_name = key[5..].split("_").map do |part|
143
- case part.upcase
144
- when "API" then "API" # Keep API in all caps
145
- else part.capitalize
146
- end
147
- end.join("-")
148
- headers[header_name] = value
149
- end
150
-
151
- # Add special headers
152
- headers["Authorization"] = env["HTTP_AUTHORIZATION"] if env["HTTP_AUTHORIZATION"]
153
- headers["Content-Type"] = env["CONTENT_TYPE"] if env["CONTENT_TYPE"]
154
-
155
- # Extract query parameters
156
- params = {}
157
- if env["QUERY_STRING"]
158
- require "uri"
159
- params = URI.decode_www_form(env["QUERY_STRING"]).to_h
160
- end
161
-
162
136
  {
163
- headers: headers,
164
- params: params,
137
+ headers: VectorMCP::Util.extract_headers_from_rack_env(env),
138
+ params: VectorMCP::Util.extract_params_from_rack_env(env),
165
139
  method: env["REQUEST_METHOD"],
166
140
  path: env["PATH_INFO"],
167
141
  rack_env: env
@@ -96,36 +96,14 @@ module VectorMCP
96
96
  # @param env [Hash] the Rack environment
97
97
  # @return [Hash] normalized headers
98
98
  def extract_headers_from_rack_env(env)
99
- headers = {}
100
- env.each do |key, value|
101
- next unless key.start_with?("HTTP_")
102
-
103
- # Convert HTTP_X_API_KEY to X-API-Key format
104
- header_name = key[5..].split("_").map do |part|
105
- case part.upcase
106
- when "API" then "API" # Keep API in all caps
107
- else part.capitalize
108
- end
109
- end.join("-")
110
- headers[header_name] = value
111
- end
112
-
113
- # Add special headers
114
- headers["Authorization"] = env["HTTP_AUTHORIZATION"] if env["HTTP_AUTHORIZATION"]
115
- headers["Content-Type"] = env["CONTENT_TYPE"] if env["CONTENT_TYPE"]
116
- headers
99
+ VectorMCP::Util.extract_headers_from_rack_env(env)
117
100
  end
118
101
 
119
102
  # Extract params from Rack environment
120
103
  # @param env [Hash] the Rack environment
121
104
  # @return [Hash] normalized params
122
105
  def extract_params_from_rack_env(env)
123
- params = {}
124
- if env["QUERY_STRING"]
125
- require "uri"
126
- params = URI.decode_www_form(env["QUERY_STRING"]).to_h
127
- end
128
- params
106
+ VectorMCP::Util.extract_params_from_rack_env(env)
129
107
  end
130
108
 
131
109
  # Extract API key from headers
@@ -34,7 +34,6 @@ module VectorMCP
34
34
  # @return [void]
35
35
  def clear_prompts_list_changed
36
36
  @prompts_list_changed = false
37
- logger.debug("Prompts listChanged flag cleared.")
38
37
  end
39
38
 
40
39
  # Notifies connected clients that the list of available prompts has changed.
@@ -45,10 +44,10 @@ module VectorMCP
45
44
  notification_method = "notifications/prompts/list_changed"
46
45
  begin
47
46
  if transport.respond_to?(:broadcast_notification)
48
- logger.info("Broadcasting prompts list changed notification.")
47
+ logger.debug("Broadcasting prompts list changed notification.")
49
48
  transport.broadcast_notification(notification_method)
50
49
  elsif transport.respond_to?(:send_notification)
51
- logger.info("Sending prompts list changed notification (transport may broadcast or send to first client).")
50
+ logger.debug("Sending prompts list changed notification (transport may broadcast or send to first client).")
52
51
  transport.send_notification(notification_method)
53
52
  else
54
53
  logger.warn("Transport does not support sending notifications/prompts/list_changed.")
@@ -62,7 +61,6 @@ module VectorMCP
62
61
  # @return [void]
63
62
  def clear_roots_list_changed
64
63
  @roots_list_changed = false
65
- logger.debug("Roots listChanged flag cleared.")
66
64
  end
67
65
 
68
66
  # Notifies connected clients that the list of available roots has changed.
@@ -73,10 +71,10 @@ module VectorMCP
73
71
  notification_method = "notifications/roots/list_changed"
74
72
  begin
75
73
  if transport.respond_to?(:broadcast_notification)
76
- logger.info("Broadcasting roots list changed notification.")
74
+ logger.debug("Broadcasting roots list changed notification.")
77
75
  transport.broadcast_notification(notification_method)
78
76
  elsif transport.respond_to?(:send_notification)
79
- logger.info("Sending roots list changed notification (transport may broadcast or send to first client).")
77
+ logger.debug("Sending roots list changed notification (transport may broadcast or send to first client).")
80
78
  transport.send_notification(notification_method)
81
79
  else
82
80
  logger.warn("Transport does not support sending notifications/roots/list_changed.")
@@ -90,7 +88,7 @@ module VectorMCP
90
88
  # @api private
91
89
  def subscribe_prompts(session)
92
90
  @prompt_subscribers << session unless @prompt_subscribers.include?(session)
93
- logger.debug("Session subscribed to prompt list changes: #{session.object_id}")
91
+ # Session subscribed to prompt list changes
94
92
  end
95
93
 
96
94
  private
@@ -21,10 +21,10 @@ module VectorMCP
21
21
  params = message["params"] || {}
22
22
 
23
23
  if id && method # Request
24
- logger.info("[#{session_id}] Request [#{id}]: #{method} with params: #{params.inspect}")
24
+ logger.debug("[#{session_id}] Request [#{id}]: #{method} with params: #{params.inspect}")
25
25
  handle_request(id, method, params, session)
26
26
  elsif method # Notification
27
- logger.info("[#{session_id}] Notification: #{method} with params: #{params.inspect}")
27
+ logger.debug("[#{session_id}] Notification: #{method} with params: #{params.inspect}")
28
28
  handle_notification(method, params, session)
29
29
  nil # Notifications do not have a return value to send back to client
30
30
  elsif id # Invalid: Has ID but no method
@@ -74,7 +74,9 @@ module VectorMCP
74
74
  # Validates that the session is properly initialized for the given request.
75
75
  # @api private
76
76
  def validate_session_initialization(id, method, _params, session)
77
- return if session.initialized?
77
+ # Handle both direct VectorMCP::Session and BaseSessionManager::Session wrapper
78
+ actual_session = session.respond_to?(:context) ? session.context : session
79
+ return if actual_session.initialized?
78
80
 
79
81
  # Allow "initialize" even if not marked initialized yet by server
80
82
  return if method == "initialize"
@@ -113,7 +115,9 @@ module VectorMCP
113
115
  # Internal handler for JSON-RPC notifications.
114
116
  # @api private
115
117
  def handle_notification(method, params, session)
116
- unless session.initialized? || method == "initialized"
118
+ # Handle both direct VectorMCP::Session and BaseSessionManager::Session wrapper
119
+ actual_session = session.respond_to?(:context) ? session.context : session
120
+ unless actual_session.initialized? || method == "initialized"
117
121
  logger.warn("Ignoring notification '#{method}' before session is initialized. Params: #{params.inspect}")
118
122
  return
119
123
  end
@@ -158,7 +162,9 @@ module VectorMCP
158
162
  # @api private
159
163
  def session_method(method_name)
160
164
  lambda do |params, session, _server|
161
- session.public_send(method_name, params)
165
+ # Handle both direct VectorMCP::Session and BaseSessionManager::Session wrapper
166
+ actual_session = session.respond_to?(:context) ? session.context : session
167
+ actual_session.public_send(method_name, params)
162
168
  end
163
169
  end
164
170
  end