vector_mcp 0.3.0 → 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 +349 -0
- data/README.md +292 -501
- data/lib/vector_mcp/errors.rb +30 -3
- data/lib/vector_mcp/handlers/core.rb +270 -33
- 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/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 +121 -0
- data/lib/vector_mcp/security.rb +46 -0
- data/lib/vector_mcp/server.rb +189 -5
- data/lib/vector_mcp/session.rb +37 -4
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +16 -8
- metadata +49 -4
data/lib/vector_mcp/errors.rb
CHANGED
|
@@ -28,7 +28,8 @@ module VectorMCP
|
|
|
28
28
|
# @param details [Hash, nil] Additional details for the error (optional).
|
|
29
29
|
# @param request_id [String, Integer, nil] The ID of the originating request.
|
|
30
30
|
def initialize(message, code: -32_600, details: nil, request_id: nil)
|
|
31
|
-
VectorMCP.
|
|
31
|
+
logger = VectorMCP.logger_for("errors")
|
|
32
|
+
logger.debug("Initializing ProtocolError with code: #{code}")
|
|
32
33
|
@code = code
|
|
33
34
|
@message = message
|
|
34
35
|
@details = details # NOTE: `data` in JSON-RPC is often used for this purpose.
|
|
@@ -104,7 +105,8 @@ module VectorMCP
|
|
|
104
105
|
# @param details [Hash, nil] Additional details for the error (optional).
|
|
105
106
|
# @param request_id [String, Integer, nil] The ID of the originating request.
|
|
106
107
|
def initialize(message = "Server error", code: -32_000, details: nil, request_id: nil)
|
|
107
|
-
VectorMCP.
|
|
108
|
+
logger = VectorMCP.logger_for("errors")
|
|
109
|
+
logger.debug("Initializing ServerError with code: #{code}")
|
|
108
110
|
unless (-32_099..-32_000).cover?(code)
|
|
109
111
|
warn "Server error code #{code} is outside of the reserved range (-32099 to -32000). Using -32000 instead."
|
|
110
112
|
code = -32_000
|
|
@@ -131,7 +133,8 @@ module VectorMCP
|
|
|
131
133
|
# @param details [Hash, nil] Additional details for the error (optional).
|
|
132
134
|
# @param request_id [String, Integer, nil] The ID of the originating request.
|
|
133
135
|
def initialize(message = "Not Found", details: nil, request_id: nil)
|
|
134
|
-
VectorMCP.
|
|
136
|
+
logger = VectorMCP.logger_for("errors")
|
|
137
|
+
logger.debug("Initializing NotFoundError with code: -32001")
|
|
135
138
|
super(message, code: -32_001, details: details, request_id: request_id)
|
|
136
139
|
end
|
|
137
140
|
end
|
|
@@ -174,4 +177,28 @@ module VectorMCP
|
|
|
174
177
|
# super(message, code: client_code, details: client_details, request_id: request_id)
|
|
175
178
|
# end
|
|
176
179
|
# end
|
|
180
|
+
|
|
181
|
+
# --- Security Specific Errors ---
|
|
182
|
+
|
|
183
|
+
# Represents an authentication required error (-32401).
|
|
184
|
+
# Indicates the request requires authentication.
|
|
185
|
+
class UnauthorizedError < ProtocolError
|
|
186
|
+
# @param message [String] The error message.
|
|
187
|
+
# @param details [Hash, nil] Additional details for the error (optional).
|
|
188
|
+
# @param request_id [String, Integer, nil] The ID of the originating request.
|
|
189
|
+
def initialize(message = "Authentication required", details: nil, request_id: nil)
|
|
190
|
+
super(message, code: -32_401, details: details, request_id: request_id)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Represents an authorization failed error (-32403).
|
|
195
|
+
# Indicates the authenticated user does not have permission to perform the requested action.
|
|
196
|
+
class ForbiddenError < ProtocolError
|
|
197
|
+
# @param message [String] The error message.
|
|
198
|
+
# @param details [Hash, nil] Additional details for the error (optional).
|
|
199
|
+
# @param request_id [String, Integer, nil] The ID of the originating request.
|
|
200
|
+
def initialize(message = "Access denied", details: nil, request_id: nil)
|
|
201
|
+
super(message, code: -32_403, details: details, request_id: request_id)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
177
204
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "uri"
|
|
5
5
|
require "json-schema"
|
|
6
|
+
require_relative "../middleware"
|
|
6
7
|
|
|
7
8
|
module VectorMCP
|
|
8
9
|
module Handlers
|
|
@@ -21,7 +22,8 @@ module VectorMCP
|
|
|
21
22
|
# @param _server [VectorMCP::Server] The server instance (ignored).
|
|
22
23
|
# @return [Hash] An empty hash, as per MCP spec for ping.
|
|
23
24
|
def self.ping(_params, _session, _server)
|
|
24
|
-
VectorMCP.
|
|
25
|
+
logger = VectorMCP.logger_for("handlers.core")
|
|
26
|
+
logger.debug("Handling ping request")
|
|
25
27
|
{}
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -42,28 +44,35 @@ module VectorMCP
|
|
|
42
44
|
#
|
|
43
45
|
# @param params [Hash] The request parameters.
|
|
44
46
|
# Expected keys: "name" (String), "arguments" (Hash, optional).
|
|
45
|
-
# @param
|
|
47
|
+
# @param session [VectorMCP::Session] The current session.
|
|
46
48
|
# @param server [VectorMCP::Server] The server instance.
|
|
47
49
|
# @return [Hash] A hash containing the tool call result or an error indication.
|
|
48
50
|
# Example success: `{ isError: false, content: [{ type: "text", ... }] }`
|
|
49
51
|
# @raise [VectorMCP::NotFoundError] if the requested tool is not found.
|
|
50
52
|
# @raise [VectorMCP::InvalidParamsError] if arguments validation fails.
|
|
51
|
-
|
|
53
|
+
# @raise [VectorMCP::UnauthorizedError] if authentication fails.
|
|
54
|
+
# @raise [VectorMCP::ForbiddenError] if authorization fails.
|
|
55
|
+
def self.call_tool(params, session, server)
|
|
52
56
|
tool_name = params["name"]
|
|
53
57
|
arguments = params["arguments"] || {}
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
context = create_tool_context(tool_name, params, session, server)
|
|
60
|
+
context = server.middleware_manager.execute_hooks(:before_tool_call, context)
|
|
61
|
+
return handle_middleware_error(context) if context.error?
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
begin
|
|
64
|
+
tool = find_tool!(tool_name, server)
|
|
65
|
+
security_result = validate_tool_security!(session, tool, server)
|
|
66
|
+
validate_input_arguments!(tool_name, tool, arguments)
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
result = execute_tool_handler(tool, arguments, security_result)
|
|
69
|
+
context.result = build_tool_result(result)
|
|
70
|
+
|
|
71
|
+
context = server.middleware_manager.execute_hooks(:after_tool_call, context)
|
|
72
|
+
context.result
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
handle_tool_error(e, context, server)
|
|
75
|
+
end
|
|
67
76
|
end
|
|
68
77
|
|
|
69
78
|
# Handles the `resources/list` request.
|
|
@@ -83,24 +92,33 @@ module VectorMCP
|
|
|
83
92
|
#
|
|
84
93
|
# @param params [Hash] The request parameters.
|
|
85
94
|
# Expected key: "uri" (String).
|
|
86
|
-
# @param
|
|
95
|
+
# @param session [VectorMCP::Session] The current session.
|
|
87
96
|
# @param server [VectorMCP::Server] The server instance.
|
|
88
97
|
# @return [Hash] A hash containing an array of content items from the resource.
|
|
89
98
|
# Example: `{ contents: [{ type: "text", text: "...", uri: "memory://data" }] }`
|
|
90
99
|
# @raise [VectorMCP::NotFoundError] if the requested resource URI is not found.
|
|
91
|
-
|
|
100
|
+
# @raise [VectorMCP::UnauthorizedError] if authentication fails.
|
|
101
|
+
# @raise [VectorMCP::ForbiddenError] if authorization fails.
|
|
102
|
+
def self.read_resource(params, session, server)
|
|
92
103
|
uri_s = params["uri"]
|
|
93
|
-
raise VectorMCP::NotFoundError.new("Not Found", details: "Resource not found: #{uri_s}") unless server.resources[uri_s]
|
|
94
104
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
context = create_resource_context(uri_s, params, session, server)
|
|
106
|
+
context = server.middleware_manager.execute_hooks(:before_resource_read, context)
|
|
107
|
+
return handle_middleware_error(context) if context.error?
|
|
108
|
+
|
|
109
|
+
begin
|
|
110
|
+
resource = find_resource!(uri_s, server)
|
|
111
|
+
security_result = validate_resource_security!(session, resource, server)
|
|
112
|
+
|
|
113
|
+
content_raw = execute_resource_handler(resource, params, security_result)
|
|
114
|
+
contents = process_resource_content(content_raw, resource, uri_s)
|
|
115
|
+
|
|
116
|
+
context.result = { contents: contents }
|
|
117
|
+
context = server.middleware_manager.execute_hooks(:after_resource_read, context)
|
|
118
|
+
context.result
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
handle_resource_error(e, context, server)
|
|
102
121
|
end
|
|
103
|
-
{ contents: contents }
|
|
104
122
|
end
|
|
105
123
|
|
|
106
124
|
# Handles the `prompts/list` request.
|
|
@@ -161,20 +179,51 @@ module VectorMCP
|
|
|
161
179
|
# @raise [VectorMCP::NotFoundError] if the prompt name is not found.
|
|
162
180
|
# @raise [VectorMCP::InvalidParamsError] if arguments are invalid.
|
|
163
181
|
# @raise [VectorMCP::InternalError] if the prompt handler returns an invalid data structure.
|
|
164
|
-
def self.get_prompt(params,
|
|
182
|
+
def self.get_prompt(params, session, server)
|
|
165
183
|
prompt_name = params["name"]
|
|
166
|
-
prompt = fetch_prompt(prompt_name, server)
|
|
167
184
|
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
# Create middleware context
|
|
186
|
+
context = VectorMCP::Middleware::Context.new(
|
|
187
|
+
operation_type: :prompt_get,
|
|
188
|
+
operation_name: prompt_name,
|
|
189
|
+
params: params,
|
|
190
|
+
session: session,
|
|
191
|
+
server: server,
|
|
192
|
+
metadata: { start_time: Time.now }
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Execute before_prompt_get hooks
|
|
196
|
+
context = server.middleware_manager.execute_hooks(:before_prompt_get, context)
|
|
197
|
+
return handle_middleware_error(context) if context.error?
|
|
198
|
+
|
|
199
|
+
begin
|
|
200
|
+
prompt = fetch_prompt(prompt_name, server)
|
|
201
|
+
|
|
202
|
+
arguments = params["arguments"] || {}
|
|
203
|
+
validate_arguments!(prompt_name, prompt, arguments)
|
|
170
204
|
|
|
171
|
-
|
|
172
|
-
|
|
205
|
+
# Call the registered handler after arguments were validated
|
|
206
|
+
result_data = prompt.handler.call(arguments)
|
|
173
207
|
|
|
174
|
-
|
|
208
|
+
validate_prompt_response!(prompt_name, result_data, server)
|
|
175
209
|
|
|
176
|
-
|
|
177
|
-
|
|
210
|
+
# Set result in context
|
|
211
|
+
context.result = result_data
|
|
212
|
+
|
|
213
|
+
# Execute after_prompt_get hooks
|
|
214
|
+
context = server.middleware_manager.execute_hooks(:after_prompt_get, context)
|
|
215
|
+
|
|
216
|
+
context.result
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
# Set error in context and execute error hooks
|
|
219
|
+
context.error = e
|
|
220
|
+
context = server.middleware_manager.execute_hooks(:on_prompt_error, context)
|
|
221
|
+
|
|
222
|
+
# Re-raise unless middleware handled the error
|
|
223
|
+
raise e unless context.result
|
|
224
|
+
|
|
225
|
+
context.result
|
|
226
|
+
end
|
|
178
227
|
end
|
|
179
228
|
|
|
180
229
|
# --- Notification Handlers ---
|
|
@@ -344,6 +393,194 @@ module VectorMCP
|
|
|
344
393
|
end
|
|
345
394
|
private_class_method :validate_input_arguments!
|
|
346
395
|
private_class_method :raise_tool_validation_error
|
|
396
|
+
|
|
397
|
+
# Security helper methods
|
|
398
|
+
|
|
399
|
+
# Check security for tool access
|
|
400
|
+
# @api private
|
|
401
|
+
# @param session [VectorMCP::Session] The current session
|
|
402
|
+
# @param tool [VectorMCP::Definitions::Tool] The tool being accessed
|
|
403
|
+
# @param server [VectorMCP::Server] The server instance
|
|
404
|
+
# @return [Hash] Security check result
|
|
405
|
+
def self.check_tool_security(session, tool, server)
|
|
406
|
+
# Extract request context from session for security middleware
|
|
407
|
+
request = extract_request_from_session(session)
|
|
408
|
+
server.security_middleware.process_request(request, action: :call, resource: tool)
|
|
409
|
+
end
|
|
410
|
+
private_class_method :check_tool_security
|
|
411
|
+
|
|
412
|
+
# Check security for resource access
|
|
413
|
+
# @api private
|
|
414
|
+
# @param session [VectorMCP::Session] The current session
|
|
415
|
+
# @param resource [VectorMCP::Definitions::Resource] The resource being accessed
|
|
416
|
+
# @param server [VectorMCP::Server] The server instance
|
|
417
|
+
# @return [Hash] Security check result
|
|
418
|
+
def self.check_resource_security(session, resource, server)
|
|
419
|
+
request = extract_request_from_session(session)
|
|
420
|
+
server.security_middleware.process_request(request, action: :read, resource: resource)
|
|
421
|
+
end
|
|
422
|
+
private_class_method :check_resource_security
|
|
423
|
+
|
|
424
|
+
# Extract request context from session for security processing
|
|
425
|
+
# @api private
|
|
426
|
+
# @param session [VectorMCP::Session] The current session
|
|
427
|
+
# @return [Hash] Request context for security middleware
|
|
428
|
+
def self.extract_request_from_session(session)
|
|
429
|
+
# Extract security context from session
|
|
430
|
+
# This will be enhanced as we integrate with transport layers
|
|
431
|
+
{
|
|
432
|
+
headers: session.instance_variable_get(:@request_headers) || {},
|
|
433
|
+
params: session.instance_variable_get(:@request_params) || {},
|
|
434
|
+
session_id: session.respond_to?(:id) ? session.id : "test-session"
|
|
435
|
+
}
|
|
436
|
+
end
|
|
437
|
+
private_class_method :extract_request_from_session
|
|
438
|
+
|
|
439
|
+
# Handle security failure by raising appropriate error
|
|
440
|
+
# @api private
|
|
441
|
+
# @param security_result [Hash] The security check result
|
|
442
|
+
# @return [void]
|
|
443
|
+
# @raise [VectorMCP::UnauthorizedError, VectorMCP::ForbiddenError]
|
|
444
|
+
def self.handle_security_failure(security_result)
|
|
445
|
+
case security_result[:error_code]
|
|
446
|
+
when "AUTHENTICATION_REQUIRED"
|
|
447
|
+
raise VectorMCP::UnauthorizedError, security_result[:error]
|
|
448
|
+
when "AUTHORIZATION_FAILED"
|
|
449
|
+
raise VectorMCP::ForbiddenError, security_result[:error]
|
|
450
|
+
else
|
|
451
|
+
# Fallback to generic unauthorized error
|
|
452
|
+
raise VectorMCP::UnauthorizedError, security_result[:error] || "Security check failed"
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
private_class_method :handle_security_failure
|
|
456
|
+
|
|
457
|
+
# Handle middleware error by returning appropriate response or raising error
|
|
458
|
+
# @api private
|
|
459
|
+
# @param context [VectorMCP::Middleware::Context] The middleware context with error
|
|
460
|
+
# @return [Hash, nil] Response hash if middleware provided one
|
|
461
|
+
# @raise [StandardError] Re-raises the original error if not handled
|
|
462
|
+
def self.handle_middleware_error(context)
|
|
463
|
+
# If middleware provided a result, return it
|
|
464
|
+
return context.result if context.result
|
|
465
|
+
|
|
466
|
+
# Otherwise, re-raise the middleware error
|
|
467
|
+
raise context.error
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Tool helper methods
|
|
471
|
+
|
|
472
|
+
# Create middleware context for tool operations
|
|
473
|
+
def self.create_tool_context(tool_name, params, session, server)
|
|
474
|
+
VectorMCP::Middleware::Context.new(
|
|
475
|
+
operation_type: :tool_call,
|
|
476
|
+
operation_name: tool_name,
|
|
477
|
+
params: params,
|
|
478
|
+
session: session,
|
|
479
|
+
server: server,
|
|
480
|
+
metadata: { start_time: Time.now }
|
|
481
|
+
)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Find and validate tool exists
|
|
485
|
+
def self.find_tool!(tool_name, server)
|
|
486
|
+
tool = server.tools[tool_name]
|
|
487
|
+
raise VectorMCP::NotFoundError.new("Not Found", details: "Tool not found: #{tool_name}") unless tool
|
|
488
|
+
|
|
489
|
+
tool
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Validate tool security
|
|
493
|
+
def self.validate_tool_security!(session, tool, server)
|
|
494
|
+
security_result = check_tool_security(session, tool, server)
|
|
495
|
+
handle_security_failure(security_result) unless security_result[:success]
|
|
496
|
+
security_result
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Execute tool handler with proper arity handling
|
|
500
|
+
def self.execute_tool_handler(tool, arguments, security_result)
|
|
501
|
+
if [1, -1].include?(tool.handler.arity)
|
|
502
|
+
tool.handler.call(arguments)
|
|
503
|
+
else
|
|
504
|
+
tool.handler.call(arguments, security_result[:session_context])
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Build tool result response
|
|
509
|
+
def self.build_tool_result(result)
|
|
510
|
+
{
|
|
511
|
+
isError: false,
|
|
512
|
+
content: VectorMCP::Util.convert_to_mcp_content(result)
|
|
513
|
+
}
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Handle tool execution errors
|
|
517
|
+
def self.handle_tool_error(error, context, server)
|
|
518
|
+
context.error = error
|
|
519
|
+
context = server.middleware_manager.execute_hooks(:on_tool_error, context)
|
|
520
|
+
raise error unless context.result
|
|
521
|
+
|
|
522
|
+
context.result
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Resource helper methods
|
|
526
|
+
|
|
527
|
+
# Create middleware context for resource operations
|
|
528
|
+
def self.create_resource_context(uri_s, params, session, server)
|
|
529
|
+
VectorMCP::Middleware::Context.new(
|
|
530
|
+
operation_type: :resource_read,
|
|
531
|
+
operation_name: uri_s,
|
|
532
|
+
params: params,
|
|
533
|
+
session: session,
|
|
534
|
+
server: server,
|
|
535
|
+
metadata: { start_time: Time.now }
|
|
536
|
+
)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Find and validate resource exists
|
|
540
|
+
def self.find_resource!(uri_s, server)
|
|
541
|
+
raise VectorMCP::NotFoundError.new("Not Found", details: "Resource not found: #{uri_s}") unless server.resources[uri_s]
|
|
542
|
+
|
|
543
|
+
server.resources[uri_s]
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Validate resource security
|
|
547
|
+
def self.validate_resource_security!(session, resource, server)
|
|
548
|
+
security_result = check_resource_security(session, resource, server)
|
|
549
|
+
handle_security_failure(security_result) unless security_result[:success]
|
|
550
|
+
security_result
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# Execute resource handler with proper arity handling
|
|
554
|
+
def self.execute_resource_handler(resource, params, security_result)
|
|
555
|
+
if [1, -1].include?(resource.handler.arity)
|
|
556
|
+
resource.handler.call(params)
|
|
557
|
+
else
|
|
558
|
+
resource.handler.call(params, security_result[:session_context])
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# Process resource content and add URI
|
|
563
|
+
def self.process_resource_content(content_raw, resource, uri_s)
|
|
564
|
+
contents = VectorMCP::Util.convert_to_mcp_content(content_raw, mime_type: resource.mime_type)
|
|
565
|
+
contents.each do |item|
|
|
566
|
+
item[:uri] ||= uri_s
|
|
567
|
+
end
|
|
568
|
+
contents
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# Handle resource execution errors
|
|
572
|
+
def self.handle_resource_error(error, context, server)
|
|
573
|
+
context.error = error
|
|
574
|
+
context = server.middleware_manager.execute_hooks(:on_resource_error, context)
|
|
575
|
+
raise error unless context.result
|
|
576
|
+
|
|
577
|
+
context.result
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
private_class_method :handle_middleware_error, :create_tool_context, :find_tool!, :validate_tool_security!,
|
|
581
|
+
:execute_tool_handler, :build_tool_result, :handle_tool_error, :create_resource_context,
|
|
582
|
+
:find_resource!, :validate_resource_security!, :execute_resource_handler,
|
|
583
|
+
:process_resource_content, :handle_resource_error
|
|
347
584
|
end
|
|
348
585
|
end
|
|
349
586
|
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module VectorMCP
|
|
7
|
+
# Simple, environment-driven logger for VectorMCP
|
|
8
|
+
# Supports JSON and text formats with component-based identification
|
|
9
|
+
class Logger
|
|
10
|
+
LEVELS = {
|
|
11
|
+
"TRACE" => ::Logger::DEBUG,
|
|
12
|
+
"DEBUG" => ::Logger::DEBUG,
|
|
13
|
+
"INFO" => ::Logger::INFO,
|
|
14
|
+
"WARN" => ::Logger::WARN,
|
|
15
|
+
"ERROR" => ::Logger::ERROR,
|
|
16
|
+
"FATAL" => ::Logger::FATAL
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :component, :ruby_logger
|
|
20
|
+
|
|
21
|
+
def initialize(component = "vectormcp")
|
|
22
|
+
@component = component.to_s
|
|
23
|
+
@ruby_logger = create_ruby_logger
|
|
24
|
+
@format = ENV.fetch("VECTORMCP_LOG_FORMAT", "text").downcase
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.for(component)
|
|
28
|
+
new(component)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Log methods with context support and block evaluation
|
|
32
|
+
def debug(message = nil, **context, &block)
|
|
33
|
+
log(:debug, message || block&.call, context)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def info(message = nil, **context, &block)
|
|
37
|
+
log(:info, message || block&.call, context)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def warn(message = nil, **context, &block)
|
|
41
|
+
log(:warn, message || block&.call, context)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def error(message = nil, **context, &block)
|
|
45
|
+
log(:error, message || block&.call, context)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def fatal(message = nil, **context, &block)
|
|
49
|
+
log(:fatal, message || block&.call, context)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Security-specific logging
|
|
53
|
+
def security(message, **context)
|
|
54
|
+
log(:error, "[SECURITY] #{message}", context.merge(security_event: true))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Performance measurement
|
|
58
|
+
def measure(description, **context)
|
|
59
|
+
start_time = Time.now
|
|
60
|
+
result = yield
|
|
61
|
+
duration = Time.now - start_time
|
|
62
|
+
|
|
63
|
+
info("#{description} completed", **context, duration_ms: (duration * 1000).round(2),
|
|
64
|
+
success: true)
|
|
65
|
+
|
|
66
|
+
result
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
duration = Time.now - start_time
|
|
69
|
+
error("#{description} failed", **context, duration_ms: (duration * 1000).round(2),
|
|
70
|
+
success: false,
|
|
71
|
+
error: e.class.name,
|
|
72
|
+
error_message: e.message)
|
|
73
|
+
raise
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def log(level, message, context)
|
|
79
|
+
return unless @ruby_logger.send("#{level}?")
|
|
80
|
+
|
|
81
|
+
if @format == "json"
|
|
82
|
+
log_json(level, message, context)
|
|
83
|
+
else
|
|
84
|
+
log_text(level, message, context)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def log_json(level, message, context)
|
|
89
|
+
entry = {
|
|
90
|
+
timestamp: Time.now.iso8601(3),
|
|
91
|
+
level: level.to_s.upcase,
|
|
92
|
+
component: @component,
|
|
93
|
+
message: message,
|
|
94
|
+
thread_id: Thread.current.object_id
|
|
95
|
+
}
|
|
96
|
+
entry.merge!(context) unless context.empty?
|
|
97
|
+
|
|
98
|
+
@ruby_logger.send(level, entry.to_json)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def log_text(level, message, context)
|
|
102
|
+
formatted_message = if context.empty?
|
|
103
|
+
"[#{@component}] #{message}"
|
|
104
|
+
else
|
|
105
|
+
context_str = context.map { |k, v| "#{k}=#{v}" }.join(" ")
|
|
106
|
+
"[#{@component}] #{message} (#{context_str})"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@ruby_logger.send(level, formatted_message)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def create_ruby_logger
|
|
113
|
+
output = determine_output
|
|
114
|
+
logger = ::Logger.new(output)
|
|
115
|
+
logger.level = determine_level
|
|
116
|
+
logger.formatter = method(:format_log_entry)
|
|
117
|
+
logger
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def determine_output
|
|
121
|
+
case ENV.fetch("VECTORMCP_LOG_OUTPUT", "stderr").downcase
|
|
122
|
+
when "stdout"
|
|
123
|
+
$stdout
|
|
124
|
+
when "file"
|
|
125
|
+
file_path = ENV.fetch("VECTORMCP_LOG_FILE", "./vectormcp.log")
|
|
126
|
+
File.open(file_path, "a")
|
|
127
|
+
else
|
|
128
|
+
$stderr
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def determine_level
|
|
133
|
+
level_name = ENV.fetch("VECTORMCP_LOG_LEVEL", "INFO").upcase
|
|
134
|
+
LEVELS.fetch(level_name, ::Logger::INFO)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def format_log_entry(severity, datetime, _progname, msg)
|
|
138
|
+
if @format == "json"
|
|
139
|
+
# JSON messages are already formatted
|
|
140
|
+
"#{msg}\n"
|
|
141
|
+
else
|
|
142
|
+
# Text format with timestamp
|
|
143
|
+
timestamp = datetime.strftime("%Y-%m-%d %H:%M:%S.%3N")
|
|
144
|
+
"#{timestamp} [#{severity}] #{msg}\n"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|