vector_mcp 0.3.4 → 0.4.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.
@@ -52,18 +52,21 @@ module VectorMCP
52
52
  # @raise [VectorMCP::ForbiddenError] if authorization fails.
53
53
  def self.call_tool(params, session, server)
54
54
  tool_name = params["name"]
55
- arguments = params["arguments"] || {}
56
-
57
55
  context = create_tool_context(tool_name, params, session, server)
58
- context = server.middleware_manager.execute_hooks(:before_tool_call, context)
59
- return handle_middleware_error(context) if context.error?
60
56
 
61
57
  begin
58
+ session_context = authenticate_session!(session, server, operation_type: :tool_call, operation_name: tool_name)
59
+
60
+ context = server.middleware_manager.execute_hooks(:before_tool_call, context)
61
+ return handle_middleware_error(context) if context.error?
62
+
63
+ tool_name = context.params["name"] || tool_name
64
+ arguments = context.params["arguments"] || {}
62
65
  tool = find_tool!(tool_name, server)
63
- security_result = validate_tool_security!(session, tool, server)
66
+ authorize_action!(session_context, :call, tool, server)
64
67
  validate_input_arguments!(tool_name, tool, arguments)
65
68
 
66
- result = execute_tool_handler(tool, arguments, security_result, session)
69
+ result = execute_tool_handler(tool, arguments, session)
67
70
  context.result = build_tool_result(result)
68
71
 
69
72
  context = server.middleware_manager.execute_hooks(:after_tool_call, context)
@@ -99,16 +102,19 @@ module VectorMCP
99
102
  # @raise [VectorMCP::ForbiddenError] if authorization fails.
100
103
  def self.read_resource(params, session, server)
101
104
  uri_s = params["uri"]
102
-
103
105
  context = create_resource_context(uri_s, params, session, server)
104
- context = server.middleware_manager.execute_hooks(:before_resource_read, context)
105
- return handle_middleware_error(context) if context.error?
106
106
 
107
107
  begin
108
+ session_context = authenticate_session!(session, server, operation_type: :resource_read, operation_name: uri_s)
109
+
110
+ context = server.middleware_manager.execute_hooks(:before_resource_read, context)
111
+ return handle_middleware_error(context) if context.error?
112
+
113
+ uri_s = context.params["uri"] || uri_s
108
114
  resource = find_resource!(uri_s, server)
109
- security_result = validate_resource_security!(session, resource, server)
115
+ authorize_action!(session_context, :read, resource, server)
110
116
 
111
- content_raw = execute_resource_handler(resource, params, security_result)
117
+ content_raw = execute_resource_handler(resource, context.params, session_context)
112
118
  contents = process_resource_content(content_raw, resource, uri_s)
113
119
 
114
120
  context.result = { contents: contents }
@@ -195,9 +201,11 @@ module VectorMCP
195
201
  return handle_middleware_error(context) if context.error?
196
202
 
197
203
  begin
204
+ context_params = context.params
205
+ prompt_name = context_params["name"] || prompt_name
198
206
  prompt = fetch_prompt(prompt_name, server)
199
207
 
200
- arguments = params["arguments"] || {}
208
+ arguments = context_params["arguments"] || {}
201
209
  validate_arguments!(prompt_name, prompt, arguments)
202
210
 
203
211
  # Call the registered handler after arguments were validated
@@ -491,15 +499,34 @@ module VectorMCP
491
499
  tool
492
500
  end
493
501
 
494
- # Validate tool security
495
- def self.validate_tool_security!(session, tool, server)
496
- security_result = check_tool_security(session, tool, server)
497
- handle_security_failure(security_result) unless security_result[:success]
498
- security_result
502
+ # Resolve the session authentication state before operation middleware runs.
503
+ def self.authenticate_session!(session, server, operation_type:, operation_name:)
504
+ request = extract_request_from_session(session)
505
+ context = create_auth_context(operation_type, operation_name, request, session, server)
506
+ context = server.middleware_manager.execute_hooks(:before_auth, context)
507
+ raise context.error if context.error?
508
+
509
+ request = context.params
510
+ session_context = if server.security_middleware.respond_to?(:authenticate_request)
511
+ server.security_middleware.authenticate_request(request)
512
+ else
513
+ legacy_result = server.security_middleware.process_request(request)
514
+ legacy_result[:session_context]
515
+ end
516
+ session.security_context = session_context if session.respond_to?(:security_context=)
517
+
518
+ auth_required = server.respond_to?(:auth_manager) ? server.auth_manager.required? : false
519
+ raise VectorMCP::UnauthorizedError, "Authentication required" if auth_required && !session_context.authenticated?
520
+
521
+ context.result = session_context
522
+ server.middleware_manager.execute_hooks(:after_auth, context)
523
+ session_context
524
+ rescue StandardError => e
525
+ handle_auth_error(e, context, server)
499
526
  end
