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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c251e50dd9545c58cb748a420426193de3d8b2ccc5c110b340f79ff305ad769
4
- data.tar.gz: d19ebc22fe53066a8113eea7e7219093fc0fe546a960aa392add005ae5bb1fa8
3
+ metadata.gz: edd8f7804cea74064b13b6432daa19149d4b2d6c34efbd7ccfbae393870b648a
4
+ data.tar.gz: bd04d07372ef947498c39021adfba055d8030b94bf76b567fc8b5cb73aae994b
5
5
  SHA512:
6
- metadata.gz: c7e6d56309c8e30ef4cdc20d2db4c9a74750508ea6c035dfe2ac64ae3759174d4c6c8b20b8726037318b5f5a14dee59e1a81097160c8df6b43a1d302f7e05f33
7
- data.tar.gz: f8a32dbeadb7d17459745edefd7ebbdf784e7d9dc646a3362fe3ddd2ae3ae8d3c20f9d2374e6905d31a0bfcddbc369d5cb1ef583c544cffadc37fb6ed38ae8d1
6
+ metadata.gz: 3a47a3ad028e39de9bd8d4973779040fd0c571067e4c3588ad406d26dd138cdfad06921d497a3922d1eb2406df3ede02379df7f24620539cb1170ac6fdb1bed5
7
+ data.tar.gz: ad1d3d147c3b4ef8624651aeabb0809f689bbfb5c4ae702dcc0c5fa458cc500ff7ffd54e46a4ea02ae995d908af0122f5a88b7ab895eeaab8c1d780e6eec8043
data/CHANGELOG.md CHANGED
@@ -1,3 +1,71 @@
1
+ ## [0.3.2] – 2025-07-02
2
+
3
+ ### Added
4
+
5
+ * **Comprehensive Middleware System**: Pluggable hook system for custom behavior around all MCP operations
6
+ * **Hook Points**: Support for all major operations including tools, resources, prompts, sampling, transport, and authentication
7
+ * **Priority-Based Execution**: Control middleware execution order with configurable priorities
8
+ * **Conditional Execution**: Run middleware only for specific operations, users, or conditions
9
+ * **Context Management**: Rich execution context with operation metadata and session information
10
+ * **Error Handling**: Graceful error recovery with middleware-specific error hooks
11
+ * **Built-in Middleware**: PII redaction, request retry, rate limiting, and enhanced logging examples
12
+
13
+ * **Enhanced Examples Organization**: Comprehensive example reorganization for better developer experience
14
+ * **Getting Started Examples**: `examples/getting_started/` with basic server implementations
15
+ * **Core Features Examples**: `examples/core_features/` demonstrating key capabilities
16
+ * **Use Cases Examples**: `examples/use_cases/` with real-world application scenarios
17
+ * **Logging Examples**: `examples/logging/` showcasing structured logging capabilities
18
+ * **Middleware Examples**: `examples/middleware_examples.rb` and `examples/simple_middleware_demo.rb`
19
+
20
+ * **Refactored Logging System**: Enhanced logging architecture with better performance and flexibility
21
+ * **Simplified API**: Streamlined `VectorMCP.logger_for(component)` interface
22
+ * **Performance Improvements**: Optimized log formatting and output handling
23
+ * **Better Component Organization**: Hierarchical logger management with cleaner separation
24
+
25
+ ### Changed
26
+
27
+ * **Middleware Integration**: Core server architecture enhanced to support middleware hooks
28
+ * **Server Methods**: New `use_middleware`, `middleware_stats`, `remove_middleware`, and `clear_middleware` methods
29
+ * **Handler Integration**: All core handlers now support middleware execution around operations
30
+ * **Session Context**: Enhanced session context with middleware metadata and execution tracking
31
+
32
+ * **Example Structure**: Major reorganization of examples for better discoverability
33
+ * **Categorized Examples**: Logical grouping by functionality and use case
34
+ * **Enhanced Documentation**: Each example category includes detailed README files
35
+ * **Use Case Focus**: Real-world scenarios like data analysis, file operations, and web scraping
36
+
37
+ * **Backward Compatibility**: All middleware features are opt-in with zero impact on existing servers
38
+ * **Default Behavior**: Servers without middleware continue working exactly as before
39
+ * **Optional Integration**: Middleware can be added incrementally to existing applications
40
+
41
+ ### Fixed
42
+
43
+ * **Ruby Version Compatibility**: Enhanced support for older Ruby versions
44
+ * **Code Quality**: Multiple bug fixes and improvements identified through expanded test coverage
45
+ * **Performance**: Optimized middleware execution path for minimal overhead when no middleware is registered
46
+
47
+ ### Security
48
+
49
+ * **Middleware Security**: Security-aware middleware execution
50
+ * **Session Context Integration**: Middleware has access to authentication and authorization context
51
+ * **Secure Error Handling**: Middleware errors handled securely without information leakage
52
+ * **Permission-Aware Hooks**: Middleware can respect user permissions and security policies
53
+
54
+ ### Testing
55
+
56
+ * **Comprehensive Middleware Tests**: 50+ tests covering all middleware functionality
57
+ * **Hook Execution Tests**: Verification of all hook types and execution order
58
+ * **Priority and Condition Tests**: Complex scenario testing for middleware orchestration
59
+ * **Integration Tests**: End-to-end testing with real server operations
60
+ * **Performance Tests**: Overhead measurement and resource usage validation
61
+
62
+ ### Technical Details
63
+
64
+ * **API Compatibility**: All middleware features maintain full backward compatibility
65
+ * **Performance**: Minimal overhead when middleware is not used, efficient execution when enabled
66
+ * **Memory Management**: Proper cleanup and resource management for long-running servers
67
+ * **Thread Safety**: Concurrent middleware execution with proper synchronization
68
+
1
69
  ## [0.3.1] – 2025-06-25
