vector_mcp 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +75 -0
- data/lib/vector_mcp/definitions.rb +25 -9
- data/lib/vector_mcp/errors.rb +2 -6
- data/lib/vector_mcp/handlers/core.rb +12 -10
- data/lib/vector_mcp/image_util.rb +27 -2
- data/lib/vector_mcp/log_filter.rb +48 -0
- data/lib/vector_mcp/middleware/base.rb +1 -7
- data/lib/vector_mcp/middleware/manager.rb +3 -15
- data/lib/vector_mcp/request_context.rb +182 -0
- data/lib/vector_mcp/sampling/result.rb +11 -1
- data/lib/vector_mcp/security/middleware.rb +2 -28
- data/lib/vector_mcp/security/strategies/api_key.rb +29 -28
- data/lib/vector_mcp/security/strategies/jwt_token.rb +10 -5
- data/lib/vector_mcp/server/capabilities.rb +5 -7
- data/lib/vector_mcp/server/message_handling.rb +11 -5
- data/lib/vector_mcp/server.rb +21 -10
- data/lib/vector_mcp/session.rb +96 -6
- data/lib/vector_mcp/transport/base_session_manager.rb +320 -0
- data/lib/vector_mcp/transport/http_stream/event_store.rb +157 -0
- data/lib/vector_mcp/transport/http_stream/session_manager.rb +191 -0
- data/lib/vector_mcp/transport/http_stream/stream_handler.rb +270 -0
- data/lib/vector_mcp/transport/http_stream.rb +961 -0
- data/lib/vector_mcp/transport/sse/client_connection.rb +1 -1
- data/lib/vector_mcp/transport/sse/stream_manager.rb +1 -1
- data/lib/vector_mcp/transport/sse.rb +74 -19
- data/lib/vector_mcp/transport/sse_session_manager.rb +188 -0
- data/lib/vector_mcp/transport/stdio.rb +70 -13
- data/lib/vector_mcp/transport/stdio_session_manager.rb +181 -0
- data/lib/vector_mcp/util.rb +39 -1
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +1 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96c22c8497dcfd618605017a41a11d747e596654d86e856551159303133e0ab9
|
|
4
|
+
data.tar.gz: 301099d4a3b6b21c28ad82adf95c77f3469a0c4ba40dbd63f9f4f1060391b37f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d464e8ae1e4472eead1580582b2e39dcaadcc5da54087e3022386c05c2dafd0a45f80509a23653273d615a7f739e0cbd8de052acb6d816c1442819bb775b374
|
|
7
|
+
data.tar.gz: 26818191d6c915a31562356b3dec35ada9a1e0bd42f662d37e5884ce05ba139bd54f4fe768d6a2b5106de21e97da2b5a3c9970797eb05082256761e82f276238
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
+
## [0.3.4] – 2026-03-17
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
* **Batch JSON-RPC Support**: Added batch request dispatch on POST endpoint for processing multiple JSON-RPC requests in a single HTTP call
|
|
6
|
+
* **SSE Response Mode for POST**: POST requests can now receive responses via Server-Sent Events streaming
|
|
7
|
+
* **Accept Header Validation**: Added proper Accept header validation on POST and GET endpoints per MCP specification
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
* **Constant-Time API Key Comparison** (SECURITY-001): Replaced direct string comparison with `Rack::Utils.secure_compare` to prevent timing attacks
|
|
12
|
+
* **Disable Query Parameter Token Extraction** (SECURITY-002): Token extraction from query parameters now disabled by default to prevent token leakage via URL logging and browser history
|
|
13
|
+
* **Path Traversal Protection for ImageUtil** (SECURITY-004): Added path traversal validation to `ImageUtil` file operations to prevent unauthorized file access
|
|
14
|
+
* **Sensitive Data Filtering in Debug Logs** (SECURITY-005): Debug log output now filters sensitive fields (tokens, keys, credentials) to prevent accidental credential exposure
|
|
15
|
+
* **Cross-Session Event Leakage Prevention**: Fixed EventStore to prevent events from one session being accessible to another session
|
|
16
|
+
* **Session ID Validation on POST**: Fixed session fixation vulnerability where unknown session IDs were silently accepted; server now returns 404 for invalid sessions
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
* **Code Quality**: RuboCop style fixes and cleanup of unnecessary files
|
|
21
|
+
|
|
22
|
+
## [0.3.3] – 2025-07-29
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
* **Critical Security Fix - SSE Transport Session Isolation**: Fixed default behavior where SSE transport shared session state across all clients
|
|
27
|
+
- **BREAKING CHANGE**: SSE transport now defaults to secure session isolation mode
|
|
28
|
+
- Session manager is now enabled by default to prevent race conditions and data leakage
|
|
29
|
+
- Legacy shared session mode available via `disable_session_manager: true` option (deprecated with warning)
|
|
30
|
+
- Enhanced security tests to prevent regression of this critical vulnerability
|
|
31
|
+
|
|
32
|
+
* **Critical Security Fix - Path Traversal Vulnerability**: Replaced naive path traversal validation with robust canonicalization
|
|
33
|
+
- **Path Validation Enhancement**: Now uses `File.expand_path` for proper path canonicalization
|
|
34
|
+
- **Attack Prevention**: Eliminates false positives from simple string-based `.` checks
|
|
35
|
+
- **Security Monitoring**: Added warnings for potential path traversal attempts
|
|
36
|
+
- **Bypass Protection**: Prevents sophisticated path traversal attacks through encoding or complex patterns
|
|
37
|
+
|
|
38
|
+
* **Session Compatibility**: Fixed session object type detection across different transports
|
|
39
|
+
- **Transport Compatibility**: Added automatic detection between `BaseSessionManager::Session` and `VectorMCP::Session` types
|
|
40
|
+
- **Method Resolution**: Fixed `undefined method 'initialized?'` and `'initialize!'` errors
|
|
41
|
+
- **Transport Layer**: Enhanced stdio, SSE, and HTTP stream transports for consistent session handling
|
|
42
|
+
|
|
43
|
+
* **Race Condition Fix**: Resolved concurrent session creation test failures in SSE transport
|
|
44
|
+
- **Thread Safety**: Implemented `Concurrent::Array` for thread-safe session tracking
|
|
45
|
+
- **Test Stability**: Enhanced test reliability for concurrent operations
|
|
46
|
+
|
|
47
|
+
* **Code Quality**: Fixed RuboCop style and linting violations
|
|
48
|
+
- **Naming Conventions**: Updated method names to follow Ruby conventions (removed `get_` prefixes)
|
|
49
|
+
- **Style Compliance**: Fixed line length violations and predicate method naming
|
|
50
|
+
- **Consistency**: Applied consistent coding standards across the codebase
|
|
51
|
+
|
|
52
|
+
### Security
|
|
53
|
+
|
|
54
|
+
* **Defense in Depth**: Major security improvements addressing critical vulnerabilities
|
|
55
|
+
- **Multi-Client Security**: Eliminated shared state vulnerabilities in SSE transport
|
|
56
|
+
- **Path Security**: Comprehensive path traversal protection using canonical path resolution
|
|
57
|
+
- **Session Isolation**: Proper session boundary enforcement across all transport types
|
|
58
|
+
|
|
59
|
+
* **Backward Compatibility**: Security fixes maintain API compatibility while improving defaults
|
|
60
|
+
- **Opt-in Legacy Mode**: Deprecated insecure modes available for gradual migration
|
|
61
|
+
- **Migration Path**: Clear deprecation warnings guide users to secure configurations
|
|
62
|
+
|
|
63
|
+
### Testing
|
|
64
|
+
|
|
65
|
+
* **Enhanced Security Test Coverage**: Comprehensive test suites for critical security fixes
|
|
66
|
+
- **Path Traversal Tests**: 20+ test cases covering legitimate paths, attack vectors, and edge cases
|
|
67
|
+
- **SSE Security Tests**: Verification of default secure behavior and session isolation
|
|
68
|
+
- **Integration Tests**: Cross-transport compatibility and session handling validation
|
|
69
|
+
|
|
70
|
+
### Technical Details
|
|
71
|
+
|
|
72
|
+
* **Session Architecture**: Improved session management layer for better transport compatibility
|
|
73
|
+
* **Security Monitoring**: Enhanced logging and warning systems for security events
|
|
74
|
+
* **Error Handling**: Better error messages and debugging information for session-related issues
|
|
75
|
+
|
|
1
76
|
## [0.3.2] – 2025-07-02
|
|
2
77
|
|
|
3
78
|
### 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
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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:
|
|
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
|
-
|
|
254
|
-
|
|
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
|
data/lib/vector_mcp/errors.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
430
|
-
|
|
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.
|
|
433
|
-
params: session.
|
|
434
|
-
session_id: 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,
|
|
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,
|
|
506
|
+
tool.handler.call(arguments, session)
|
|
505
507
|
end
|
|
506
508
|
end
|
|
507
509
|
|
|
@@ -217,12 +217,22 @@ module VectorMCP
|
|
|
217
217
|
# @param file_path [String] Path to the image file.
|
|
218
218
|
# @param validate [Boolean] Whether to validate the image.
|
|
219
219
|
# @param max_size [Integer] Maximum allowed size for validation.
|
|
220
|
+
# @param base_directory [String, nil] Optional base directory for path traversal protection.
|
|
221
|
+
# When provided, the resolved file_path must reside within this directory.
|
|
220
222
|
# @return [Hash] MCP image content hash.
|
|
221
|
-
# @raise [ArgumentError] If file doesn't exist or
|
|
223
|
+
# @raise [ArgumentError] If file doesn't exist, validation fails, or path traversal is detected.
|
|
222
224
|
#
|
|
223
225
|
# @example
|
|
224
226
|
# content = VectorMCP::ImageUtil.file_to_mcp_image_content("./avatar.png")
|
|
225
|
-
|
|
227
|
+
#
|
|
228
|
+
# @example With path traversal protection
|
|
229
|
+
# content = VectorMCP::ImageUtil.file_to_mcp_image_content(
|
|
230
|
+
# user_input_path,
|
|
231
|
+
# base_directory: "/app/uploads"
|
|
232
|
+
# )
|
|
233
|
+
def file_to_mcp_image_content(file_path, validate: true, max_size: DEFAULT_MAX_SIZE, base_directory: nil)
|
|
234
|
+
validate_path_safety!(file_path, base_directory) if base_directory
|
|
235
|
+
|
|
226
236
|
raise ArgumentError, "Image file not found: #{file_path}" unless File.exist?(file_path)
|
|
227
237
|
|
|
228
238
|
raise ArgumentError, "Image file not readable: #{file_path}" unless File.readable?(file_path)
|
|
@@ -231,6 +241,21 @@ module VectorMCP
|
|
|
231
241
|
to_mcp_image_content(binary_data, validate: validate, max_size: max_size)
|
|
232
242
|
end
|
|
233
243
|
|
|
244
|
+
# Validates that a file path does not escape the given base directory.
|
|
245
|
+
#
|
|
246
|
+
# @param file_path [String] The file path to validate.
|
|
247
|
+
# @param base_directory [String] The base directory boundary.
|
|
248
|
+
# @raise [ArgumentError] If the resolved path is outside base_directory.
|
|
249
|
+
# @api private
|
|
250
|
+
def validate_path_safety!(file_path, base_directory)
|
|
251
|
+
resolved_base = File.expand_path(base_directory)
|
|
252
|
+
resolved_path = File.expand_path(file_path, resolved_base)
|
|
253
|
+
|
|
254
|
+
return if resolved_path.start_with?("#{resolved_base}/") || resolved_path == resolved_base
|
|
255
|
+
|
|
256
|
+
raise ArgumentError, "Path traversal detected: resolved path is outside the allowed base directory"
|
|
257
|
+
end
|
|
258
|
+
|
|
234
259
|
# Extracts image metadata from binary data.
|
|
235
260
|
#
|
|
236
261
|
# @param data [String] Binary image data.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VectorMCP
|
|
4
|
+
# Filters sensitive data from values before they are written to logs.
|
|
5
|
+
# Redacts known sensitive keys in hashes and token patterns in strings.
|
|
6
|
+
module LogFilter
|
|
7
|
+
SENSITIVE_KEYS = %w[
|
|
8
|
+
authorization x-api-key api_key apikey token jwt_token
|
|
9
|
+
password secret cookie set-cookie x-jwt-token
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
FILTERED = "[FILTERED]"
|
|
13
|
+
|
|
14
|
+
# Bearer/Basic token pattern: "Bearer <token>" or "Basic <token>"
|
|
15
|
+
TOKEN_PATTERN = /\b(Bearer|Basic|API-Key)\s+\S+/i
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Deep-redacts sensitive keys from a hash.
|
|
20
|
+
# @param hash [Hash] the hash to filter
|
|
21
|
+
# @return [Hash] a copy with sensitive values replaced by "[FILTERED]"
|
|
22
|
+
def filter_hash(hash)
|
|
23
|
+
return hash unless hash.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
hash.each_with_object({}) do |(key, value), filtered|
|
|
26
|
+
str_key = key.to_s.downcase
|
|
27
|
+
filtered[key] = if SENSITIVE_KEYS.include?(str_key)
|
|
28
|
+
FILTERED
|
|
29
|
+
elsif value.is_a?(Hash)
|
|
30
|
+
filter_hash(value)
|
|
31
|
+
elsif value.is_a?(String)
|
|
32
|
+
filter_string(value)
|
|
33
|
+
else
|
|
34
|
+
value
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Redacts Bearer/Basic/API-Key token patterns in a string.
|
|
40
|
+
# @param str [String] the string to filter
|
|
41
|
+
# @return [String] the filtered string
|
|
42
|
+
def filter_string(str)
|
|
43
|
+
return str unless str.is_a?(String)
|
|
44
|
+
|
|
45
|
+
str.gsub(TOKEN_PATTERN, '\1 [FILTERED]')
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -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
|
-
|
|
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,
|
|
116
|
+
def initialize_execution_state(hook_type_str, hooks, _context)
|
|
117
117
|
start_time = Time.now
|
|
118
118
|
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
164
|
-
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
|