vector_mcp 0.3.1 → 0.3.3

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +122 -0
  3. data/lib/vector_mcp/definitions.rb +25 -9
  4. data/lib/vector_mcp/errors.rb +2 -3
  5. data/lib/vector_mcp/handlers/core.rb +206 -50
  6. data/lib/vector_mcp/logger.rb +148 -0
  7. data/lib/vector_mcp/middleware/base.rb +171 -0
  8. data/lib/vector_mcp/middleware/context.rb +76 -0
  9. data/lib/vector_mcp/middleware/hook.rb +169 -0
  10. data/lib/vector_mcp/middleware/manager.rb +179 -0
  11. data/lib/vector_mcp/middleware.rb +43 -0
  12. data/lib/vector_mcp/request_context.rb +182 -0
  13. data/lib/vector_mcp/sampling/result.rb +11 -1
  14. data/lib/vector_mcp/security/middleware.rb +2 -28
  15. data/lib/vector_mcp/security/strategies/api_key.rb +2 -24
  16. data/lib/vector_mcp/security/strategies/jwt_token.rb +6 -3
  17. data/lib/vector_mcp/server/capabilities.rb +5 -7
  18. data/lib/vector_mcp/server/message_handling.rb +11 -5
  19. data/lib/vector_mcp/server.rb +74 -20
  20. data/lib/vector_mcp/session.rb +131 -8
  21. data/lib/vector_mcp/transport/base_session_manager.rb +320 -0
  22. data/lib/vector_mcp/transport/http_stream/event_store.rb +151 -0
  23. data/lib/vector_mcp/transport/http_stream/session_manager.rb +189 -0
  24. data/lib/vector_mcp/transport/http_stream/stream_handler.rb +269 -0
  25. data/lib/vector_mcp/transport/http_stream.rb +779 -0
  26. data/lib/vector_mcp/transport/sse.rb +74 -19
  27. data/lib/vector_mcp/transport/sse_session_manager.rb +188 -0
  28. data/lib/vector_mcp/transport/stdio.rb +70 -13
  29. data/lib/vector_mcp/transport/stdio_session_manager.rb +181 -0
  30. data/lib/vector_mcp/util.rb +39 -1
  31. data/lib/vector_mcp/version.rb +1 -1
  32. data/lib/vector_mcp.rb +10 -35
  33. metadata +25 -24
  34. data/lib/vector_mcp/logging/component.rb +0 -131
  35. data/lib/vector_mcp/logging/configuration.rb +0 -156
  36. data/lib/vector_mcp/logging/constants.rb +0 -21
  37. data/lib/vector_mcp/logging/core.rb +0 -175
  38. data/lib/vector_mcp/logging/filters/component.rb +0 -69
  39. data/lib/vector_mcp/logging/filters/level.rb +0 -23
  40. data/lib/vector_mcp/logging/formatters/base.rb +0 -52
  41. data/lib/vector_mcp/logging/formatters/json.rb +0 -83
  42. data/lib/vector_mcp/logging/formatters/text.rb +0 -72
  43. data/lib/vector_mcp/logging/outputs/base.rb +0 -64
  44. data/lib/vector_mcp/logging/outputs/console.rb +0 -35
  45. data/lib/vector_mcp/logging/outputs/file.rb +0 -157
  46. data/lib/vector_mcp/logging.rb +0 -71
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_session_manager"
4
+
5
+ module VectorMCP
6
+ module Transport
7
+ # Session manager for Stdio transport with single global session.
8
+ # Extends BaseSessionManager with stdio-specific functionality.
9
+ #
10
+ # The Stdio transport uses a single global session for the entire transport lifetime.
11
+ class StdioSessionManager < BaseSessionManager
12
+ GLOBAL_SESSION_ID = "stdio_global_session"
13
+
14
+ # Initializes a new Stdio session manager.
15
+ #
16
+ # @param transport [Stdio] The parent transport instance
17
+ # @param session_timeout [Integer] Session timeout in seconds (ignored for stdio)
18
+ def initialize(transport, session_timeout = 300)
19
+ super
20
+
21
+ # Create the single global session for stdio transport
22
+ @global_session = create_global_session
23
+ end
24
+
25
+ # Gets the global session for stdio transport.
26
+ # Stdio uses a single global session for the entire transport lifetime.
27
+ #
28
+ # @return [Session] The global session
29
+ def global_session
30
+ @global_session&.touch!
31
+ @global_session
32
+ end
33
+
34
+ # Gets or creates the global session for stdio transport.
35
+ # This is an alias for global_session for stdio transport.
36
+ #
37
+ # @return [Session] The global session
38
+ def global_session_or_create
39
+ global_session
40
+ end
41
+
42
+ # Override: Gets session by ID, but always returns the global session for stdio.
43
+ #
44
+ # @param session_id [String] The session ID (ignored for stdio)
45
+ # @return [Session] The global session
46
+ def session(_session_id = nil)
47
+ global_session
48
+ end
49
+
50
+ # Override: Always returns the global session for stdio.
51
+ #
52
+ # @param session_id [String, nil] The session ID (ignored)
53
+ # @return [Session] The global session
54
+ def session_or_create(_session_id = nil)
55
+ global_session
56
+ end
57
+
58
+ # Override: Cannot create additional sessions in stdio transport.
59
+ #
60
+ # @param session_id [String, nil] The session ID (ignored)
61
+ # @return [Session] The global session
62
+ def create_session(_session_id = nil)
63
+ # For stdio, always return the existing global session
64
+ global_session
65
+ end
66
+
67
+ # Override: Cannot terminate the global session while transport is running.
68
+ #
69
+ # @param session_id [String] The session ID (ignored)
70
+ # @return [Boolean] Always false (session cannot be terminated individually)
71
+ def session_terminated?(_session_id)
72
+ # For stdio, the session is only terminated when the transport shuts down
73
+ false
74
+ end
75
+
76
+ # Override: Always returns 1 for the single global session.
77
+ #
78
+ # @return [Integer] Always 1
79
+ def session_count
80
+ 1
81
+ end
82
+
83
+ # Override: Always returns the global session ID.
84
+ #
85
+ # @return [Array<String>] Array containing the global session ID
86
+ def active_session_ids
87
+ [GLOBAL_SESSION_ID]
88
+ end
89
+
90
+ # Override: Always returns true for the single session.
91
+ #
92
+ # @return [Boolean] Always true
93
+ def sessions?
94
+ true
95
+ end
96
+
97
+ # Gets all sessions for stdio transport (just the one global session).
98
+ #
99
+ # @return [Array<Session>] Array containing the global session
100
+ def all_sessions
101
+ [@global_session].compact
102
+ end
103
+
104
+ # Alias for global_session for compatibility with tests.
105
+ #
106
+ # @return [Session] The global session
107
+ def current_global_session
108
+ global_session
109
+ end
110
+
111
+ # Alias for global_session_or_create for compatibility with tests.
112
+ #
113
+ # @return [Session] The global session
114
+ def global_session_or_create_current
115
+ global_session_or_create
116
+ end
117
+
118
+ # Alias for all_sessions for compatibility with tests.
119
+ #
120
+ # @return [Array<Session>] Array containing the global session
121
+ def current_all_sessions
122
+ all_sessions
123
+ end
124
+
125
+ protected
126
+
127
+ # Override: Stdio doesn't need automatic cleanup since it has a single persistent session.
128
+ def auto_cleanup_enabled?
129
+ false
130
+ end
131
+
132
+ # Override: Returns metadata for stdio sessions.
133
+ def create_session_metadata
134
+ { session_type: :stdio_global, created_via: :transport_startup }
135
+ end
136
+
137
+ # Override: Stdio can always send messages (single session assumption).
138
+ def can_send_message_to_session?(_session)
139
+ true
140
+ end
141
+
142
+ # Override: Sends messages via the transport's notification mechanism.
143
+ def message_sent_to_session?(_session, message)
144
+ # For stdio, we send notifications directly via the transport
145
+ @transport.send_notification(message["method"], message["params"])
146
+ true
147
+ end
148
+
149
+ # Override: Stdio broadcasts to the single session (same as regular send).
150
+ def broadcast_message(message)
151
+ message_sent_to_session?(@global_session, message) ? 1 : 0
152
+ end
153
+
154
+ private
155
+
156
+ # Creates the single global session for stdio transport.
157
+ #
158
+ # @return [BaseSessionManager::Session] The global session
159
+ def create_global_session
160
+ now = Time.now
161
+
162
+ # Create VectorMCP session context with minimal request context
163
+ request_context = VectorMCP::RequestContext.minimal("stdio")
164
+ session_context = VectorMCP::Session.new(@transport.server, @transport, id: GLOBAL_SESSION_ID, request_context: request_context)
165
+
166
+ # Create internal session record using base session manager struct
167
+ session = BaseSessionManager::Session.new(
168
+ GLOBAL_SESSION_ID,
169
+ session_context,
170
+ now,
171
+ now,
172
+ create_session_metadata
173
+ )
174
+
175
+ @sessions[GLOBAL_SESSION_ID] = session
176
+ logger.info { "Global stdio session created: #{GLOBAL_SESSION_ID}" }
177
+ session
178
+ end
179
+ end
180
+ end
181
+ end
@@ -231,8 +231,46 @@ module VectorMCP
231
231
  content
