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 +4 -4
- data/CHANGELOG.md +54 -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/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 +2 -24
- 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 +17 -7
- 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 +151 -0
- data/lib/vector_mcp/transport/http_stream/session_manager.rb +189 -0
- data/lib/vector_mcp/transport/http_stream/stream_handler.rb +269 -0
- data/lib/vector_mcp/transport/http_stream.rb +779 -0
- 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
- metadata +9 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64b4dac9a0a1e9c782d3b5d693e7582803621ac81555f992031f9edbdd3f5b6b
|
4
|
+
data.tar.gz: 571e540ac540859b1fefaadc5e2b3cda39a1415c3496af38cee46cb839316c85
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
|
@@ -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
|
@@ -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
|
-
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
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
|
-
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|