vector_mcp 0.3.1 → 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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +122 -0
  3. data/lib/vector_mcp/definitions.rb +25 -9
  4. data/lib/vector_mcp/errors.rb +2 -3
  5. data/lib/vector_mcp/handlers/core.rb +206 -50
  6. data/lib/vector_mcp/logger.rb +148 -0
  7. data/lib/vector_mcp/middleware/base.rb +171 -0
  8. data/lib/vector_mcp/middleware/context.rb +76 -0
  9. data/lib/vector_mcp/middleware/hook.rb +169 -0
  10. data/lib/vector_mcp/middleware/manager.rb +179 -0
  11. data/lib/vector_mcp/middleware.rb +43 -0
  12. data/lib/vector_mcp/request_context.rb +182 -0
  13. data/lib/vector_mcp/sampling/result.rb +11 -1
  14. data/lib/vector_mcp/security/middleware.rb +2 -28
  15. data/lib/vector_mcp/security/strategies/api_key.rb +2 -24
  16. data/lib/vector_mcp/security/strategies/jwt_token.rb +6 -3
  17. data/lib/vector_mcp/server/capabilities.rb +5 -7
  18. data/lib/vector_mcp/server/message_handling.rb +11 -5
  19. data/lib/vector_mcp/server.rb +74 -20
  20. data/lib/vector_mcp/session.rb +131 -8
  21. data/lib/vector_mcp/transport/base_session_manager.rb +320 -0
  22. data/lib/vector_mcp/transport/http_stream/event_store.rb +151 -0
  23. data/lib/vector_mcp/transport/http_stream/session_manager.rb +189 -0
  24. data/lib/vector_mcp/transport/http_stream/stream_handler.rb +269 -0
  25. data/lib/vector_mcp/transport/http_stream.rb +779 -0
  26. data/lib/vector_mcp/transport/sse.rb +74 -19
  27. data/lib/vector_mcp/transport/sse_session_manager.rb +188 -0
  28. data/lib/vector_mcp/transport/stdio.rb +70 -13
  29. data/lib/vector_mcp/transport/stdio_session_manager.rb +181 -0
  30. data/lib/vector_mcp/util.rb +39 -1
  31. data/lib/vector_mcp/version.rb +1 -1
  32. data/lib/vector_mcp.rb +10 -35
  33. metadata +25 -24
  34. data/lib/vector_mcp/logging/component.rb +0 -131
  35. data/lib/vector_mcp/logging/configuration.rb +0 -156
  36. data/lib/vector_mcp/logging/constants.rb +0 -21
  37. data/lib/vector_mcp/logging/core.rb +0 -175
  38. data/lib/vector_mcp/logging/filters/component.rb +0 -69
  39. data/lib/vector_mcp/logging/filters/level.rb +0 -23
  40. data/lib/vector_mcp/logging/formatters/base.rb +0 -52
  41. data/lib/vector_mcp/logging/formatters/json.rb +0 -83
  42. data/lib/vector_mcp/logging/formatters/text.rb +0 -72
  43. data/lib/vector_mcp/logging/outputs/base.rb +0 -64
  44. data/lib/vector_mcp/logging/outputs/console.rb +0 -35
  45. data/lib/vector_mcp/logging/outputs/file.rb +0 -157
  46. data/lib/vector_mcp/logging.rb +0 -71
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c251e50dd9545c58cb748a420426193de3d8b2ccc5c110b340f79ff305ad769
4
- data.tar.gz: d19ebc22fe53066a8113eea7e7219093fc0fe546a960aa392add005ae5bb1fa8
3
+ metadata.gz: 64b4dac9a0a1e9c782d3b5d693e7582803621ac81555f992031f9edbdd3f5b6b
4
+ data.tar.gz: 571e540ac540859b1fefaadc5e2b3cda39a1415c3496af38cee46cb839316c85
5
5
  SHA512:
