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.
@@ -6,9 +6,12 @@ require_relative "definitions"
6
6
  require_relative "session"
7
7
  require_relative "errors"
8
8
  require_relative "transport/stdio" # Default transport
9
- require_relative "transport/sse"
9
+ # require_relative "transport/sse" # Load on demand to avoid async dependencies
10
10
  require_relative "handlers/core" # Default handlers
11
11
  require_relative "util" # Needed if not using Handlers::Core
12
+ require_relative "server/registry"
13
+ require_relative "server/capabilities"
14
+ require_relative "server/message_handling"
12
15
 
13
16
  module VectorMCP
14
17
  # The `Server` class is the central component for an MCP server implementation.
@@ -44,155 +47,70 @@ module VectorMCP
44
47
  # @return [Hash<String, VectorMCP::Definitions::Resource>] Registered resources, keyed by URI string.
45
48
  # @!attribute [r] prompts
46
49
  # @return [Hash<String, VectorMCP::Definitions::Prompt>] Registered prompts, keyed by name.
50
+ # @!attribute [r] roots
51
+ # @return [Hash<String, VectorMCP::Definitions::Root>] Registered roots, keyed by URI string.
47
52
  # @!attribute [r] in_flight_requests
48
53
  # @return [Hash] A hash tracking currently processing requests, for cancellation purposes.
49
54
  # @!attribute [rw] transport
50
55
  # @return [VectorMCP::Transport::Base, nil] The active transport instance, if any.
51
56
  class Server
52
- include Definitions # Make Tool, Resource, Prompt structs easily available
57
+ include Definitions # Make Tool, Resource, Prompt, Root structs easily available
58
+ include Registry
59
+ include Capabilities
60
+ include MessageHandling
53
61
 
54
62
  # The specific version of the Model Context Protocol this server implements.
55
63
  PROTOCOL_VERSION = "2024-11-05"
56
64
 
57
- attr_reader :logger, :name, :version, :protocol_version, :tools, :resources, :prompts, :in_flight_requests
65
+ attr_reader :logger, :name, :version, :protocol_version, :tools, :resources, :prompts, :roots, :in_flight_requests
58
66
  attr_accessor :transport
59
67
 
60
- # Initializes a new MCP Server.
68
+ # Initializes a new VectorMCP server.
61
69
  #
62
- # @param name_pos [String, nil] Positional argument for the server name.
63
- # Superseded by `name:` keyword if both provided and different.
64
- # @param name [String] The name of the server (required).
65
- # @param version [String] The version of this server application (default: "0.1.0").
66
- # @param log_level [Integer] The logger level (e.g., `Logger::INFO`, `Logger::DEBUG`).
67
- # Defaults to `Logger::INFO` via the shared `VectorMCP.logger`.
68
- # @param protocol_version [String] The MCP protocol version string this server uses
69
- # (default: {PROTOCOL_VERSION}).
70
- # @raise [ArgumentError] if name is not provided or is empty.
71
- # @raise [ArgumentError] if `name_pos` and `name:` are both provided but differ.
72
- # rubocop:disable Metrics/ParameterLists
73
- def initialize(name_pos = nil, *, name: nil, version: "0.1.0", log_level: Logger::INFO, protocol_version: PROTOCOL_VERSION)
74
- if name_pos && name && name_pos != name
75
- raise ArgumentError, "Specify the server name either positionally or with the `name:` keyword (not both)."
76
- end
77
-
78
- @name = name_pos || name
79
- raise ArgumentError, "Server name is required" if @name.nil? || @name.to_s.strip.empty?
80
-
70
+ # @param name_pos [String] Positional name argument (deprecated, use name: instead).
71
+ # @param name [String] The name of the server.
72
+ # @param version [String] The version of the server.
73
+ # @param options [Hash] Additional server options:
74
+ # - :log_level [Integer] The logging level (Logger::DEBUG, Logger::INFO, etc.).
75
+ # - :protocol_version [String] The MCP protocol version to use.
76
+ # - :sampling_config [Hash] Configuration for sampling capabilities. Available options:
77
+ # - :enabled [Boolean] Whether sampling is enabled (default: true)
78
+ # - :methods [Array<String>] Supported sampling methods (default: ["createMessage"])
79
+ # - :supports_streaming [Boolean] Whether streaming is supported (default: false)
80
+ # - :supports_tool_calls [Boolean] Whether tool calls are supported (default: false)
81
+ # - :supports_images [Boolean] Whether image content is supported (default: false)
82
+ # - :max_tokens_limit [Integer, nil] Maximum tokens limit (default: nil, no limit)
83
+ # - :timeout_seconds [Integer] Default timeout for sampling requests (default: 30)
84
+ # - :context_inclusion_methods [Array<String>] Supported context inclusion methods
85
+ # (default: ["none", "thisServer"])
86
+ # - :model_preferences_supported [Boolean] Whether model preferences are supported (default: true)
87
+ def initialize(name_pos = nil, *, name: nil, version: "0.1.0", **options)
88
+ raise ArgumentError, "Name provided both positionally (#{name_pos}) and as keyword argument (#{name})" if name_pos && name && name_pos != name
89
+
90
+ @name = name_pos || name || "UnnamedServer"
81
91
  @version = version
