vector_mcp 0.2.0 → 0.3.1
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 +281 -0
- data/README.md +302 -373
- data/lib/vector_mcp/definitions.rb +3 -1
- data/lib/vector_mcp/errors.rb +24 -0
- data/lib/vector_mcp/handlers/core.rb +132 -6
- data/lib/vector_mcp/logging/component.rb +131 -0
- data/lib/vector_mcp/logging/configuration.rb +156 -0
- data/lib/vector_mcp/logging/constants.rb +21 -0
- data/lib/vector_mcp/logging/core.rb +175 -0
- data/lib/vector_mcp/logging/filters/component.rb +69 -0
- data/lib/vector_mcp/logging/filters/level.rb +23 -0
- data/lib/vector_mcp/logging/formatters/base.rb +52 -0
- data/lib/vector_mcp/logging/formatters/json.rb +83 -0
- data/lib/vector_mcp/logging/formatters/text.rb +72 -0
- data/lib/vector_mcp/logging/outputs/base.rb +64 -0
- data/lib/vector_mcp/logging/outputs/console.rb +35 -0
- data/lib/vector_mcp/logging/outputs/file.rb +157 -0
- data/lib/vector_mcp/logging.rb +71 -0
- data/lib/vector_mcp/security/auth_manager.rb +79 -0
- data/lib/vector_mcp/security/authorization.rb +96 -0
- data/lib/vector_mcp/security/middleware.rb +172 -0
- data/lib/vector_mcp/security/session_context.rb +147 -0
- data/lib/vector_mcp/security/strategies/api_key.rb +167 -0
- data/lib/vector_mcp/security/strategies/custom.rb +71 -0
- data/lib/vector_mcp/security/strategies/jwt_token.rb +118 -0
- data/lib/vector_mcp/security.rb +46 -0
- data/lib/vector_mcp/server/registry.rb +24 -0
- data/lib/vector_mcp/server.rb +141 -1
- data/lib/vector_mcp/transport/sse/client_connection.rb +113 -0
- data/lib/vector_mcp/transport/sse/message_handler.rb +166 -0
- data/lib/vector_mcp/transport/sse/puma_config.rb +77 -0
- data/lib/vector_mcp/transport/sse/stream_manager.rb +92 -0
- data/lib/vector_mcp/transport/sse.rb +119 -460
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +35 -2
- metadata +63 -21
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Security
|
5
|
+
# Represents the security context for a user session
|
6
|
+
# Contains authentication and authorization information
|
7
|
+
class SessionContext
|
8
|
+
attr_reader :user, :authenticated, :permissions, :auth_strategy, :authenticated_at
|
9
|
+
|
10
|
+
# Initialize session context
|
11
|
+
# @param user [Object] the authenticated user object
|
12
|
+
# @param authenticated [Boolean] whether the user is authenticated
|
13
|
+
# @param auth_strategy [String] the authentication strategy used
|
14
|
+
# @param authenticated_at [Time] when authentication occurred
|
15
|
+
def initialize(user: nil, authenticated: false, auth_strategy: nil, authenticated_at: nil)
|
16
|
+
@user = user
|
17
|
+
@authenticated = authenticated
|
18
|
+
@auth_strategy = auth_strategy
|
19
|
+
@authenticated_at = authenticated_at || Time.now
|
20
|
+
@permissions = Set.new
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check if the session is authenticated
|
24
|
+
# @return [Boolean] true if authenticated
|
25
|
+
def authenticated?
|
26
|
+
@authenticated
|
27
|
+
end
|
28
|
+
|
29
|
+
# Check if the user has a specific permission
|
30
|
+
# @param permission [String, Symbol] the permission to check
|
31
|
+
# @return [Boolean] true if user has the permission
|
32
|
+
def can?(permission)
|
33
|
+
@permissions.include?(permission.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Check if the user can perform an action on a resource
|
37
|
+
# @param action [String, Symbol] the action (e.g., 'read', 'write', 'execute')
|
38
|
+
# @param resource [String, Symbol] the resource (e.g., 'tools', 'resources')
|
39
|
+
# @return [Boolean] true if user can perform the action
|
40
|
+
def can_access?(action, resource)
|
41
|
+
can?("#{action}:#{resource}") || can?("#{action}:*") || can?("*:#{resource}") || can?("*:*")
|
42
|
+
end
|
43
|
+
|
44
|
+
# Add a permission to the session
|
45
|
+
# @param permission [String, Symbol] the permission to add
|
46
|
+
def add_permission(permission)
|
47
|
+
@permissions << permission.to_s
|
48
|
+
end
|
49
|
+
|
50
|
+
# Add multiple permissions to the session
|
51
|
+
# @param permissions [Array<String, Symbol>] the permissions to add
|
52
|
+
def add_permissions(permissions)
|
53
|
+
permissions.each { |perm| add_permission(perm) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Remove a permission from the session
|
57
|
+
# @param permission [String, Symbol] the permission to remove
|
58
|
+
def remove_permission(permission)
|
59
|
+
@permissions.delete(permission.to_s)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Clear all permissions
|
63
|
+
def clear_permissions
|
64
|
+
@permissions.clear
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get user identifier for logging/auditing
|
68
|
+
# @return [String] a string identifying the user
|
69
|
+
def user_identifier
|
70
|
+
return "anonymous" unless authenticated?
|
71
|
+
return "anonymous" if @user.nil?
|
72
|
+
|
73
|
+
case @user
|
74
|
+
when Hash
|
75
|
+
@user[:user_id] || @user[:sub] || @user[:email] || @user[:api_key] || "authenticated_user"
|
76
|
+
when String
|
77
|
+
@user
|
78
|
+
else
|
79
|
+
@user.respond_to?(:id) ? @user.id.to_s : "authenticated_user"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get authentication method used
|
84
|
+
# @return [String] the authentication strategy
|
85
|
+
def auth_method
|
86
|
+
@auth_strategy || "none"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Check if authentication is recent (within specified seconds)
|
90
|
+
# @param max_age [Integer] maximum age in seconds (default: 3600 = 1 hour)
|
91
|
+
# @return [Boolean] true if authentication is recent
|
92
|
+
def auth_recent?(max_age: 3600)
|
93
|
+
return false unless authenticated?
|
94
|
+
|
95
|
+
(Time.now - @authenticated_at) <= max_age
|
96
|
+
end
|
97
|
+
|
98
|
+
# Convert to hash for serialization
|
99
|
+
# @return [Hash] session context as hash
|
100
|
+
def to_h
|
101
|
+
{
|
102
|
+
authenticated: @authenticated,
|
103
|
+
user_identifier: user_identifier,
|
104
|
+
auth_strategy: @auth_strategy,
|
105
|
+
authenticated_at: @authenticated_at&.iso8601,
|
106
|
+
permissions: @permissions.to_a
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
# Create an anonymous (unauthenticated) session context
|
111
|
+
# @return [SessionContext] an unauthenticated session
|
112
|
+
def self.anonymous
|
113
|
+
new(authenticated: false)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Create an authenticated session context from auth result
|
117
|
+
# @param auth_result [Hash] the authentication result
|
118
|
+
# @return [SessionContext] an authenticated session
|
119
|
+
def self.from_auth_result(auth_result)
|
120
|
+
return anonymous unless auth_result&.dig(:authenticated)
|
121
|
+
|
122
|
+
user_data = auth_result[:user]
|
123
|
+
|
124
|
+
# Handle special marker for authenticated nil user
|
125
|
+
if user_data == :authenticated_nil_user
|
126
|
+
new(
|
127
|
+
user: nil,
|
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
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Security
|
5
|
+
module Strategies
|
6
|
+
# API Key authentication strategy
|
7
|
+
# Supports multiple key formats and storage methods
|
8
|
+
class ApiKey
|
9
|
+
attr_reader :valid_keys
|
10
|
+
|
11
|
+
# Initialize with a list of valid API keys
|
12
|
+
# @param keys [Array<String>] array of valid API keys
|
13
|
+
def initialize(keys: [])
|
14
|
+
@valid_keys = Set.new(keys.map(&:to_s))
|
15
|
+
end
|
16
|
+
|
17
|
+
# Add a valid API key
|
18
|
+
# @param key [String] the API key to add
|
19
|
+
def add_key(key)
|
20
|
+
@valid_keys << key.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
# Remove an API key
|
24
|
+
# @param key [String] the API key to remove
|
25
|
+
def remove_key(key)
|
26
|
+
@valid_keys.delete(key.to_s)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Authenticate a request using API key
|
30
|
+
# @param request [Hash] the request object
|
31
|
+
# @return [Hash, false] user info hash or false if authentication failed
|
32
|
+
def authenticate(request)
|
33
|
+
api_key = extract_api_key(request)
|
34
|
+
return false unless api_key&.length&.positive?
|
35
|
+
|
36
|
+
if @valid_keys.include?(api_key)
|
37
|
+
{
|
38
|
+
api_key: api_key,
|
39
|
+
strategy: "api_key",
|
40
|
+
authenticated_at: Time.now
|
41
|
+
}
|
42
|
+
else
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Check if any keys are configured
|
48
|
+
# @return [Boolean] true if keys are available
|
49
|
+
def configured?
|
50
|
+
!@valid_keys.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get count of configured keys (for debugging)
|
54
|
+
# @return [Integer] number of configured keys
|
55
|
+
def key_count
|
56
|
+
@valid_keys.size
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Extract API key from various request formats
|
62
|
+
# @param request [Hash] the request object
|
63
|
+
# @return [String, nil] the extracted API key
|
64
|
+
def extract_api_key(request)
|
65
|
+
headers = normalize_headers(request)
|
66
|
+
params = normalize_params(request)
|
67
|
+
|
68
|
+
extract_from_headers(headers) || extract_from_params(params)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Normalize headers to handle different formats
|
72
|
+
# @param request [Hash] the request object
|
73
|
+
# @return [Hash] normalized headers
|
74
|
+
def normalize_headers(request)
|
75
|
+
# Check if it's a Rack environment (has REQUEST_METHOD)
|
76
|
+
if request["REQUEST_METHOD"]
|
77
|
+
extract_headers_from_rack_env(request)
|
78
|
+
else
|
79
|
+
request[:headers] || request["headers"] || {}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Normalize params to handle different formats
|
84
|
+
# @param request [Hash] the request object
|
85
|
+
# @return [Hash] normalized params
|
86
|
+
def normalize_params(request)
|
87
|
+
# Check if it's a Rack environment (has REQUEST_METHOD)
|
88
|
+
if request["REQUEST_METHOD"]
|
89
|
+
extract_params_from_rack_env(request)
|
90
|
+
else
|
91
|
+
request[:params] || request["params"] || {}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Extract headers from Rack environment
|
96
|
+
# @param env [Hash] the Rack environment
|
97
|
+
# @return [Hash] normalized headers
|
98
|
+
def extract_headers_from_rack_env(env)
|
99
|
+
headers = {}
|
100
|
+
env.each do |key, value|
|
101
|
+
next unless key.start_with?("HTTP_")
|
102
|
+
|
103
|
+
# Convert HTTP_X_API_KEY to X-API-Key format
|
104
|
+
header_name = key[5..].split("_").map do |part|
|
105
|
+
case part.upcase
|
106
|
+
when "API" then "API" # Keep API in all caps
|
107
|
+
else part.capitalize
|
108
|
+
end
|
109
|
+
end.join("-")
|
110
|
+
headers[header_name] = value
|
111
|
+
end
|
112
|
+
|
113
|
+
# Add special headers
|
114
|
+
headers["Authorization"] = env["HTTP_AUTHORIZATION"] if env["HTTP_AUTHORIZATION"]
|
115
|
+
headers["Content-Type"] = env["CONTENT_TYPE"] if env["CONTENT_TYPE"]
|
116
|
+
headers
|
117
|
+
end
|
118
|
+
|
119
|
+
# Extract params from Rack environment
|
120
|
+
# @param env [Hash] the Rack environment
|
121
|
+
# @return [Hash] normalized params
|
122
|
+
def extract_params_from_rack_env(env)
|
123
|
+
params = {}
|
124
|
+
if env["QUERY_STRING"]
|
125
|
+
require "uri"
|
126
|
+
params = URI.decode_www_form(env["QUERY_STRING"]).to_h
|
127
|
+
end
|
128
|
+
params
|
129
|
+
end
|
130
|
+
|
131
|
+
# Extract API key from headers
|
132
|
+
# @param headers [Hash] request headers
|
133
|
+
# @return [String, nil] the API key if found
|
134
|
+
def extract_from_headers(headers)
|
135
|
+
# 1. X-API-Key header (most common)
|
136
|
+
api_key = headers["X-API-Key"] || headers["x-api-key"]
|
137
|
+
return api_key if api_key
|
138
|
+
|
139
|
+
# 2. Authorization header
|
140
|
+
extract_from_auth_header(headers["Authorization"] || headers["authorization"])
|
141
|
+
end
|
142
|
+
|
143
|
+
# Extract API key from Authorization header
|
144
|
+
# @param auth_header [String, nil] the authorization header value
|
145
|
+
# @return [String, nil] the API key if found
|
146
|
+
def extract_from_auth_header(auth_header)
|
147
|
+
return nil unless auth_header
|
148
|
+
|
149
|
+
# Bearer token format
|
150
|
+
return auth_header[7..] if auth_header.start_with?("Bearer ")
|
151
|
+
|
152
|
+
# API-Key scheme format
|
153
|
+
return auth_header[8..] if auth_header.start_with?("API-Key ")
|
154
|
+
|
155
|
+
nil
|
156
|
+
end
|
157
|
+
|
158
|
+
# Extract API key from query parameters
|
159
|
+
# @param params [Hash] request parameters
|
160
|
+
# @return [String, nil] the API key if found
|
161
|
+
def extract_from_params(params)
|
162
|
+
params["api_key"] || params["apikey"]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Security
|
5
|
+
module Strategies
|
6
|
+
# Custom authentication strategy
|
7
|
+
# Allows developers to implement their own authentication logic
|
8
|
+
class Custom
|
9
|
+
attr_reader :handler
|
10
|
+
|
11
|
+
# Initialize with a custom authentication handler
|
12
|
+
# @param handler [Proc] a block that takes a request and returns user info or false
|
13
|
+
def initialize(&handler)
|
14
|
+
raise ArgumentError, "Custom authentication strategy requires a block" unless handler
|
15
|
+
|
16
|
+
@handler = handler
|
17
|
+
end
|
18
|
+
|
19
|
+
# Authenticate a request using the custom handler
|
20
|
+
# @param request [Hash] the request object
|
21
|
+
# @return [Object, false] result from custom handler or false if authentication failed
|
22
|
+
def authenticate(request)
|
23
|
+
result = @handler.call(request)
|
24
|
+
|
25
|
+
# Ensure result includes strategy info if it's successful
|
26
|
+
if result && result != false
|
27
|
+
format_successful_result(result)
|
28
|
+
else
|
29
|
+
false
|
30
|
+
end
|
31
|
+
rescue NoMemoryError, StandardError
|
32
|
+
# Log error but return false for security
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
# Check if handler is configured
|
37
|
+
# @return [Boolean] true if handler is present
|
38
|
+
def configured?
|
39
|
+
!@handler.nil?
|
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
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "jwt"
|
5
|
+
rescue LoadError
|
6
|
+
# JWT gem is optional - will raise error when trying to use JWT strategy
|
7
|
+
end
|
8
|
+
|
9
|
+
module VectorMCP
|
10
|
+
module Security
|
11
|
+
module Strategies
|
12
|
+
# JWT Token authentication strategy
|
13
|
+
# Provides stateless authentication using JSON Web Tokens
|
14
|
+
class JwtToken
|
15
|
+
attr_reader :secret, :algorithm, :options
|
16
|
+
|
17
|
+
# Initialize JWT strategy
|
18
|
+
# @param secret [String] the secret key for JWT verification
|
19
|
+
# @param algorithm [String] the JWT algorithm (default: HS256)
|
20
|
+
# @param options [Hash] additional JWT verification options
|
21
|
+
def initialize(secret:, algorithm: "HS256", **options)
|
22
|
+
raise LoadError, "JWT gem is required for JWT authentication strategy" unless defined?(JWT)
|
23
|
+
|
24
|
+
@secret = secret
|
25
|
+
@algorithm = algorithm
|
26
|
+
@options = {
|
27
|
+
algorithm: @algorithm,
|
28
|
+
verify_expiration: true,
|
29
|
+
verify_iat: true,
|
30
|
+
verify_iss: false,
|
31
|
+
verify_aud: false
|
32
|
+
}.merge(options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Authenticate a request using JWT token
|
36
|
+
# @param request [Hash] the request object
|
37
|
+
# @return [Hash, false] decoded JWT payload or false if authentication failed
|
38
|
+
def authenticate(request)
|
39
|
+
token = extract_token(request)
|
40
|
+
return false unless token
|
41
|
+
|
42
|
+
begin
|
43
|
+
decoded = JWT.decode(token, @secret, true, @options)
|
44
|
+
payload = decoded[0] # First element is the payload
|
45
|
+
headers = decoded[1] # Second element is the headers
|
46
|
+
|
47
|
+
# Return user info from JWT payload
|
48
|
+
{
|
49
|
+
**payload,
|
50
|
+
strategy: "jwt",
|
51
|
+
authenticated_at: Time.now,
|
52
|
+
jwt_headers: headers
|
53
|
+
}
|
54
|
+
rescue JWT::ExpiredSignature, JWT::InvalidIssuerError,
|
55
|
+
JWT::DecodeError, StandardError
|
56
|
+
false # Token validation failed
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generate a JWT token (utility method for testing/development)
|
61
|
+
# @param payload [Hash] the payload to encode
|
62
|
+
# @param expires_in [Integer] expiration time in seconds from now
|
63
|
+
# @return [String] the generated JWT token
|
64
|
+
def generate_token(payload, expires_in: 3600)
|
65
|
+
exp_payload = payload.merge(
|
66
|
+
exp: Time.now.to_i + expires_in,
|
67
|
+
iat: Time.now.to_i
|
68
|
+
)
|
69
|
+
JWT.encode(exp_payload, @secret, @algorithm)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check if JWT gem is available
|
73
|
+
# @return [Boolean] true if JWT gem is loaded
|
74
|
+
def self.available?
|
75
|
+
defined?(JWT)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
# Extract JWT token from request
|
81
|
+
# @param request [Hash] the request object
|
82
|
+
# @return [String, nil] the extracted token
|
83
|
+
def extract_token(request)
|
84
|
+
headers = request[:headers] || request["headers"] || {}
|
85
|
+
params = request[:params] || request["params"] || {}
|
86
|
+
|
87
|
+
extract_from_auth_header(headers) ||
|
88
|
+
extract_from_jwt_header(headers) ||
|
89
|
+
extract_from_params(params)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Extract token from Authorization header
|
93
|
+
# @param headers [Hash] request headers
|
94
|
+
# @return [String, nil] the token if found
|
95
|
+
def extract_from_auth_header(headers)
|
96
|
+
auth_header = headers["Authorization"] || headers["authorization"]
|
97
|
+
return nil unless auth_header&.start_with?("Bearer ")
|
98
|
+
|
99
|
+
auth_header[7..] # Remove 'Bearer ' prefix
|
100
|
+
end
|
101
|
+
|
102
|
+
# Extract token from custom JWT header
|
103
|
+
# @param headers [Hash] request headers
|
104
|
+
# @return [String, nil] the token if found
|
105
|
+
def extract_from_jwt_header(headers)
|
106
|
+
headers["X-JWT-Token"] || headers["x-jwt-token"]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Extract token from query parameters
|
110
|
+
# @param params [Hash] request parameters
|
111
|
+
# @return [String, nil] the token if found
|
112
|
+
def extract_from_params(params)
|
113
|
+
params["jwt_token"] || params["token"]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Security namespace for VectorMCP
|
4
|
+
# Contains authentication, authorization, and security middleware components
|
5
|
+
|
6
|
+
require_relative "security/auth_manager"
|
7
|
+
require_relative "security/authorization"
|
8
|
+
require_relative "security/middleware"
|
9
|
+
require_relative "security/session_context"
|
10
|
+
require_relative "security/strategies/api_key"
|
11
|
+
require_relative "security/strategies/jwt_token"
|
12
|
+
require_relative "security/strategies/custom"
|
13
|
+
|
14
|
+
module VectorMCP
|
15
|
+
# Security components for VectorMCP servers
|
16
|
+
# Provides opt-in authentication and authorization
|
17
|
+
module Security
|
18
|
+
# Get default authentication manager
|
19
|
+
# @return [AuthManager] a new authentication manager instance
|
20
|
+
def self.auth_manager
|
21
|
+
AuthManager.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# Get default authorization manager
|
25
|
+
# @return [Authorization] a new authorization manager instance
|
26
|
+
def self.authorization
|
27
|
+
Authorization.new
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create security middleware with default components
|
31
|
+
# @param auth_manager [AuthManager] optional custom auth manager
|
32
|
+
# @param authorization [Authorization] optional custom authorization
|
33
|
+
# @return [Middleware] configured security middleware
|
34
|
+
def self.middleware(auth_manager: nil, authorization: nil)
|
35
|
+
auth_manager ||= self.auth_manager
|
36
|
+
authorization ||= self.authorization
|
37
|
+
Middleware.new(auth_manager, authorization)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Check if JWT support is available
|
41
|
+
# @return [Boolean] true if JWT gem is loaded
|
42
|
+
def self.jwt_available?
|
43
|
+
Strategies::JwtToken.available?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "json-schema"
|
4
|
+
|
3
5
|
module VectorMCP
|
4
6
|
class Server
|
5
7
|
# Handles registration of tools, resources, prompts, and roots
|
@@ -19,6 +21,9 @@ module VectorMCP
|
|
19
21
|
name_s = name.to_s
|
20
22
|
raise ArgumentError, "Tool '#{name_s}' already registered" if @tools[name_s]
|
21
23
|
|
24
|
+
# Validate schema format during registration
|
25
|
+
validate_schema_format!(input_schema) if input_schema
|
26
|
+
|
22
27
|
@tools[name_s] = VectorMCP::Definitions::Tool.new(name_s, description, input_schema, handler)
|
23
28
|
logger.debug("Registered tool: #{name_s}")
|
24
29
|
self
|
@@ -206,6 +211,25 @@ module VectorMCP
|
|
206
211
|
|
207
212
|
private
|
208
213
|
|
214
|
+
# Validates that the provided schema is a valid JSON Schema.
|
215
|
+
# @api private
|
216
|
+
# @param schema [Hash, nil] The JSON Schema to validate.
|
217
|
+
# @return [void]
|
218
|
+
# @raise [ArgumentError] if the schema is invalid.
|
219
|
+
def validate_schema_format!(schema)
|
220
|
+
return if schema.nil? || schema.empty?
|
221
|
+
return unless schema.is_a?(Hash)
|
222
|
+
|
223
|
+
# Use JSON::Validator to validate the schema format itself
|
224
|
+
validation_errors = JSON::Validator.fully_validate_schema(schema)
|
225
|
+
|
226
|
+
raise ArgumentError, "Invalid input_schema format: #{validation_errors.join("; ")}" unless validation_errors.empty?
|
227
|
+
rescue JSON::Schema::ValidationError => e
|
228
|
+
raise ArgumentError, "Invalid input_schema format: #{e.message}"
|
229
|
+
rescue JSON::Schema::SchemaError => e
|
230
|
+
raise ArgumentError, "Invalid input_schema structure: #{e.message}"
|
231
|
+
end
|
232
|
+
|
209
233
|
# Validates the structure of the `arguments` array provided to {#register_prompt}.
|
210
234
|
# @api private
|
211
235
|
def validate_prompt_arguments(argument_defs)
|