6
- metadata.gz: c7e6d56309c8e30ef4cdc20d2db4c9a74750508ea6c035dfe2ac64ae3759174d4c6c8b20b8726037318b5f5a14dee59e1a81097160c8df6b43a1d302f7e05f33
7
- data.tar.gz: f8a32dbeadb7d17459745edefd7ebbdf784e7d9dc646a3362fe3ddd2ae3ae8d3c20f9d2374e6905d31a0bfcddbc369d5cb1ef583c544cffadc37fb6ed38ae8d1
6
+ metadata.gz: cc7c13562e31070ce7aef3d0d20801e4a7368a44b22ea58d01ec872f7f9191e7b70d96e72c3cdcd42b9ecc40dc9b7ea30e63ce12b5b54987734a933402269b71
7
+ data.tar.gz: 6f989302cbd97c00aa5f2dfe28950cffb5c2c53c204c3ab10eb886a9dc680e0db6a704fef54229488910c837ebe3857c7e493ae5aa8729d8d6677fe048c223e1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,125 @@
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
+
55
+ ## [0.3.2] – 2025-07-02
56
+
57
+ ### Added
58
+
59
+ * **Comprehensive Middleware System**: Pluggable hook system for custom behavior around all MCP operations
60
+ * **Hook Points**: Support for all major operations including tools, resources, prompts, sampling, transport, and authentication
61
+ * **Priority-Based Execution**: Control middleware execution order with configurable priorities
62
+ * **Conditional Execution**: Run middleware only for specific operations, users, or conditions
63
+ * **Context Management**: Rich execution context with operation metadata and session information
64
+ * **Error Handling**: Graceful error recovery with middleware-specific error hooks
65
+ * **Built-in Middleware**: PII redaction, request retry, rate limiting, and enhanced logging examples
66
+
67
+ * **Enhanced Examples Organization**: Comprehensive example reorganization for better developer experience
68
+ * **Getting Started Examples**: `examples/getting_started/` with basic server implementations
69
+ * **Core Features Examples**: `examples/core_features/` demonstrating key capabilities
70
+ * **Use Cases Examples**: `examples/use_cases/` with real-world application scenarios
71
+ * **Logging Examples**: `examples/logging/` showcasing structured logging capabilities
72
+ * **Middleware Examples**: `examples/middleware_examples.rb` and `examples/simple_middleware_demo.rb`
73
+
74
+ * **Refactored Logging System**: Enhanced logging architecture with better performance and flexibility
75
+ * **Simplified API**: Streamlined `VectorMCP.logger_for(component)` interface
76
+ * **Performance Improvements**: Optimized log formatting and output handling
77
+ * **Better Component Organization**: Hierarchical logger management with cleaner separation
78
+
79
+ ### Changed
80
+
81
+ * **Middleware Integration**: Core server architecture enhanced to support middleware hooks
82
+ * **Server Methods**: New `use_middleware`, `middleware_stats`, `remove_middleware`, and `clear_middleware` methods
83
+ * **Handler Integration**: All core handlers now support middleware execution around operations
84
+ * **Session Context**: Enhanced session context with middleware metadata and execution tracking
85
+
86
+ * **Example Structure**: Major reorganization of examples for better discoverability
87
+ * **Categorized Examples**: Logical grouping by functionality and use case
88
+ * **Enhanced Documentation**: Each example category includes detailed README files
89
+ * **Use Case Focus**: Real-world scenarios like data analysis, file operations, and web scraping
90
+
91
+ * **Backward Compatibility**: All middleware features are opt-in with zero impact on existing servers
92
+ * **Default Behavior**: Servers without middleware continue working exactly as before
93
+ * **Optional Integration**: Middleware can be added incrementally to existing applications
94
+
95
+ ### Fixed
96
+
97
+ * **Ruby Version Compatibility**: Enhanced support for older Ruby versions
98
+ * **Code Quality**: Multiple bug fixes and improvements identified through expanded test coverage
99
+ * **Performance**: Optimized middleware execution path for minimal overhead when no middleware is registered
100
+
101
+ ### Security
102
+
103
+ * **Middleware Security**: Security-aware middleware execution
104
+ * **Session Context Integration**: Middleware has access to authentication and authorization context
105
+ * **Secure Error Handling**: Middleware errors handled securely without information leakage
106
+ * **Permission-Aware Hooks**: Middleware can respect user permissions and security policies
107
+
108
+ ### Testing
109
+
110
+ * **Comprehensive Middleware Tests**: 50+ tests covering all middleware functionality
111
+ * **Hook Execution Tests**: Verification of all hook types and execution order
112
+ * **Priority and Condition Tests**: Complex scenario testing for middleware orchestration
113
+ * **Integration Tests**: End-to-end testing with real server operations
114
+ * **Performance Tests**: Overhead measurement and resource usage validation
115
+
116
+ ### Technical Details
117
+
118
+ * **API Compatibility**: All middleware features maintain full backward compatibility
119
+ * **Performance**: Minimal overhead when middleware is not used, efficient execution when enabled
120
+ * **Memory Management**: Proper cleanup and resource management for long-running servers
121
+ * **Thread Safety**: Concurrent middleware execution with proper synchronization
122
+
1
123
  ## [0.3.1] – 2025-06-25