500
527
 
501
528
  # Execute tool handler with proper arity handling
502
- def self.execute_tool_handler(tool, arguments, _security_result, session)
529
+ def self.execute_tool_handler(tool, arguments, session)
503
530
  if [1, -1].include?(tool.handler.arity)
504
531
  tool.handler.call(arguments)
505
532
  else
@@ -546,18 +573,24 @@ module VectorMCP
546
573
  end
547
574
 
548
575
  # Validate resource security
549
- def self.validate_resource_security!(session, resource, server)
550
- security_result = check_resource_security(session, resource, server)
551
- handle_security_failure(security_result) unless security_result[:success]
552
- security_result
576
+ def self.authorize_action!(session_context, action, resource, server)
577
+ authorized = if server.security_middleware.respond_to?(:authorize_action)
578
+ server.security_middleware.authorize_action(session_context, action, resource)
579
+ else
580
+ legacy_result = server.security_middleware.process_request({}, action: action, resource: resource)
581
+ legacy_result[:success]
582
+ end
583
+ return if authorized
584
+
585
+ raise VectorMCP::ForbiddenError, "Access denied"
553
586
  end
554
587
 
555
588
  # Execute resource handler with proper arity handling
556
- def self.execute_resource_handler(resource, params, security_result)
589
+ def self.execute_resource_handler(resource, params, session_context)
557
590
  if [1, -1].include?(resource.handler.arity)
558
591
  resource.handler.call(params)
559
592
  else
560
- resource.handler.call(params, security_result[:session_context])
593
+ resource.handler.call(params, session_context)
561
594
  end
562
595
  end
563
596
 
@@ -579,10 +612,32 @@ module VectorMCP
579
612
  context.result
580
613
  end
581
614
 
582
- private_class_method :handle_middleware_error, :create_tool_context, :find_tool!, :validate_tool_security!,
615
+ # Create middleware context for authentication hooks.
616
+ def self.create_auth_context(operation_type, operation_name, request, session, server)
617
+ VectorMCP::Middleware::Context.new(
618
+ operation_type: operation_type,
619
+ operation_name: operation_name,
620
+ params: request,
621
+ session: session,
622
+ server: server,
623
+ metadata: { start_time: Time.now }
624
+ )
625
+ end
626
+
627
+ # Run auth error hooks and re-raise the original error.
628
+ def self.handle_auth_error(error, context, server)
629
+ return raise error unless context
630
+
631
+ context.error = error
632
+ server.middleware_manager.execute_hooks(:on_auth_error, context)
633
+ raise error
634
+ end
635
+
636
+ private_class_method :handle_middleware_error, :create_tool_context, :find_tool!, :authenticate_session!,
583
637
  :execute_tool_handler, :build_tool_result, :handle_tool_error, :create_resource_context,
584
- :find_resource!, :validate_resource_security!, :execute_resource_handler,
585
- :process_resource_content, :handle_resource_error
638
+ :find_resource!, :authorize_action!, :execute_resource_handler,
639
+ :process_resource_content, :handle_resource_error, :create_auth_context,
640
+ :handle_auth_error
586
641
  end
587
642
  end
588
643
  end
@@ -231,29 +231,52 @@ module VectorMCP
231
231
  # base_directory: "/app/uploads"
232
232
  # )
233
233
  def file_to_mcp_image_content(file_path, validate: true, max_size: DEFAULT_MAX_SIZE, base_directory: nil)
234
- validate_path_safety!(file_path, base_directory) if base_directory
234
+ validated_path = validate_path_safety!(file_path, base_directory)
235
235
 
236
- raise ArgumentError, "Image file not found: #{file_path}" unless File.exist?(file_path)
236
+ raise ArgumentError, "Image file not found: #{validated_path}" unless File.exist?(validated_path)
237
237
 
