vector_mcp 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +281 -0
- data/README.md +292 -501
- data/lib/vector_mcp/errors.rb +24 -0
- data/lib/vector_mcp/handlers/core.rb +89 -6
- data/lib/vector_mcp/logging/component.rb +131 -0
- data/lib/vector_mcp/logging/configuration.rb +156 -0
- data/lib/vector_mcp/logging/constants.rb +21 -0
- data/lib/vector_mcp/logging/core.rb +175 -0
- data/lib/vector_mcp/logging/filters/component.rb +69 -0
- data/lib/vector_mcp/logging/filters/level.rb +23 -0
- data/lib/vector_mcp/logging/formatters/base.rb +52 -0
- data/lib/vector_mcp/logging/formatters/json.rb +83 -0
- data/lib/vector_mcp/logging/formatters/text.rb +72 -0
- data/lib/vector_mcp/logging/outputs/base.rb +64 -0
- data/lib/vector_mcp/logging/outputs/console.rb +35 -0
- data/lib/vector_mcp/logging/outputs/file.rb +157 -0
- data/lib/vector_mcp/logging.rb +71 -0
- data/lib/vector_mcp/security/auth_manager.rb +79 -0
- data/lib/vector_mcp/security/authorization.rb +96 -0
- data/lib/vector_mcp/security/middleware.rb +172 -0
- data/lib/vector_mcp/security/session_context.rb +147 -0
- data/lib/vector_mcp/security/strategies/api_key.rb +167 -0
- data/lib/vector_mcp/security/strategies/custom.rb +71 -0
- data/lib/vector_mcp/security/strategies/jwt_token.rb +118 -0
- data/lib/vector_mcp/security.rb +46 -0
- data/lib/vector_mcp/server.rb +141 -1
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +35 -2
- metadata +55 -3
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module VectorMCP
|
6
|
+
module Logging
|
7
|
+
module Filters
|
8
|
+
class Component
|
9
|
+
def initialize(allowed_components = nil, blocked_components = nil)
|
10
|
+
@allowed_components = normalize_components(allowed_components)
|
11
|
+
@blocked_components = normalize_components(blocked_components)
|
12
|
+
end
|
13
|
+
|
14
|
+
def accept?(log_entry)
|
15
|
+
return false if blocked?(log_entry.component)
|
16
|
+
return true if @allowed_components.nil?
|
17
|
+
|
18
|
+
allowed?(log_entry.component)
|
19
|
+
end
|
20
|
+
|
21
|
+
def allow_component(component)
|
22
|
+
@allowed_components ||= Set.new
|
23
|
+
@allowed_components.add(component.to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
def block_component(component)
|
27
|
+
@blocked_components ||= Set.new
|
28
|
+
@blocked_components.add(component.to_s)
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove_component_filter(component)
|
32
|
+
@allowed_components&.delete(component.to_s)
|
33
|
+
@blocked_components&.delete(component.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def allowed?(component)
|
39
|
+
return true if @allowed_components.nil?
|
40
|
+
|
41
|
+
@allowed_components.include?(component) ||
|
42
|
+
@allowed_components.any? { |pattern| component.start_with?(pattern) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def blocked?(component)
|
46
|
+
return false if @blocked_components.nil?
|
47
|
+
|
48
|
+
@blocked_components.include?(component) ||
|
49
|
+
@blocked_components.any? { |pattern| component.start_with?(pattern) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def normalize_components(components)
|
53
|
+
case components
|
54
|
+
when nil
|
55
|
+
nil
|
56
|
+
when String
|
57
|
+
Set.new([components])
|
58
|
+
when Array
|
59
|
+
Set.new(components.map(&:to_s))
|
60
|
+
when Set
|
61
|
+
components
|
62
|
+
else
|
63
|
+
Set.new([components.to_s])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Logging
|
5
|
+
module Filters
|
6
|
+
class Level
|
7
|
+
def initialize(min_level)
|
8
|
+
@min_level = min_level.is_a?(Integer) ? min_level : Logging.level_value(min_level)
|
9
|
+
end
|
10
|
+
|
11
|
+
def accept?(log_entry)
|
12
|
+
log_entry.level >= @min_level
|
13
|
+
end
|
14
|
+
|
15
|
+
def min_level=(new_level)
|
16
|
+
@min_level = new_level.is_a?(Integer) ? new_level : Logging.level_value(new_level)
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :min_level
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../constants"
|
4
|
+
|
5
|
+
module VectorMCP
|
6
|
+
module Logging
|
7
|
+
module Formatters
|
8
|
+
class Base
|
9
|
+
def initialize(options = {})
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def format(log_entry)
|
14
|
+
raise NotImplementedError, "Subclasses must implement #format"
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def format_timestamp(timestamp)
|
20
|
+
timestamp.strftime("%Y-%m-%d %H:%M:%S.%#{Constants::TIMESTAMP_PRECISION}N")
|
21
|
+
end
|
22
|
+
|
23
|
+
def format_level(level_name, width = Constants::DEFAULT_LEVEL_WIDTH)
|
24
|
+
level_name.ljust(width)
|
25
|
+
end
|
26
|
+
|
27
|
+
def format_component(component, width = Constants::DEFAULT_COMPONENT_WIDTH)
|
28
|
+
if component.length > width
|
29
|
+
"#{component[0..(width - Constants::TRUNCATION_SUFFIX_LENGTH)]}..."
|
30
|
+
else
|
31
|
+
component.ljust(width)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def format_context(context)
|
36
|
+
return "" if context.empty?
|
37
|
+
|
38
|
+
pairs = context.map do |key, value|
|
39
|
+
"#{key}=#{value}"
|
40
|
+
end
|
41
|
+
" (#{pairs.join(", ")})"
|
42
|
+
end
|
43
|
+
|
44
|
+
def truncate_message(message, max_length = Constants::DEFAULT_MAX_MESSAGE_LENGTH)
|
45
|
+
return message if message.length <= max_length
|
46
|
+
|
47
|
+
"#{message[0..(max_length - Constants::TRUNCATION_SUFFIX_LENGTH)]}..."
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require_relative "../constants"
|
5
|
+
|
6
|
+
module VectorMCP
|
7
|
+
module Logging
|
8
|
+
module Formatters
|
9
|
+
class Json < Base
|
10
|
+
def initialize(options = {})
|
11
|
+
super
|
12
|
+
@pretty = options.fetch(:pretty, false)
|
13
|
+
@include_thread = options.fetch(:include_thread, false)
|
14
|
+
end
|
15
|
+
|
16
|
+
def format(log_entry)
|
17
|
+
data = {
|
18
|
+
timestamp: log_entry.timestamp.iso8601(Constants::TIMESTAMP_PRECISION),
|
19
|
+
level: log_entry.level_name,
|
20
|
+
component: log_entry.component,
|
21
|
+
message: log_entry.message
|
22
|
+
}
|
23
|
+
|
24
|
+
data[:context] = log_entry.context unless log_entry.context.empty?
|
25
|
+
data[:thread_id] = log_entry.thread_id if @include_thread
|
26
|
+
|
27
|
+
if @pretty
|
28
|
+
"#{JSON.pretty_generate(data)}\n"
|
29
|
+
else
|
30
|
+
"#{JSON.generate(data)}\n"
|
31
|
+
end
|
32
|
+
rescue JSON::GeneratorError, JSON::NestingError => e
|
33
|
+
fallback_format(log_entry, e)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def fallback_format(log_entry, error)
|
39
|
+
safe_data = {
|
40
|
+
timestamp: log_entry.timestamp.iso8601(Constants::TIMESTAMP_PRECISION),
|
41
|
+
level: log_entry.level_name,
|
42
|
+
component: log_entry.component,
|
43
|
+
message: "JSON serialization failed: #{error.message}",
|
44
|
+
original_message: log_entry.message.to_s,
|
45
|
+
context: sanitize_context(log_entry.context)
|
46
|
+
}
|
47
|
+
|
48
|
+
"#{JSON.generate(safe_data)}\n"
|
49
|
+
end
|
50
|
+
|
51
|
+
def sanitize_context(context, depth = 0)
|
52
|
+
return "<max_depth_reached>" if depth > Constants::MAX_SERIALIZATION_DEPTH
|
53
|
+
return {} unless context.is_a?(Hash)
|
54
|
+
|
55
|
+
context.each_with_object({}) do |(key, value), sanitized|
|
56
|
+
sanitized[key.to_s] = sanitize_value(value, depth + 1)
|
57
|
+
end
|
58
|
+
rescue StandardError
|
59
|
+
{ "<sanitization_error>" => true }
|
60
|
+
end
|
61
|
+
|
62
|
+
def sanitize_value(value, depth = 0)
|
63
|
+
return "<max_depth_reached>" if depth > Constants::MAX_SERIALIZATION_DEPTH
|
64
|
+
|
65
|
+
case value
|
66
|
+
when String, Numeric, TrueClass, FalseClass, NilClass
|
67
|
+
value
|
68
|
+
when Array
|
69
|
+
return "<complex_array>" if depth > Constants::MAX_ARRAY_SERIALIZATION_DEPTH
|
70
|
+
|
71
|
+
value.first(Constants::MAX_ARRAY_ELEMENTS_TO_SERIALIZE).map { |v| sanitize_value(v, depth + 1) }
|
72
|
+
when Hash
|
73
|
+
sanitize_context(value, depth)
|
74
|
+
else
|
75
|
+
value.to_s
|
76
|
+
end
|
77
|
+
rescue StandardError
|
78
|
+
"<serialization_error>"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../constants"
|
4
|
+
|
5
|
+
module VectorMCP
|
6
|
+
module Logging
|
7
|
+
module Formatters
|
8
|
+
class Text < Base
|
9
|
+
COLORS = {
|
10
|
+
TRACE: "\e[90m", # gray
|
11
|
+
DEBUG: "\e[36m", # cyan
|
12
|
+
INFO: "\e[32m", # green
|
13
|
+
WARN: "\e[33m", # yellow
|
14
|
+
ERROR: "\e[31m", # red
|
15
|
+
FATAL: "\e[35m", # magenta
|
16
|
+
SECURITY: "\e[1;31m" # bold red
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
RESET_COLOR = "\e[0m"
|
20
|
+
|
21
|
+
def initialize(options = {})
|
22
|
+
super
|
23
|
+
@colorize = options.fetch(:colorize, true)
|
24
|
+
@include_timestamp = options.fetch(:include_timestamp, true)
|
25
|
+
@include_thread = options.fetch(:include_thread, false)
|
26
|
+
@include_component = options.fetch(:include_component, true)
|
27
|
+
@max_message_length = options.fetch(:max_message_length, Constants::DEFAULT_MAX_MESSAGE_LENGTH)
|
28
|
+
end
|
29
|
+
|
30
|
+
def format(log_entry)
|
31
|
+
parts = []
|
32
|
+
|
33
|
+
parts << format_timestamp_part(log_entry.timestamp) if @include_timestamp
|
34
|
+
|
35
|
+
parts << format_level_part(log_entry.level_name)
|
36
|
+
|
37
|
+
parts << format_component_part(log_entry.component) if @include_component
|
38
|
+
|
39
|
+
parts << format_thread_part(log_entry.thread_id) if @include_thread
|
40
|
+
|
41
|
+
message = truncate_message(log_entry.message, @max_message_length)
|
42
|
+
context_str = format_context(log_entry.context)
|
43
|
+
|
44
|
+
"#{parts.join(" ")} #{message}#{context_str}\n"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def format_timestamp_part(timestamp)
|
50
|
+
"[#{format_timestamp(timestamp)}]"
|
51
|
+
end
|
52
|
+
|
53
|
+
def format_level_part(level_name)
|
54
|
+
level_str = format_level(level_name.to_s, Constants::DEFAULT_LEVEL_WIDTH)
|
55
|
+
if @colorize && COLORS[level_name.to_sym]
|
56
|
+
"#{COLORS[level_name.to_sym]}#{level_str}#{RESET_COLOR}"
|
57
|
+
else
|
58
|
+
level_str
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def format_component_part(component)
|
63
|
+
format_component(component, Constants::DEFAULT_COMPONENT_WIDTH)
|
64
|
+
end
|
65
|
+
|
66
|
+
def format_thread_part(thread_id)
|
67
|
+
"[#{thread_id}]"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Logging
|
5
|
+
module Outputs
|
6
|
+
class Base
|
7
|
+
attr_reader :formatter, :config
|
8
|
+
|
9
|
+
def initialize(config = {})
|
10
|
+
@config = config
|
11
|
+
@formatter = create_formatter
|
12
|
+
@closed = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def write(log_entry)
|
16
|
+
return if @closed
|
17
|
+
|
18
|
+
formatted_message = @formatter.format(log_entry)
|
19
|
+
write_formatted(formatted_message)
|
20
|
+
rescue StandardError => e
|
21
|
+
fallback_write("Logging output error: #{e.message}\n")
|
22
|
+
end
|
23
|
+
|
24
|
+
def close
|
25
|
+
@closed = true
|
26
|
+
end
|
27
|
+
|
28
|
+
def closed?
|
29
|
+
@closed
|
30
|
+
end
|
31
|
+
|
32
|
+
def reconfigure
|
33
|
+
@formatter = create_formatter
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def write_formatted(message)
|
39
|
+
raise NotImplementedError, "Subclasses must implement #write_formatted"
|
40
|
+
end
|
41
|
+
|
42
|
+
def fallback_write(message)
|
43
|
+
$stderr.write(message)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def create_formatter
|
49
|
+
format_type = @config[:format] || "text"
|
50
|
+
formatter_options = @config.except(:format)
|
51
|
+
|
52
|
+
case format_type.to_s.downcase
|
53
|
+
when "json"
|
54
|
+
Formatters::Json.new(formatter_options)
|
55
|
+
when "text"
|
56
|
+
Formatters::Text.new(formatter_options)
|
57
|
+
else
|
58
|
+
raise OutputError, "Unknown formatter type: #{format_type}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Logging
|
5
|
+
module Outputs
|
6
|
+
class Console < Base
|
7
|
+
def initialize(config = {})
|
8
|
+
super
|
9
|
+
@stream = determine_stream
|
10
|
+
@mutex = Mutex.new
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def write_formatted(message)
|
16
|
+
@mutex.synchronize do
|
17
|
+
@stream.write(message)
|
18
|
+
@stream.flush if @stream.respond_to?(:flush)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def determine_stream
|
25
|
+
case @config[:stream]&.to_s&.downcase
|
26
|
+
when "stdout"
|
27
|
+
$stdout
|
28
|
+
else
|
29
|
+
$stderr # Default to stderr for logging
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "date"
|
5
|
+
|
6
|
+
module VectorMCP
|
7
|
+
module Logging
|
8
|
+
module Outputs
|
9
|
+
class File < Base
|
10
|
+
def initialize(config = {})
|
11
|
+
super
|
12
|
+
@path = @config[:path] or raise OutputError, "File path required"
|
13
|
+
@max_size = parse_size(@config[:max_size] || "100MB")
|
14
|
+
@max_files = @config[:max_files] || 7
|
15
|
+
@rotation = @config[:rotation] || "daily"
|
16
|
+
@mutex = Mutex.new
|
17
|
+
@file = nil
|
18
|
+
@current_date = nil
|
19
|
+
|
20
|
+
ensure_directory_exists
|
21
|
+
open_file
|
22
|
+
end
|
23
|
+
|
24
|
+
def close
|
25
|
+
@mutex.synchronize do
|
26
|
+
@file&.close
|
27
|
+
@file = nil
|
28
|
+
end
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def write_formatted(message)
|
35
|
+
@mutex.synchronize do
|
36
|
+
rotate_if_needed
|
37
|
+
@file.write(message)
|
38
|
+
@file.flush
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def ensure_directory_exists
|
45
|
+
dir = ::File.dirname(@path)
|
46
|
+
FileUtils.mkdir_p(dir) unless ::File.directory?(dir)
|
47
|
+
rescue StandardError => e
|
48
|
+
raise OutputError, "Cannot create log directory #{dir}: #{e.message}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def open_file
|
52
|
+
@file = ::File.open(current_log_path, "a")
|
53
|
+
@file.sync = true
|
54
|
+
@current_date = Date.today if daily_rotation?
|
55
|
+
rescue StandardError => e
|
56
|
+
raise OutputError, "Cannot open log file #{current_log_path}: #{e.message}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def current_log_path
|
60
|
+
if daily_rotation?
|
61
|
+
base, ext = split_path(@path)
|
62
|
+
"#{base}_#{Date.today.strftime("%Y%m%d")}#{ext}"
|
63
|
+
else
|
64
|
+
@path
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def rotate_if_needed
|
69
|
+
return unless should_rotate?
|
70
|
+
|
71
|
+
rotate_file
|
72
|
+
open_file
|
73
|
+
end
|
74
|
+
|
75
|
+
def should_rotate?
|
76
|
+
return false unless @file
|
77
|
+
|
78
|
+
case @rotation
|
79
|
+
when "daily"
|
80
|
+
daily_rotation? && @current_date != Date.today
|
81
|
+
when "size"
|
82
|
+
@file.size >= @max_size
|
83
|
+
else
|
84
|
+
false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def rotate_file
|
89
|
+
@file&.close
|
90
|
+
|
91
|
+
if daily_rotation?
|
92
|
+
cleanup_old_files
|
93
|
+
else
|
94
|
+
rotate_numbered_files
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def daily_rotation?
|
99
|
+
@rotation == "daily"
|
100
|
+
end
|
101
|
+
|
102
|
+
def rotate_numbered_files
|
103
|
+
return unless ::File.exist?(@path)
|
104
|
+
|
105
|
+
(@max_files - 1).downto(1) do |i|
|
106
|
+
old_file = "#{@path}.#{i}"
|
107
|
+
new_file = "#{@path}.#{i + 1}"
|
108
|
+
|
109
|
+
::File.rename(old_file, new_file) if ::File.exist?(old_file)
|
110
|
+
end
|
111
|
+
|
112
|
+
::File.rename(@path, "#{@path}.1")
|
113
|
+
end
|
114
|
+
|
115
|
+
def cleanup_old_files
|
116
|
+
base, ext = split_path(@path)
|
117
|
+
pattern = "#{base}_*#{ext}"
|
118
|
+
|
119
|
+
old_files = Dir.glob(pattern).reverse
|
120
|
+
files_to_remove = old_files[@max_files..] || []
|
121
|
+
|
122
|
+
files_to_remove.each do |file|
|
123
|
+
::File.unlink(file)
|
124
|
+
rescue StandardError => e
|
125
|
+
fallback_write("Warning: Could not remove old log file #{file}: #{e.message}\n")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def split_path(path)
|
130
|
+
ext = ::File.extname(path)
|
131
|
+
base = path.chomp(ext)
|
132
|
+
[base, ext]
|
133
|
+
end
|
134
|
+
|
135
|
+
def parse_size(size_str)
|
136
|
+
size_str = size_str.to_s.upcase
|
137
|
+
|
138
|
+
raise OutputError, "Invalid size format: #{size_str}" unless size_str =~ /\A(\d+)(KB|MB|GB)?\z/
|
139
|
+
|
140
|
+
number = ::Regexp.last_match(1).to_i
|
141
|
+
unit = ::Regexp.last_match(2) || "B"
|
142
|
+
|
143
|
+
case unit
|
144
|
+
when "KB"
|
145
|
+
number * 1024
|
146
|
+
when "MB"
|
147
|
+
number * 1024 * 1024
|
148
|
+
when "GB"
|
149
|
+
number * 1024 * 1024 * 1024
|
150
|
+
else
|
151
|
+
number
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "logging/constants"
|
4
|
+
require_relative "logging/core"
|
5
|
+
require_relative "logging/configuration"
|
6
|
+
require_relative "logging/component"
|
7
|
+
require_relative "logging/formatters/base"
|
8
|
+
require_relative "logging/formatters/text"
|
9
|
+
require_relative "logging/formatters/json"
|
10
|
+
require_relative "logging/outputs/base"
|
11
|
+
require_relative "logging/outputs/console"
|
12
|
+
require_relative "logging/outputs/file"
|
13
|
+
require_relative "logging/filters/level"
|
14
|
+
require_relative "logging/filters/component"
|
15
|
+
|
16
|
+
module VectorMCP
|
17
|
+
module Logging
|
18
|
+
class Error < StandardError; end
|
19
|
+
class ConfigurationError < Error; end
|
20
|
+
class FormatterError < Error; end
|
21
|
+
class OutputError < Error; end
|
22
|
+
|
23
|
+
LEVELS = {
|
24
|
+
TRACE: 0,
|
25
|
+
DEBUG: 1,
|
26
|
+
INFO: 2,
|
27
|
+
WARN: 3,
|
28
|
+
ERROR: 4,
|
29
|
+
FATAL: 5,
|
30
|
+
SECURITY: 6
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
LEVEL_NAMES = LEVELS.invert.freeze
|
34
|
+
|
35
|
+
def self.level_name(level)
|
36
|
+
(LEVEL_NAMES[level] || "UNKNOWN").to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.level_value(name)
|
40
|
+
LEVELS[name.to_s.upcase.to_sym] || LEVELS[:INFO]
|
41
|
+
end
|
42
|
+
|
43
|
+
class LogEntry
|
44
|
+
attr_reader :timestamp, :level, :component, :message, :context, :thread_id
|
45
|
+
|
46
|
+
def initialize(attributes = {})
|
47
|
+
@timestamp = attributes[:timestamp]
|
48
|
+
@level = attributes[:level]
|
49
|
+
@component = attributes[:component]
|
50
|
+
@message = attributes[:message]
|
51
|
+
@context = attributes[:context] || {}
|
52
|
+
@thread_id = attributes[:thread_id]
|
53
|
+
end
|
54
|
+
|
55
|
+
def level_name
|
56
|
+
Logging.level_name(@level)
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_h
|
60
|
+
{
|
61
|
+
timestamp: @timestamp.iso8601(Constants::TIMESTAMP_PRECISION),
|
62
|
+
level: level_name,
|
63
|
+
component: @component,
|
64
|
+
message: @message,
|
65
|
+
context: @context,
|
66
|
+
thread_id: @thread_id
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Security
|
5
|
+
# Manages authentication strategies for VectorMCP servers
|
6
|
+
# Provides opt-in authentication with zero configuration by default
|
7
|
+
class AuthManager
|
8
|
+
attr_reader :strategies, :enabled, :default_strategy
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@strategies = {}
|
12
|
+
@enabled = false
|
13
|
+
@default_strategy = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Enable authentication with optional default strategy
|
17
|
+
# @param default_strategy [Symbol] the default authentication strategy to use
|
18
|
+
def enable!(default_strategy: :api_key)
|
19
|
+
@enabled = true
|
20
|
+
@default_strategy = default_strategy
|
21
|
+
end
|
22
|
+
|
23
|
+
# Disable authentication (return to pass-through mode)
|
24
|
+
def disable!
|
25
|
+
@enabled = false
|
26
|
+
@default_strategy = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add an authentication strategy
|
30
|
+
# @param name [Symbol] the strategy name
|
31
|
+
# @param strategy [Object] the strategy instance
|
32
|
+
def add_strategy(name, strategy)
|
33
|
+
@strategies[name] = strategy
|
34
|
+
end
|
35
|
+
|
36
|
+
# Remove an authentication strategy
|
37
|
+
# @param name [Symbol] the strategy name to remove
|
38
|
+
def remove_strategy(name)
|
39
|
+
@strategies.delete(name)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Authenticate a request using the specified or default strategy
|
43
|
+
# @param request [Hash] the request object containing headers, params, etc.
|
44
|
+
# @param strategy [Symbol] optional strategy override
|
45
|
+
# @return [Object, false] authentication result or false if failed
|
46
|
+
def authenticate(request, strategy: nil)
|
47
|
+
return { authenticated: true, user: nil } unless @enabled
|
48
|
+
|
49
|
+
strategy_name = strategy || @default_strategy
|
50
|
+
auth_strategy = @strategies[strategy_name]
|
51
|
+
|
52
|
+
return { authenticated: false, error: "Unknown strategy: #{strategy_name}" } unless auth_strategy
|
53
|
+
|
54
|
+
begin
|
55
|
+
result = auth_strategy.authenticate(request)
|
56
|
+
if result
|
57
|
+
{ authenticated: true, user: result }
|
58
|
+
else
|
59
|
+
{ authenticated: false, error: "Authentication failed" }
|
60
|
+
end
|
61
|
+
rescue StandardError => e
|
62
|
+
{ authenticated: false, error: "Authentication error: #{e.message}" }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check if authentication is required
|
67
|
+
# @return [Boolean] true if authentication is enabled
|
68
|
+
def required?
|
69
|
+
@enabled
|
70
|
+
end
|
71
|
+
|
72
|
+
# Get list of available strategies
|
73
|
+
# @return [Array<Symbol>] array of strategy names
|
74
|
+
def available_strategies
|
75
|
+
@strategies.keys
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|