2
124
 
3
125
  ### 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,7 +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
- VectorMCP.logger.debug("Initializing ProtocolError with code: #{code}")
32
31
  @code = code
33
32
  @message = message
34
33
  @details = details # NOTE: `data` in JSON-RPC is often used for this purpose.
@@ -104,7 +103,7 @@ module VectorMCP
104
103
  # @param details [Hash, nil] Additional details for the error (optional).
105
104
  # @param request_id [String, Integer, nil] The ID of the originating request.
106
105
  def initialize(message = "Server error", code: -32_000, details: nil, request_id: nil)
107
- VectorMCP.logger.debug("Initializing ServerError with code: #{code}")
106
+ # ServerError initialization
108
107
  unless (-32_099..-32_000).cover?(code)
109
108
  warn "Server error code #{code} is outside of the reserved range (-32099 to -32000). Using -32000 instead."
110
109
  code = -32_000
@@ -131,7 +130,7 @@ module VectorMCP
131
130
  # @param details [Hash, nil] Additional details for the error (optional).
132
131
  # @param request_id [String, Integer, nil] The ID of the originating request.
133
132
  def initialize(message = "Not Found", details: nil, request_id: nil)
134
- VectorMCP.logger.debug("Initializing NotFoundError with code: -32001")
133
+ # NotFoundError initialization
135
134
  super(message, code: -32_001, details: details, request_id: request_id)
136
135
  end
137
136
  end
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
  require "uri"
5
5
  require "json-schema"
6
+ require_relative "../middleware"
6
7
 
7
8
  module VectorMCP
8
9
  module Handlers
@@ -21,7 +22,6 @@ module VectorMCP
21
22
  # @param _server [VectorMCP::Server] The server instance (ignored).
22
23
  # @return [Hash] An empty hash, as per MCP spec for ping.
23
24
  def self.ping(_params, _session, _server)
24
- VectorMCP.logger.debug("Handling ping request")
25
25
  {}
26
26
  end
27
27
 
@@ -54,27 +54,23 @@ module VectorMCP
54
54
  tool_name = params["name"]
55
55
  arguments = params["arguments"] || {}
56
56
 
57
- tool = server.tools[tool_name]
58
- raise VectorMCP::NotFoundError.new("Not Found", details: "Tool not found: #{tool_name}") unless tool
57
+ context = create_tool_context(tool_name, params, session, server)
58
+ context = server.middleware_manager.execute_hooks(:before_tool_call, context)
59
+ return handle_middleware_error(context) if context.error?
59
60
 
60
- # Security check: authenticate and authorize the request
61
- security_result = check_tool_security(session, tool, server)
62
- handle_security_failure(security_result) unless security_result[:success]
61
+ begin
62
+ tool = find_tool!(tool_name, server)
63
+ security_result = validate_tool_security!(session, tool, server)
64
+ validate_input_arguments!(tool_name, tool, arguments)
63
65
 
