vector_mcp 0.3.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +147 -337
- data/lib/vector_mcp/definitions.rb +30 -0
- data/lib/vector_mcp/handlers/core.rb +78 -81
- data/lib/vector_mcp/image_util.rb +34 -11
- data/lib/vector_mcp/middleware/anonymizer.rb +186 -0
- data/lib/vector_mcp/middleware/base.rb +1 -5
- data/lib/vector_mcp/middleware/context.rb +11 -1
- data/lib/vector_mcp/middleware/hook.rb +7 -24
- data/lib/vector_mcp/middleware.rb +26 -9
- data/lib/vector_mcp/rails/tool.rb +85 -0
- data/lib/vector_mcp/request_context.rb +1 -1
- data/lib/vector_mcp/security/auth_manager.rb +12 -13
- data/lib/vector_mcp/security/auth_result.rb +33 -0
- data/lib/vector_mcp/security/authorization.rb +5 -9
- data/lib/vector_mcp/security/middleware.rb +2 -2
- data/lib/vector_mcp/security/session_context.rb +11 -27
- data/lib/vector_mcp/security/strategies/api_key.rb +1 -5
- data/lib/vector_mcp/security/strategies/custom.rb +10 -37
- data/lib/vector_mcp/security/strategies/jwt_token.rb +1 -10
- data/lib/vector_mcp/server/capabilities.rb +22 -32
- data/lib/vector_mcp/server/message_handling.rb +21 -14
- data/lib/vector_mcp/server/registry.rb +102 -120
- data/lib/vector_mcp/server.rb +98 -57
- data/lib/vector_mcp/session.rb +5 -3
- data/lib/vector_mcp/token_store.rb +80 -0
- data/lib/vector_mcp/tool.rb +221 -0
- data/lib/vector_mcp/transport/base_session_manager.rb +1 -17
- data/lib/vector_mcp/transport/http_stream/event_store.rb +29 -17
- data/lib/vector_mcp/transport/http_stream/session_manager.rb +41 -36
- data/lib/vector_mcp/transport/http_stream/stream_handler.rb +132 -47
- data/lib/vector_mcp/transport/http_stream.rb +242 -124
- data/lib/vector_mcp/util/token_sweeper.rb +74 -0
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +8 -8
- metadata +8 -10
- data/lib/vector_mcp/transport/sse/client_connection.rb +0 -113
- data/lib/vector_mcp/transport/sse/message_handler.rb +0 -166
- data/lib/vector_mcp/transport/sse/puma_config.rb +0 -77
- data/lib/vector_mcp/transport/sse/stream_manager.rb +0 -92
- data/lib/vector_mcp/transport/sse.rb +0 -377
- data/lib/vector_mcp/transport/sse_session_manager.rb +0 -188
- data/lib/vector_mcp/transport/stdio.rb +0 -473
- data/lib/vector_mcp/transport/stdio_session_manager.rb +0 -181
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
5
|
+
require "vector_mcp/tool"
|
|
6
|
+
|
|
7
|
+
module VectorMCP
|
|
8
|
+
module Rails
|
|
9
|
+
# Rails-aware base class for declarative tool definitions.
|
|
10
|
+
#
|
|
11
|
+
# Adds ergonomics for the common patterns that show up in ActiveRecord-
|
|
12
|
+
# backed MCP tools:
|
|
13
|
+
#
|
|
14
|
+
# * +find!+ -- fetch a record or raise +VectorMCP::NotFoundError+
|
|
15
|
+
# * +respond_with+ -- standard success/error payload from a record
|
|
16
|
+
# * +with_transaction+ -- wrap a mutation in an AR transaction
|
|
17
|
+
# * Auto-rescue of +ActiveRecord::RecordNotFound+ (-> NotFoundError)
|
|
18
|
+
# and +ActiveRecord::RecordInvalid+ (-> error payload)
|
|
19
|
+
# * Arguments delivered to +#call+ as a +HashWithIndifferentAccess+
|
|
20
|
+
# so +args[:id]+ and +args["id"]+ both work
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# class UpdateProvider < VectorMCP::Rails::Tool
|
|
24
|
+
# tool_name "update_provider"
|
|
25
|
+
# description "Update an existing provider"
|
|
26
|
+
#
|
|
27
|
+
# param :id, type: :integer, required: true
|
|
28
|
+
# param :name, type: :string
|
|
29
|
+
#
|
|
30
|
+
# def call(args, _session)
|
|
31
|
+
# provider = find!(Provider, args[:id])
|
|
32
|
+
# provider.update(args.except(:id))
|
|
33
|
+
# respond_with(provider, name: provider.name)
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
class Tool < VectorMCP::Tool
|
|
37
|
+
# Overrides the parent handler to add indifferent-access args and
|
|
38
|
+
# auto-rescue ActiveRecord exceptions.
|
|
39
|
+
def self.build_handler
|
|
40
|
+
klass = self
|
|
41
|
+
params = @params
|
|
42
|
+
lambda do |args, session|
|
|
43
|
+
coerced = klass.coerce_args(args, params).with_indifferent_access
|
|
44
|
+
klass.new.call(coerced, session)
|
|
45
|
+
rescue ActiveRecord::RecordNotFound => e
|
|
46
|
+
raise VectorMCP::NotFoundError, e.message
|
|
47
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
48
|
+
{ success: false, errors: e.record.errors.full_messages }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
private_class_method :build_handler
|
|
52
|
+
|
|
53
|
+
# Finds a record by id or raises VectorMCP::NotFoundError.
|
|
54
|
+
#
|
|
55
|
+
# @param model [Class] an ActiveRecord model class
|
|
56
|
+
# @param id [Integer, String] the record id
|
|
57
|
+
# @return [ActiveRecord::Base]
|
|
58
|
+
def find!(model, id)
|
|
59
|
+
model.find_by(id: id) ||
|
|
60
|
+
raise(VectorMCP::NotFoundError, "#{model.name} #{id} not found")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Builds a standard response payload from a record.
|
|
64
|
+
#
|
|
65
|
+
# Success shape: +{ success: true, id: record.id, **extras }+
|
|
66
|
+
# Error shape: +{ success: false, errors: record.errors.full_messages }+
|
|
67
|
+
#
|
|
68
|
+
# @param record [ActiveRecord::Base]
|
|
69
|
+
# @param extras [Hash] additional keys to merge into the success payload
|
|
70
|
+
# @return [Hash]
|
|
71
|
+
def respond_with(record, **extras)
|
|
72
|
+
if record.persisted? && record.errors.empty?
|
|
73
|
+
{ success: true, id: record.id, **extras }
|
|
74
|
+
else
|
|
75
|
+
{ success: false, errors: record.errors.full_messages }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Runs the given block inside an ActiveRecord transaction.
|
|
80
|
+
def with_transaction(&)
|
|
81
|
+
ActiveRecord::Base.transaction(&)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -92,7 +92,7 @@ module VectorMCP
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# Create a minimal request context for non-HTTP transports.
|
|
95
|
-
# This is useful for
|
|
95
|
+
# This is useful for non-HTTP transports or testing contexts.
|
|
96
96
|
#
|
|
97
97
|
# @param transport_type [String] The transport type identifier
|
|
98
98
|
# @return [RequestContext] A minimal request context
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "auth_result"
|
|
4
|
+
|
|
3
5
|
module VectorMCP
|
|
4
6
|
module Security
|
|
5
7
|
# Manages authentication strategies for VectorMCP servers
|
|
@@ -42,25 +44,22 @@ module VectorMCP
|
|
|
42
44
|
# Authenticate a request using the specified or default strategy
|
|
43
45
|
# @param request [Hash] the request object containing headers, params, etc.
|
|
44
46
|
# @param strategy [Symbol] optional strategy override
|
|
45
|
-
# @return [
|
|
47
|
+
# @return [AuthResult] the authentication outcome
|
|
46
48
|
def authenticate(request, strategy: nil)
|
|
47
|
-
return
|
|
49
|
+
return AuthResult.passthrough unless @enabled
|
|
48
50
|
|
|
49
51
|
strategy_name = strategy || @default_strategy
|
|
50
52
|
auth_strategy = @strategies[strategy_name]
|
|
53
|
+
return AuthResult.failure unless auth_strategy
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{ authenticated: true, user: result }
|
|
58
|
-
else
|
|
59
|
-
{ authenticated: false, error: "Authentication failed" }
|
|
60
|
-
end
|
|
61
|
-
rescue StandardError => e
|
|
62
|
-
{ authenticated: false, error: "Authentication error: #{e.message}" }
|
|
55
|
+
result = auth_strategy.authenticate(request)
|
|
56
|
+
if result == false
|
|
57
|
+
AuthResult.failure
|
|
58
|
+
else
|
|
59
|
+
AuthResult.success(user: result, strategy: strategy_name.to_s)
|
|
63
60
|
end
|
|
61
|
+
rescue StandardError
|
|
62
|
+
AuthResult.failure
|
|
64
63
|
end
|
|
65
64
|
|
|
66
65
|
# Check if authentication is required
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VectorMCP
|
|
4
|
+
module Security
|
|
5
|
+
# Value object representing the outcome of an authentication attempt.
|
|
6
|
+
# Replaces the unstructured Hash that previously flowed through the auth pipeline.
|
|
7
|
+
class AuthResult
|
|
8
|
+
attr_reader :user, :strategy, :authenticated_at
|
|
9
|
+
|
|
10
|
+
def initialize(authenticated:, user: nil, strategy: nil, authenticated_at: nil)
|
|
11
|
+
@authenticated = authenticated
|
|
12
|
+
@user = user
|
|
13
|
+
@strategy = strategy
|
|
14
|
+
@authenticated_at = authenticated_at || (Time.now if authenticated)
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def authenticated? = @authenticated
|
|
19
|
+
|
|
20
|
+
def self.success(user:, strategy:, authenticated_at: Time.now)
|
|
21
|
+
new(authenticated: true, user: user, strategy: strategy, authenticated_at: authenticated_at)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.failure
|
|
25
|
+
new(authenticated: false)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.passthrough
|
|
29
|
+
new(authenticated: true)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -10,6 +10,7 @@ module VectorMCP
|
|
|
10
10
|
def initialize
|
|
11
11
|
@policies = {}
|
|
12
12
|
@enabled = false
|
|
13
|
+
@logger = VectorMCP.logger_for("authorization")
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
# Enable authorization system
|
|
@@ -45,17 +46,12 @@ module VectorMCP
|
|
|
45
46
|
|
|
46
47
|
resource_type = determine_resource_type(resource)
|
|
47
48
|
policy = @policies[resource_type]
|
|
48
|
-
|
|
49
|
-
# If no policy is defined, allow access (opt-in authorization)
|
|
50
49
|
return true unless policy
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# Log error but deny access for safety
|
|
57
|
-
false
|
|
58
|
-
end
|
|
51
|
+
!!policy.call(user, action, resource)
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
@logger.error("Authorization policy error for #{resource_type}: #{e.message}")
|
|
54
|
+
false
|
|
59
55
|
end
|
|
60
56
|
|
|
61
57
|
# Check if authorization is required
|
|
@@ -120,11 +120,11 @@ module VectorMCP
|
|
|
120
120
|
# @param transport_request [Object] the transport request
|
|
121
121
|
# @return [Hash] extracted request data
|
|
122
122
|
def extract_request_data(transport_request)
|
|
123
|
-
# Handle Rack environment (for
|
|
123
|
+
# Handle Rack environment (for HTTP transports)
|
|
124
124
|
if transport_request.respond_to?(:[]) && transport_request["REQUEST_METHOD"]
|
|
125
125
|
extract_from_rack_env(transport_request)
|
|
126
126
|
else
|
|
127
|
-
# Default fallback
|
|
127
|
+
# Default fallback for non-HTTP request formats
|
|
128
128
|
{ headers: {}, params: {} }
|
|
129
129
|
end
|
|
130
130
|
end
|
|
@@ -113,34 +113,18 @@ module VectorMCP
|
|
|
113
113
|
new(authenticated: false)
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
# Create an authenticated session context from
|
|
117
|
-
# @param auth_result [
|
|
118
|
-
# @return [SessionContext] an authenticated session
|
|
116
|
+
# Create an authenticated session context from an AuthResult
|
|
117
|
+
# @param auth_result [VectorMCP::Security::AuthResult] the authentication outcome
|
|
118
|
+
# @return [SessionContext] an authenticated or anonymous session
|
|
119
119
|
def self.from_auth_result(auth_result)
|
|
120
|
-
return anonymous unless auth_result&.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
authenticated: true,
|
|
129
|
-
auth_strategy: "custom",
|
|
130
|
-
authenticated_at: Time.now
|
|
131
|
-
)
|
|
132
|
-
else
|
|
133
|
-
# Extract strategy and authenticated_at only if user_data is a Hash
|
|
134
|
-
strategy = user_data.is_a?(Hash) ? user_data[:strategy] : nil
|
|
135
|
-
auth_time = user_data.is_a?(Hash) ? user_data[:authenticated_at] : nil
|
|
136
|
-
|
|
137
|
-
new(
|
|
138
|
-
user: user_data,
|
|
139
|
-
authenticated: true,
|
|
140
|
-
auth_strategy: strategy,
|
|
141
|
-
authenticated_at: auth_time
|
|
142
|
-
)
|
|
143
|
-
end
|
|
120
|
+
return anonymous unless auth_result&.authenticated?
|
|
121
|
+
|
|
122
|
+
new(
|
|
123
|
+
user: auth_result.user,
|
|
124
|
+
authenticated: true,
|
|
125
|
+
auth_strategy: auth_result.strategy,
|
|
126
|
+
authenticated_at: auth_result.authenticated_at
|
|
127
|
+
)
|
|
144
128
|
end
|
|
145
129
|
end
|
|
146
130
|
end
|
|
@@ -16,20 +16,20 @@ module VectorMCP
|
|
|
16
16
|
@handler = handler
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# Authenticate a request using the custom handler
|
|
19
|
+
# Authenticate a request using the custom handler.
|
|
20
|
+
# If the handler returns a Hash with a :user key, the value is extracted
|
|
21
|
+
# so that AuthManager receives the user data directly.
|
|
20
22
|
# @param request [Hash] the request object
|
|
21
|
-
# @return [Object, false]
|
|
23
|
+
# @return [Object, nil, false] user data or false if authentication failed.
|
|
24
|
+
# A return of nil (from { user: nil }) signals "authenticated, no user object."
|
|
22
25
|
def authenticate(request)
|
|
23
26
|
result = @handler.call(request)
|
|
27
|
+
return false unless result && result != false
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
false
|
|
30
|
-
end
|
|
31
|
-
rescue NoMemoryError, StandardError
|
|
32
|
-
# Log error but return false for security
|
|
29
|
+
return result unless result.is_a?(Hash) && result.key?(:user)
|
|
30
|
+
|
|
31
|
+
result[:user]
|
|
32
|
+
rescue StandardError, NoMemoryError
|
|
33
33
|
false
|
|
34
34
|
end
|
|
35
35
|
|
|
@@ -38,33 +38,6 @@ module VectorMCP
|
|
|
38
38
|
def configured?
|
|
39
39
|
!@handler.nil?
|
|
40
40
|
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
# Format successful authentication result with strategy metadata
|
|
45
|
-
# @param result [Object] the result from the custom handler
|
|
46
|
-
# @return [Object] formatted result with strategy metadata
|
|
47
|
-
def format_successful_result(result)
|
|
48
|
-
case result
|
|
49
|
-
when Hash
|
|
50
|
-
# If result has a :user key, extract it and use as main user data
|
|
51
|
-
if result.key?(:user)
|
|
52
|
-
user_data = result[:user]
|
|
53
|
-
# For nil user, return a marker that will become nil in session context
|
|
54
|
-
return :authenticated_nil_user if user_data.nil?
|
|
55
|
-
|
|
56
|
-
user_data
|
|
57
|
-
else
|
|
58
|
-
result.merge(strategy: "custom", authenticated_at: Time.now)
|
|
59
|
-
end
|
|
60
|
-
else
|
|
61
|
-
{
|
|
62
|
-
user: result,
|
|
63
|
-
strategy: "custom",
|
|
64
|
-
authenticated_at: Time.now
|
|
65
|
-
}
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
41
|
end
|
|
69
42
|
end
|
|
70
43
|
end
|
|
@@ -43,16 +43,7 @@ module VectorMCP
|
|
|
43
43
|
|
|
44
44
|
begin
|
|
45
45
|
decoded = JWT.decode(token, @secret, true, @options)
|
|
46
|
-
|
|
47
|
-
headers = decoded[1] # Second element is the headers
|
|
48
|
-
|
|
49
|
-
# Return user info from JWT payload
|
|
50
|
-
{
|
|
51
|
-
**payload,
|
|
52
|
-
strategy: "jwt",
|
|
53
|
-
authenticated_at: Time.now,
|
|
54
|
-
jwt_headers: headers
|
|
55
|
-
}
|
|
46
|
+
decoded[0]
|
|
56
47
|
rescue JWT::ExpiredSignature, JWT::InvalidIssuerError, JWT::InvalidAudienceError,
|
|
57
48
|
JWT::VerificationError, JWT::DecodeError, StandardError
|
|
58
49
|
false # Token validation failed
|
|
@@ -39,22 +39,7 @@ module VectorMCP
|
|
|
39
39
|
# Notifies connected clients that the list of available prompts has changed.
|
|
40
40
|
# @return [void]
|
|
41
41
|
def notify_prompts_list_changed
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
notification_method = "notifications/prompts/list_changed"
|
|
45
|
-
begin
|
|
46
|
-
if transport.respond_to?(:broadcast_notification)
|
|
47
|
-
logger.debug("Broadcasting prompts list changed notification.")
|
|
48
|
-
transport.broadcast_notification(notification_method)
|
|
49
|
-
elsif transport.respond_to?(:send_notification)
|
|
50
|
-
logger.debug("Sending prompts list changed notification (transport may broadcast or send to first client).")
|
|
51
|
-
transport.send_notification(notification_method)
|
|
52
|
-
else
|
|
53
|
-
logger.warn("Transport does not support sending notifications/prompts/list_changed.")
|
|
54
|
-
end
|
|
55
|
-
rescue StandardError => e
|
|
56
|
-
logger.error("Failed to send prompts list changed notification: #{e.class.name}: #{e.message}")
|
|
57
|
-
end
|
|
42
|
+
send_list_changed_notification("prompts") if @prompts_list_changed
|
|
58
43
|
end
|
|
59
44
|
|
|
60
45
|
# Resets the `roots_list_changed` flag to false.
|
|
@@ -66,22 +51,7 @@ module VectorMCP
|
|
|
66
51
|
# Notifies connected clients that the list of available roots has changed.
|
|
67
52
|
# @return [void]
|
|
68
53
|
def notify_roots_list_changed
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
notification_method = "notifications/roots/list_changed"
|
|
72
|
-
begin
|
|
73
|
-
if transport.respond_to?(:broadcast_notification)
|
|
74
|
-
logger.debug("Broadcasting roots list changed notification.")
|
|
75
|
-
transport.broadcast_notification(notification_method)
|
|
76
|
-
elsif transport.respond_to?(:send_notification)
|
|
77
|
-
logger.debug("Sending roots list changed notification (transport may broadcast or send to first client).")
|
|
78
|
-
transport.send_notification(notification_method)
|
|
79
|
-
else
|
|
80
|
-
logger.warn("Transport does not support sending notifications/roots/list_changed.")
|
|
81
|
-
end
|
|
82
|
-
rescue StandardError => e
|
|
83
|
-
logger.error("Failed to send roots list changed notification: #{e.class.name}: #{e.message}")
|
|
84
|
-
end
|
|
54
|
+
send_list_changed_notification("roots") if @roots_list_changed
|
|
85
55
|
end
|
|
86
56
|
|
|
87
57
|
# Registers a session as a subscriber to prompt list changes.
|
|
@@ -93,6 +63,26 @@ module VectorMCP
|
|
|
93
63
|
|
|
94
64
|
private
|
|
95
65
|
|
|
66
|
+
# Sends a `notifications/<kind>/list_changed` notification to the transport.
|
|
67
|
+
# No-op if no transport is attached. Logs a warning if the transport does not
|
|
68
|
+
# implement `send_notification` (intentional extension point for alternate
|
|
69
|
+
# transports).
|
|
70
|
+
# @api private
|
|
71
|
+
# @param kind [String] One of "prompts" or "roots".
|
|
72
|
+
def send_list_changed_notification(kind)
|
|
73
|
+
return unless transport
|
|
74
|
+
|
|
75
|
+
notification_method = "notifications/#{kind}/list_changed"
|
|
76
|
+
if transport.respond_to?(:send_notification)
|
|
77
|
+
logger.debug("Sending #{kind} list changed notification.")
|
|
78
|
+
transport.send_notification(notification_method)
|
|
79
|
+
else
|
|
80
|
+
logger.warn("Transport does not support sending #{notification_method}.")
|
|
81
|
+
end
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
logger.error("Failed to send #{kind} list changed notification: #{e.class.name}: #{e.message}")
|
|
84
|
+
end
|
|
85
|
+
|
|
96
86
|
# Configures sampling capabilities based on provided configuration.
|
|
97
87
|
# @api private
|
|
98
88
|
def configure_sampling_capabilities(config)
|
|
@@ -20,17 +20,18 @@ module VectorMCP
|
|
|
20
20
|
method = message["method"]
|
|
21
21
|
params = message["params"] || {}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
case classify_message(id, method)
|
|
24
|
+
when :request
|
|
24
25
|
logger.debug("[#{session_id}] Request [#{id}]: #{method} with params: #{VectorMCP::LogFilter.filter_hash(params).inspect}")
|
|
25
26
|
handle_request(id, method, params, session)
|
|
26
|
-
|
|
27
|
+
when :notification
|
|
27
28
|
logger.debug("[#{session_id}] Notification: #{method} with params: #{VectorMCP::LogFilter.filter_hash(params).inspect}")
|
|
28
29
|
handle_notification(method, params, session)
|
|
29
|
-
nil
|
|
30
|
-
|
|
30
|
+
nil
|
|
31
|
+
when :invalid_missing_method
|
|
31
32
|
logger.warn("[#{session_id}] Invalid message: Has ID [#{id}] but no method. #{message.inspect}")
|
|
32
33
|
raise VectorMCP::InvalidRequestError.new("Request object must include a 'method' member.", request_id: id)
|
|
33
|
-
|
|
34
|
+
when :invalid_missing_both
|
|
34
35
|
logger.warn("[#{session_id}] Invalid message: Missing both 'id' and 'method'. #{message.inspect}")
|
|
35
36
|
raise VectorMCP::InvalidRequestError.new("Invalid message format", request_id: nil)
|
|
36
37
|
end
|
|
@@ -60,6 +61,16 @@ module VectorMCP
|
|
|
60
61
|
|
|
61
62
|
private
|
|
62
63
|
|
|
64
|
+
# Classify a JSON-RPC message based on presence of id and method fields.
|
|
65
|
+
# @api private
|
|
66
|
+
def classify_message(id, method)
|
|
67
|
+
return :request if id && method
|
|
68
|
+
return :notification if method
|
|
69
|
+
return :invalid_missing_method if id
|
|
70
|
+
|
|
71
|
+
:invalid_missing_both
|
|
72
|
+
end
|
|
73
|
+
|
|
63
74
|
# Internal handler for JSON-RPC requests.
|
|
64
75
|
# @api private
|
|
65
76
|
def handle_request(id, method, params, session)
|
|
@@ -72,11 +83,11 @@ module VectorMCP
|
|
|
72
83
|
end
|
|
73
84
|
|
|
74
85
|
# Validates that the session is properly initialized for the given request.
|
|
86
|
+
# Transports are contractually required to pass a VectorMCP::Session here —
|
|
87
|
+
# never the SessionManager wrapper struct.
|
|
75
88
|
# @api private
|
|
76
89
|
def validate_session_initialization(id, method, _params, session)
|
|
77
|
-
|
|
78
|
-
actual_session = session.respond_to?(:context) ? session.context : session
|
|
79
|
-
return if actual_session.initialized?
|
|
90
|
+
return if session.initialized?
|
|
80
91
|
|
|
81
92
|
# Allow "initialize" even if not marked initialized yet by server
|
|
82
93
|
return if method == "initialize"
|
|
@@ -115,9 +126,7 @@ module VectorMCP
|
|
|
115
126
|
# Internal handler for JSON-RPC notifications.
|
|
116
127
|
# @api private
|
|
117
128
|
def handle_notification(method, params, session)
|
|
118
|
-
|
|
119
|
-
actual_session = session.respond_to?(:context) ? session.context : session
|
|
120
|
-
unless actual_session.initialized? || method == "initialized"
|
|
129
|
+
unless session.initialized? || method == "initialized"
|
|
121
130
|
logger.warn("Ignoring notification '#{method}' before session is initialized. Params: #{params.inspect}")
|
|
122
131
|
return
|
|
123
132
|
end
|
|
@@ -162,9 +171,7 @@ module VectorMCP
|
|
|
162
171
|
# @api private
|
|
163
172
|
def session_method(method_name)
|
|
164
173
|
lambda do |params, session, _server|
|
|
165
|
-
|
|
166
|
-
actual_session = session.respond_to?(:context) ? session.context : session
|
|
167
|
-
actual_session.public_send(method_name, params)
|
|
174
|
+
session.public_send(method_name, params)
|
|
168
175
|
end
|
|
169
176
|
end
|
|
170
177
|
end
|