vector_mcp 0.3.1 → 0.3.2

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.
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VectorMCP
4
+ module Middleware
5
+ # Base class for all middleware implementations
6
+ # Provides common functionality and hook method templates
7
+ class Base
8
+ # Initialize middleware with optional configuration
9
+ # @param config [Hash] Configuration options
10
+ def initialize(config = {})
11
+ @config = config
12
+ @logger = VectorMCP.logger_for("middleware.#{self.class.name.split("::").last.downcase}")
13
+ end
14
+
15
+ # Generic hook dispatcher - override specific hook methods instead
16
+ # @param hook_type [String] Type of hook being executed
17
+ # @param context [VectorMCP::Middleware::Context] Execution context
18
+ def call(hook_type, context)
19
+ @logger.debug("Executing middleware hook") do
20
+ {
21
+ middleware: self.class.name,
22
+ hook_type: hook_type,
23
+ operation: context.operation_name
24
+ }
25
+ end
26
+ end
27
+
28
+ # Tool operation hooks
29
+
30
+ # Called before tool execution
31
+ # @param context [VectorMCP::Middleware::Context] Execution context
32
+ def before_tool_call(context)
33
+ # Override in subclasses
34
+ end
35
+
36
+ # Called after successful tool execution
37
+ # @param context [VectorMCP::Middleware::Context] Execution context with result
38
+ def after_tool_call(context)
39
+ # Override in subclasses
40
+ end
41
+
42
+ # Called when tool execution fails
43
+ # @param context [VectorMCP::Middleware::Context] Execution context with error
44
+ def on_tool_error(context)
45
+ # Override in subclasses
46
+ end
47
+
48
+ # Resource operation hooks
49
+
50
+ # Called before resource read
51
+ # @param context [VectorMCP::Middleware::Context] Execution context
52
+ def before_resource_read(context)
53
+ # Override in subclasses
54
+ end
55
+
56
+ # Called after successful resource read
57
+ # @param context [VectorMCP::Middleware::Context] Execution context with result
58
+ def after_resource_read(context)
59
+ # Override in subclasses
60
+ end
61
+
62
+ # Called when resource read fails
63
+ # @param context [VectorMCP::Middleware::Context] Execution context with error
64
+ def on_resource_error(context)
65
+ # Override in subclasses
66
+ end
67
+
68
+ # Prompt operation hooks
69
+
70
+ # Called before prompt get
71
+ # @param context [VectorMCP::Middleware::Context] Execution context
72
+ def before_prompt_get(context)
73
+ # Override in subclasses
74
+ end
75
+
76
+ # Called after successful prompt get
77
+ # @param context [VectorMCP::Middleware::Context] Execution context with result
78
+ def after_prompt_get(context)
79
+ # Override in subclasses
80
+ end
81
+
82
+ # Called when prompt get fails
83
+ # @param context [VectorMCP::Middleware::Context] Execution context with error
84
+ def on_prompt_error(context)
85
+ # Override in subclasses
86
+ end
87
+
88
+ # Sampling operation hooks
89
+
90
+ # Called before sampling request
91
+ # @param context [VectorMCP::Middleware::Context] Execution context
92
+ def before_sampling_request(context)
93
+ # Override in subclasses
94
+ end
95
+
96
+ # Called after successful sampling response
97
+ # @param context [VectorMCP::Middleware::Context] Execution context with result
98
+ def after_sampling_response(context)
99
+ # Override in subclasses
100
+ end
101
+
102
+ # Called when sampling fails
103
+ # @param context [VectorMCP::Middleware::Context] Execution context with error
104
+ def on_sampling_error(context)
105
+ # Override in subclasses
106
+ end
107
+
108
+ # Transport operation hooks
109
+
110
+ # Called before any request processing
111
+ # @param context [VectorMCP::Middleware::Context] Execution context
112
+ def before_request(context)
113
+ # Override in subclasses
114
+ end
115
+
116
+ # Called after successful response
117
+ # @param context [VectorMCP::Middleware::Context] Execution context with result
118
+ def after_response(context)
119
+ # Override in subclasses
120
+ end
121
+
122
+ # Called when transport error occurs
123
+ # @param context [VectorMCP::Middleware::Context] Execution context with error
124
+ def on_transport_error(context)
125
+ # Override in subclasses
126
+ end
127
+
128
+ # Authentication hooks
129
+
130
+ # Called before authentication
131
+ # @param context [VectorMCP::Middleware::Context] Execution context
132
+ def before_auth(context)
133
+ # Override in subclasses
134
+ end
135
+
136
+ # Called after successful authentication
137
+ # @param context [VectorMCP::Middleware::Context] Execution context with result
138
+ def after_auth(context)
139
+ # Override in subclasses
140
+ end
141
+
142
+ # Called when authentication fails
143
+ # @param context [VectorMCP::Middleware::Context] Execution context with error
144
+ def on_auth_error(context)
145
+ # Override in subclasses
146
+ end
147
+
148
+ protected
149
+
150
+ attr_reader :config, :logger
151
+
152
+ # Helper method to modify request parameters (if mutable)
153
+ # @param context [VectorMCP::Middleware::Context] Execution context
154
+ # @param new_params [Hash] New parameters to set
155
+ def modify_params(context, new_params)
156
+ if context.respond_to?(:params=)
157
+ context.params = new_params
158
+ else
159
+ @logger.warn("Cannot modify immutable params in context")
160
+ end
161
+ end
162
+
163
+ # Helper method to modify response result
164
+ # @param context [VectorMCP::Middleware::Context] Execution context
165
+ # @param new_result [Object] New result to set
166
+ def modify_result(context, new_result)
167
+ context.result = new_result
168
+ end
169
+
170
+ # Helper method to skip remaining hooks in the chain
171
+ # @param context [VectorMCP::Middleware::Context] Execution context
172
+ def skip_remaining_hooks(context)
173
+ context.skip_remaining_hooks = true
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VectorMCP
4
+ module Middleware
5
+ # Context object passed to middleware hooks containing operation metadata
6
+ # Provides access to request data, session info, and mutable response data
7
+ class Context
8
+ attr_reader :operation_type, :operation_name, :params, :session, :server, :metadata
9
+ attr_accessor :result, :error, :skip_remaining_hooks
10
+
11
+ # @param options [Hash] Context initialization options
12
+ # @option options [Symbol] :operation_type Type of operation (:tool_call, :resource_read, etc.)
13
+ # @option options [String] :operation_name Name of the specific operation being performed
14
+ # @option options [Hash] :params Request parameters
15
+ # @option options [VectorMCP::Session] :session Current session
16
+ # @option options [VectorMCP::Server] :server Server instance
17
+ # @option options [Hash] :metadata Additional metadata about the operation
18
+ def initialize(options = {})
19
+ @operation_type = options[:operation_type]
20
+ @operation_name = options[:operation_name]
21
+ @params = (options[:params] || {}).dup.freeze # Immutable copy
22
+ @session = options[:session]
23
+ @server = options[:server]
24
+ @metadata = (options[:metadata] || {}).dup
25
+ @result = nil
26
+ @error = nil
27
+ @skip_remaining_hooks = false
28
+ end
29
+
30
+ # Check if operation completed successfully
31
+ # @return [Boolean] true if no error occurred
32
+ def success?
33
+ @error.nil?
34
+ end
35
+
36
+ # Check if operation failed
37
+ # @return [Boolean] true if error occurred
38
+ def error?
39
+ !@error.nil?
40
+ end
41
+
42
+ # Get user context from session if available
43
+ # @return [Hash, nil] User context or nil if not authenticated
44
+ def user
45
+ @session&.security_context&.user
46
+ end
47
+
48
+ # Get operation timing information
49
+ # @return [Hash] Timing metadata
50
+ def timing
51
+ @metadata[:timing] || {}
52
+ end
53
+
54
+ # Add custom metadata
55
+ # @param key [Symbol, String] Metadata key
56
+ # @param value [Object] Metadata value
57
+ def add_metadata(key, value)
58
+ @metadata[key] = value
59
+ end
60
+
61
+ # Get all available data as hash for logging/debugging
62
+ # @return [Hash] Context summary
63
+ def to_h
64
+ {
65
+ operation_type: @operation_type,
66
+ operation_name: @operation_name,
67
+ params: @params,
68
+ session_id: @session&.id,
69
+ metadata: @metadata,
70
+ success: success?,
71
+ error: @error&.class&.name
72
+ }
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VectorMCP
4
+ module Middleware
5
+ # Represents a single middleware hook with priority and execution logic
6
+ class Hook
7
+ attr_reader :middleware_class, :hook_type, :priority, :conditions
8
+
9
+ # Default priority for middleware (lower numbers execute first)
10
+ DEFAULT_PRIORITY = 100
11
+
12
+ # @param middleware_class [Class] The middleware class to execute
13
+ # @param hook_type [String, Symbol] Type of hook (before_tool_call, etc.)
14
+ # @param priority [Integer] Execution priority (lower numbers execute first)
15
+ # @param conditions [Hash] Conditions for when this hook should run
16
+ def initialize(middleware_class, hook_type, priority: DEFAULT_PRIORITY, conditions: {})
17
+ @middleware_class = middleware_class
18
+ @hook_type = hook_type.to_s
19
+ @priority = priority
20
+ @conditions = conditions
21
+
22
+ validate_hook_type!
23
+ validate_middleware_class!
24
+ end
25
+
26
+ # Execute this hook with the given context
27
+ # @param context [VectorMCP::Middleware::Context] Execution context
28
+ # @return [void]
29
+ def execute(context)
30
+ return unless should_execute?(context)
31
+
32
+ # Create middleware instance and execute hook
33
+ middleware_instance = create_middleware_instance(context)
34
+ execute_hook_method(middleware_instance, context)
35
+ rescue StandardError => e
36
+ handle_hook_error(e, context)
37
+ end
38
+
39
+ # Check if this hook should execute for the given context
40
+ # @param context [VectorMCP::Middleware::Context] Execution context
41
+ # @return [Boolean] true if hook should execute
42
+ def should_execute?(context)
43
+ return false if context.skip_remaining_hooks
44
+
45
+ # Check operation type match
46
+ return false unless matches_operation_type?(context)
47
+
48
+ # Check custom conditions
49
+ @conditions.all? { |key, value| condition_matches?(key, value, context) }
50
+ end
51
+
52
+ # Compare hooks for sorting by priority
53
+ # @param other [Hook] Other hook to compare
54
+ # @return [Integer] Comparison result
55
+ def <=>(other)
56
+ @priority <=> other.priority
57
+ end
58
+
59
+ private
60
+
61
+ def validate_hook_type!
62
+ return if HOOK_TYPES.include?(@hook_type)
63
+
64
+ raise InvalidHookTypeError, @hook_type
65
+ end
66
+
67
+ def validate_middleware_class!
68
+ raise ArgumentError, "middleware_class must be a Class, got #{@middleware_class.class}" unless @middleware_class.is_a?(Class)
69
+
70
+ return if @middleware_class < VectorMCP::Middleware::Base
71
+
72
+ raise ArgumentError, "middleware_class must inherit from VectorMCP::Middleware::Base"
73
+ end
74
+
75
+ def matches_operation_type?(context)
76
+ operation_prefix = @hook_type.split("_")[1..].join("_")
77
+
78
+ return true if transport_or_auth_hook?(operation_prefix)
79
+
80
+ operation_matches?(operation_prefix, context.operation_type)
81
+ end
82
+
83
+ def condition_matches?(key, value, context)
84
+ case key
85
+ when :only_operations, :except_operations
86
+ operation_condition_matches?(key, value, context)
87
+ when :only_users, :except_users
88
+ user_condition_matches?(key, value, context)
89
+ else
90
+ true # Unknown conditions are ignored
91
+ end
92
+ end
93
+
94
+ def transport_or_auth_hook?(operation_prefix)
95
+ %w[request response transport_error auth auth_error].include?(operation_prefix)
96
+ end
97
+
98
+ def operation_matches?(operation_prefix, operation_type)
99
+ case operation_prefix
100
+ when "tool_call", "tool_error"
101
+ operation_type == :tool_call
102
+ when "resource_read", "resource_error"
103
+ operation_type == :resource_read
104
+ when "prompt_get", "prompt_error"
105
+ operation_type == :prompt_get
106
+ when "sampling_request", "sampling_response", "sampling_error"
107
+ operation_type == :sampling
108
+ else
109
+ true
110
+ end
111
+ end
112
+
113
+ def operation_condition_matches?(key, value, context)
114
+ included = Array(value).include?(context.operation_name)
115
+ key == :only_operations ? included : !included
116
+ end
117
+
118
+ def user_condition_matches?(key, value, context)
119
+ user_id = context.user&.[](:user_id) || context.user&.[]("user_id")
120
+ included = Array(value).include?(user_id)
121
+ key == :only_users ? included : !included
122
+ end
123
+
124
+ def create_middleware_instance(_context)
125
+ if @middleware_class.respond_to?(:new)
126
+ @middleware_class.new
127
+ else
128
+ @middleware_class
129
+ end
130
+ end
131
+
132
+ def execute_hook_method(middleware_instance, context)
133
+ method_name = @hook_type
134
+
135
+ if middleware_instance.respond_to?(method_name)
136
+ middleware_instance.public_send(method_name, context)
137
+ elsif middleware_instance.respond_to?(:call)
138
+ # Fallback to generic call method
139
+ middleware_instance.call(@hook_type, context)
140
+ else
141
+ raise MiddlewareError,
142
+ "Middleware #{@middleware_class} does not respond to #{method_name} or call",
143
+ middleware_class: @middleware_class
144
+ end
145
+ end
146
+
147
+ def handle_hook_error(error, context)
148
+ # Log the error but don't break the chain unless it's critical
149
+ logger = VectorMCP.logger_for("middleware")
150
+ logger.error("Middleware hook failed") do
151
+ {
152
+ middleware: @middleware_class.name,
153
+ hook_type: @hook_type,
154
+ operation: context.operation_name,
155
+ error: error.message
156
+ }
157
+ end
158
+
159
+ # Re-raise if it's a critical error that should stop execution
160
+ return unless error.is_a?(VectorMCP::Error) || @conditions[:critical] == true
161
+
162
+ raise MiddlewareError,
163
+ "Critical middleware failure in #{@middleware_class}",
164
+ original_error: error,
165
+ middleware_class: @middleware_class
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent-ruby"
4
+
5
+ module VectorMCP
6
+ module Middleware
7
+ # Central manager for middleware hooks and execution
8
+ # Thread-safe registry and execution engine for all middleware
9
+ class Manager
10
+ def initialize
11
+ @hooks = Concurrent::Map.new { |h, k| h[k] = Concurrent::Array.new }
12
+ @logger = VectorMCP.logger_for("middleware.manager")
13
+ end
14
+
15
+ # Register a middleware for specific hook types
16
+ # @param middleware_class [Class] Middleware class inheriting from Base
17
+ # @param hooks [Array<String, Symbol>] Hook types to register for
18
+ # @param priority [Integer] Execution priority (lower numbers execute first)
19
+ # @param conditions [Hash] Conditions for when middleware should run
20
+ # @example
21
+ # manager.register(MyMiddleware, [:before_tool_call, :after_tool_call])
22
+ # manager.register(AuthMiddleware, :before_request, priority: 10)
23
+ def register(middleware_class, hooks, priority: Hook::DEFAULT_PRIORITY, conditions: {})
24
+ Array(hooks).each do |hook_type|
25
+ hook = Hook.new(middleware_class, hook_type, priority: priority, conditions: conditions)
26
+ add_hook(hook)
27
+ end
28
+
29
+ @logger.debug("Registered middleware") do
30
+ {
31
+ middleware: middleware_class.name,
32
+ hooks: Array(hooks),
33
+ priority: priority
34
+ }
35
+ end
36
+ end
37
+
38
+ # Remove all hooks for a specific middleware class
39
+ # @param middleware_class [Class] Middleware class to remove
40
+ def unregister(middleware_class)
41
+ removed_count = 0
42
+
43
+ @hooks.each_value do |hook_array|
44
+ removed_count += hook_array.delete_if { |hook| hook.middleware_class == middleware_class }.size
45
+ end
46
+
47
+ @logger.debug("Unregistered middleware") do
48
+ {
49
+ middleware: middleware_class.name,
50
+ hooks_removed: removed_count
51
+ }
52
+ end
53
+ end
54
+
55
+ # Execute all hooks for a specific hook type with timing
56
+ # @param hook_type [String, Symbol] Type of hook to execute
57
+ # @param context [VectorMCP::Middleware::Context] Execution context
58
+ # @return [VectorMCP::Middleware::Context] Modified context
59
+ def execute_hooks(hook_type, context)
60
+ hook_type_str = hook_type.to_s
61
+ hooks = get_sorted_hooks(hook_type_str)
62
+
63
+ return context if hooks.empty?
64
+
65
+ execution_state = initialize_execution_state(hook_type_str, hooks, context)
66
+ execute_hook_chain(hooks, context, execution_state)
67
+ finalize_execution(context, execution_state)
68
+
69
+ context
70
+ end
71
+
72
+ # Get statistics about registered middleware
73
+ # @return [Hash] Statistics summary
74
+ def stats
75
+ hook_counts = {}
76
+ total_hooks = 0
77
+
78
+ @hooks.each do |hook_type, hook_array|
79
+ count = hook_array.size
80
+ hook_counts[hook_type] = count
81
+ total_hooks += count
82
+ end
83
+
84
+ {
85
+ total_hooks: total_hooks,
86
+ hook_types: hook_counts.keys.sort,
87
+ hooks_by_type: hook_counts
88
+ }
89
+ end
90
+
91
+ # Clear all registered hooks (useful for testing)
92
+ def clear!
93
+ @hooks.clear
94
+ @logger.debug("Cleared all middleware hooks")
95
+ end
96
+
97
+ private
98
+
99
+ def add_hook(hook)
100
+ hook_type = hook.hook_type
101
+ hook_array = @hooks[hook_type]
102
+
103
+ # Insert hook in sorted position by priority
104
+ insertion_index = hook_array.find_index { |existing_hook| existing_hook.priority > hook.priority }
105
+ if insertion_index
106
+ hook_array.insert(insertion_index, hook)
107
+ else
108
+ hook_array << hook
109
+ end
110
+ end
111
+
112
+ def get_sorted_hooks(hook_type)
113
+ @hooks[hook_type].to_a
114
+ end
115
+
116
+ def initialize_execution_state(hook_type_str, hooks, context)
117
+ start_time = Time.now
118
+
119
+ @logger.debug("Executing middleware hooks") do
120
+ {
121
+ hook_type: hook_type_str,
122
+ hook_count: hooks.size,
123
+ operation: context.operation_name
124
+ }
125
+ end
126
+
127
+ {
128
+ hook_type: hook_type_str,
129
+ start_time: start_time,
130
+ executed_count: 0,
131
+ total_hooks: hooks.size
132
+ }
133
+ end
134
+
135
+ def execute_hook_chain(hooks, context, execution_state)
136
+ hooks.each do |hook|
137
+ break if context.skip_remaining_hooks
138
+
139
+ hook.execute(context)
140
+ execution_state[:executed_count] += 1
141
+ rescue MiddlewareError => e
142
+ handle_critical_error(e, execution_state[:hook_type], context)
143
+ break
144
+ rescue StandardError => e
145
+ handle_standard_error(e, execution_state[:hook_type])
146
+ # Continue with other hooks for non-critical errors
147
+ end
148
+ end
149
+
150
+ def finalize_execution(context, execution_state)
151
+ execution_time = Time.now - execution_state[:start_time]
152
+
153
+ context.add_metadata(:middleware_timing, {
154
+ hook_type: execution_state[:hook_type],
155
+ execution_time: execution_time,
156
+ hooks_executed: execution_state[:executed_count],
157
+ hooks_total: execution_state[:total_hooks]
158
+ })
159
+
160
+ @logger.debug("Completed middleware execution") do
161
+ {
162
+ hook_type: execution_state[:hook_type],
163
+ execution_time: execution_time,
164
+ hooks_executed: execution_state[:executed_count]
165
+ }
166
+ end
167
+ end
168
+
169
+ def handle_critical_error(error, hook_type, context)
170
+ @logger.error("Critical middleware error") do
171
+ {
172
+ middleware: error.middleware_class&.name,
173
+ hook_type: hook_type,
174
+ error: error.message
175
+ }
176
+ end
177
+
178
+ context.error = error
179
+ end
180
+
181
+ def handle_standard_error(error, hook_type)
182
+ @logger.error("Unexpected middleware error") do
183
+ {
184
+ hook_type: hook_type,
185
+ error: error.message
186
+ }
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Middleware framework for VectorMCP
4
+ # Provides pluggable hooks around MCP operations
5
+ require_relative "errors"
6
+ require_relative "middleware/context"
7
+ require_relative "middleware/hook"
8
+ require_relative "middleware/manager"
9
+ require_relative "middleware/base"
10
+
11
+ module VectorMCP
12
+ # Middleware system for pluggable hooks around MCP operations
13
+ # Allows developers to add custom behavior without modifying core code
14
+ module Middleware
15
+ # Hook types available in the system
16
+ HOOK_TYPES = %w[
17
+ before_tool_call after_tool_call on_tool_error
18
+ before_resource_read after_resource_read on_resource_error
19
+ before_prompt_get after_prompt_get on_prompt_error
20
+ before_sampling_request after_sampling_response on_sampling_error
21
+ before_request after_response on_transport_error
22
+ before_auth after_auth on_auth_error
23
+ ].freeze
24
+
25
+ # Error raised when invalid hook type is specified
26
+ class InvalidHookTypeError < VectorMCP::Error
27
+ def initialize(hook_type)
28
+ super("Invalid hook type: #{hook_type}. Valid types: #{HOOK_TYPES.join(", ")}")
29
+ end
30
+ end
31
+
32
+ # Error raised when middleware execution fails
33
+ class MiddlewareError < VectorMCP::Error
34
+ attr_reader :original_error, :middleware_class
35
+
36
+ def initialize(message, original_error: nil, middleware_class: nil)
37
+ super(message)
38
+ @original_error = original_error
39
+ @middleware_class = middleware_class
40
+ end
41
+ end
42
+ end
43
+ end
@@ -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