vector_mcp 0.3.4 → 0.5.0

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +147 -337
  4. data/lib/vector_mcp/definitions.rb +30 -0
  5. data/lib/vector_mcp/handlers/core.rb +78 -81
  6. data/lib/vector_mcp/image_util.rb +34 -11
  7. data/lib/vector_mcp/middleware/anonymizer.rb +186 -0
  8. data/lib/vector_mcp/middleware/base.rb +1 -5
  9. data/lib/vector_mcp/middleware/context.rb +11 -1
  10. data/lib/vector_mcp/middleware/hook.rb +7 -24
  11. data/lib/vector_mcp/middleware.rb +26 -9
  12. data/lib/vector_mcp/rails/tool.rb +85 -0
  13. data/lib/vector_mcp/request_context.rb +1 -1
  14. data/lib/vector_mcp/security/auth_manager.rb +12 -13
  15. data/lib/vector_mcp/security/auth_result.rb +33 -0
  16. data/lib/vector_mcp/security/authorization.rb +5 -9
  17. data/lib/vector_mcp/security/middleware.rb +2 -2
  18. data/lib/vector_mcp/security/session_context.rb +11 -27
  19. data/lib/vector_mcp/security/strategies/api_key.rb +1 -5
  20. data/lib/vector_mcp/security/strategies/custom.rb +10 -37
  21. data/lib/vector_mcp/security/strategies/jwt_token.rb +1 -10
  22. data/lib/vector_mcp/server/capabilities.rb +22 -32
  23. data/lib/vector_mcp/server/message_handling.rb +21 -14
  24. data/lib/vector_mcp/server/registry.rb +102 -120
  25. data/lib/vector_mcp/server.rb +98 -57
  26. data/lib/vector_mcp/session.rb +5 -3
  27. data/lib/vector_mcp/token_store.rb +80 -0
  28. data/lib/vector_mcp/tool.rb +221 -0
  29. data/lib/vector_mcp/transport/base_session_manager.rb +1 -17
  30. data/lib/vector_mcp/transport/http_stream/event_store.rb +29 -17
  31. data/lib/vector_mcp/transport/http_stream/session_manager.rb +41 -36
  32. data/lib/vector_mcp/transport/http_stream/stream_handler.rb +132 -47
  33. data/lib/vector_mcp/transport/http_stream.rb +242 -124
  34. data/lib/vector_mcp/util/token_sweeper.rb +74 -0
  35. data/lib/vector_mcp/version.rb +1 -1
  36. data/lib/vector_mcp.rb +8 -8
  37. metadata +8 -10
  38. data/lib/vector_mcp/transport/sse/client_connection.rb +0 -113
  39. data/lib/vector_mcp/transport/sse/message_handler.rb +0 -166
  40. data/lib/vector_mcp/transport/sse/puma_config.rb +0 -77
  41. data/lib/vector_mcp/transport/sse/stream_manager.rb +0 -92
  42. data/lib/vector_mcp/transport/sse.rb +0 -377
  43. data/lib/vector_mcp/transport/sse_session_manager.rb +0 -188
  44. data/lib/vector_mcp/transport/stdio.rb +0 -473
  45. data/lib/vector_mcp/transport/stdio_session_manager.rb +0 -181
@@ -29,6 +29,38 @@ module VectorMCP
29
29
  self
30
30
  end
31
31
 
32
+ # Registers one or more class-based tool definitions with the server.
33
+ #
34
+ # Each argument must be a subclass of +VectorMCP::Tool+ that declares
35
+ # its metadata via the class-level DSL (+tool_name+, +description+,
36
+ # +param+) and implements +#call+.
37
+ #
38
+ # @param tool_classes [Array<Class>] One or more +VectorMCP::Tool+ subclasses.
39
+ # @return [self] The server instance, for chaining.
40
+ # @raise [ArgumentError] If any argument is not a +VectorMCP::Tool+ subclass.
41
+ #
42
+ # @example Register a single tool
43
+ # server.register(ListProviders)
44
+ #
45
+ # @example Register multiple tools
46
+ # server.register(ListProviders, CreateProvider, UpdateProvider)
47
+ def register(*tool_classes)
48
+ tool_classes.each do |tool_class|
49
+ unless tool_class.is_a?(Class) && tool_class < VectorMCP::Tool
50
+ raise ArgumentError, "#{tool_class.inspect} is not a VectorMCP::Tool subclass"
51
+ end
52
+
53
+ definition = tool_class.to_definition
54
+ register_tool(
55
+ name: definition.name,
56
+ description: definition.description,
57
+ input_schema: definition.input_schema,
58
+ &definition.handler
59
+ )
60
+ end
61
+ self
62
+ end
63
+
32
64
  # Registers a new resource with the server.