64
- # Validate arguments against the tool's input schema
65
- validate_input_arguments!(tool_name, tool, arguments)
66
+ result = execute_tool_handler(tool, arguments, security_result, session)
67
+ context.result = build_tool_result(result)
66
68
 
67
- # Let StandardError propagate to Server#handle_request
68
- # Pass session_context only if the handler supports it (for backward compatibility)
69
- result = if [1, -1].include?(tool.handler.arity)
70
- tool.handler.call(arguments)
71
- else
72
- tool.handler.call(arguments, security_result[:session_context])
73
- end
74
- {
75
- isError: false,
76
- content: VectorMCP::Util.convert_to_mcp_content(result)
77
- }
69
+ context = server.middleware_manager.execute_hooks(:after_tool_call, context)
70
+ context.result
71
+ rescue StandardError => e
72
+ handle_tool_error(e, context, server)
73
+ end
78
74
  end
79
75
 
80
76
  # Handles the `resources/list` request.
@@ -103,27 +99,24 @@ module VectorMCP
103
99
  # @raise [VectorMCP::ForbiddenError] if authorization fails.
104
100
  def self.read_resource(params, session, server)
105
101
  uri_s = params["uri"]
106
- raise VectorMCP::NotFoundError.new("Not Found", details: "Resource not found: #{uri_s}") unless server.resources[uri_s]
107
102
 
108
- resource = server.resources[uri_s]
103
+ context = create_resource_context(uri_s, params, session, server)
104
+ context = server.middleware_manager.execute_hooks(:before_resource_read, context)
105
+ return handle_middleware_error(context) if context.error?
109
106
 
110
- # Security check: authenticate and authorize the request
111
- security_result = check_resource_security(session, resource, server)
112
- handle_security_failure(security_result) unless security_result[:success]
107
+ begin
108
+ resource = find_resource!(uri_s, server)
109
+ security_result = validate_resource_security!(session, resource, server)
113
110
 
114
- # Let StandardError propagate to Server#handle_request
115
- # Pass session_context only if the handler supports it (for backward compatibility)
116
- content_raw = if [1, -1].include?(resource.handler.arity)
117
- resource.handler.call(params)
118
- else
119
- resource.handler.call(params, security_result[:session_context])
120
- end
121
- contents = VectorMCP::Util.convert_to_mcp_content(content_raw, mime_type: resource.mime_type)
122
- contents.each do |item|
123
- # Add URI to each content item if not already present
124
- item[:uri] ||= uri_s
111
+ content_raw = execute_resource_handler(resource, params, security_result)
112
+ contents = process_resource_content(content_raw, resource, uri_s)
113
+
114
+ context.result = { contents: contents }
115
+ context = server.middleware_manager.execute_hooks(:after_resource_read, context)
116
+ context.result
117
+ rescue StandardError => e
118
+ handle_resource_error(e, context, server)
125
119
  end
126
- { contents: contents }
127
120
  end
128
121
 
129
122
  # Handles the `prompts/list` request.
@@ -184,20 +177,51 @@ module VectorMCP
184
177
  # @raise [VectorMCP::NotFoundError] if the prompt name is not found.
185
178
  # @raise [VectorMCP::InvalidParamsError] if arguments are invalid.
186
179
  # @raise [VectorMCP::InternalError] if the prompt handler returns an invalid data structure.
187
- def self.get_prompt(params, _session, server)
180
+ def self.get_prompt(params, session, server)
188
181
  prompt_name = params["name"]
189
- prompt = fetch_prompt(prompt_name, server)
190
182
 
