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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/lib/vector_mcp/errors.rb +6 -3
- data/lib/vector_mcp/handlers/core.rb +199 -45
- data/lib/vector_mcp/logger.rb +148 -0
- data/lib/vector_mcp/middleware/base.rb +177 -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 +191 -0
- data/lib/vector_mcp/middleware.rb +43 -0
- data/lib/vector_mcp/security/strategies/jwt_token.rb +6 -3
- data/lib/vector_mcp/server.rb +61 -17
- data/lib/vector_mcp/session.rb +37 -4
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +10 -35
- metadata +17 -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,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
|