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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +281 -0
- data/README.md +302 -373
- data/lib/vector_mcp/definitions.rb +3 -1
- data/lib/vector_mcp/errors.rb +24 -0
- data/lib/vector_mcp/handlers/core.rb +132 -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/registry.rb +24 -0
- data/lib/vector_mcp/server.rb +141 -1
- data/lib/vector_mcp/transport/sse/client_connection.rb +113 -0
- data/lib/vector_mcp/transport/sse/message_handler.rb +166 -0
- data/lib/vector_mcp/transport/sse/puma_config.rb +77 -0
- data/lib/vector_mcp/transport/sse/stream_manager.rb +92 -0
- data/lib/vector_mcp/transport/sse.rb +119 -460
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +35 -2
- metadata +63 -21
@@ -230,6 +230,7 @@ module VectorMCP
|
|
230
230
|
# Validates that the root URI is properly formatted and secure.
|
231
231
|
# @return [Boolean] True if the root is valid.
|
232
232
|
# @raise [ArgumentError] If the root is invalid.
|
233
|
+
# rubocop:disable Naming/PredicateMethod
|
233
234
|
def validate!
|
234
235
|
# Validate URI format
|
235
236
|
parsed_uri = begin
|
@@ -248,13 +249,14 @@ module VectorMCP
|
|
248
249
|
raise ArgumentError, "Root path is not a directory: #{path}" unless File.directory?(path)
|
249
250
|
|
250
251
|
# Security check: ensure we can read the directory
|
251
|
-
raise ArgumentError, "Root directory is not readable: #{path}" unless File.readable?(path)
|
252
252
|
|
253
|
+
raise ArgumentError, "Root directory is not readable: #{path}" unless File.readable?(path)
|
253
254
|
# Validate against path traversal attempts in the URI itself
|
254
255
|
raise ArgumentError, "Root path contains unsafe traversal patterns: #{path}" if path.include?("..") || path.include?("./")
|
255
256
|
|
256
257
|
true
|
257
258
|
end
|
259
|
+
# rubocop:enable Naming/PredicateMethod
|
258
260
|
|
259
261
|
# Class method to create a root from a local directory path.
|
260
262
|
# @param path [String] Local filesystem path to the directory.
|
data/lib/vector_mcp/errors.rb
CHANGED
@@ -174,4 +174,28 @@ module VectorMCP
|
|
174
174
|
# super(message, code: client_code, details: client_details, request_id: request_id)
|
175
175
|
# end
|
176
176
|
# end
|
177
|
+
|
178
|
+
# --- Security Specific Errors ---
|
179
|
+
|
180
|
+
# Represents an authentication required error (-32401).
|
181
|
+
# Indicates the request requires authentication.
|
182
|
+
class UnauthorizedError < ProtocolError
|
183
|
+
# @param message [String] The error message.
|
184
|
+
# @param details [Hash, nil] Additional details for the error (optional).
|
185
|
+
# @param request_id [String, Integer, nil] The ID of the originating request.
|
186
|
+
def initialize(message = "Authentication required", details: nil, request_id: nil)
|
187
|
+
super(message, code: -32_401, details: details, request_id: request_id)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Represents an authorization failed error (-32403).
|
192
|
+
# Indicates the authenticated user does not have permission to perform the requested action.
|
193
|
+
class ForbiddenError < ProtocolError
|
194
|
+
# @param message [String] The error message.
|
195
|
+
# @param details [Hash, nil] Additional details for the error (optional).
|
196
|
+
# @param request_id [String, Integer, nil] The ID of the originating request.
|
197
|
+
def initialize(message = "Access denied", details: nil, request_id: nil)
|
198
|
+
super(message, code: -32_403, details: details, request_id: request_id)
|
199
|
+
end
|
200
|
+
end
|
177
201
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "json"
|
4
4
|
require "uri"
|
5
|
+
require "json-schema"
|
5
6
|
|
6
7
|
module VectorMCP
|
7
8
|
module Handlers
|
@@ -41,20 +42,35 @@ module VectorMCP
|
|
41
42
|
#
|
42
43
|
# @param params [Hash] The request parameters.
|
43
44
|
# Expected keys: "name" (String), "arguments" (Hash, optional).
|
44
|
-
# @param
|
45
|
+
# @param session [VectorMCP::Session] The current session.
|
45
46
|
# @param server [VectorMCP::Server] The server instance.
|
46
47
|
# @return [Hash] A hash containing the tool call result or an error indication.
|
47
48
|
# Example success: `{ isError: false, content: [{ type: "text", ... }] }`
|
48
49
|
# @raise [VectorMCP::NotFoundError] if the requested tool is not found.
|
49
|
-
|
50
|
+
# @raise [VectorMCP::InvalidParamsError] if arguments validation fails.
|
51
|
+
# @raise [VectorMCP::UnauthorizedError] if authentication fails.
|
52
|
+
# @raise [VectorMCP::ForbiddenError] if authorization fails.
|
53
|
+
def self.call_tool(params, session, server)
|
50
54
|
tool_name = params["name"]
|
51
55
|
arguments = params["arguments"] || {}
|
52
56
|
|
53
57
|
tool = server.tools[tool_name]
|
54
58
|
raise VectorMCP::NotFoundError.new("Not Found", details: "Tool not found: #{tool_name}") unless tool
|
55
59
|
|
60
|
+
# Security check: authenticate and authorize the request
|
61
|
+
security_result = check_tool_security(session, tool, server)
|
62
|
+
handle_security_failure(security_result) unless security_result[:success]
|
63
|
+
|
64
|
+
# Validate arguments against the tool's input schema
|
65
|
+
validate_input_arguments!(tool_name, tool, arguments)
|
66
|
+
|
56
67
|
# Let StandardError propagate to Server#handle_request
|
57
|
-
|
68
|
+
# Pass session_context only if the handler supports it (for backward compatibility)
|
69
|
+
result = if [1, -1].include?(tool.handler.arity)
|
70
|
+
tool.handler.call(arguments)
|
71
|
+
else
|
72
|
+
tool.handler.call(arguments, security_result[:session_context])
|
73
|
+
end
|
58
74
|
{
|
59
75
|
isError: false,
|
60
76
|
content: VectorMCP::Util.convert_to_mcp_content(result)
|
@@ -78,18 +94,30 @@ module VectorMCP
|
|
78
94
|
#
|
79
95
|
# @param params [Hash] The request parameters.
|
80
96
|
# Expected key: "uri" (String).
|
81
|
-
# @param
|
97
|
+
# @param session [VectorMCP::Session] The current session.
|
82
98
|
# @param server [VectorMCP::Server] The server instance.
|
83
99
|
# @return [Hash] A hash containing an array of content items from the resource.
|
84
100
|
# Example: `{ contents: [{ type: "text", text: "...", uri: "memory://data" }] }`
|
85
101
|
# @raise [VectorMCP::NotFoundError] if the requested resource URI is not found.
|
86
|
-
|
102
|
+
# @raise [VectorMCP::UnauthorizedError] if authentication fails.
|
103
|
+
# @raise [VectorMCP::ForbiddenError] if authorization fails.
|
104
|
+
def self.read_resource(params, session, server)
|
87
105
|
uri_s = params["uri"]
|
88
106
|
raise VectorMCP::NotFoundError.new("Not Found", details: "Resource not found: #{uri_s}") unless server.resources[uri_s]
|
89
107
|
|
90
108
|
resource = server.resources[uri_s]
|
109
|
+
|
110
|
+
# Security check: authenticate and authorize the request
|
111
|
+
security_result = check_resource_security(session, resource, server)
|
112
|
+
handle_security_failure(security_result) unless security_result[:success]
|
113
|
+
|
91
114
|
# Let StandardError propagate to Server#handle_request
|
92
|
-
|
115
|
+
# Pass session_context only if the handler supports it (for backward compatibility)
|
116
|
+
content_raw = if [1, -1].include?(resource.handler.arity)
|
117
|
+
resource.handler.call(params)
|
118
|
+
else
|
119
|
+
resource.handler.call(params, security_result[:session_context])
|
120
|
+
end
|
93
121
|
contents = VectorMCP::Util.convert_to_mcp_content(content_raw, mime_type: resource.mime_type)
|
94
122
|
contents.each do |item|
|
95
123
|
# Add URI to each content item if not already present
|
@@ -301,6 +329,104 @@ module VectorMCP
|
|
301
329
|
end
|
302
330
|
end
|
303
331
|
private_class_method :validate_prompt_response!
|
332
|
+
|
333
|
+
# Validates arguments provided for a tool against its input schema using json-schema.
|
334
|
+
# @api private
|
335
|
+
# @param tool_name [String] The name of the tool.
|
336
|
+
# @param tool [VectorMCP::Definitions::Tool] The tool definition.
|
337
|
+
# @param arguments [Hash] The arguments supplied by the client.
|
338
|
+
# @return [void]
|
339
|
+
# @raise [VectorMCP::InvalidParamsError] if arguments fail validation.
|
340
|
+
def self.validate_input_arguments!(tool_name, tool, arguments)
|
341
|
+
return unless tool.input_schema.is_a?(Hash)
|
342
|
+
return if tool.input_schema.empty?
|
343
|
+
|
344
|
+
validation_errors = JSON::Validator.fully_validate(tool.input_schema, arguments)
|
345
|
+
return if validation_errors.empty?
|
346
|
+
|
347
|
+
raise_tool_validation_error(tool_name, validation_errors)
|
348
|
+
rescue JSON::Schema::ValidationError => e
|
349
|
+
raise_tool_validation_error(tool_name, [e.message])
|
350
|
+
end
|
351
|
+
|
352
|
+
# Raises InvalidParamsError with formatted validation details.
|
353
|
+
# @api private
|
354
|
+
# @param tool_name [String] The name of the tool.
|
355
|
+
# @param validation_errors [Array<String>] The validation error messages.
|
356
|
+
# @return [void]
|
357
|
+
# @raise [VectorMCP::InvalidParamsError] Always raises with formatted details.
|
358
|
+
def self.raise_tool_validation_error(tool_name, validation_errors)
|
359
|
+
raise VectorMCP::InvalidParamsError.new(
|
360
|
+
"Invalid arguments for tool '#{tool_name}'",
|
361
|
+
details: {
|
362
|
+
tool: tool_name,
|
363
|
+
validation_errors: validation_errors,
|
364
|
+
message: validation_errors.join("; ")
|
365
|
+
}
|
366
|
+
)
|
367
|
+
end
|
368
|
+
private_class_method :validate_input_arguments!
|
369
|
+
private_class_method :raise_tool_validation_error
|
370
|
+
|
371
|
+
# Security helper methods
|
372
|
+
|
373
|
+
# Check security for tool access
|
374
|
+
# @api private
|
375
|
+
# @param session [VectorMCP::Session] The current session
|
376
|
+
# @param tool [VectorMCP::Definitions::Tool] The tool being accessed
|
377
|
+
# @param server [VectorMCP::Server] The server instance
|
378
|
+
# @return [Hash] Security check result
|
379
|
+
def self.check_tool_security(session, tool, server)
|
380
|
+
# Extract request context from session for security middleware
|
381
|
+
request = extract_request_from_session(session)
|
382
|
+
server.security_middleware.process_request(request, action: :call, resource: tool)
|
383
|
+
end
|
384
|
+
private_class_method :check_tool_security
|
385
|
+
|
386
|
+
# Check security for resource access
|
387
|
+
# @api private
|
388
|
+
# @param session [VectorMCP::Session] The current session
|
389
|
+
# @param resource [VectorMCP::Definitions::Resource] The resource being accessed
|
390
|
+
# @param server [VectorMCP::Server] The server instance
|
391
|
+
# @return [Hash] Security check result
|
392
|
+
def self.check_resource_security(session, resource, server)
|
393
|
+
request = extract_request_from_session(session)
|
394
|
+
server.security_middleware.process_request(request, action: :read, resource: resource)
|
395
|
+
end
|
396
|
+
private_class_method :check_resource_security
|
397
|
+
|
398
|
+
# Extract request context from session for security processing
|
399
|
+
# @api private
|
400
|
+
# @param session [VectorMCP::Session] The current session
|
401
|
+
# @return [Hash] Request context for security middleware
|
402
|
+
def self.extract_request_from_session(session)
|
403
|
+
# Extract security context from session
|
404
|
+
# This will be enhanced as we integrate with transport layers
|
405
|
+
{
|
406
|
+
headers: session.instance_variable_get(:@request_headers) || {},
|
407
|
+
params: session.instance_variable_get(:@request_params) || {},
|
408
|
+
session_id: session.respond_to?(:id) ? session.id : "test-session"
|
409
|
+
}
|
410
|
+
end
|
411
|
+
private_class_method :extract_request_from_session
|
412
|
+
|
413
|
+
# Handle security failure by raising appropriate error
|
414
|
+
# @api private
|
415
|
+
# @param security_result [Hash] The security check result
|
416
|
+
# @return [void]
|
417
|
+
# @raise [VectorMCP::UnauthorizedError, VectorMCP::ForbiddenError]
|
418
|
+
def self.handle_security_failure(security_result)
|
419
|
+
case security_result[:error_code]
|
420
|
+
when "AUTHENTICATION_REQUIRED"
|
421
|
+
raise VectorMCP::UnauthorizedError, security_result[:error]
|
422
|
+
when "AUTHORIZATION_FAILED"
|
423
|
+
raise VectorMCP::ForbiddenError, security_result[:error]
|
424
|
+
else
|
425
|
+
# Fallback to generic unauthorized error
|
426
|
+
raise VectorMCP::UnauthorizedError, security_result[:error] || "Security check failed"
|
427
|
+
end
|
428
|
+
end
|
429
|
+
private_class_method :handle_security_failure
|
304
430
|
end
|
305
431
|
end
|
306
432
|
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Logging
|
5
|
+
class Component
|
6
|
+
attr_reader :name, :core, :config
|
7
|
+
|
8
|
+
def initialize(name, core, config = {})
|
9
|
+
@name = name.to_s
|
10
|
+
@core = core
|
11
|
+
@config = config
|
12
|
+
@context = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def with_context(context)
|
16
|
+
old_context = @context
|
17
|
+
@context = @context.merge(context)
|
18
|
+
yield
|
19
|
+
ensure
|
20
|
+
@context = old_context
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_context(context)
|
24
|
+
@context = @context.merge(context)
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear_context
|
28
|
+
@context = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def trace(message = nil, context: {}, &block)
|
32
|
+
log_with_block(Logging::LEVELS[:TRACE], message, context, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def debug(message = nil, context: {}, &block)
|
36
|
+
log_with_block(Logging::LEVELS[:DEBUG], message, context, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def info(message = nil, context: {}, &block)
|
40
|
+
log_with_block(Logging::LEVELS[:INFO], message, context, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def warn(message = nil, context: {}, &block)
|
44
|
+
log_with_block(Logging::LEVELS[:WARN], message, context, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
def error(message = nil, context: {}, &block)
|
48
|
+
log_with_block(Logging::LEVELS[:ERROR], message, context, &block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def fatal(message = nil, context: {}, &block)
|
52
|
+
log_with_block(Logging::LEVELS[:FATAL], message, context, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def security(message = nil, context: {}, &block)
|
56
|
+
log_with_block(Logging::LEVELS[:SECURITY], message, context, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def level
|
60
|
+
@core.configuration.level_for(@name)
|
61
|
+
end
|
62
|
+
|
63
|
+
def level_enabled?(level)
|
64
|
+
level >= self.level
|
65
|
+
end
|
66
|
+
|
67
|
+
def trace?
|
68
|
+
level_enabled?(Logging::LEVELS[:TRACE])
|
69
|
+
end
|
70
|
+
|
71
|
+
def debug?
|
72
|
+
level_enabled?(Logging::LEVELS[:DEBUG])
|
73
|
+
end
|
74
|
+
|
75
|
+
def info?
|
76
|
+
level_enabled?(Logging::LEVELS[:INFO])
|
77
|
+
end
|
78
|
+
|
79
|
+
def warn?
|
80
|
+
level_enabled?(Logging::LEVELS[:WARN])
|
81
|
+
end
|
82
|
+
|
83
|
+
def error?
|
84
|
+
level_enabled?(Logging::LEVELS[:ERROR])
|
85
|
+
end
|
86
|
+
|
87
|
+
def fatal?
|
88
|
+
level_enabled?(Logging::LEVELS[:FATAL])
|
89
|
+
end
|
90
|
+
|
91
|
+
def security?
|
92
|
+
level_enabled?(Logging::LEVELS[:SECURITY])
|
93
|
+
end
|
94
|
+
|
95
|
+
def measure(message, context: {}, level: :info, &block)
|
96
|
+
start_time = Time.now
|
97
|
+
result = nil
|
98
|
+
error = nil
|
99
|
+
|
100
|
+
begin
|
101
|
+
result = block.call
|
102
|
+
rescue StandardError => e
|
103
|
+
error = e
|
104
|
+
raise
|
105
|
+
ensure
|
106
|
+
duration = Time.now - start_time
|
107
|
+
measure_context = context.merge(
|
108
|
+
duration_ms: (duration * 1000).round(2),
|
109
|
+
success: error.nil?
|
110
|
+
)
|
111
|
+
measure_context[:error] = error.class.name if error
|
112
|
+
|
113
|
+
send(level, "#{message} completed", context: measure_context)
|
114
|
+
end
|
115
|
+
|
116
|
+
result
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def log_with_block(level, message, context, &block)
|
122
|
+
return unless level_enabled?(level)
|
123
|
+
|
124
|
+
message = block.call if block_given?
|
125
|
+
|
126
|
+
full_context = @context.merge(context)
|
127
|
+
@core.log(level, @name, message, full_context)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
module VectorMCP
|
6
|
+
module Logging
|
7
|
+
class Configuration
|
8
|
+
DEFAULT_CONFIG = {
|
9
|
+
level: "INFO",
|
10
|
+
format: "text",
|
11
|
+
output: "console",
|
12
|
+
components: {},
|
13
|
+
console: {
|
14
|
+
colorize: true,
|
15
|
+
include_timestamp: true,
|
16
|
+
include_thread: false
|
17
|
+
},
|
18
|
+
file: {
|
19
|
+
path: nil,
|
20
|
+
rotation: "daily",
|
21
|
+
max_size: "100MB",
|
22
|
+
max_files: 7
|
23
|
+
}
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
attr_reader :config
|
27
|
+
|
28
|
+
def initialize(config = {})
|
29
|
+
@config = deep_merge(DEFAULT_CONFIG, normalize_config(config))
|
30
|
+
validate_config!
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.from_file(path)
|
34
|
+
config = YAML.load_file(path)
|
35
|
+
new(config["logging"] || config)
|
36
|
+
rescue StandardError => e
|
37
|
+
raise ConfigurationError, "Failed to load configuration from #{path}: #{e.message}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.from_env
|
41
|
+
config = {}
|
42
|
+
|
43
|
+
config[:level] = ENV["VECTORMCP_LOG_LEVEL"] if ENV["VECTORMCP_LOG_LEVEL"]
|
44
|
+
config[:format] = ENV["VECTORMCP_LOG_FORMAT"] if ENV["VECTORMCP_LOG_FORMAT"]
|
45
|
+
config[:output] = ENV["VECTORMCP_LOG_OUTPUT"] if ENV["VECTORMCP_LOG_OUTPUT"]
|
46
|
+
|
47
|
+
config[:file] = { path: ENV["VECTORMCP_LOG_FILE_PATH"] } if ENV["VECTORMCP_LOG_FILE_PATH"]
|
48
|
+
|
49
|
+
new(config)
|
50
|
+
end
|
51
|
+
|
52
|
+
def level_for(component)
|
53
|
+
component_level = @config[:components][component.to_s]
|
54
|
+
level_value = component_level || @config[:level]
|
55
|
+
Logging.level_value(level_value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_component_level(component, level)
|
59
|
+
@config[:components][component.to_s] = if level.is_a?(Integer)
|
60
|
+
Logging.level_name(level)
|
61
|
+
else
|
62
|
+
level.to_s.upcase
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def component_config(component_name)
|
67
|
+
@config[:components][component_name.to_s] || {}
|
68
|
+
end
|
69
|
+
|
70
|
+
def console_config
|
71
|
+
@config[:console]
|
72
|
+
end
|
73
|
+
|
74
|
+
def file_config
|
75
|
+
@config[:file]
|
76
|
+
end
|
77
|
+
|
78
|
+
def format
|
79
|
+
@config[:format]
|
80
|
+
end
|
81
|
+
|
82
|
+
def output
|
83
|
+
@config[:output]
|
84
|
+
end
|
85
|
+
|
86
|
+
def configure(&)
|
87
|
+
instance_eval(&) if block_given?
|
88
|
+
validate_config!
|
89
|
+
end
|
90
|
+
|
91
|
+
def level(new_level)
|
92
|
+
@config[:level] = new_level.to_s.upcase
|
93
|
+
end
|
94
|
+
|
95
|
+
def component(name, level:)
|
96
|
+
@config[:components][name.to_s] = level.to_s.upcase
|
97
|
+
end
|
98
|
+
|
99
|
+
def console(options = {})
|
100
|
+
@config[:console].merge!(options)
|
101
|
+
end
|
102
|
+
|
103
|
+
def file(options = {})
|
104
|
+
@config[:file].merge!(options)
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_h
|
108
|
+
@config.dup
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def normalize_config(config)
|
114
|
+
case config
|
115
|
+
when Hash
|
116
|
+
config.transform_keys(&:to_sym)
|
117
|
+
when String
|
118
|
+
{ level: config }
|
119
|
+
else
|
120
|
+
{}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def deep_merge(hash1, hash2)
|
125
|
+
result = hash1.dup
|
126
|
+
hash2.each do |key, value|
|
127
|
+
result[key] = if result[key].is_a?(Hash) && value.is_a?(Hash)
|
128
|
+
deep_merge(result[key], value)
|
129
|
+
else
|
130
|
+
value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
result
|
134
|
+
end
|
135
|
+
|
136
|
+
def validate_config!
|
137
|
+
validate_level!(@config[:level])
|
138
|
+
@config[:components].each_value do |level|
|
139
|
+
validate_level!(level)
|
140
|
+
end
|
141
|
+
|
142
|
+
raise ConfigurationError, "Invalid format: #{@config[:format]}" unless %w[text json].include?(@config[:format])
|
143
|
+
|
144
|
+
return if %w[console file both].include?(@config[:output])
|
145
|
+
|
146
|
+
raise ConfigurationError, "Invalid output: #{@config[:output]}"
|
147
|
+
end
|
148
|
+
|
149
|
+
def validate_level!(level)
|
150
|
+
return if Logging::LEVELS.key?(level.to_s.upcase.to_sym)
|
151
|
+
|
152
|
+
raise ConfigurationError, "Invalid log level: #{level}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
module Logging
|
5
|
+
module Constants
|
6
|
+
# JSON serialization limits
|
7
|
+
MAX_SERIALIZATION_DEPTH = 5
|
8
|
+
MAX_ARRAY_SERIALIZATION_DEPTH = 3
|
9
|
+
MAX_ARRAY_ELEMENTS_TO_SERIALIZE = 10
|
10
|
+
|
11
|
+
# Text formatting limits
|
12
|
+
DEFAULT_MAX_MESSAGE_LENGTH = 1000
|
13
|
+
DEFAULT_COMPONENT_WIDTH = 20
|
14
|
+
DEFAULT_LEVEL_WIDTH = 8
|
15
|
+
TRUNCATION_SUFFIX_LENGTH = 4 # for "..."
|
16
|
+
|
17
|
+
# ISO timestamp precision
|
18
|
+
TIMESTAMP_PRECISION = 3 # milliseconds
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|