232
232
  end
233
233
 
234
+ # Extracts HTTP headers from a Rack environment hash.
235
+ # Converts Rack's HTTP_* environment variables to proper HTTP header names.
236
+ # @param env [Hash] The Rack environment hash.
237
+ # @return [Hash] Normalized headers with proper casing.
238
+ def extract_headers_from_rack_env(env)
239
+ headers = {}
240
+ return headers if env.nil?
241
+
242
+ env.each do |key, value|
243
+ next unless key.start_with?("HTTP_")
244
+
245
+ # Convert HTTP_X_API_KEY to X-API-Key format
246
+ header_name = key[5..].split("_").map do |part|
247
+ case part.upcase
248
+ when "API" then "API" # Keep API in all caps
249
+ else part.capitalize
250
+ end
251
+ end.join("-")
252
+ headers[header_name] = value
253
+ end
254
+
255
+ # Add special headers
256
+ headers["Authorization"] = env["HTTP_AUTHORIZATION"] if env["HTTP_AUTHORIZATION"]
257
+ headers["Content-Type"] = env["CONTENT_TYPE"] if env["CONTENT_TYPE"]
258
+ headers
259
+ end
260
+
261
+ # Extracts query parameters from a Rack environment hash.
262
+ # Parses the QUERY_STRING into a hash of parameters.
263
+ # @param env [Hash] The Rack environment hash.
264
+ # @return [Hash] Normalized parameters as key-value pairs.
265
+ def extract_params_from_rack_env(env)
266
+ params = {}
267
+ params = URI.decode_www_form(env["QUERY_STRING"]).to_h if env && env["QUERY_STRING"]
268
+ params
269
+ end
270
+
234
271
  module_function :looks_like_image_file_path?, :binary_image_data?,
