vector_mcp 0.1.0 → 0.2.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/README.md +421 -114
- data/lib/vector_mcp/definitions.rb +210 -1
- data/lib/vector_mcp/errors.rb +39 -0
- data/lib/vector_mcp/handlers/core.rb +17 -0
- data/lib/vector_mcp/image_util.rb +358 -0
- data/lib/vector_mcp/sampling/request.rb +193 -0
- data/lib/vector_mcp/sampling/result.rb +80 -0
- data/lib/vector_mcp/server/capabilities.rb +156 -0
- data/lib/vector_mcp/server/message_handling.rb +166 -0
- data/lib/vector_mcp/server/registry.rb +289 -0
- data/lib/vector_mcp/server.rb +53 -415
- data/lib/vector_mcp/session.rb +100 -23
- data/lib/vector_mcp/transport/stdio.rb +174 -16
- data/lib/vector_mcp/util.rb +135 -10
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +2 -1
- metadata +21 -1
@@ -0,0 +1,289 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VectorMCP
|
4
|
+
class Server
|
5
|
+
# Handles registration of tools, resources, prompts, and roots
|
6
|
+
module Registry
|
7
|
+
# --- Registration Methods ---
|
8
|
+
|
9
|
+
# Registers a new tool with the server.
|
10
|
+
#
|
11
|
+
# @param name [String, Symbol] The unique name for the tool.
|
12
|
+
# @param description [String] A human-readable description of the tool.
|
13
|
+
# @param input_schema [Hash] A JSON Schema object that precisely describes the
|
14
|
+
# structure of the argument hash your tool expects.
|
15
|
+
# @yield [Hash] A block implementing the tool logic.
|
16
|
+
# @return [self] Returns the server instance so you can chain registrations.
|
17
|
+
# @raise [ArgumentError] If another tool with the same name is already registered.
|
18
|
+
def register_tool(name:, description:, input_schema:, &handler)
|
19
|
+
name_s = name.to_s
|
20
|
+
raise ArgumentError, "Tool '#{name_s}' already registered" if @tools[name_s]
|
21
|
+
|
22
|
+
@tools[name_s] = VectorMCP::Definitions::Tool.new(name_s, description, input_schema, handler)
|
23
|
+
logger.debug("Registered tool: #{name_s}")
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
# Registers a new resource with the server.
|
28
|
+
#
|
29
|
+
# @param uri [String, URI] The unique URI for the resource.
|
30
|
+
# @param name [String] A human-readable name for the resource.
|
31
|
+
# @param description [String] A description of the resource.
|
32
|
+
# @param mime_type [String] The MIME type of the resource's content (default: "text/plain").
|
33
|
+
# @yield [Hash] A block that provides the resource's content.
|
34
|
+
# @return [self] The server instance, for chaining.
|
35
|
+
# @raise [ArgumentError] if a resource with the same URI is already registered.
|
36
|
+
def register_resource(uri:, name:, description:, mime_type: "text/plain", &handler)
|
37
|
+
uri_s = uri.to_s
|
38
|
+
raise ArgumentError, "Resource '#{uri_s}' already registered" if @resources[uri_s]
|
39
|
+
|
40
|
+
@resources[uri_s] = VectorMCP::Definitions::Resource.new(uri, name, description, mime_type, handler)
|
41
|
+
logger.debug("Registered resource: #{uri_s}")
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# Registers a new prompt with the server.
|
46
|
+
#
|
47
|
+
# @param name [String, Symbol] The unique name for the prompt.
|
48
|
+
# @param description [String] A human-readable description of the prompt.
|
49
|
+
# @param arguments [Array<Hash>] An array defining the prompt's arguments.
|
50
|
+
# @yield [Hash] A block that generates the prompt.
|
51
|
+
# @return [self] The server instance, for chaining.
|
52
|
+
# @raise [ArgumentError] if a prompt with the same name is already registered.
|
53
|
+
def register_prompt(name:, description:, arguments: [], &handler)
|
54
|
+
name_s = name.to_s
|
55
|
+
raise ArgumentError, "Prompt '#{name_s}' already registered" if @prompts[name_s]
|
56
|
+
|
57
|
+
validate_prompt_arguments(arguments)
|
58
|
+
@prompts[name_s] = VectorMCP::Definitions::Prompt.new(name_s, description, arguments, handler)
|
59
|
+
@prompts_list_changed = true
|
60
|
+
notify_prompts_list_changed
|
61
|
+
logger.debug("Registered prompt: #{name_s}")
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
# Registers a new root with the server.
|
66
|
+
#
|
67
|
+
# @param uri [String, URI] The unique URI for the root (must be file:// scheme).
|
68
|
+
# @param name [String] A human-readable name for the root.
|
69
|
+
# @return [self] The server instance, for chaining.
|
70
|
+
# @raise [ArgumentError] if a root with the same URI is already registered.
|
71
|
+
def register_root(uri:, name:)
|
72
|
+
uri_s = uri.to_s
|
73
|
+
raise ArgumentError, "Root '#{uri_s}' already registered" if @roots[uri_s]
|
74
|
+
|
75
|
+
root = VectorMCP::Definitions::Root.new(uri, name)
|
76
|
+
root.validate! # This will raise ArgumentError if invalid
|
77
|
+
|
78
|
+
@roots[uri_s] = root
|
79
|
+
@roots_list_changed = true
|
80
|
+
notify_roots_list_changed
|
81
|
+
logger.debug("Registered root: #{uri_s} (#{name})")
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
# Helper method to register a root from a local directory path.
|
86
|
+
#
|
87
|
+
# @param path [String] Local filesystem path to the directory.
|
88
|
+
# @param name [String, nil] Human-readable name for the root.
|
89
|
+
# @return [self] The server instance, for chaining.
|
90
|
+
# @raise [ArgumentError] if the path is invalid or not accessible.
|
91
|
+
def register_root_from_path(path, name: nil)
|
92
|
+
root = VectorMCP::Definitions::Root.from_path(path, name: name)
|
93
|
+
register_root(uri: root.uri, name: root.name)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Helper method to register an image resource from a file path.
|
97
|
+
#
|
98
|
+
# @param uri [String] Unique URI for the resource.
|
99
|
+
# @param file_path [String] Path to the image file.
|
100
|
+
# @param name [String, nil] Human-readable name (auto-generated if nil).
|
101
|
+
# @param description [String, nil] Description (auto-generated if nil).
|
102
|
+
# @return [VectorMCP::Definitions::Resource] The registered resource.
|
103
|
+
# @raise [ArgumentError] If the file doesn't exist or isn't a valid image.
|
104
|
+
def register_image_resource(uri:, file_path:, name: nil, description: nil)
|
105
|
+
resource = VectorMCP::Definitions::Resource.from_image_file(
|
106
|
+
uri: uri,
|
107
|
+
file_path: file_path,
|
108
|
+
name: name,
|
109
|
+
description: description
|
110
|
+
)
|
111
|
+
|
112
|
+
register_resource(
|
113
|
+
uri: resource.uri,
|
114
|
+
name: resource.name,
|
115
|
+
description: resource.description,
|
116
|
+
mime_type: resource.mime_type,
|
117
|
+
&resource.handler
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Helper method to register an image resource from binary data.
|
122
|
+
#
|
123
|
+
# @param uri [String] Unique URI for the resource.
|
124
|
+
# @param image_data [String] Binary image data.
|
125
|
+
# @param name [String] Human-readable name.
|
126
|
+
# @param description [String, nil] Description (auto-generated if nil).
|
127
|
+
# @param mime_type [String, nil] MIME type (auto-detected if nil).
|
128
|
+
# @return [VectorMCP::Definitions::Resource] The registered resource.
|
129
|
+
# @raise [ArgumentError] If the data isn't valid image data.
|
130
|
+
def register_image_resource_from_data(uri:, image_data:, name:, description: nil, mime_type: nil)
|
131
|
+
resource = VectorMCP::Definitions::Resource.from_image_data(
|
132
|
+
uri: uri,
|
133
|
+
image_data: image_data,
|
134
|
+
name: name,
|
135
|
+
description: description,
|
136
|
+
mime_type: mime_type
|
137
|
+
)
|
138
|
+
|
139
|
+
register_resource(
|
140
|
+
uri: resource.uri,
|
141
|
+
name: resource.name,
|
142
|
+
description: resource.description,
|
143
|
+
mime_type: resource.mime_type,
|
144
|
+
&resource.handler
|
145
|
+
)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Helper method to register a tool that accepts image inputs.
|
149
|
+
#
|
150
|
+
# @param name [String] Unique name for the tool.
|
151
|
+
# @param description [String] Human-readable description.
|
152
|
+
# @param image_parameter [String] Name of the image parameter (default: "image").
|
153
|
+
# @param additional_parameters [Hash] Additional JSON Schema properties.
|
154
|
+
# @param required_parameters [Array<String>] List of required parameter names.
|
155
|
+
# @param block [Proc] The tool handler block.
|
156
|
+
# @return [VectorMCP::Definitions::Tool] The registered tool.
|
157
|
+
def register_image_tool(name:, description:, image_parameter: "image", additional_parameters: {}, required_parameters: [], &block)
|
158
|
+
# Build the input schema with image support
|
159
|
+
image_property = {
|
160
|
+
type: "string",
|
161
|
+
description: "Base64 encoded image data or file path to image",
|
162
|
+
contentEncoding: "base64",
|
163
|
+
contentMediaType: "image/*"
|
164
|
+
}
|
165
|
+
|
166
|
+
properties = { image_parameter => image_property }.merge(additional_parameters)
|
167
|
+
|
168
|
+
input_schema = {
|
169
|
+
type: "object",
|
170
|
+
properties: properties,
|
171
|
+
required: required_parameters
|
172
|
+
}
|
173
|
+
|
174
|
+
register_tool(
|
175
|
+
name: name,
|
176
|
+
description: description,
|
177
|
+
input_schema: input_schema,
|
178
|
+
&block
|
179
|
+
)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Helper method to register a prompt that supports image arguments.
|
183
|
+
#
|
184
|
+
# @param name [String] Unique name for the prompt.
|
185
|
+
# @param description [String] Human-readable description.
|
186
|
+
# @param image_argument [String] Name of the image argument (default: "image").
|
187
|
+
# @param additional_arguments [Array<Hash>] Additional prompt arguments.
|
188
|
+
# @param block [Proc] The prompt handler block.
|
189
|
+
# @return [VectorMCP::Definitions::Prompt] The registered prompt.
|
190
|
+
def register_image_prompt(name:, description:, image_argument: "image", additional_arguments: [], &block)
|
191
|
+
prompt = VectorMCP::Definitions::Prompt.with_image_support(
|
192
|
+
name: name,
|
193
|
+
description: description,
|
194
|
+
image_argument_name: image_argument,
|
195
|
+
additional_arguments: additional_arguments,
|
196
|
+
&block
|
197
|
+
)
|
198
|
+
|
199
|
+
register_prompt(
|
200
|
+
name: prompt.name,
|
201
|
+
description: prompt.description,
|
202
|
+
arguments: prompt.arguments,
|
203
|
+
&prompt.handler
|
204
|
+
)
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
|
209
|
+
# Validates the structure of the `arguments` array provided to {#register_prompt}.
|
210
|
+
# @api private
|
211
|
+
def validate_prompt_arguments(argument_defs)
|
212
|
+
raise ArgumentError, "Prompt arguments definition must be an Array of Hashes." unless argument_defs.is_a?(Array)
|
213
|
+
|
214
|
+
argument_defs.each_with_index { |arg, idx| validate_single_prompt_argument(arg, idx) }
|
215
|
+
end
|
216
|
+
|
217
|
+
# Defines the keys allowed in a prompt argument definition hash.
|
218
|
+
ALLOWED_PROMPT_ARG_KEYS = %w[name description required type].freeze
|
219
|
+
private_constant :ALLOWED_PROMPT_ARG_KEYS
|
220
|
+
|
221
|
+
# Validates a single prompt argument definition hash.
|
222
|
+
# @api private
|
223
|
+
def validate_single_prompt_argument(arg, idx)
|
224
|
+
raise ArgumentError, "Prompt argument definition at index #{idx} must be a Hash. Found: #{arg.class}" unless arg.is_a?(Hash)
|
225
|
+
|
226
|
+
validate_prompt_arg_name!(arg, idx)
|
227
|
+
validate_prompt_arg_description!(arg, idx)
|
228
|
+
validate_prompt_arg_required_flag!(arg, idx)
|
229
|
+
validate_prompt_arg_type!(arg, idx)
|
230
|
+
validate_prompt_arg_unknown_keys!(arg, idx)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Validates the :name key of a prompt argument definition.
|
234
|
+
# @api private
|
235
|
+
def validate_prompt_arg_name!(arg, idx)
|
236
|
+
name_val = arg[:name] || arg["name"]
|
237
|
+
raise ArgumentError, "Prompt argument at index #{idx} missing :name" if name_val.nil?
|
238
|
+
unless name_val.is_a?(String) || name_val.is_a?(Symbol)
|
239
|
+
raise ArgumentError, "Prompt argument :name at index #{idx} must be a String or Symbol. Found: #{name_val.class}"
|
240
|
+
end
|
241
|
+
raise ArgumentError, "Prompt argument :name at index #{idx} cannot be empty." if name_val.to_s.strip.empty?
|
242
|
+
end
|
243
|
+
|
244
|
+
# Validates the :description key of a prompt argument definition.
|
245
|
+
# @api private
|
246
|
+
def validate_prompt_arg_description!(arg, idx)
|
247
|
+
return unless arg.key?(:description) || arg.key?("description")
|
248
|
+
|
249
|
+
desc_val = arg[:description] || arg["description"]
|
250
|
+
return if desc_val.nil? || desc_val.is_a?(String)
|
251
|
+
|
252
|
+
raise ArgumentError, "Prompt argument :description at index #{idx} must be a String if provided. Found: #{desc_val.class}"
|
253
|
+
end
|
254
|
+
|
255
|
+
# Validates the :required key of a prompt argument definition.
|
256
|
+
# @api private
|
257
|
+
def validate_prompt_arg_required_flag!(arg, idx)
|
258
|
+
return unless arg.key?(:required) || arg.key?("required")
|
259
|
+
|
260
|
+
req_val = arg[:required] || arg["required"]
|
261
|
+
return if [true, false].include?(req_val)
|
262
|
+
|
263
|
+
raise ArgumentError, "Prompt argument :required at index #{idx} must be true or false if provided. Found: #{req_val.inspect}"
|
264
|
+
end
|
265
|
+
|
266
|
+
# Validates the :type key of a prompt argument definition.
|
267
|
+
# @api private
|
268
|
+
def validate_prompt_arg_type!(arg, idx)
|
269
|
+
return unless arg.key?(:type) || arg.key?("type")
|
270
|
+
|
271
|
+
type_val = arg[:type] || arg["type"]
|
272
|
+
return if type_val.nil? || type_val.is_a?(String)
|
273
|
+
|
274
|
+
raise ArgumentError, "Prompt argument :type at index #{idx} must be a String if provided (e.g., JSON schema type). Found: #{type_val.class}"
|
275
|
+
end
|
276
|
+
|
277
|
+
# Checks for any unknown keys in a prompt argument definition.
|
278
|
+
# @api private
|
279
|
+
def validate_prompt_arg_unknown_keys!(arg, idx)
|
280
|
+
unknown_keys = arg.transform_keys(&:to_s).keys - ALLOWED_PROMPT_ARG_KEYS
|
281
|
+
return if unknown_keys.empty?
|
282
|
+
|
283
|
+
raise ArgumentError,
|
284
|
+
"Prompt argument definition at index #{idx} contains unknown keys: #{unknown_keys.join(", ")}. " \
|
285
|
+
"Allowed: #{ALLOWED_PROMPT_ARG_KEYS.join(", ")}."
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|