vector_mcp 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +122 -0
- data/lib/vector_mcp/definitions.rb +25 -9
- data/lib/vector_mcp/errors.rb +2 -3
- data/lib/vector_mcp/handlers/core.rb +206 -50
- data/lib/vector_mcp/logger.rb +148 -0
- data/lib/vector_mcp/middleware/base.rb +171 -0
- data/lib/vector_mcp/middleware/context.rb +76 -0
- data/lib/vector_mcp/middleware/hook.rb +169 -0
- data/lib/vector_mcp/middleware/manager.rb +179 -0
- data/lib/vector_mcp/middleware.rb +43 -0
- 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/security/strategies/jwt_token.rb +6 -3
- 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 +74 -20
- data/lib/vector_mcp/session.rb +131 -8
- 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
- data/lib/vector_mcp.rb +10 -35
- metadata +25 -24
- data/lib/vector_mcp/logging/component.rb +0 -131
- data/lib/vector_mcp/logging/configuration.rb +0 -156
- data/lib/vector_mcp/logging/constants.rb +0 -21
- data/lib/vector_mcp/logging/core.rb +0 -175
- data/lib/vector_mcp/logging/filters/component.rb +0 -69
- data/lib/vector_mcp/logging/filters/level.rb +0 -23
- data/lib/vector_mcp/logging/formatters/base.rb +0 -52
- data/lib/vector_mcp/logging/formatters/json.rb +0 -83
- data/lib/vector_mcp/logging/formatters/text.rb +0 -72
- data/lib/vector_mcp/logging/outputs/base.rb +0 -64
- data/lib/vector_mcp/logging/outputs/console.rb +0 -35
- data/lib/vector_mcp/logging/outputs/file.rb +0 -157
- data/lib/vector_mcp/logging.rb +0 -71
@@ -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
|
@@ -51,8 +51,8 @@ module VectorMCP
|
|
51
51
|
authenticated_at: Time.now,
|
52
52
|
jwt_headers: headers
|
53
53
|
}
|
54
|
-
rescue JWT::ExpiredSignature, JWT::InvalidIssuerError,
|
55
|
-
JWT::DecodeError, StandardError
|
54
|
+
rescue JWT::ExpiredSignature, JWT::InvalidIssuerError, JWT::InvalidAudienceError,
|
55
|
+
JWT::VerificationError, JWT::DecodeError, StandardError
|
56
56
|
false # Token validation failed
|
57
57
|
end
|
58
58
|
end
|
@@ -96,7 +96,10 @@ module VectorMCP
|
|
96
96
|
auth_header = headers["Authorization"] || headers["authorization"]
|
97
97
|
return nil unless auth_header&.start_with?("Bearer ")
|
98
98
|
|
99
|
-
auth_header[7..] # Remove 'Bearer ' prefix
|
99
|
+
token = auth_header[7..] # Remove 'Bearer ' prefix
|
100
|
+
return nil if token.nil? || token.strip.empty?
|
101
|
+
|
102
|
+
token.strip
|
100
103
|
end
|
101
104
|
|
102
105
|
# Extract token from custom JWT header
|
@@ -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
|
data/lib/vector_mcp/server.rb
CHANGED
@@ -19,6 +19,7 @@ require_relative "security/session_context"
|
|
19
19
|
require_relative "security/strategies/api_key"
|
20
20
|
require_relative "security/strategies/jwt_token"
|
21
21
|
require_relative "security/strategies/custom"
|
22
|
+
require_relative "middleware"
|
22
23
|
|
23
24
|
module VectorMCP
|
24
25
|
# The `Server` class is the central component for an MCP server implementation.
|
@@ -70,7 +71,7 @@ module VectorMCP
|
|
70
71
|
PROTOCOL_VERSION = "2024-11-05"
|
71
72
|
|
72
73
|
attr_reader :logger, :name, :version, :protocol_version, :tools, :resources, :prompts, :roots, :in_flight_requests,
|
73
|
-
:auth_manager, :authorization, :security_middleware
|
74
|
+
:auth_manager, :authorization, :security_middleware, :middleware_manager
|
74
75
|
attr_accessor :transport
|
75
76
|
|
76
77
|
# Initializes a new VectorMCP server.
|
@@ -98,8 +99,8 @@ module VectorMCP
|
|
98
99
|
@name = name_pos || name || "UnnamedServer"
|
99
100
|
@version = version
|
100
101
|
@protocol_version = options[:protocol_version] || PROTOCOL_VERSION
|
101
|
-
@logger = VectorMCP.
|
102
|
-
|
102
|
+
@logger = VectorMCP.logger_for("server")
|
103
|
+
# NOTE: log level should be configured via VectorMCP.configure_logging instead
|
103
104
|
|
104
105
|
@transport = nil
|
105
106
|
@tools = {}
|
@@ -121,6 +122,9 @@ module VectorMCP
|
|
121
122
|
@authorization = Security::Authorization.new
|
122
123
|
@security_middleware = Security::Middleware.new(@auth_manager, @authorization)
|
123
124
|
|
125
|
+
# Initialize middleware manager
|
126
|
+
@middleware_manager = Middleware::Manager.new
|
127
|
+
|
124
128
|
setup_default_handlers
|
125
129
|
|
126
130
|
@logger.info("Server instance '#{@name}' v#{@version} (MCP Protocol: #{@protocol_version}, Gem: v#{VectorMCP::VERSION}) initialized.")
|
@@ -130,11 +134,12 @@ module VectorMCP
|
|
130
134
|
|
131
135
|
# Runs the server using the specified transport mechanism.
|
132
136
|
#
|
133
|
-
# @param transport [:stdio, :sse, VectorMCP::Transport::Base] The transport to use.
|
134
|
-
# Can be a symbol (`:stdio`, `:sse`) or an initialized transport instance.
|
137
|
+
# @param transport [:stdio, :sse, :http_stream, VectorMCP::Transport::Base] The transport to use.
|
138
|
+
# Can be a symbol (`:stdio`, `:sse`, `:http_stream`) or an initialized transport instance.
|
135
139
|
# If a symbol is provided, the method will instantiate the corresponding transport class.
|
136
|
-
# If `:sse` is chosen,
|
137
|
-
#
|
140
|
+
# If `:sse` is chosen, it uses Puma as the HTTP server (deprecated).
|
141
|
+
# If `:http_stream` is chosen, it uses the MCP-compliant streamable HTTP transport.
|
142
|
+
# @param options [Hash] Transport-specific options (e.g., `:host`, `:port` for HTTP transports).
|
138
143
|
# These are passed to the transport's constructor if a symbol is provided for `transport`.
|
139
144
|
# @return [void]
|
140
145
|
# @raise [ArgumentError] if an unsupported transport symbol is given.
|
@@ -146,11 +151,20 @@ module VectorMCP
|
|
146
151
|
when :sse
|
147
152
|
begin
|
148
153
|
require_relative "transport/sse"
|
154
|
+
logger.warn("SSE transport is deprecated. Please use :http_stream instead.")
|
149
155
|
VectorMCP::Transport::SSE.new(self, **options)
|
150
156
|
rescue LoadError => e
|
151
|
-
logger.fatal("SSE transport requires additional dependencies.
|
157
|
+
logger.fatal("SSE transport requires additional dependencies.")
|
152
158
|
raise NotImplementedError, "SSE transport dependencies not available: #{e.message}"
|
153
159
|
end
|
160
|
+
when :http_stream
|
161
|
+
begin
|
162
|
+
require_relative "transport/http_stream"
|
163
|
+
VectorMCP::Transport::HttpStream.new(self, **options)
|
164
|
+
rescue LoadError => e
|
165
|
+
logger.fatal("HttpStream transport requires additional dependencies.")
|
166
|
+
raise NotImplementedError, "HttpStream transport dependencies not available: #{e.message}"
|
167
|
+
end
|
154
168
|
when VectorMCP::Transport::Base # Allow passing an initialized transport instance
|
155
169
|
transport.server = self if transport.respond_to?(:server=) && transport.server.nil? # Ensure server is set
|
156
170
|
transport
|
@@ -202,9 +216,9 @@ module VectorMCP
|
|
202
216
|
# Enable authorization with optional policy configuration block
|
203
217
|
# @param block [Proc] optional block for configuring authorization policies
|
204
218
|
# @return [void]
|
205
|
-
def enable_authorization!(&)
|
219
|
+
def enable_authorization!(&block)
|
206
220
|
@authorization.enable!
|
207
|
-
instance_eval(&) if block_given?
|
221
|
+
instance_eval(&block) if block_given?
|
208
222
|
@logger.info("Authorization enabled")
|
209
223
|
end
|
210
224
|
|
@@ -218,29 +232,29 @@ module VectorMCP
|
|
218
232
|
# Add authorization policy for tools
|
219
233
|
# @param block [Proc] policy block that receives (user, action, tool)
|
220
234
|
# @return [void]
|
221
|
-
def authorize_tools(&)
|
222
|
-
@authorization.add_policy(:tool, &)
|
235
|
+
def authorize_tools(&block)
|
236
|
+
@authorization.add_policy(:tool, &block)
|
223
237
|
end
|
224
238
|
|
225
239
|
# Add authorization policy for resources
|
226
240
|
# @param block [Proc] policy block that receives (user, action, resource)
|
227
241
|
# @return [void]
|
228
|
-
def authorize_resources(&)
|
229
|
-
@authorization.add_policy(:resource, &)
|
242
|
+
def authorize_resources(&block)
|
243
|
+
@authorization.add_policy(:resource, &block)
|
230
244
|
end
|
231
245
|
|
232
246
|
# Add authorization policy for prompts
|
233
247
|
# @param block [Proc] policy block that receives (user, action, prompt)
|
234
248
|
# @return [void]
|
235
|
-
def authorize_prompts(&)
|
236
|
-
@authorization.add_policy(:prompt, &)
|
249
|
+
def authorize_prompts(&block)
|
250
|
+
@authorization.add_policy(:prompt, &block)
|
237
251
|
end
|
238
252
|
|
239
253
|
# Add authorization policy for roots
|
240
254
|
# @param block [Proc] policy block that receives (user, action, root)
|
241
255
|
# @return [void]
|
242
|
-
def authorize_roots(&)
|
243
|
-
@authorization.add_policy(:root, &)
|
256
|
+
def authorize_roots(&block)
|
257
|
+
@authorization.add_policy(:root, &block)
|
244
258
|
end
|
245
259
|
|
246
260
|
# Check if security features are enabled
|
@@ -255,6 +269,46 @@ module VectorMCP
|
|
255
269
|
@security_middleware.security_status
|
256
270
|
end
|
257
271
|
|
272
|
+
# --- Middleware Management ---
|
273
|
+
|
274
|
+
# Register middleware for specific hook types
|
275
|
+
# @param middleware_class [Class] Middleware class inheriting from VectorMCP::Middleware::Base
|
276
|
+
# @param hooks [Symbol, Array<Symbol>] Hook types to register for (e.g., :before_tool_call, [:before_tool_call, :after_tool_call])
|
277
|
+
# @param priority [Integer] Execution priority (lower numbers execute first, default: 100)
|
278
|
+
# @param conditions [Hash] Conditions for when middleware should run
|
279
|
+
# @option conditions [Array<String>] :only_operations Only run for these operations
|
280
|
+
# @option conditions [Array<String>] :except_operations Don't run for these operations
|
281
|
+
# @option conditions [Array<String>] :only_users Only run for these user IDs
|
282
|
+
# @option conditions [Array<String>] :except_users Don't run for these user IDs
|
283
|
+
# @option conditions [Boolean] :critical If true, errors in this middleware stop execution
|
284
|
+
# @example
|
285
|
+
# server.use_middleware(MyMiddleware, :before_tool_call)
|
286
|
+
# server.use_middleware(AuthMiddleware, [:before_request, :after_response], priority: 10)
|
287
|
+
# server.use_middleware(LoggingMiddleware, :after_tool_call, conditions: { only_operations: ['important_tool'] })
|
288
|
+
def use_middleware(middleware_class, hooks, priority: Middleware::Hook::DEFAULT_PRIORITY, conditions: {})
|
289
|
+
@middleware_manager.register(middleware_class, hooks, priority: priority, conditions: conditions)
|
290
|
+
@logger.debug("Registered middleware: #{middleware_class.name}")
|
291
|
+
end
|
292
|
+
|
293
|
+
# Remove all middleware hooks for a specific class
|
294
|
+
# @param middleware_class [Class] Middleware class to remove
|
295
|
+
def remove_middleware(middleware_class)
|
296
|
+
@middleware_manager.unregister(middleware_class)
|
297
|
+
@logger.debug("Removed middleware: #{middleware_class.name}")
|
298
|
+
end
|
299
|
+
|
300
|
+
# Get middleware statistics
|
301
|
+
# @return [Hash] Statistics about registered middleware
|
302
|
+
def middleware_stats
|
303
|
+
@middleware_manager.stats
|
304
|
+
end
|
305
|
+
|
306
|
+
# Clear all middleware (useful for testing)
|
307
|
+
def clear_middleware!
|
308
|
+
@middleware_manager.clear!
|
309
|
+
@logger.debug("Cleared all middleware")
|
310
|
+
end
|
311
|
+
|
258
312
|
private
|
259
313
|
|
260
314
|
# Add API key authentication strategy
|
@@ -276,8 +330,8 @@ module VectorMCP
|
|
276
330
|
# Add custom authentication strategy
|
277
331
|
# @param handler [Proc] custom authentication handler block
|
278
332
|
# @return [void]
|
279
|
-
def add_custom_auth(&)
|
280
|
-
strategy = Security::Strategies::Custom.new(&)
|
333
|
+
def add_custom_auth(&block)
|
334
|
+
strategy = Security::Strategies::Custom.new(&block)
|
281
335
|
@auth_manager.add_strategy(:custom, strategy)
|
282
336
|
end
|
283
337
|
|