235
272
  :file_path_to_image_content, :binary_image_to_content,
236
- :validate_and_enhance_image_content
273
+ :validate_and_enhance_image_content, :extract_headers_from_rack_env,
274
+ :extract_params_from_rack_env
237
275
  end
238
276
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module VectorMCP
4
4
  # The current version of the VectorMCP gem.
5
- VERSION = "0.3.1"
5
+ VERSION = "0.3.3"
6
6
  end
data/lib/vector_mcp.rb CHANGED
@@ -11,15 +11,16 @@ require_relative "vector_mcp/util"
11
11
  require_relative "vector_mcp/image_util"
12
12
  require_relative "vector_mcp/handlers/core"
13
13
  require_relative "vector_mcp/transport/stdio"
14
- # require_relative "vector_mcp/transport/sse" # Load on demand to avoid async dependencies
15
- require_relative "vector_mcp/logging"
14
+ # require_relative "vector_mcp/transport/sse" # Load on demand
15
+ require_relative "vector_mcp/logger"
16
+ require_relative "vector_mcp/middleware"
16
17
  require_relative "vector_mcp/server"
17
18
 
18
19
  # The VectorMCP module provides a full-featured, opinionated Ruby implementation
19
20
  # of the **Model Context Protocol (MCP)**. It gives developers everything needed