191
- arguments = params["arguments"] || {}
192
- validate_arguments!(prompt_name, prompt, arguments)
183
+ # Create middleware context
184
+ context = VectorMCP::Middleware::Context.new(
185
+ operation_type: :prompt_get,
186
+ operation_name: prompt_name,
187
+ params: params,
188
+ session: session,
189
+ server: server,
190
+ metadata: { start_time: Time.now }
191
+ )
192
+
193
+ # Execute before_prompt_get hooks
194
+ context = server.middleware_manager.execute_hooks(:before_prompt_get, context)
195
+ return handle_middleware_error(context) if context.error?
196
+
197
+ begin
198
+ prompt = fetch_prompt(prompt_name, server)
199
+
200
+ arguments = params["arguments"] || {}
201
+ validate_arguments!(prompt_name, prompt, arguments)
193
202
 
194
- # Call the registered handler after arguments were validated
195
- result_data = prompt.handler.call(arguments)
203
+ # Call the registered handler after arguments were validated
204
+ result_data = prompt.handler.call(arguments)
196
205
 
197
- validate_prompt_response!(prompt_name, result_data, server)
206
+ validate_prompt_response!(prompt_name, result_data, server)
198
207
 
199
- # Return the handler response directly (must match GetPromptResult schema)
200
- result_data
208
+ # Set result in context
209
+ context.result = result_data
210
+
211
+ # Execute after_prompt_get hooks
212
+ context = server.middleware_manager.execute_hooks(:after_prompt_get, context)
213
+
214
+ context.result
215
+ rescue StandardError => e
216
+ # Set error in context and execute error hooks
217
+ context.error = e
218
+ context = server.middleware_manager.execute_hooks(:on_prompt_error, context)
219
+
220
+ # Re-raise unless middleware handled the error
221
+ raise e unless context.result
222
+
223
+ context.result
224
+ end
201
225
  end
202
226
 
203
227
  # --- Notification Handlers ---
@@ -400,12 +424,16 @@ module VectorMCP
400
424
  # @param session [VectorMCP::Session] The current session
401
425
  # @return [Hash] Request context for security middleware
402
426
  def self.extract_request_from_session(session)
403
- # Extract security context from session
404
- # 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
+
405
433
  {
406
- headers: session.instance_variable_get(:@request_headers) || {},
407
- params: session.instance_variable_get(:@request_params) || {},
408
- 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
409
437
  }
410
438
  end
411
439
  private_class_method :extract_request_from_session
@@ -427,6 +455,134 @@ module VectorMCP
427
455
  end
428
456
  end
429
457
  private_class_method :handle_security_failure
