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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +147 -337
- data/lib/vector_mcp/definitions.rb +30 -0
- data/lib/vector_mcp/handlers/core.rb +78 -81
- data/lib/vector_mcp/image_util.rb +34 -11
- data/lib/vector_mcp/middleware/anonymizer.rb +186 -0
- data/lib/vector_mcp/middleware/base.rb +1 -5
- data/lib/vector_mcp/middleware/context.rb +11 -1
- data/lib/vector_mcp/middleware/hook.rb +7 -24
- data/lib/vector_mcp/middleware.rb +26 -9
- data/lib/vector_mcp/rails/tool.rb +85 -0
- data/lib/vector_mcp/request_context.rb +1 -1
- data/lib/vector_mcp/security/auth_manager.rb +12 -13
- data/lib/vector_mcp/security/auth_result.rb +33 -0
- data/lib/vector_mcp/security/authorization.rb +5 -9
- data/lib/vector_mcp/security/middleware.rb +2 -2
- data/lib/vector_mcp/security/session_context.rb +11 -27
- data/lib/vector_mcp/security/strategies/api_key.rb +1 -5
- data/lib/vector_mcp/security/strategies/custom.rb +10 -37
- data/lib/vector_mcp/security/strategies/jwt_token.rb +1 -10
- data/lib/vector_mcp/server/capabilities.rb +22 -32
- data/lib/vector_mcp/server/message_handling.rb +21 -14
- data/lib/vector_mcp/server/registry.rb +102 -120
- data/lib/vector_mcp/server.rb +98 -57
- data/lib/vector_mcp/session.rb +5 -3
- data/lib/vector_mcp/token_store.rb +80 -0
- 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 +29 -17
- data/lib/vector_mcp/transport/http_stream/session_manager.rb +41 -36
- data/lib/vector_mcp/transport/http_stream/stream_handler.rb +132 -47
- data/lib/vector_mcp/transport/http_stream.rb +242 -124
- data/lib/vector_mcp/util/token_sweeper.rb +74 -0
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +8 -8
- metadata +8 -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
|
@@ -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 [
|
|
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 [
|
|
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
|
-
# @
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
# @
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
251
|
-
|
|
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
|
|
270
|
+
# Validates a single field of a prompt argument hash against its schema spec.
|
|
291
271
|
# @api private
|
|
292
|
-
def
|
|
293
|
-
|
|
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
|
-
|
|
296
|
-
return
|
|
276
|
+
raise ArgumentError, "Prompt argument at index #{idx} #{spec[:missing_message]}" if spec[:required] && value.nil?
|
|
277
|
+
return unless present
|
|
297
278
|
|
|
298
|
-
|
|
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
|
|
283
|
+
# Checks a prompt argument hash for keys outside PROMPT_ARG_SCHEMA.
|
|
302
284
|
# @api private
|
|
303
|
-
def validate_prompt_arg_unknown_keys
|
|
304
|
-
unknown_keys = arg.transform_keys(&:to_s).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: #{
|
|
291
|
+
"Allowed: #{PROMPT_ARG_SCHEMA.keys.join(", ")}."
|
|
310
292
|
end
|
|
311
293
|
end
|
|
312
294
|
end
|
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,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 = "
|
|
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 [:
|
|
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).
|
|
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
|
-
|
|
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, **
|
|
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!(&
|
|
218
|
+
def enable_authorization!(&)
|
|
220
219
|
@authorization.enable!
|
|
221
|
-
instance_eval(&
|
|
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(&
|
|
236
|
-
@authorization.add_policy(:tool, &
|
|
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(&
|
|
243
|
-
@authorization.add_policy(:resource, &
|
|
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(&
|
|
250
|
-
@authorization.add_policy(:prompt, &
|
|
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(&
|
|
257
|
-
@authorization.add_policy(:root, &
|
|
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(&
|
|
335
|
-
strategy = Security::Strategies::Custom.new(&
|
|
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.,
|
|
391
|
+
# Real transport classes (e.g., HttpStream) are separate concrete classes.
|
|
351
392
|
class Base # :nodoc:
|
|
352
393
|
end
|
|
353
394
|
end
|
data/lib/vector_mcp/session.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
|
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
|
|