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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -0
- data/README.md +132 -342
- data/lib/vector_mcp/handlers/core.rb +82 -27
- data/lib/vector_mcp/image_util.rb +34 -11
- data/lib/vector_mcp/middleware/base.rb +1 -5
- data/lib/vector_mcp/middleware/context.rb +11 -1
- data/lib/vector_mcp/rails/tool.rb +85 -0
- data/lib/vector_mcp/request_context.rb +1 -1
- data/lib/vector_mcp/security/middleware.rb +2 -2
- data/lib/vector_mcp/server/capabilities.rb +4 -10
- data/lib/vector_mcp/server/registry.rb +36 -4
- data/lib/vector_mcp/server.rb +45 -38
- data/lib/vector_mcp/session.rb +5 -3
- data/lib/vector_mcp/tool.rb +221 -0
- data/lib/vector_mcp/transport/base_session_manager.rb +1 -17
- data/lib/vector_mcp/transport/http_stream/event_store.rb +18 -4
- data/lib/vector_mcp/transport/http_stream/session_manager.rb +34 -11
- data/lib/vector_mcp/transport/http_stream/stream_handler.rb +132 -47
- data/lib/vector_mcp/transport/http_stream.rb +161 -82
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +6 -8
- metadata +4 -10
- data/lib/vector_mcp/transport/sse/client_connection.rb +0 -113
- data/lib/vector_mcp/transport/sse/message_handler.rb +0 -166
- data/lib/vector_mcp/transport/sse/puma_config.rb +0 -77
- data/lib/vector_mcp/transport/sse/stream_manager.rb +0 -92
- data/lib/vector_mcp/transport/sse.rb +0 -377
- data/lib/vector_mcp/transport/sse_session_manager.rb +0 -188
- data/lib/vector_mcp/transport/stdio.rb +0 -473
- data/lib/vector_mcp/transport/stdio_session_manager.rb +0 -181
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
115
|
+
authorize_action!(session_context, :read, resource, server)
|
|
110
116
|
|
|
111
|
-
content_raw = execute_resource_handler(resource, params,
|
|
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 =
|
|
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
|
-
#
|
|
495
|
-
def self.
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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,
|
|
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.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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!, :
|
|
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)
|
|
234
|
+
validated_path = validate_path_safety!(file_path, base_directory)
|
|
235
235
|
|
|
236
|
-
raise ArgumentError, "Image file not found: #{
|
|
236
|
+
raise ArgumentError, "Image file not found: #{validated_path}" unless File.exist?(validated_path)
|
|
237
237
|
|
|
238
|
-
raise ArgumentError, "Image file not readable: #{
|
|
238
|
+
raise ArgumentError, "Image file not readable: #{validated_path}" unless File.readable?(validated_path)
|
|
239
239
|
|
|
240
|
-
binary_data = File.binread(
|
|
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
|
|
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]
|
|
248
|
-
# @
|
|
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
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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?(:
|
|
47
|
-
logger.debug("
|
|
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?(:
|
|
74
|
-
logger.debug("
|
|
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: [], &
|
|
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
|
-
&
|
|
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: [], &
|
|
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
|
-
&
|
|
233
|
+
&
|
|
202
234
|
)
|
|
203
235
|
|
|
204
236
|
register_prompt(
|
data/lib/vector_mcp/server.rb
CHANGED
|
@@ -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.,
|
|
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
|
|
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 = "
|
|
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 [:
|
|
138
|
-
# Can be
|
|
139
|
-
# If
|
|
140
|
-
#
|
|
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
|
-
|
|
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, **
|
|
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!(&
|
|
226
|
+
def enable_authorization!(&)
|
|
220
227
|
@authorization.enable!
|
|
221
|
-
instance_eval(&
|
|
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(&
|
|
236
|
-
@authorization.add_policy(:tool, &
|
|
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(&
|
|
243
|
-
@authorization.add_policy(:resource, &
|
|
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(&
|
|
250
|
-
@authorization.add_policy(:prompt, &
|
|
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(&
|
|
257
|
-
@authorization.add_policy(:root, &
|
|
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(&
|
|
335
|
-
strategy = Security::Strategies::Custom.new(&
|
|
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.,
|
|
357
|
+
# Real transport classes (e.g., HttpStream) are separate concrete classes.
|
|
351
358
|
class Base # :nodoc:
|
|
352
359
|
end
|
|
353
360
|
end
|