vector_mcp 0.2.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +281 -0
  3. data/README.md +302 -373
  4. data/lib/vector_mcp/definitions.rb +3 -1
  5. data/lib/vector_mcp/errors.rb +24 -0
  6. data/lib/vector_mcp/handlers/core.rb +132 -6
  7. data/lib/vector_mcp/logging/component.rb +131 -0
  8. data/lib/vector_mcp/logging/configuration.rb +156 -0
  9. data/lib/vector_mcp/logging/constants.rb +21 -0
  10. data/lib/vector_mcp/logging/core.rb +175 -0
  11. data/lib/vector_mcp/logging/filters/component.rb +69 -0
  12. data/lib/vector_mcp/logging/filters/level.rb +23 -0
  13. data/lib/vector_mcp/logging/formatters/base.rb +52 -0
  14. data/lib/vector_mcp/logging/formatters/json.rb +83 -0
  15. data/lib/vector_mcp/logging/formatters/text.rb +72 -0
  16. data/lib/vector_mcp/logging/outputs/base.rb +64 -0
  17. data/lib/vector_mcp/logging/outputs/console.rb +35 -0
  18. data/lib/vector_mcp/logging/outputs/file.rb +157 -0
  19. data/lib/vector_mcp/logging.rb +71 -0
  20. data/lib/vector_mcp/security/auth_manager.rb +79 -0
  21. data/lib/vector_mcp/security/authorization.rb +96 -0
  22. data/lib/vector_mcp/security/middleware.rb +172 -0
  23. data/lib/vector_mcp/security/session_context.rb +147 -0
  24. data/lib/vector_mcp/security/strategies/api_key.rb +167 -0
  25. data/lib/vector_mcp/security/strategies/custom.rb +71 -0
  26. data/lib/vector_mcp/security/strategies/jwt_token.rb +118 -0
  27. data/lib/vector_mcp/security.rb +46 -0
  28. data/lib/vector_mcp/server/registry.rb +24 -0
  29. data/lib/vector_mcp/server.rb +141 -1
  30. data/lib/vector_mcp/transport/sse/client_connection.rb +113 -0
  31. data/lib/vector_mcp/transport/sse/message_handler.rb +166 -0
  32. data/lib/vector_mcp/transport/sse/puma_config.rb +77 -0
  33. data/lib/vector_mcp/transport/sse/stream_manager.rb +92 -0
  34. data/lib/vector_mcp/transport/sse.rb +119 -460
  35. data/lib/vector_mcp/version.rb +1 -1
  36. data/lib/vector_mcp.rb +35 -2
  37. metadata +63 -21
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module VectorMCP
6
+ module Logging
7
+ class Core
8
+ attr_reader :configuration, :components, :outputs
9
+
10
+ def initialize(configuration = nil)
11
+ @configuration = configuration || Configuration.new
12
+ @components = {}
13
+ @outputs = []
14
+ @mutex = Mutex.new
15
+ @legacy_logger = nil
16
+
17
+ setup_default_output
18
+ end
19
+
20
+ def logger_for(component_name)
21
+ @mutex.synchronize do
22
+ @components[component_name] ||= Component.new(
23
+ component_name,
24
+ self,
25
+ @configuration.component_config(component_name)
26
+ )
27
+ end
28
+ end
29
+
30
+ def legacy_logger
31
+ @legacy_logger ||= LegacyAdapter.new(self)
32
+ end
33
+
34
+ def log(level, component, message, context = {})
35
+ return unless should_log?(level, component)
36
+
37
+ log_entry = Logging::LogEntry.new({
38
+ timestamp: Time.now,
39
+ level: level,
40
+ component: component,
41
+ message: message,
42
+ context: context,
43
+ thread_id: Thread.current.object_id
44
+ })
45
+
46
+ @outputs.each do |output|
47
+ output.write(log_entry)
48
+ rescue StandardError => e
49
+ warn "Failed to write to output #{output.class}: #{e.message}"
50
+ end
51
+ end
52
+
53
+ def add_output(output)
54
+ @mutex.synchronize do
55
+ @outputs << output unless @outputs.include?(output)
56
+ end
57
+ end
58
+
59
+ def remove_output(output)
60
+ @mutex.synchronize do
61
+ @outputs.delete(output)
62
+ end
63
+ end
64
+
65
+ def configure(&)
66
+ @configuration.configure(&)
67
+ reconfigure_outputs
68
+ end
69
+
70
+ def shutdown
71
+ @outputs.each(&:close)
72
+ @outputs.clear
73
+ end
74
+
75
+ private
76
+
77
+ def should_log?(level, component)
78
+ min_level = @configuration.level_for(component)
79
+ level >= min_level
80
+ end
81
+
82
+ def setup_default_output
83
+ console_output = Outputs::Console.new(@configuration.console_config)
84
+ add_output(console_output)
85
+ end
86
+
87
+ def reconfigure_outputs
88
+ @outputs.each(&:reconfigure)
89
+ end
90
+ end
91
+
92
+ class LegacyAdapter
93
+ def initialize(core)
94
+ @core = core
95
+ @component = "legacy"
96
+ @progname = "VectorMCP"
97
+ end
98
+
99
+ def debug(message = nil, &)
100
+ log_with_block(Logging::LEVELS[:DEBUG], message, &)
101
+ end
102
+
103
+ def info(message = nil, &)
104
+ log_with_block(Logging::LEVELS[:INFO], message, &)
105
+ end
106
+
107
+ def warn(message = nil, &)
108
+ log_with_block(Logging::LEVELS[:WARN], message, &)
109
+ end
110
+
111
+ def error(message = nil, &)
112
+ log_with_block(Logging::LEVELS[:ERROR], message, &)
113
+ end
114
+
115
+ def fatal(message = nil, &)
116
+ log_with_block(Logging::LEVELS[:FATAL], message, &)
117
+ end
118
+
119
+ def level
120
+ @core.configuration.level_for(@component)
121
+ end
122
+
123
+ def level=(new_level)
124
+ @core.configuration.set_component_level(@component, new_level)
125
+ end
126
+
127
+ attr_accessor :progname
128
+
129
+ def add(severity, message = nil, progname = nil, &block)
130
+ actual_message = message || block&.call || progname
131
+ @core.log(severity, @component, actual_message)
132
+ end
133
+
134
+ # For backward compatibility with Logger interface checks
135
+ def is_a?(klass)
136
+ return true if klass == Logger
137
+
138
+ super
139
+ end
140
+
141
+ def kind_of?(klass)
142
+ return true if klass == Logger
143
+
144
+ super
145
+ end
146
+
147
+ # Simulate Logger's logdev for compatibility
148
+ def instance_variable_get(var_name)
149
+ if var_name == :@logdev
150
+ # Return a mock object that simulates Logger's logdev
151
+ MockLogdev.new
152
+ else
153
+ super
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ def log_with_block(level, message, &block)
160
+ if block_given?
161
+ return unless @core.configuration.level_for(@component) <= level
162
+
163
+ message = block.call
164
+ end
165
+ @core.log(level, @component, message)
166
+ end
167
+ end
168
+
169
+ class MockLogdev
170
+ def dev
171
+ $stderr
172
+ end
173
+ end
174
+ end
175
+ end
@@ -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