238
- raise ArgumentError, "Image file not readable: #{file_path}" unless File.readable?(file_path)
238
+ raise ArgumentError, "Image file not readable: #{validated_path}" unless File.readable?(validated_path)
239
239
 
240
- binary_data = File.binread(file_path)
240
+ binary_data = File.binread(validated_path)
241
241
  to_mcp_image_content(binary_data, validate: validate, max_size: max_size)
242
242
  end
243
243
 
244
- # Validates that a file path does not escape the given base directory.
244
+ # Validates that a file path is safe to access.
245
+ #
246
+ # When +base_directory+ is provided, the resolved path must reside within it.
247
+ # When +base_directory+ is omitted, the path is still canonicalized and
248
+ # rejected if it contains path traversal sequences (+..+) that resolve
249
+ # outside the current working directory.
245
250
  #
246
251
  # @param file_path [String] The file path to validate.
247
- # @param base_directory [String] The base directory boundary.
248
- # @raise [ArgumentError] If the resolved path is outside base_directory.
252
+ # @param base_directory [String, nil] Optional base directory boundary.
253
+ # @return [String] The canonicalized, validated path.
254
+ # @raise [ArgumentError] If path traversal is detected.
249
255
  # @api private
250
256
  def validate_path_safety!(file_path, base_directory)
251
- resolved_base = File.expand_path(base_directory)
252
- resolved_path = File.expand_path(file_path, resolved_base)
257
+ if base_directory
258
+ resolved_base = File.expand_path(base_directory)
259
+ resolved_path = File.expand_path(file_path, resolved_base)
253
260
 
254
- return if resolved_path.start_with?("#{resolved_base}/") || resolved_path == resolved_base
261
+ unless resolved_path.start_with?("#{resolved_base}/") || resolved_path == resolved_base
262
+ raise ArgumentError, "Path traversal detected: resolved path is outside the allowed base directory"
263
+ end
264
+ else
265
+ resolved_path = File.expand_path(file_path)
266
+
267
+ # Reject paths that use traversal sequences to escape upward, even without
268
+ # an explicit base directory. This catches the common case of
269
+ # user-supplied input like "../../etc/passwd".
270
+ if file_path.to_s.include?("..")
271
+ canonical_base = File.expand_path(".")
272
+ unless resolved_path.start_with?("#{canonical_base}/") || resolved_path == canonical_base
273
+ raise ArgumentError,
274
+ "Path traversal detected: '#{file_path}' resolves outside the working directory"
275
+ end
276
+ end
277
+ end
255
278
 
256
- raise ArgumentError, "Path traversal detected: resolved path is outside the allowed base directory"
279
+ resolved_path
257
280
  end
258
281
 
259
282
  # Extracts image metadata from binary data.
@@ -147,11 +147,7 @@ module VectorMCP
147
147
  # @param context [VectorMCP::Middleware::Context] Execution context
148
148
  # @param new_params [Hash] New parameters to set
149
149
  def modify_params(context, new_params)
150
- if context.respond_to?(:params=)
151
- context.params = new_params
152
- else
153
- @logger.warn("Cannot modify immutable params in context")
154
- end
150
+ context.params = new_params
155
151
  end
156
152
 
157
153
  # Helper method to modify response result
@@ -18,7 +18,7 @@ module VectorMCP
18
18
  def initialize(options = {})
19
19
  @operation_type = options[:operation_type]
20
20
  @operation_name = options[:operation_name]
21
- @params = (options[:params] || {}).dup.freeze # Immutable copy
21
+ self.params = options[:params]
22
22
  @session = options[:session]
23
23
  @server = options[:server]
24
24
  @metadata = (options[:metadata] || {}).dup
@@ -27,6 +27,16 @@ module VectorMCP
27
27
  @skip_remaining_hooks = false
28
28
  end
29
29
 
30
+ # Replace request parameters for the current operation.
31
+ #
32
+ # @param value [Hash, nil] New request parameters.
33
+ # @return [Hash] The normalized parameter hash.
34
+ def params=(value)
35
+ raise ArgumentError, "params must be a Hash" unless value.nil? || value.is_a?(Hash)
36
+
37
+ @params = (value || {}).dup
38
+ end
39
+
30
40
  # Check if operation completed successfully
31
41
  # @return [Boolean] true if no error occurred
