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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +122 -0
  3. data/lib/vector_mcp/definitions.rb +25 -9
  4. data/lib/vector_mcp/errors.rb +2 -3
  5. data/lib/vector_mcp/handlers/core.rb +206 -50
  6. data/lib/vector_mcp/logger.rb +148 -0
  7. data/lib/vector_mcp/middleware/base.rb +171 -0
  8. data/lib/vector_mcp/middleware/context.rb +76 -0
  9. data/lib/vector_mcp/middleware/hook.rb +169 -0
  10. data/lib/vector_mcp/middleware/manager.rb +179 -0
  11. data/lib/vector_mcp/middleware.rb +43 -0
  12. data/lib/vector_mcp/request_context.rb +182 -0
  13. data/lib/vector_mcp/sampling/result.rb +11 -1
  14. data/lib/vector_mcp/security/middleware.rb +2 -28
  15. data/lib/vector_mcp/security/strategies/api_key.rb +2 -24
  16. data/lib/vector_mcp/security/strategies/jwt_token.rb +6 -3
  17. data/lib/vector_mcp/server/capabilities.rb +5 -7
  18. data/lib/vector_mcp/server/message_handling.rb +11 -5
  19. data/lib/vector_mcp/server.rb +74 -20
  20. data/lib/vector_mcp/session.rb +131 -8
  21. data/lib/vector_mcp/transport/base_session_manager.rb +320 -0
  22. data/lib/vector_mcp/transport/http_stream/event_store.rb +151 -0
  23. data/lib/vector_mcp/transport/http_stream/session_manager.rb +189 -0
  24. data/lib/vector_mcp/transport/http_stream/stream_handler.rb +269 -0
  25. data/lib/vector_mcp/transport/http_stream.rb +779 -0
  26. data/lib/vector_mcp/transport/sse.rb +74 -19
  27. data/lib/vector_mcp/transport/sse_session_manager.rb +188 -0
  28. data/lib/vector_mcp/transport/stdio.rb +70 -13
  29. data/lib/vector_mcp/transport/stdio_session_manager.rb +181 -0
  30. data/lib/vector_mcp/util.rb +39 -1
  31. data/lib/vector_mcp/version.rb +1 -1
  32. data/lib/vector_mcp.rb +10 -35
  33. metadata +25 -24
  34. data/lib/vector_mcp/logging/component.rb +0 -131
  35. data/lib/vector_mcp/logging/configuration.rb +0 -156
  36. data/lib/vector_mcp/logging/constants.rb +0 -21
  37. data/lib/vector_mcp/logging/core.rb +0 -175
  38. data/lib/vector_mcp/logging/filters/component.rb +0 -69
  39. data/lib/vector_mcp/logging/filters/level.rb +0 -23
  40. data/lib/vector_mcp/logging/formatters/base.rb +0 -52
  41. data/lib/vector_mcp/logging/formatters/json.rb +0 -83
  42. data/lib/vector_mcp/logging/formatters/text.rb +0 -72
  43. data/lib/vector_mcp/logging/outputs/base.rb +0 -64
  44. data/lib/vector_mcp/logging/outputs/console.rb +0 -35
  45. data/lib/vector_mcp/logging/outputs/file.rb +0 -157
  46. 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
- @content = (@raw_result[:content] || {}).transform_keys(&:to_sym)
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: headers,
164
- params: 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
- 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
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
- params = {}
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.info("Broadcasting prompts list changed notification.")
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.info("Sending prompts list changed notification (transport may broadcast or send to first client).")
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.info("Broadcasting roots list changed notification.")
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.info("Sending roots list changed notification (transport may broadcast or send to first client).")
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
- logger.debug("Session subscribed to prompt list changes: #{session.object_id}")
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.info("[#{session_id}] Request [#{id}]: #{method} with params: #{params.inspect}")
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.info("[#{session_id}] Notification: #{method} with params: #{params.inspect}")
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
- return if session.initialized?
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
- unless session.initialized? || method == "initialized"
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
- session.public_send(method_name, params)
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
@@ -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.logger
102
- @logger.level = options[:log_level] if options[:log_level]
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, ensure `async` and `falcon` gems are available.
137
- # @param options [Hash] Transport-specific options (e.g., `:host`, `:port` for SSE).
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. Install the 'async' and 'falcon' gems.")
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