33
65
  #
34
66
  # @param uri [String, URI] The unique URI for the resource.
@@ -99,114 +131,74 @@ module VectorMCP
99
131
  end
100
132
 
101
133
  # Helper method to register an image resource from a file path.
134
+ # Thin wrapper: delegates schema-building to Definitions::Resource.from_image_file,
135
+ # then stores the result via register_resource.
102
136
  #
103
137
  # @param uri [String] Unique URI for the resource.
104
138
  # @param file_path [String] Path to the image file.
105
139
  # @param name [String, nil] Human-readable name (auto-generated if nil).
106
140
  # @param description [String, nil] Description (auto-generated if nil).
107
- # @return [VectorMCP::Definitions::Resource] The registered resource.
141
+ # @return [self]
108
142
  # @raise [ArgumentError] If the file doesn't exist or isn't a valid image.
109
143
  def register_image_resource(uri:, file_path:, name: nil, description: nil)
110
144
  resource = VectorMCP::Definitions::Resource.from_image_file(
111
- uri: uri,
112
- file_path: file_path,
113
- name: name,
114
- description: description
115
- )
116
-
117
- register_resource(
118
- uri: resource.uri,
119
- name: resource.name,
120
- description: resource.description,
121
- mime_type: resource.mime_type,
122
- &resource.handler
145
+ uri: uri, file_path: file_path, name: name, description: description
123
146
  )
147
+ register_resource(uri: resource.uri, name: resource.name,
148
+ description: resource.description, mime_type: resource.mime_type, &resource.handler)
124
149
  end
125
150
 
126
151
  # Helper method to register an image resource from binary data.
152
+ # Thin wrapper: delegates to Definitions::Resource.from_image_data.
127
153
  #
128
154
  # @param uri [String] Unique URI for the resource.
129
155
  # @param image_data [String] Binary image data.
130
156
  # @param name [String] Human-readable name.
131
157
  # @param description [String, nil] Description (auto-generated if nil).
132
158
  # @param mime_type [String, nil] MIME type (auto-detected if nil).
133
- # @return [VectorMCP::Definitions::Resource] The registered resource.
134
- # @raise [ArgumentError] If the data isn't valid image data.
159
+ # @return [self]
135
160
  def register_image_resource_from_data(uri:, image_data:, name:, description: nil, mime_type: nil)
136
161
  resource = VectorMCP::Definitions::Resource.from_image_data(
137
- uri: uri,
138
- image_data: image_data,
139
- name: name,
140
- description: description,
141
- mime_type: mime_type
142
- )
143
-
144
- register_resource(
145
- uri: resource.uri,
146
- name: resource.name,
147
- description: resource.description,
148
- mime_type: resource.mime_type,
149
- &resource.handler
162
+ uri: uri, image_data: image_data, name: name, description: description, mime_type: mime_type
150
163
  )
164
+ register_resource(uri: resource.uri, name: resource.name,
165
+ description: resource.description, mime_type: resource.mime_type, &resource.handler)
151
166
  end
152
167
 
153
168
  # Helper method to register a tool that accepts image inputs.
169
+ # Thin wrapper: delegates schema-building to Definitions::Tool.with_image_support.
154
170
  #
155
171
  # @param name [String] Unique name for the tool.
156
172
  # @param description [String] Human-readable description.
157
173
  # @param image_parameter [String] Name of the image parameter (default: "image").
158
174
  # @param additional_parameters [Hash] Additional JSON Schema properties.
159
175
  # @param required_parameters [Array<String>] List of required parameter names.