20
21
  # to spin up an MCP-compatible server—including:
21
22
  #
22
- # * **Transport adapters** (synchronous `stdio` or asynchronous HTTP + SSE)
23
+ # * **Transport adapters** (synchronous `stdio` or HTTP + SSE)
23
24
  # * **High-level abstractions** for *tools*, *resources*, and *prompts*
24
25
  # * **JSON-RPC 2.0** message handling with sensible defaults and detailed
25
26
  # error reporting helpers
@@ -45,44 +46,18 @@ require_relative "vector_mcp/server"
45
46
  # order to serve multiple concurrent clients over HTTP.
46
47
  #
47
48
  module VectorMCP
48
- # @return [Logger] the shared logger instance for the library.
49
- @logger = Logger.new($stderr, level: Logger::INFO, progname: "VectorMCP")
50
-
51
- # @return [VectorMCP::Logging::Core] the new structured logging system
52
- @logging_core = nil
53
-
54
49
  class << self
55
- # @!attribute [r] logger
56
- # @return [Logger] the shared logger instance for the library (legacy compatibility).
57
- def logger
58
- if @logging_core
59
- @logging_core.legacy_logger
60
- else
61
- @logger
62
- end
63
- end
64
-
65
- # Initialize the new structured logging system
66
- # @param config [Hash, VectorMCP::Logging::Configuration] logging configuration
67
- # @return [VectorMCP::Logging::Core] the logging core instance
68
- def setup_logging(config = {})
69
- configuration = config.is_a?(Logging::Configuration) ? config : Logging::Configuration.new(config)
70
- @logging_core = Logging::Core.new(configuration)
71
- end
72
-
73
50
  # Get a component-specific logger
74
51
  # @param component [String, Symbol] the component name
75
- # @return [VectorMCP::Logging::Component] component logger
52
+ # @return [VectorMCP::Logger] component logger
76
53
  def logger_for(component)
77
- setup_logging unless @logging_core
78
- @logging_core.logger_for(component)
54
+ Logger.for(component)
79
55
  end
80
56
 
81
- # Configure the logging system
82
- # @yield [VectorMCP::Logging::Configuration] configuration block
83
- def configure_logging(&)
84
- setup_logging unless @logging_core
85
- @logging_core.configure(&)
57
+ # Get the default logger
58
+ # @return [VectorMCP::Logger] default logger
59
+ def logger
60
+ @logger ||= Logger.for("vectormcp")
86
61
  end
87
62
 
88
63
  # Creates a new {VectorMCP::Server} instance. This is a **thin wrapper** around
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vector_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergio Bayona
@@ -66,47 +66,47 @@ dependencies:
66
66
  - !ruby/object:Gem::Version
67
67
  version: '3.0'
68
68
  - !ruby/object:Gem::Dependency
69
- name: puma
69
+ name: jwt
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '6.4'
74
+ version: '2.7'
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '6.4'
81
+ version: '2.7'
82
82
  - !ruby/object:Gem::Dependency
