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.
@@ -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