32
42
  def success?
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "active_support/core_ext/hash/indifferent_access"
5
+ require "vector_mcp/tool"
6
+
7
+ module VectorMCP
8
+ module Rails
9
+ # Rails-aware base class for declarative tool definitions.
10
+ #
11
+ # Adds ergonomics for the common patterns that show up in ActiveRecord-
12
+ # backed MCP tools:
13
+ #
14
+ # * +find!+ -- fetch a record or raise +VectorMCP::NotFoundError+
15
+ # * +respond_with+ -- standard success/error payload from a record
16
+ # * +with_transaction+ -- wrap a mutation in an AR transaction
17
+ # * Auto-rescue of +ActiveRecord::RecordNotFound+ (-> NotFoundError)
18
+ # and +ActiveRecord::RecordInvalid+ (-> error payload)
19
+ # * Arguments delivered to +#call+ as a +HashWithIndifferentAccess+
20
+ # so +args[:id]+ and +args["id"]+ both work
21
+ #
22
+ # @example
23
+ # class UpdateProvider < VectorMCP::Rails::Tool
24
+ # tool_name "update_provider"
25
+ # description "Update an existing provider"
26
+ #
27
+ # param :id, type: :integer, required: true
28
+ # param :name, type: :string
29
+ #
30
+ # def call(args, _session)
31
+ # provider = find!(Provider, args[:id])
32
+ # provider.update(args.except(:id))
33
+ # respond_with(provider, name: provider.name)
34
+ # end
35
+ # end
36
+ class Tool < VectorMCP::Tool
37
+ # Overrides the parent handler to add indifferent-access args and
38
+ # auto-rescue ActiveRecord exceptions.
39
+ def self.build_handler
40
+ klass = self
41
+ params = @params
42
+ lambda do |args, session|
43
+ coerced = klass.coerce_args(args, params).with_indifferent_access
44
+ klass.new.call(coerced, session)
45
+ rescue ActiveRecord::RecordNotFound => e
46
+ raise VectorMCP::NotFoundError, e.message
47
+ rescue ActiveRecord::RecordInvalid => e
48
+ { success: false, errors: e.record.errors.full_messages }
49
+ end
50
+ end
51
+ private_class_method :build_handler
52
+
53
+ # Finds a record by id or raises VectorMCP::NotFoundError.
54
+ #
55
+ # @param model [Class] an ActiveRecord model class
56
+ # @param id [Integer, String] the record id
57
+ # @return [ActiveRecord::Base]
58
+ def find!(model, id)
59
+ model.find_by(id: id) ||
60
+ raise(VectorMCP::NotFoundError, "#{model.name} #{id} not found")
61
+ end
62
+
63
+ # Builds a standard response payload from a record.
64
+ #
65
+ # Success shape: +{ success: true, id: record.id, **extras }+
66
+ # Error shape: +{ success: false, errors: record.errors.full_messages }+
67
+ #
68
+ # @param record [ActiveRecord::Base]
69
+ # @param extras [Hash] additional keys to merge into the success payload
70
+ # @return [Hash]
71
+ def respond_with(record, **extras)
72
+ if record.persisted? && record.errors.empty?
73
+ { success: true, id: record.id, **extras }
74
+ else
75
+ { success: false, errors: record.errors.full_messages }
76
+ end
77
+ end
78
+
79
+ # Runs the given block inside an ActiveRecord transaction.
80
+ def with_transaction(&)
81
+ ActiveRecord::Base.transaction(&)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -92,7 +92,7 @@ module VectorMCP
92
92
  end
93
93
 
94
94
  # Create a minimal request context for non-HTTP transports.
95
- # This is useful for stdio and other command-line transports.
95
+ # This is useful for non-HTTP transports or testing contexts.
96
96
  #
97
97
  # @param transport_type [String] The transport type identifier
98
98
  # @return [RequestContext] A minimal request context
@@ -120,11 +120,11 @@ module VectorMCP
120
120
  # @param transport_request [Object] the transport request
121
121
  # @return [Hash] extracted request data
122
122
  def extract_request_data(transport_request)
123
- # Handle Rack environment (for SSE transport)
123
+ # Handle Rack environment (for HTTP transports)
124
124
  if transport_request.respond_to?(:[]) && transport_request["REQUEST_METHOD"]
125
125
  extract_from_rack_env(transport_request)
126
126
  else
127
- # Default fallback
127
+ # Default fallback for non-HTTP request formats
128
128
  { headers: {}, params: {} }