458
+
459
+ # Handle middleware error by returning appropriate response or raising error
460
+ # @api private
461
+ # @param context [VectorMCP::Middleware::Context] The middleware context with error
462
+ # @return [Hash, nil] Response hash if middleware provided one
463
+ # @raise [StandardError] Re-raises the original error if not handled
464
+ def self.handle_middleware_error(context)
465
+ # If middleware provided a result, return it
466
+ return context.result if context.result
467
+
468
+ # Otherwise, re-raise the middleware error
469
+ raise context.error
470
+ end
471
+
472
+ # Tool helper methods
473
+
474
+ # Create middleware context for tool operations
475
+ def self.create_tool_context(tool_name, params, session, server)
476
+ VectorMCP::Middleware::Context.new(
477
+ operation_type: :tool_call,
478
+ operation_name: tool_name,
479
+ params: params,
480
+ session: session,
481
+ server: server,
482
+ metadata: { start_time: Time.now }
483
+ )
484
+ end
485
+
486
+ # Find and validate tool exists
487
+ def self.find_tool!(tool_name, server)
488
+ tool = server.tools[tool_name]
489
+ raise VectorMCP::NotFoundError.new("Not Found", details: "Tool not found: #{tool_name}") unless tool
490
+
491
+ tool
492
+ end
493
+
494
+ # Validate tool security
495
+ def self.validate_tool_security!(session, tool, server)
496
+ security_result = check_tool_security(session, tool, server)
497
+ handle_security_failure(security_result) unless security_result[:success]
498
+ security_result
499
+ end
500
+
501
+ # Execute tool handler with proper arity handling
502
+ def self.execute_tool_handler(tool, arguments, _security_result, session)
503
+ if [1, -1].include?(tool.handler.arity)
504
+ tool.handler.call(arguments)
505
+ else
506
+ tool.handler.call(arguments, session)
507
+ end
508
+ end
509
+
510
+ # Build tool result response
511
+ def self.build_tool_result(result)
512
+ {
513
+ isError: false,
514
+ content: VectorMCP::Util.convert_to_mcp_content(result)
515
+ }
516
+ end
517
+
518
+ # Handle tool execution errors
519
+ def self.handle_tool_error(error, context, server)
520
+ context.error = error
521
+ context = server.middleware_manager.execute_hooks(:on_tool_error, context)
522
+ raise error unless context.result
523
+
524
+ context.result
525
+ end
526
+
527
+ # Resource helper methods
528
+
529
+ # Create middleware context for resource operations
530
+ def self.create_resource_context(uri_s, params, session, server)
531
+ VectorMCP::Middleware::Context.new(
532
+ operation_type: :resource_read,
533
+ operation_name: uri_s,
534
+ params: params,
535
+ session: session,
536
+ server: server,
537
+ metadata: { start_time: Time.now }
538
+ )
539
+ end
540
+
541
+ # Find and validate resource exists
542
+ def self.find_resource!(uri_s, server)
543
+ raise VectorMCP::NotFoundError.new("Not Found", details: "Resource not found: #{uri_s}") unless server.resources[uri_s]
544
+
545
+ server.resources[uri_s]
546
+ end
547
+
548
+ # Validate resource security
549
+ def self.validate_resource_security!(session, resource, server)
550
+ security_result = check_resource_security(session, resource, server)
551
+ handle_security_failure(security_result) unless security_result[:success]
552
+ security_result
553
+ end
554
+
555
+ # Execute resource handler with proper arity handling
556
+ def self.execute_resource_handler(resource, params, security_result)
557
+ if [1, -1].include?(resource.handler.arity)
558
+ resource.handler.call(params)
559
+ else
560
+ resource.handler.call(params, security_result[:session_context])
561
+ end
562
+ end
563
+
564
+ # Process resource content and add URI
565
+ def self.process_resource_content(content_raw, resource, uri_s)
566
+ contents = VectorMCP::Util.convert_to_mcp_content(content_raw, mime_type: resource.mime_type)
567
+ contents.each do |item|
568
+ item[:uri] ||= uri_s
569
+ end
570
+ contents
571
+ end
572
+
573
+ # Handle resource execution errors
574
+ def self.handle_resource_error(error, context, server)
575
+ context.error = error
576
+ context = server.middleware_manager.execute_hooks(:on_resource_error, context)
577
+ raise error unless context.result
578
+
579
+ context.result
580
+ end
581
+
582
+ private_class_method :handle_middleware_error, :create_tool_context, :find_tool!, :validate_tool_security!,
583
+ :execute_tool_handler, :build_tool_result, :handle_tool_error, :create_resource_context,
584
+ :find_resource!, :validate_resource_security!, :execute_resource_handler,
585
+ :process_resource_content, :handle_resource_error
430
586
  end
431
587
  end
432
588
  end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "json"
