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
data/lib/vector_mcp/server.rb
CHANGED
@@ -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
|
68
|
+
# Initializes a new VectorMCP server.
|
61
69
|
#
|
62
|
-
# @param name_pos [String
|
63
|
-
#
|
64
|
-
# @param
|
65
|
-
# @param
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
@
|
87
|
-
@notification_handlers = {}
|
96
|
+
@transport = nil
|
88
97
|
@tools = {}
|
89
98
|
@resources = {}
|
90
99
|
@prompts = {}
|
91
|
-
@
|
100
|
+
@roots = {}
|
101
|
+
@request_handlers = {}
|
102
|
+
@notification_handlers = {}
|
103
|
+
@in_flight_requests = {}
|
92
104
|
@prompts_list_changed = false
|
93
|
-
@prompt_subscribers = []
|
105
|
+
@prompt_subscribers = []
|
106
|
+
@roots_list_changed = false
|
94
107
|
|
95
|
-
|
96
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
217
|
-
|
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
|