129
129
  end
130
130
  end
@@ -43,11 +43,8 @@ module VectorMCP
43
43
 
44
44
  notification_method = "notifications/prompts/list_changed"
45
45
  begin
46
- if transport.respond_to?(:broadcast_notification)
47
- logger.debug("Broadcasting prompts list changed notification.")
48
- transport.broadcast_notification(notification_method)
49
- elsif transport.respond_to?(:send_notification)
50
- logger.debug("Sending prompts list changed notification (transport may broadcast or send to first client).")
46
+ if transport.respond_to?(:send_notification)
47
+ logger.debug("Sending prompts list changed notification.")
51
48
  transport.send_notification(notification_method)
52
49
  else
53
50
  logger.warn("Transport does not support sending notifications/prompts/list_changed.")
@@ -70,11 +67,8 @@ module VectorMCP
70
67
 
71
68
  notification_method = "notifications/roots/list_changed"
72
69
  begin
73
- if transport.respond_to?(:broadcast_notification)
74
- logger.debug("Broadcasting roots list changed notification.")
75
- transport.broadcast_notification(notification_method)
76
- elsif transport.respond_to?(:send_notification)
77
- logger.debug("Sending roots list changed notification (transport may broadcast or send to first client).")
70
+ if transport.respond_to?(:send_notification)
71
+ logger.debug("Sending roots list changed notification.")
78
72
  transport.send_notification(notification_method)
79
73
  else
80
74
  logger.warn("Transport does not support sending notifications/roots/list_changed.")
@@ -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.
@@ -159,7 +191,7 @@ module VectorMCP
159
191
  # @param required_parameters [Array<String>] List of required parameter names.
160
192
  # @param block [Proc] The tool handler block.
161
193
  # @return [VectorMCP::Definitions::Tool] The registered tool.
162
- def register_image_tool(name:, description:, image_parameter: "image", additional_parameters: {}, required_parameters: [], &block)
194
+ def register_image_tool(name:, description:, image_parameter: "image", additional_parameters: {}, required_parameters: [], &)
163
195
  # Build the input schema with image support
164
196
  image_property = {
165
197
  type: "string",
@@ -180,7 +212,7 @@ module VectorMCP
180
212
  name: name,
181
213
  description: description,
182
214
  input_schema: input_schema,
183
- &block
215
+ &
184
216
  )
185
217
  end
186
218
 
@@ -192,13 +224,13 @@ module VectorMCP
192
224
  # @param additional_arguments [Array<Hash>] Additional prompt arguments.
193
225
  # @param block [Proc] The prompt handler block.
194
226
  # @return [VectorMCP::Definitions::Prompt] The registered prompt.
195
- def register_image_prompt(name:, description:, image_argument: "image", additional_arguments: [], &block)
227
+ def register_image_prompt(name:, description:, image_argument: "image", additional_arguments: [], &)
196
228
  prompt = VectorMCP::Definitions::Prompt.with_image_support(
197
229
  name: name,
198
230
  description: description,
199
231
  image_argument_name: image_argument,
200
232
  additional_arguments: additional_arguments,
201
- &block
233
+ &
202
234
  )
203
235
 
204
236
  register_prompt(
@@ -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,7 +66,10 @@ 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
75
  :auth_manager, :authorization, :security_middleware, :middleware_manager
@@ -134,33 +135,19 @@ module VectorMCP
134
135
 
135
136
  # Runs the server using the specified transport mechanism.
136
137
  #
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).
138
+ # @param transport [:http_stream, VectorMCP::Transport::Base] The transport to use.
139
+ # Can be the symbol `:http_stream` or an initialized transport instance.
140
+ # If `:http_stream` is provided, the method will instantiate the MCP-compliant streamable HTTP transport.
141
+ # @param options [Hash] Transport-specific options (e.g., `:host`, `:port`).
143
142
  # These are passed to the transport's constructor if a symbol is provided for `transport`.
144
143
  # @return [void]
145
144
  # @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)
145
+ def run(transport: :http_stream, **)
148
146
  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
147
  when :http_stream
161
148
  begin
162
149
  require_relative "transport/http_stream"
163
- VectorMCP::Transport::HttpStream.new(self, **options)
150
+ VectorMCP::Transport::HttpStream.new(self, **)
164
151
  rescue LoadError => e