160
- # @param block [Proc] The tool handler block.
161
- # @return [VectorMCP::Definitions::Tool] The registered tool.
162
- def register_image_tool(name:, description:, image_parameter: "image", additional_parameters: {}, required_parameters: [], &block)
163
- # Build the input schema with image support
164
- image_property = {
165
- type: "string",
166
- description: "Base64 encoded image data or file path to image",
167
- contentEncoding: "base64",
168
- contentMediaType: "image/*"
169
- }
170
-
171
- properties = { image_parameter => image_property }.merge(additional_parameters)
172
-
173
- input_schema = {
174
- type: "object",
175
- properties: properties,
176
- required: required_parameters
177
- }
178
-
179
- register_tool(
180
- name: name,
181
- description: description,
182
- input_schema: input_schema,
183
- &block
176
+ # @return [self]
177
+ def register_image_tool(name:, description:, image_parameter: "image",
178
+ additional_parameters: {}, required_parameters: [], &handler)
179
+ tool = VectorMCP::Definitions::Tool.with_image_support(
180
+ name: name, description: description, image_parameter: image_parameter,
181
+ additional_parameters: additional_parameters, required_parameters: required_parameters, &handler
184
182
  )
183
+ register_tool(name: tool.name, description: tool.description,
184
+ input_schema: tool.input_schema, &tool.handler)
185
185
  end
186
186
 
187
187
  # Helper method to register a prompt that supports image arguments.
188
+ # Thin wrapper: delegates to Definitions::Prompt.with_image_support.
188
189
  #
189
190
  # @param name [String] Unique name for the prompt.
190
191
  # @param description [String] Human-readable description.
191
192
  # @param image_argument [String] Name of the image argument (default: "image").
192
193
  # @param additional_arguments [Array<Hash>] Additional prompt arguments.
193
- # @param block [Proc] The prompt handler block.
194
- # @return [VectorMCP::Definitions::Prompt] The registered prompt.
195
- def register_image_prompt(name:, description:, image_argument: "image", additional_arguments: [], &block)
194
+ # @return [self]
195
+ def register_image_prompt(name:, description:, image_argument: "image", additional_arguments: [], &handler)
196
196
  prompt = VectorMCP::Definitions::Prompt.with_image_support(
197
- name: name,
198
- description: description,
199
- image_argument_name: image_argument,
200
- additional_arguments: additional_arguments,
201
- &block
202
- )
203
-
204
- register_prompt(
205
- name: prompt.name,
206
- description: prompt.description,
207
- arguments: prompt.arguments,
208
- &prompt.handler
197
+ name: name, description: description, image_argument_name: image_argument,
198
+ additional_arguments: additional_arguments, &handler
209
199
  )
200
+ register_prompt(name: prompt.name, description: prompt.description,
201
+ arguments: prompt.arguments, &prompt.handler)
210
202
  end
211
203
 
212
204
  private
@@ -230,6 +222,34 @@ module VectorMCP
230
222
  raise ArgumentError, "Invalid input_schema structure: #{e.message}"
231
223
  end
232
224
 
225
+ # Schema for a single prompt argument definition. Each entry names the
226
+ # required/optional key, whether it is required, and the rule that validates
227
+ # its value. Rules return nil on success or an error message fragment.
228
+ PROMPT_ARG_SCHEMA = {
229
+ "name" => {
230
+ required: true,
231
+ missing_message: "missing :name",
232
+ rule: lambda { |v|
233
+ next "must be a String or Symbol. Found: #{v.class}" unless v.is_a?(String) || v.is_a?(Symbol)
234
+
235
+ "cannot be empty." if v.to_s.strip.empty?
236
+ }
237
+ },
238
+ "description" => {
239
+ required: false,
240
+ rule: ->(v) { "must be a String if provided. Found: #{v.class}" unless v.nil? || v.is_a?(String) }
241
+ },
242
+ "required" => {
243
+ required: false,
244
+ rule: ->(v) { "must be true or false if provided. Found: #{v.inspect}" unless [true, false].include?(v) }
245
+ },
246
+ "type" => {
247
+ required: false,
248
+ rule: ->(v) { "must be a String if provided (e.g., JSON schema type). Found: #{v.class}" unless v.nil? || v.is_a?(String) }
249
+ }
250
+ }.freeze
251
+ private_constant :PROMPT_ARG_SCHEMA
252
+
233
253
  # Validates the structure of the `arguments` array provided to {#register_prompt}.
234
254
  # @api private
235
255
  def validate_prompt_arguments(argument_defs)
@@ -238,75 +258,37 @@ module VectorMCP
238
258
  argument_defs.each_with_index { |arg, idx| validate_single_prompt_argument(arg, idx) }
239
259
  end
240
260
 
241
- # Defines the keys allowed in a prompt argument definition hash.
242
- ALLOWED_PROMPT_ARG_KEYS = %w[name description required type].freeze
243
- private_constant :ALLOWED_PROMPT_ARG_KEYS
244
-
245
- # Validates a single prompt argument definition hash.
261
+ # Validates a single prompt argument definition hash against PROMPT_ARG_SCHEMA.
246
262
  # @api private
247
263
  def validate_single_prompt_argument(arg, idx)
248
264
  raise ArgumentError, "Prompt argument definition at index #{idx} must be a Hash. Found: #{arg.class}" unless arg.is_a?(Hash)
249
265
 
250
- validate_prompt_arg_name!(arg, idx)
251
- validate_prompt_arg_description!(arg, idx)
252
- validate_prompt_arg_required_flag!(arg, idx)
253
- validate_prompt_arg_type!(arg, idx)
254
- validate_prompt_arg_unknown_keys!(arg, idx)
255
- end
256
-
257
- # Validates the :name key of a prompt argument definition.
258
- # @api private
259
- def validate_prompt_arg_name!(arg, idx)
260
- name_val = arg[:name] || arg["name"]
261
- raise ArgumentError, "Prompt argument at index #{idx} missing :name" if name_val.nil?
262
- unless name_val.is_a?(String) || name_val.is_a?(Symbol)
263
- raise ArgumentError, "Prompt argument :name at index #{idx} must be a String or Symbol. Found: #{name_val.class}"
264
- end
265
- raise ArgumentError, "Prompt argument :name at index #{idx} cannot be empty." if name_val.to_s.strip.empty?
266
- end
267
-
268
- # Validates the :description key of a prompt argument definition.
269
- # @api private
270
- def validate_prompt_arg_description!(arg, idx)
271
- return unless arg.key?(:description) || arg.key?("description")
272
-
273
- desc_val = arg[:description] || arg["description"]
274
- return if desc_val.nil? || desc_val.is_a?(String)
275
-
276
- raise ArgumentError, "Prompt argument :description at index #{idx} must be a String if provided. Found: #{desc_val.class}"
277
- end
278
-
279
- # Validates the :required key of a prompt argument definition.
280
- # @api private
281
- def validate_prompt_arg_required_flag!(arg, idx)
282
- return unless arg.key?(:required) || arg.key?("required")
283
-
284
- req_val = arg[:required] || arg["required"]
285
- return if [true, false].include?(req_val)
286
-
287
- raise ArgumentError, "Prompt argument :required at index #{idx} must be true or false if provided. Found: #{req_val.inspect}"
266
+ PROMPT_ARG_SCHEMA.each { |key, spec| validate_prompt_arg_field(arg, idx, key, spec) }
267
+ validate_prompt_arg_unknown_keys(arg, idx)
288
268
  end
289
269
 
290
- # Validates the :type key of a prompt argument definition.
270
+ # Validates a single field of a prompt argument hash against its schema spec.
291
271
  # @api private
292
- def validate_prompt_arg_type!(arg, idx)
293
- return unless arg.key?(:type) || arg.key?("type")
272
+ def validate_prompt_arg_field(arg, idx, key, spec)
273
+ present = arg.key?(key.to_sym) || arg.key?(key)
274
+ value = arg[key.to_sym] || arg[key]
294
275
 
295
- type_val = arg[:type] || arg["type"]
296
- return if type_val.nil? || type_val.is_a?(String)
276
+ raise ArgumentError, "Prompt argument at index #{idx} #{spec[:missing_message]}" if spec[:required] && value.nil?
277
+ return unless present
297
278
 
298
- raise ArgumentError, "Prompt argument :type at index #{idx} must be a String if provided (e.g., JSON schema type). Found: #{type_val.class}"
279
+ error_fragment = spec[:rule].call(value)
280
+ raise ArgumentError, "Prompt argument :#{key} at index #{idx} #{error_fragment}" if error_fragment
299
281
  end
300
282
 
301
- # Checks for any unknown keys in a prompt argument definition.
283
+ # Checks a prompt argument hash for keys outside PROMPT_ARG_SCHEMA.
302
284
  # @api private
303
- def validate_prompt_arg_unknown_keys!(arg, idx)
304
- unknown_keys = arg.transform_keys(&:to_s).keys - ALLOWED_PROMPT_ARG_KEYS
285
+ def validate_prompt_arg_unknown_keys(arg, idx)
286
+ unknown_keys = arg.transform_keys(&:to_s).keys - PROMPT_ARG_SCHEMA.keys
305
287
  return if unknown_keys.empty?
306
288
 
307
289
  raise ArgumentError,
308
290
  "Prompt argument definition at index #{idx} contains unknown keys: #{unknown_keys.join(", ")}. " \
309
- "Allowed: #{ALLOWED_PROMPT_ARG_KEYS.join(", ")}."
291
+ "Allowed: #{PROMPT_ARG_SCHEMA.keys.join(", ")}."
310
292
  end
311
293
  end
312
294
  end
@@ -5,8 +5,6 @@ require "logger"
5
5
  require_relative "definitions"
6
6
  require_relative "session"
7
7
  require_relative "errors"
8
- require_relative "transport/stdio" # Default transport
9
- # require_relative "transport/sse" # Load on demand to avoid async dependencies
10
8
  require_relative "handlers/core" # Default handlers
11
9
  require_relative "util" # Needed if not using Handlers::Core
12
10
  require_relative "server/registry"
@@ -26,7 +24,7 @@ module VectorMCP
26
24
  # It manages tools, resources, prompts, and handles the MCP message lifecycle.
27
25
  #
28
26
  # A server instance is typically initialized, configured with capabilities (tools,
29
- # resources, prompts), and then run with a chosen transport mechanism (e.g., Stdio, SSE).
27
+ # resources, prompts), and then run with a chosen transport mechanism (e.g., HttpStream).
30
28
  #
31
29
  # @example Creating and running a simple server
32
30
  # server = VectorMCP::Server.new(name: "MySimpleServer", version: "1.0")
@@ -39,7 +37,7 @@ module VectorMCP
39
37
  # args["message"]
40
38
  # end
41
39
  #
42
- # server.run(transport: :stdio) # Runs with Stdio transport by default
40
+ # server.run # Runs with HttpStream transport by default
43
41
  #
44
42
  # @!attribute [r] logger
45
43
  # @return [Logger] The logger instance for this server.
@@ -68,10 +66,13 @@ module VectorMCP
68
66
  include MessageHandling
69
67
 
70
68
  # The specific version of the Model Context Protocol this server implements.
71
- PROTOCOL_VERSION = "2024-11-05"
69
+ PROTOCOL_VERSION = "2025-11-25"
70
+
71
+ # All protocol versions this server accepts via the MCP-Protocol-Version header.
72
+ SUPPORTED_PROTOCOL_VERSIONS = %w[2025-11-25 2025-03-26 2024-11-05].freeze
72
73
 
73
74
  attr_reader :logger, :name, :version, :protocol_version, :tools, :resources, :prompts, :roots, :in_flight_requests,
74
- :auth_manager, :authorization, :security_middleware, :middleware_manager
75
+ :auth_manager, :authorization, :security_middleware, :middleware_manager, :oauth_resource_metadata_url
75
76
  attr_accessor :transport
76
77
 
77
78
  # Initializes a new VectorMCP server.
@@ -121,6 +122,7 @@ module VectorMCP
121
122
  @auth_manager = Security::AuthManager.new
122
123
  @authorization = Security::Authorization.new
123
124
  @security_middleware = Security::Middleware.new(@auth_manager, @authorization)
125
+ @oauth_resource_metadata_url = nil
124
126
 
125
127
  # Initialize middleware manager
126
128
  @middleware_manager = Middleware::Manager.new
@@ -134,33 +136,19 @@ module VectorMCP
134
136
 
135
137
  # Runs the server using the specified transport mechanism.
136
138
  #
137
- # @param transport [:stdio, :sse, :http_stream, VectorMCP::Transport::Base] The transport to use.
138
- # Can be a symbol (`:stdio`, `:sse`, `:http_stream`) or an initialized transport instance.
139
- # If a symbol is provided, the method will instantiate the corresponding transport class.
140
- # If `:sse` is chosen, it uses Puma as the HTTP server (deprecated).
141
- # If `:http_stream` is chosen, it uses the MCP-compliant streamable HTTP transport.
142
- # @param options [Hash] Transport-specific options (e.g., `:host`, `:port` for HTTP transports).
139
+ # @param transport [:http_stream, VectorMCP::Transport::Base] The transport to use.
140
+ # Can be the symbol `:http_stream` or an initialized transport instance.
141
+ # If `:http_stream` is provided, the method will instantiate the MCP-compliant streamable HTTP transport.
142
+ # @param options [Hash] Transport-specific options (e.g., `:host`, `:port`).
143
143
  # These are passed to the transport's constructor if a symbol is provided for `transport`.
144
144
  # @return [void]
145
145
  # @raise [ArgumentError] if an unsupported transport symbol is given.
146
- # @raise [NotImplementedError] if `:sse` transport is specified (currently a placeholder).
147
- def run(transport: :stdio, **options)
146
+ def run(transport: :http_stream, **)
148
147
  active_transport = case transport
149
- when :stdio
150
- VectorMCP::Transport::Stdio.new(self, **options)
151
- when :sse
152
- begin
153
- require_relative "transport/sse"
154
- logger.warn("SSE transport is deprecated. Please use :http_stream instead.")
155
- VectorMCP::Transport::SSE.new(self, **options)
156
- rescue LoadError => e
157
- logger.fatal("SSE transport requires additional dependencies.")
158
- raise NotImplementedError, "SSE transport dependencies not available: #{e.message}"
159
- end
160
148
  when :http_stream
161
149
  begin
162
150
  require_relative "transport/http_stream"
163
- VectorMCP::Transport::HttpStream.new(self, **options)
151
+ VectorMCP::Transport::HttpStream.new(self, **)
164
152
  rescue LoadError => e
165
153
  logger.fatal("HttpStream transport requires additional dependencies.")
166
154
  raise NotImplementedError, "HttpStream transport dependencies not available: #{e.message}"
@@ -176,33 +164,43 @@ module VectorMCP
176
164
  active_transport.run
177
165
  end
178
166
 
167
+ # Returns the MCP server as a Rack application suitable for mounting inside
168
+ # another Rack-based framework (e.g., Rails, Sinatra).
169
+ #
170
+ # Unlike {#run}, this method does NOT start its own HTTP server or block.
171
+ # The returned object responds to `#call(env)` and can be mounted directly:
172
+ #
173
+ # # config/routes.rb (Rails)
174
+ # mount MCP_APP => "/mcp"
175
+ #
176
+ # Call `server.transport.stop` on application shutdown to clean up resources.
177
+ #
178
+ # @param options [Hash] Transport options (e.g., :session_timeout, :event_retention, :allowed_origins)
179
+ # @return [VectorMCP::Transport::HttpStream] A Rack-compatible app
180
+ def rack_app(**)
181
+ require_relative "transport/http_stream"
182
+ active_transport = VectorMCP::Transport::HttpStream.new(self, mounted: true, **)
183
+ self.transport = active_transport
184
+ active_transport
185
+ end
186
+
179
187
  # --- Security Configuration ---
180
188
 
181
189
  # Enable authentication with specified strategy and configuration
182
190
  # @param strategy [Symbol] the authentication strategy (:api_key, :jwt, :custom)
183
191
  # @param options [Hash] strategy-specific configuration options
192
+ # @option options [String] :resource_metadata_url OAuth 2.1 protected resource metadata URL
193
+ # (RFC 9728). When provided, unauthenticated requests to the HTTP Stream transport's MCP
194
+ # endpoint return HTTP 401 with a +WWW-Authenticate: Bearer+ header pointing at this URL,
195
+ # enabling MCP clients (e.g. Claude Desktop) to discover the authorization server and
196
+ # initiate an OAuth 2.1 flow. When omitted (default), auth failures continue to surface
197
+ # as JSON-RPC +-32401+ errors — existing behavior is preserved for non-OAuth deployments.
184
198
  # @return [void]
185
199
  def enable_authentication!(strategy: :api_key, **options, &block)
186
- # Clear existing strategies when switching to a new configuration
187
200
  clear_auth_strategies unless @auth_manager.strategies.empty?
188
-
201
+ extract_oauth_metadata!(options)
189
202
  @auth_manager.enable!(default_strategy: strategy)
190
-
191
- case strategy
192
- when :api_key
193
- add_api_key_auth(options[:keys] || [], allow_query_params: options[:allow_query_params] || false)
194
- when :jwt
195
- add_jwt_auth(options)
196
- when :custom
197
- handler = block || options[:handler]
198
- raise ArgumentError, "Custom authentication strategy requires a handler block" unless handler
199
-
200
- add_custom_auth(&handler)
201
-
202
- else
203
- raise ArgumentError, "Unknown authentication strategy: #{strategy}"
204
- end
205
-
203
+ register_auth_strategy(strategy, options, block || options.delete(:handler))
206
204
  @logger.info("Authentication enabled with strategy: #{strategy}")
207
205
  end
208
206
 
@@ -210,15 +208,16 @@ module VectorMCP
210
208
  # @return [void]
211
209
  def disable_authentication!
212
210
  @auth_manager.disable!
211
+ @oauth_resource_metadata_url = nil
213
212
  @logger.info("Authentication disabled")
214
213
  end
215
214
 
216
215
  # Enable authorization with optional policy configuration block
217
216
  # @param block [Proc] optional block for configuring authorization policies
218
217
  # @return [void]
219
- def enable_authorization!(&block)
218
+ def enable_authorization!(&)
220
219
  @authorization.enable!
221
- instance_eval(&block) if block_given?
220
+ instance_eval(&) if block_given?
222
221
  @logger.info("Authorization enabled")
223
222
  end
224
223
 
@@ -232,29 +231,29 @@ module VectorMCP
232
231
  # Add authorization policy for tools
233
232
  # @param block [Proc] policy block that receives (user, action, tool)
234
233
  # @return [void]
235
- def authorize_tools(&block)
236
- @authorization.add_policy(:tool, &block)
234
+ def authorize_tools(&)
235
+ @authorization.add_policy(:tool, &)
237
236
  end
238
237
 
239
238
  # Add authorization policy for resources
240
239
  # @param block [Proc] policy block that receives (user, action, resource)
241
240
  # @return [void]
242
- def authorize_resources(&block)
243
- @authorization.add_policy(:resource, &block)
241
+ def authorize_resources(&)
242
+ @authorization.add_policy(:resource, &)
244
243
  end
245
244
 
246
245
  # Add authorization policy for prompts
247
246
  # @param block [Proc] policy block that receives (user, action, prompt)
248
247
  # @return [void]
249
- def authorize_prompts(&block)
250
- @authorization.add_policy(:prompt, &block)
248
+ def authorize_prompts(&)
249
+ @authorization.add_policy(:prompt, &)
251
250
  end
252
251
 
253
252
  # Add authorization policy for roots
254
253
  # @param block [Proc] policy block that receives (user, action, root)
255
254
  # @return [void]
256
- def authorize_roots(&block)
257
- @authorization.add_policy(:root, &block)
255
+ def authorize_roots(&)
256
+ @authorization.add_policy(:root, &)
258
257
  end
259
258
 
260
259
  # Check if security features are enabled
@@ -311,6 +310,34 @@ module VectorMCP
311
310
 
312
311
  private
313
312
 
313
+ # Extract OAuth resource metadata URL from options before they reach strategy constructors
314
+ # @param options [Hash] the options hash (mutated — :resource_metadata_url is removed)
315
+ # @return [void]
316
+ def extract_oauth_metadata!(options)
317
+ @oauth_resource_metadata_url = options.delete(:resource_metadata_url)
318
+ warn_on_insecure_oauth_metadata_url(@oauth_resource_metadata_url)
319
+ end
320
+
321
+ # Register the appropriate auth strategy based on the strategy name
322
+ # @param strategy [Symbol] the strategy type
323
+ # @param options [Hash] strategy-specific options
324
+ # @param handler [Proc, nil] custom handler block (for :custom strategy)
325
+ # @return [void]
326
+ def register_auth_strategy(strategy, options, handler)
327
+ case strategy
328
+ when :api_key
329
+ add_api_key_auth(options[:keys] || [], allow_query_params: options[:allow_query_params] || false)
330
+ when :jwt
331
+ add_jwt_auth(options)
332
+ when :custom
333
+ raise ArgumentError, "Custom authentication strategy requires a handler block" unless handler
334
+
335
+ add_custom_auth(&handler)
336
+ else
337
+ raise ArgumentError, "Unknown authentication strategy: #{strategy}"
338
+ end
339
+ end
340
+
314
341
  # Add API key authentication strategy
315
342
  # @param keys [Array<String>] array of valid API keys
316
343
  # @param allow_query_params [Boolean] whether to accept API keys from query parameters
@@ -331,8 +358,8 @@ module VectorMCP
331
358
  # Add custom authentication strategy
332
359
  # @param handler [Proc] custom authentication handler block
333
360
  # @return [void]
334
- def add_custom_auth(&block)
335
- strategy = Security::Strategies::Custom.new(&block)
361
+ def add_custom_auth(&)
362
+ strategy = Security::Strategies::Custom.new(&)
336
363
  @auth_manager.add_strategy(:custom, strategy)
337
364
  end
338
365
 
@@ -343,11 +370,25 @@ module VectorMCP
343
370
  @auth_manager.remove_strategy(strategy_name)
344
371
  end
345
372
  end
373
+
374
+ # Emit a warning when the OAuth resource metadata URL is not HTTPS.
375
+ # We don't raise because local development against http://localhost is a valid use case.
376
+ # @param url [String, nil] the configured metadata URL
377
+ # @return [void]
378
+ def warn_on_insecure_oauth_metadata_url(url)
379
+ return if url.nil?
380
+ return if url.start_with?("https://")
381
+
382
+ @logger.warn do
383
+ "[SECURITY] resource_metadata_url is not HTTPS (#{url}). " \
384
+ "Use HTTPS in production; plaintext is only acceptable for local development."
385
+ end
386
+ end
346
387
  end
347
388
 
348
389
  module Transport
349
390
  # Dummy base class placeholder used only for argument validation in tests.
350
- # Real transport classes (e.g., Stdio, SSE) are separate concrete classes.
391
+ # Real transport classes (e.g., HttpStream) are separate concrete classes.
351
392
  class Base # :nodoc:
352
393
  end
353
394
  end
@@ -4,6 +4,7 @@ require_relative "sampling/request"
4
4
  require_relative "sampling/result"
5
5
  require_relative "errors"
6
6
  require_relative "request_context"
7
+ require_relative "security/session_context"
7
8
 
8
9
  module VectorMCP
9
10
  # Represents the state of a single client-server connection session in MCP.
@@ -17,7 +18,7 @@ module VectorMCP
17
18
  # @attr_reader request_context [RequestContext] The request context for this session.
18
19
  class Session
19
20
  attr_reader :server_info, :server_capabilities, :protocol_version, :client_info, :client_capabilities, :server, :transport, :id, :request_context
20
- attr_accessor :data # For user-defined session-specific storage
21
+ attr_accessor :data, :security_context # For user-defined session-specific storage and resolved auth context
21
22
 
22
23
  # Initializes a new session.
23
24
  #
@@ -33,6 +34,7 @@ module VectorMCP
33
34
  @client_info = nil
34
35
  @client_capabilities = nil
35
36
  @data = {} # Initialize user data hash
37
+ @security_context = Security::SessionContext.anonymous
36
38
  @logger = server.logger
37
39
 
38
40
  # Initialize request context
@@ -243,11 +245,11 @@ module VectorMCP
243
245
  send_request_kwargs = {}
244
246
  send_request_kwargs[:timeout] = timeout if timeout
245
247
 
246
- # For HTTP transport, we need to use send_request_to_session to target this specific session
248
+ # Prefer session-targeted request sending when available
247
249
  raw_result = if @transport.respond_to?(:send_request_to_session)
248
250
  @transport.send_request_to_session(@id, *send_request_args, **send_request_kwargs)
249
251
  else
250
- # Fallback to generic send_request for other transports
252
+ # Fallback to generic send_request for transports without session targeting
251
253
  @transport.send_request(*send_request_args, **send_request_kwargs)
252
254
  end
253
255