2
70
 
3
71
  ### Added
@@ -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.logger.debug("Initializing ProtocolError with code: #{code}")
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.logger.debug("Initializing ServerError with code: #{code}")
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.logger.debug("Initializing NotFoundError with code: -32001")
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
@@ -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.logger.debug("Handling ping request")
25
+ logger = VectorMCP.logger_for("handlers.core")
26
+ logger.debug("Handling ping request")
25
27
  {}
26
28
  end
27
29
 
@@ -54,27 +56,23 @@ module VectorMCP
54
56
  tool_name = params["name"]
55
57
  arguments = params["arguments"] || {}
56
58
 
57
- tool = server.tools[tool_name]
58
- raise VectorMCP::NotFoundError.new("Not Found", details: "Tool not found: #{tool_name}") unless tool
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?
59
62
 
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
+ 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)
63
67
 
64
- # Validate arguments against the tool's input schema
65
- validate_input_arguments!(tool_name, tool, arguments)
68
+ result = execute_tool_handler(tool, arguments, security_result)
69
+ context.result = build_tool_result(result)
66
70
 
67
- # Let StandardError propagate to Server#handle_request
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
74
- {
75
- isError: false,
76
- content: VectorMCP::Util.convert_to_mcp_content(result)
77
- }
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
78
76
  end
79
77
 
80
78
  # Handles the `resources/list` request.
@@ -103,27 +101,24 @@ module VectorMCP
103
101
  # @raise [VectorMCP::ForbiddenError] if authorization fails.
104
102
  def self.read_resource(params, session, server)
105
103
  uri_s = params["uri"]
106
- raise VectorMCP::NotFoundError.new("Not Found", details: "Resource not found: #{uri_s}") unless server.resources[uri_s]
107
104
 
108
- resource = server.resources[uri_s]
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?
109
108
 
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]
109
+ begin
110
+ resource = find_resource!(uri_s, server)
111
+ security_result = validate_resource_security!(session, resource, server)
113
112
 
114
- # Let StandardError propagate to Server#handle_request
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
121
- contents = VectorMCP::Util.convert_to_mcp_content(content_raw, mime_type: resource.mime_type)
122
- contents.each do |item|
123
- # Add URI to each content item if not already present
124
- item[:uri] ||= uri_s
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)
125
121
  end
126
- { contents: contents }
127
122
  end
128
123
 
129
124
  # Handles the `prompts/list` request.
@@ -184,20 +179,51 @@ module VectorMCP
184
179
  # @raise [VectorMCP::NotFoundError] if the prompt name is not found.
185
180
  # @raise [VectorMCP::InvalidParamsError] if arguments are invalid.
186
181
  # @raise [VectorMCP::InternalError] if the prompt handler returns an invalid data structure.
187
- def self.get_prompt(params, _session, server)
182
+ def self.get_prompt(params, session, server)
188
183
  prompt_name = params["name"]
189
- prompt = fetch_prompt(prompt_name, server)
190
184
 
191
- arguments = params["arguments"] || {}
192
- validate_arguments!(prompt_name, prompt, arguments)
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)
193
201
 
194
- # Call the registered handler after arguments were validated
195
- result_data = prompt.handler.call(arguments)
202
+ arguments = params["arguments"] || {}
203
+ validate_arguments!(prompt_name, prompt, arguments)
196
204
 
197
- validate_prompt_response!(prompt_name, result_data, server)
205
+ # Call the registered handler after arguments were validated
206
+ result_data = prompt.handler.call(arguments)
198
207
 
199
- # Return the handler response directly (must match GetPromptResult schema)
200
- result_data
208
+ validate_prompt_response!(prompt_name, result_data, server)
209
+
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
201
227
  end
202
228
 
203
229
  # --- Notification Handlers ---
@@ -427,6 +453,134 @@ module VectorMCP
427
453
  end
428
454
  end
429
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
430
584
  end
431
585
  end
432
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