165
152
  logger.fatal("HttpStream transport requires additional dependencies.")
166
153
  raise NotImplementedError, "HttpStream transport dependencies not available: #{e.message}"
@@ -176,6 +163,26 @@ module VectorMCP
176
163
  active_transport.run
177
164
  end
178
165
 
166
+ # Returns the MCP server as a Rack application suitable for mounting inside
167
+ # another Rack-based framework (e.g., Rails, Sinatra).
168
+ #
169
+ # Unlike {#run}, this method does NOT start its own HTTP server or block.
170
+ # The returned object responds to `#call(env)` and can be mounted directly:
171
+ #
172
+ # # config/routes.rb (Rails)
173
+ # mount MCP_APP => "/mcp"
174
+ #
175
+ # Call `server.transport.stop` on application shutdown to clean up resources.
176
+ #
177
+ # @param options [Hash] Transport options (e.g., :session_timeout, :event_retention, :allowed_origins)
178
+ # @return [VectorMCP::Transport::HttpStream] A Rack-compatible app
179
+ def rack_app(**)
180
+ require_relative "transport/http_stream"
181
+ active_transport = VectorMCP::Transport::HttpStream.new(self, mounted: true, **)
182
+ self.transport = active_transport
183
+ active_transport
184
+ end
185
+
179
186
  # --- Security Configuration ---
180
187
 
181
188
  # Enable authentication with specified strategy and configuration
@@ -216,9 +223,9 @@ module VectorMCP
216
223
  # Enable authorization with optional policy configuration block
217
224
  # @param block [Proc] optional block for configuring authorization policies
218
225
  # @return [void]
219
- def enable_authorization!(&block)
226
+ def enable_authorization!(&)
220
227
  @authorization.enable!
221
- instance_eval(&block) if block_given?
228
+ instance_eval(&) if block_given?
222
229
  @logger.info("Authorization enabled")
223
230
  end
224
231
 
@@ -232,29 +239,29 @@ module VectorMCP
232
239
  # Add authorization policy for tools
233
240
  # @param block [Proc] policy block that receives (user, action, tool)
234
241
  # @return [void]
235
- def authorize_tools(&block)
236
- @authorization.add_policy(:tool, &block)
242
+ def authorize_tools(&)
243
+ @authorization.add_policy(:tool, &)
237
244
  end
238
245
 
239
246
  # Add authorization policy for resources
240
247
  # @param block [Proc] policy block that receives (user, action, resource)
241
248
  # @return [void]
242
- def authorize_resources(&block)
243
- @authorization.add_policy(:resource, &block)
249
+ def authorize_resources(&)
250
+ @authorization.add_policy(:resource, &)
244
251
  end
245
252
 
246
253
  # Add authorization policy for prompts
247
254
  # @param block [Proc] policy block that receives (user, action, prompt)
248
255
  # @return [void]
249
- def authorize_prompts(&block)
250
- @authorization.add_policy(:prompt, &block)
256
+ def authorize_prompts(&)
257
+ @authorization.add_policy(:prompt, &)
251
258
  end
252
259
 
253
260
  # Add authorization policy for roots
254
261
  # @param block [Proc] policy block that receives (user, action, root)
255
262
  # @return [void]
256
- def authorize_roots(&block)
257
- @authorization.add_policy(:root, &block)
263
+ def authorize_roots(&)
264
+ @authorization.add_policy(:root, &)
258
265
  end
259
266
 
260
267
  # Check if security features are enabled
@@ -331,8 +338,8 @@ module VectorMCP
331
338
  # Add custom authentication strategy
332
339
  # @param handler [Proc] custom authentication handler block
333
340
  # @return [void]
334
- def add_custom_auth(&block)
335
- strategy = Security::Strategies::Custom.new(&block)
341
+ def add_custom_auth(&)
342
+ strategy = Security::Strategies::Custom.new(&)
336
343
  @auth_manager.add_strategy(:custom, strategy)
337
344
  end
338
345
 
@@ -347,7 +354,7 @@ module VectorMCP
347
354
 
348
355
  module Transport
349
356
  # Dummy base class placeholder used only for argument validation in tests.
350
- # Real transport classes (e.g., Stdio, SSE) are separate concrete classes.
357
+ # Real transport classes (e.g., HttpStream) are separate concrete classes.
351
358
  class Base # :nodoc:
352
359
  end
353
360
  end