83
- name: rack
83
+ name: puma
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
- version: '3.0'
88
+ version: '6.4'
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
- version: '3.0'
95
+ version: '6.4'
96
96
  - !ruby/object:Gem::Dependency
97
- name: jwt
97
+ name: rack
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: '2.7'
102
+ version: '3.0'
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '2.7'
109
+ version: '3.0'
110
110
  description: A Ruby gem implementing the Model Context Protocol (MCP) server-side
111
111
  specification. Provides a framework for creating MCP servers that expose tools,
112
112
  resources, prompts, and roots to LLM clients with comprehensive security features,
@@ -129,19 +129,13 @@ files:
129
129
  - lib/vector_mcp/errors.rb
130
130
  - lib/vector_mcp/handlers/core.rb
131
131
  - lib/vector_mcp/image_util.rb
132
- - lib/vector_mcp/logging.rb
133
- - lib/vector_mcp/logging/component.rb
134
- - lib/vector_mcp/logging/configuration.rb
135
- - lib/vector_mcp/logging/constants.rb
136
- - lib/vector_mcp/logging/core.rb
137
- - lib/vector_mcp/logging/filters/component.rb
138
- - lib/vector_mcp/logging/filters/level.rb
139
- - lib/vector_mcp/logging/formatters/base.rb
140
- - lib/vector_mcp/logging/formatters/json.rb
141
- - lib/vector_mcp/logging/formatters/text.rb
142
- - lib/vector_mcp/logging/outputs/base.rb
143
- - lib/vector_mcp/logging/outputs/console.rb
144
- - lib/vector_mcp/logging/outputs/file.rb
132
+ - lib/vector_mcp/logger.rb
133
+ - lib/vector_mcp/middleware.rb
134
+ - lib/vector_mcp/middleware/base.rb
135
+ - lib/vector_mcp/middleware/context.rb
136
+ - lib/vector_mcp/middleware/hook.rb
137
+ - lib/vector_mcp/middleware/manager.rb
138
+ - lib/vector_mcp/request_context.rb
145
139
  - lib/vector_mcp/sampling/request.rb
146
140
  - lib/vector_mcp/sampling/result.rb
147
141
  - lib/vector_mcp/security.rb
@@ -157,12 +151,19 @@ files:
157
151
  - lib/vector_mcp/server/message_handling.rb
158
152
  - lib/vector_mcp/server/registry.rb
159
153
  - lib/vector_mcp/session.rb
154
+ - lib/vector_mcp/transport/base_session_manager.rb
155
+ - lib/vector_mcp/transport/http_stream.rb
156
+ - lib/vector_mcp/transport/http_stream/event_store.rb
157
+ - lib/vector_mcp/transport/http_stream/session_manager.rb
158
+ - lib/vector_mcp/transport/http_stream/stream_handler.rb
160
159
  - lib/vector_mcp/transport/sse.rb
161
160
  - lib/vector_mcp/transport/sse/client_connection.rb
162
161
  - lib/vector_mcp/transport/sse/message_handler.rb
163
162
  - lib/vector_mcp/transport/sse/puma_config.rb
164
163
  - lib/vector_mcp/transport/sse/stream_manager.rb
164
+ - lib/vector_mcp/transport/sse_session_manager.rb
165
165
  - lib/vector_mcp/transport/stdio.rb
166
+ - lib/vector_mcp/transport/stdio_session_manager.rb
166
167
  - lib/vector_mcp/util.rb
167
168
  - lib/vector_mcp/version.rb
168
169
  homepage: https://github.com/sergiobayona/vector_mcp
@@ -180,7 +181,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
180
181
  requirements:
181
182
  - - ">="
182
183
  - !ruby/object:Gem::Version
183
- version: 3.1.0
184
+ version: 3.0.6
184
185
  required_rubygems_version: !ruby/object:Gem::Requirement
185
186
  requirements:
186
187
  - - ">="
@@ -1,131 +0,0 @@
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