5
+
6
+ module VectorMCP
7
+ # Simple, environment-driven logger for VectorMCP
8
+ # Supports JSON and text formats with component-based identification
9
+ class Logger
10
+ LEVELS = {
11
+ "TRACE" => ::Logger::DEBUG,
12
+ "DEBUG" => ::Logger::DEBUG,
13
+ "INFO" => ::Logger::INFO,
14
+ "WARN" => ::Logger::WARN,
15
+ "ERROR" => ::Logger::ERROR,
16
+ "FATAL" => ::Logger::FATAL
17
+ }.freeze
18
+
19
+ attr_reader :component, :ruby_logger
20
+
21
+ def initialize(component = "vectormcp")
22
+ @component = component.to_s
23
+ @ruby_logger = create_ruby_logger
24
+ @format = ENV.fetch("VECTORMCP_LOG_FORMAT", "text").downcase
25
+ end
26
+
27
+ def self.for(component)
28
+ new(component)
29
+ end
30
+
31
+ # Log methods with context support and block evaluation
32
+ def debug(message = nil, **context, &block)
33
+ log(:debug, message || block&.call, context)
34
+ end
35
+
36
+ def info(message = nil, **context, &block)
37
+ log(:info, message || block&.call, context)
38
+ end
39
+
40
+ def warn(message = nil, **context, &block)
41
+ log(:warn, message || block&.call, context)
42
+ end
43
+
44
+ def error(message = nil, **context, &block)
45
+ log(:error, message || block&.call, context)
46
+ end
47
+
48
+ def fatal(message = nil, **context, &block)
49
+ log(:fatal, message || block&.call, context)
50
+ end
51
+
52
+ # Security-specific logging
53
+ def security(message, **context)
54
+ log(:error, "[SECURITY] #{message}", context.merge(security_event: true))
55
+ end
56
+
57
+ # Performance measurement
58
+ def measure(description, **context)
59
+ start_time = Time.now
60
+ result = yield
61
+ duration = Time.now - start_time
62
+
63
+ info("#{description} completed", **context, duration_ms: (duration * 1000).round(2),
64
+ success: true)
65
+
66
+ result
67
+ rescue StandardError => e
68
+ duration = Time.now - start_time
69
+ error("#{description} failed", **context, duration_ms: (duration * 1000).round(2),
70
+ success: false,
71
+ error: e.class.name,
72
+ error_message: e.message)
73
+ raise
74
+ end
75
+
76
+ private
77
+
78
+ def log(level, message, context)
79
+ return unless @ruby_logger.send("#{level}?")
80
+
81
+ if @format == "json"
82
+ log_json(level, message, context)
83
+ else
84
+ log_text(level, message, context)
85
+ end
86
+ end
87
+
88
+ def log_json(level, message, context)
89
+ entry = {
90
+ timestamp: Time.now.iso8601(3),
91
+ level: level.to_s.upcase,
92
+ component: @component,
93
+ message: message,
94
+ thread_id: Thread.current.object_id
95
+ }
96
+ entry.merge!(context) unless context.empty?
97
+
98
+ @ruby_logger.send(level, entry.to_json)
99
+ end
100
+
101
+ def log_text(level, message, context)
102
+ formatted_message = if context.empty?
103
+ "[#{@component}] #{message}"
104
+ else
105
+ context_str = context.map { |k, v| "#{k}=#{v}" }.join(" ")
106
+ "[#{@component}] #{message} (#{context_str})"
107
+ end
108
+
109
+ @ruby_logger.send(level, formatted_message)
110
+ end
111
+
112
+ def create_ruby_logger
113
+ output = determine_output
114
+ logger = ::Logger.new(output)
115
+ logger.level = determine_level
116
+ logger.formatter = method(:format_log_entry)
117
+ logger
118
+ end
119
+
120
+ def determine_output
121
+ case ENV.fetch("VECTORMCP_LOG_OUTPUT", "stderr").downcase
122
+ when "stdout"
123
+ $stdout
124
+ when "file"
125
+ file_path = ENV.fetch("VECTORMCP_LOG_FILE", "./vectormcp.log")
126
+ File.open(file_path, "a")
127
+ else
128
+ $stderr
129
+ end
130
+ end
131
+
132
+ def determine_level
133
+ level_name = ENV.fetch("VECTORMCP_LOG_LEVEL", "INFO").upcase
134
+ LEVELS.fetch(level_name, ::Logger::INFO)
135
+ end
136
+
137
+ def format_log_entry(severity, datetime, _progname, msg)
138
+ if @format == "json"
139
+ # JSON messages are already formatted
140
+ "#{msg}\n"
141
+ else
142
+ # Text format with timestamp
143
+ timestamp = datetime.strftime("%Y-%m-%d %H:%M:%S.%3N")
144
+ "#{timestamp} [#{severity}] #{msg}\n"
145
+ end
146
+ end
147
+ end
148
+ end