82
- @protocol_version = protocol_version
92
+ @protocol_version = options[:protocol_version] || PROTOCOL_VERSION
83
93
  @logger = VectorMCP.logger
84
- @logger.level = log_level
94
+ @logger.level = options[:log_level] if options[:log_level]
85
95
 
86
- @request_handlers = {}
87
- @notification_handlers = {}
96
+ @transport = nil
88
97
  @tools = {}
89
98
  @resources = {}
90
99
  @prompts = {}
91
- @in_flight_requests = {} # For $/cancelRequest
100
+ @roots = {}
101
+ @request_handlers = {}
102
+ @notification_handlers = {}
103
+ @in_flight_requests = {}
92
104
  @prompts_list_changed = false
93
- @prompt_subscribers = [] # For `notifications/prompts/subscribe`
105
+ @prompt_subscribers = []
106
+ @roots_list_changed = false
94
107
 
95
- setup_default_handlers
96
- logger.info("Server instance '#{@name}' v#{@version} (MCP Protocol: #{@protocol_version}, Gem: v#{VectorMCP::VERSION}) initialized.")
97
- end
98
- # rubocop:enable Metrics/ParameterLists
99
-
100
- # --- Registration Methods ---
108
+ # Configure sampling capabilities
109
+ @sampling_config = configure_sampling_capabilities(options[:sampling_config] || {})
101
110
 
102
- # Registers a new tool with the server.
103
- #
104
- # @param name [String, Symbol] The unique name for the tool.
105
- # @param description [String] A human-readable description of the tool.
106
- # @param input_schema [Hash] A JSON Schema object that precisely describes the
107
- # structure of the argument hash your tool expects. The schema **must** be
108
- # compatible with the official MCP JSON-Schema draft so that remote
109
- # validators can verify user input.
110
- # @yield [Hash] A block implementing the tool logic. The yielded argument is
111
- # the user-supplied input hash, already guaranteed (by the caller) to match
112
- # `input_schema`.
113
- # @return [self] Returns the server instance so you can chain
114
- # registrations—e.g., `server.register_tool(...).register_resource(...)`.
115
- # @raise [ArgumentError] If another tool with the same name is already registered.
116
- def register_tool(name:, description:, input_schema:, &handler)
117
- name_s = name.to_s
118
- raise ArgumentError, "Tool '#{name_s}' already registered" if @tools[name_s]
119
-
120
- @tools[name_s] = Tool.new(name_s, description, input_schema, handler)
121
- logger.debug("Registered tool: #{name_s}")
122
- self
123
- end
124
-
125
- # Registers a new resource with the server.
126
- #
127
- # @param uri [String, URI] The unique URI for the resource.
128
- # @param name [String] A human-readable name for the resource.
129
- # @param description [String] A description of the resource.
130
- # @param mime_type [String] The MIME type of the resource's content (default: "text/plain").
131
- # @yield [Hash] A block that provides the resource's content. It may receive parameters
132
- # from the `resources/read` request (for dynamic resources; for static resources, parameters may be ignored).
133
- # The block should return **any Ruby value**; the value will be normalised
134
- # to MCP `Content[]` via {VectorMCP::Util.convert_to_mcp_content}.
135
- # @return [self] The server instance, for chaining.
136
- # @raise [ArgumentError] if a resource with the same URI is already registered.
137
- def register_resource(uri:, name:, description:, mime_type: "text/plain", &handler)
138
- uri_s = uri.to_s
139
- raise ArgumentError, "Resource '#{uri_s}' already registered" if @resources[uri_s]
140
-
141
- @resources[uri_s] = Resource.new(uri, name, description, mime_type, handler)
142
- logger.debug("Registered resource: #{uri_s}")
143
- self
144
- end
145
-
146
- # Registers a new prompt with the server.
147
- #
148
- # @param name [String, Symbol] The unique name for the prompt.
149
- # @param description [String] A human-readable description of the prompt.
150
- # @param arguments [Array<Hash>] An array defining the prompt's arguments.
151
- # Each hash should conform to the prompt argument schema (e.g., `{ name:, description:, required: }`).
152
- # @yield [Hash] A block that generates the prompt. It receives a hash of arguments,
153
- # validated against the prompt's argument definitions. The block must
154
- # return a hash conforming to the MCP *GetPromptResult* schema—see
155
- # Handlers::Core#get_prompt for the exact contract enforced.
156
- # @return [self] The server instance, for chaining.
157
- # @raise [ArgumentError] if a prompt with the same name is already registered, or if
158
- # the `arguments` definition is invalid.
159
- def register_prompt(name:, description:, arguments: [], &handler)
160
- name_s = name.to_s
161
- raise ArgumentError, "Prompt '#{name_s}' already registered" if @prompts[name_s]
162
-
163
- validate_prompt_arguments(arguments)
164
- @prompts[name_s] = Prompt.new(name_s, description, arguments, handler)
165
- @prompts_list_changed = true
166
- notify_prompts_list_changed
167
- logger.debug("Registered prompt: #{name_s}")
168
- self
169
- end
170
-
171
- # --- Request/Notification Hook Methods ---
172
-
173
- # Registers a handler for a specific JSON-RPC request method.
174
- #
175
- # @param method [String, Symbol] The method name (e.g., "my/customMethod").
176
- # @yield [params, session, server] A block to handle the request.
177
- # - `params` [Hash]: The request parameters.
178
- # - `session` [VectorMCP::Session]: The current client session.
179
- # - `server` [VectorMCP::Server]: The server instance itself.
180
- # The block should return the result for the JSON-RPC response.
181
- # @return [self] The server instance.
182
- def on_request(method, &handler)
183
- @request_handlers[method.to_s] = handler
184
- self
185
- end
111
+ setup_default_handlers
186
112
 
187
- # Registers a handler for a specific JSON-RPC notification method.
188
- #
189
- # @param method [String, Symbol] The method name (e.g., "my/customNotification").
190
- # @yield [params, session, server] A block to handle the notification.
191
- # (Parameters are the same as for `on_request`.)
192
- # @return [self] The server instance.
193
- def on_notification(method, &handler)
194
- @notification_handlers[method.to_s] = handler
195
- self
113
+ @logger.info("Server instance '#{@name}' v#{@version} (MCP Protocol: #{@protocol_version}, Gem: v#{VectorMCP::VERSION}) initialized.")
196
114
  end
197
115
 
198
116
  # --- Server Execution ---
@@ -213,8 +131,13 @@ module VectorMCP
213
131
  when :stdio
214
132
  VectorMCP::Transport::Stdio.new(self, **options)
215
133
  when :sse
216
- # VectorMCP::Transport::SSE.new(self, **options)
217
- raise NotImplementedError, "SSE transport is not yet supported"
134
+ begin
135
+ require_relative "transport/sse"
136
+ VectorMCP::Transport::SSE.new(self, **options)
137
+ rescue LoadError => e
138
+ logger.fatal("SSE transport requires additional dependencies. Install the 'async' and 'falcon' gems.")
139
+ raise NotImplementedError, "SSE transport dependencies not available: #{e.message}"
140
+ end
218
141
  when VectorMCP::Transport::Base # Allow passing an initialized transport instance
219
142
  transport.server = self if transport.respond_to?(:server=) && transport.server.nil? # Ensure server is set
220
143
  transport
@@ -225,291 +148,6 @@ module VectorMCP
225
148
  self.transport = active_transport
226
149
  active_transport.run
227
150
  end
228
-
229
- # --- Message Handling Logic (primarily called by transports) ---
230
-
231
- # Handles an incoming JSON-RPC message (request or notification).
232
- # This is the main dispatch point for messages received by a transport.
233
- #
234
- # @param message [Hash] The parsed JSON-RPC message object.
235
- # @param session [VectorMCP::Session] The client session associated with this message.
236
- # @param session_id [String] A unique identifier for the underlying transport connection (e.g., socket ID, stdio pipe).
237
- # @return [Object, nil] For requests, returns the result data to be sent in the JSON-RPC response.
238
- # For notifications, returns `nil`.
239
- # @raise [VectorMCP::ProtocolError] if the message is invalid or an error occurs during handling
240
- # that should be reported as a JSON-RPC error. May also raise subclasses such as NotFoundError, InvalidParamsError, etc.
241
- def handle_message(message, session, session_id)
242
- id = message["id"]
243
- method = message["method"]
244
- params = message["params"] || {} # Default to empty hash if params is nil
245
-
246
- if id && method # Request
247
- logger.info("[#{session_id}] Request [#{id}]: #{method} with params: #{params.inspect}")
248
- handle_request(id, method, params, session)
249
- elsif method # Notification
250
- logger.info("[#{session_id}] Notification: #{method} with params: #{params.inspect}")
251
- handle_notification(method, params, session)
252
- nil # Notifications do not have a return value to send back to client
253
- elsif id # Invalid: Has ID but no method (likely a malformed request or client sending a response)
254
- logger.warn("[#{session_id}] Invalid message: Has ID [#{id}] but no method. #{message.inspect}")
255
- raise VectorMCP::InvalidRequestError.new("Request object must include a 'method' member.", request_id: id)
256
- else # Invalid: No ID and no method
257
- logger.warn("[#{session_id}] Invalid message: Missing both 'id' and 'method'. #{message.inspect}")
258
- raise VectorMCP::InvalidRequestError.new("Invalid message format", request_id: nil)
259
- end
260
- end
261
-
262
- # --- Server Information and Capabilities ---
263
-
264
- # Provides basic information about the server.
265
- # @return [Hash] Server name and version.
266
- def server_info
267
- { name: @name, version: @version }
268
- end
269
-
270
- # Describes the capabilities of this server according to MCP specifications.
271
- # @return [Hash] A capabilities object.
272
- def server_capabilities
273
- caps = {}
274
- caps[:tools] = { listChanged: false } unless @tools.empty? # `listChanged` for tools is not standard but included for symmetry
275
- caps[:resources] = { subscribe: false, listChanged: false } unless @resources.empty?
276
- caps[:prompts] = { listChanged: @prompts_list_changed } unless @prompts.empty?
277
- # `experimental` is a defined field in MCP capabilities, can be used for non-standard features.
278
- caps[:experimental] = {}
279
- caps
280
- end
281
-
282
- # Resets the `prompts_list_changed` flag to false.
283
- # Typically called by the `prompts/list` handler after a client has fetched the updated list.
284
- # @return [void]
285
- def clear_prompts_list_changed
286
- @prompts_list_changed = false
287
- logger.debug("Prompts listChanged flag cleared.")
288
- end
289
-
290
- # Notifies connected clients that the list of available prompts has changed.
291
- # This method attempts to use `broadcast_notification` if the transport supports it,
292
- # otherwise falls back to `send_notification` (which might only make sense for single-client transports like stdio).
293
- # @return [void]
294
- def notify_prompts_list_changed
295
- return unless transport && @prompts_list_changed # Only notify if there was a change and transport is up
296
-
297
- notification_method = "notifications/prompts/list_changed"
298
- begin
299
- if transport.respond_to?(:broadcast_notification)
300
- logger.info("Broadcasting prompts list changed notification.")
301
- transport.broadcast_notification(notification_method)
302
- elsif transport.respond_to?(:send_notification)
303
- # For single-client transports or as a fallback if broadcast isn't specific
304
- logger.info("Sending prompts list changed notification (transport may broadcast or send to first client).")
305
- transport.send_notification(notification_method) # Transport needs to decide target if not broadcast
306
- else
307
- logger.warn("Transport does not support sending notifications/prompts/list_changed.")
308
- end
309
- rescue StandardError => e
310
- logger.error("Failed to send prompts list changed notification: #{e.class.name}: #{e.message}")
311
- end
312
- end
313
-
314
- private
315
-
316
- # Registers a session as a subscriber to prompt list changes.
317
- # Used by the `prompts/subscribe` handler.
318
- # @api private
319
- # @param session [VectorMCP::Session] The session to subscribe.
320
- # @return [void]
321
- def subscribe_prompts(session)
322
- @prompt_subscribers << session unless @prompt_subscribers.include?(session)
323
- logger.debug("Session subscribed to prompt list changes: #{session.object_id}")
324
- end
325
-
326
- # Internal handler for JSON-RPC requests.
327
- # @api private
328
- # @param id [String, Integer] The request ID.
329
- # @param method [String] The request method name.
330
- # @param params [Hash] The request parameters.
331
- # @param session [VectorMCP::Session] The client session.
332
- # @return [Object] The result of the request handler.
333
- # @raise [VectorMCP::ProtocolError] Propagates errors from handlers or raises new ones (e.g., MethodNotFound, NotFoundError, etc.).
334
- # rubocop:disable Metrics/MethodLength
335
- def handle_request(id, method, params, session)
336
- unless session.initialized?
337
- # Allow "initialize" even if not marked initialized yet by server
338
- return session.initialize!(params) if method == "initialize"
339
-
340
- # For any other method, session must be initialized
341
- raise VectorMCP::InitializationError.new("Session not initialized. Client must send 'initialize' first.", request_id: id)
342
- end
343
-
344
- handler = @request_handlers[method]
345
- raise VectorMCP::MethodNotFoundError.new(method, request_id: id) unless handler
346
-
347
- begin
348
- @in_flight_requests[id] = { method: method, params: params, session: session, start_time: Time.now }
349
- result = handler.call(params, session, self)
350
- result
351
- rescue VectorMCP::ProtocolError => e # Includes NotFoundError, InvalidParamsError, InternalError from handlers
352
- # Ensure the request ID from the current context is on the error
353
- e.request_id = id unless e.request_id && e.request_id == id
354
- raise e # Re-raise with potentially updated request_id
355
- rescue StandardError => e
356
- logger.error("Unhandled error during request '#{method}' (ID: #{id}): #{e.message}\nBacktrace: #{e.backtrace.join("\n ")}")
357
- raise VectorMCP::InternalError.new(
358
- "Request handler failed unexpectedly",
359
- request_id: id,
360
- details: { method: method, error: "An internal error occurred" }
361
- )
362
- ensure
363
- @in_flight_requests.delete(id)
364
- end
365
- end
366
- # rubocop:enable Metrics/MethodLength
367
-
368
- # Internal handler for JSON-RPC notifications.
369
- # @api private
370
- # @param method [String] The notification method name.
371
- # @param params [Hash] The notification parameters.
372
- # @param session [VectorMCP::Session] The client session.
373
- # @return [void]
374
- # @raise [StandardError] if the notification handler raises an error (errors are logged, not propagated to the client).
375
- def handle_notification(method, params, session)
376
- unless session.initialized? || method == "initialized"
377
- logger.warn("Ignoring notification '#{method}' before session is initialized. Params: #{params.inspect}")
378
- return
379
- end
380
-
381
- handler = @notification_handlers[method]
382
- if handler
383
- begin
384
- handler.call(params, session, self)
385
- rescue StandardError => e
386
- logger.error("Error executing notification handler '#{method}': #{e.message}\nBacktrace (top 5):\n #{e.backtrace.first(5).join("\n ")}")
387
- # Notifications must not generate a response, even on error.
388
- end
389
- else
390
- logger.debug("No handler registered for notification: #{method}")
391
- end
392
- end
393
-
394
- # Sets up default handlers for core MCP methods using {VectorMCP::Handlers::Core}.
395
- # @api private
396
- # @return [void]
397
- def setup_default_handlers
398
- # Core Requests
399
- on_request("initialize", &session_method(:initialize!))
400
- on_request("ping", &Handlers::Core.method(:ping))
401
- on_request("tools/list", &Handlers::Core.method(:list_tools))
402
- on_request("tools/call", &Handlers::Core.method(:call_tool))
403
- on_request("resources/list", &Handlers::Core.method(:list_resources))
404
- on_request("resources/read", &Handlers::Core.method(:read_resource))
405
- on_request("prompts/list", &Handlers::Core.method(:list_prompts))
406
- on_request("prompts/get", &Handlers::Core.method(:get_prompt))
407
- on_request("prompts/subscribe", &Handlers::Core.method(:subscribe_prompts))
408
-
409
- # Core Notifications
410
- on_notification("initialized", &Handlers::Core.method(:initialized_notification))
411
- # Standard cancel request names
412
- %w[$/cancelRequest $/cancel notifications/cancelled].each do |cancel_method|
413
- on_notification(cancel_method, &Handlers::Core.method(:cancel_request_notification))
414
- end
415
- end
416
-
417
- # Helper to create a proc that calls a method on the session object.
418
- # Used for the `initialize` request handler.
419
- # @api private
420
- def session_method(method_name)
421
- lambda do |params, session, _server|
422
- session.public_send(method_name, params)
423
- end
424
- end
425
-
426
- # Validates the structure of the `arguments` array provided to {#register_prompt}.
427
- # Each item must be a Hash with at least a `:name`, and optionally `:description` and `:required`.
428
- # @api private
429
- # @param argument_defs [Array<Hash>] The array of argument definitions to validate.
430
- # @return [void]
431
- # @raise [ArgumentError] if `argument_defs` is not an Array or if any definition is invalid.
432
- def validate_prompt_arguments(argument_defs)
433
- raise ArgumentError, "Prompt arguments definition must be an Array of Hashes." unless argument_defs.is_a?(Array)
434
-
435
- argument_defs.each_with_index { |arg, idx| validate_single_prompt_argument(arg, idx) }
436
- end
437
-
438
- # Defines the keys allowed in a prompt argument definition hash.
439
- ALLOWED_PROMPT_ARG_KEYS = %w[name description required type].freeze # Added 'type' as per common usage
440
- private_constant :ALLOWED_PROMPT_ARG_KEYS
441
-
442
- # Validates a single prompt argument definition hash.
443
- # @api private
444
- # @param arg [Hash] The argument definition hash.
445
- # @param idx [Integer] The index of this argument in the list (for error messages).
446
- # @return [void]
447
- # @raise [ArgumentError] if the argument definition is invalid.
448
- def validate_single_prompt_argument(arg, idx)
449
- raise ArgumentError, "Prompt argument definition at index #{idx} must be a Hash. Found: #{arg.class}" unless arg.is_a?(Hash)
450
-
451
- validate_prompt_arg_name!(arg, idx)
452
- validate_prompt_arg_description!(arg, idx)
453
- validate_prompt_arg_required_flag!(arg, idx)
454
- validate_prompt_arg_type!(arg, idx) # New validation for :type
455
- validate_prompt_arg_unknown_keys!(arg, idx)
456
- end
457
-
458
- # Validates the :name key of a prompt argument definition.
459
- # @api private
460
- def validate_prompt_arg_name!(arg, idx)
461
- name_val = arg[:name] || arg["name"]
462
- raise ArgumentError, "Prompt argument at index #{idx} missing :name" if name_val.nil?
463
- unless name_val.is_a?(String) || name_val.is_a?(Symbol)
464
- raise ArgumentError, "Prompt argument :name at index #{idx} must be a String or Symbol. Found: #{name_val.class}"
465
- end
466
- raise ArgumentError, "Prompt argument :name at index #{idx} cannot be empty." if name_val.to_s.strip.empty?
467
- end
468
-
469
- # Validates the :description key of a prompt argument definition.
470
- # @api private
471
- def validate_prompt_arg_description!(arg, idx)
472
- return unless arg.key?(:description) || arg.key?("description") # Optional field
473
-
474
- desc_val = arg[:description] || arg["description"]
475
- return if desc_val.nil? || desc_val.is_a?(String) # Allow nil or String
476
-
477
- raise ArgumentError, "Prompt argument :description at index #{idx} must be a String if provided. Found: #{desc_val.class}"
478
- end
479
-
480
- # Validates the :required key of a prompt argument definition.
481
- # @api private
482
- def validate_prompt_arg_required_flag!(arg, idx)
483
- return unless arg.key?(:required) || arg.key?("required") # Optional field
484
-
485
- req_val = arg[:required] || arg["required"]
486
- return if [true, false].include?(req_val)
487
-
488
- raise ArgumentError, "Prompt argument :required at index #{idx} must be true or false if provided. Found: #{req_val.inspect}"
489
- end
490
-
491
- # Validates the :type key of a prompt argument definition (new).
492
- # @api private
493
- def validate_prompt_arg_type!(arg, idx)
494
- return unless arg.key?(:type) || arg.key?("type") # Optional field
495
-
496
- type_val = arg[:type] || arg["type"]
497
- return if type_val.nil? || type_val.is_a?(String) # Allow nil or String (e.g., "string", "number", "boolean")
498
-
499
- raise ArgumentError, "Prompt argument :type at index #{idx} must be a String if provided (e.g., JSON schema type). Found: #{type_val.class}"
500
- end
501
-
502
- # Checks for any unknown keys in a prompt argument definition.
503
- # @api private
504
- def validate_prompt_arg_unknown_keys!(arg, idx)
505
- unknown_keys = arg.transform_keys(&:to_s).keys - ALLOWED_PROMPT_ARG_KEYS
506
- return if unknown_keys.empty?
507
-
508
- # rubocop:disable Layout/LineLength
509
- raise ArgumentError,
510
- "Prompt argument definition at index #{idx} contains unknown keys: #{unknown_keys.join(", ")}. Allowed: #{ALLOWED_PROMPT_ARG_KEYS.join(", ")}."
511
- # rubocop:enable Layout/LineLength
512
- end
513
151
  end
514
152
 
515
153
  module Transport