vector_mcp 0.1.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
+ require "json"
4
+ require "uri"
5
+
6
+ module VectorMCP
7
+ module Handlers
8
+ # Provides default handlers for the core MCP methods.
9
+ # These methods are typically registered on a {VectorMCP::Server} instance.
10
+ # All public methods are designed to be called by the server's message dispatching logic.
11
+ #
12
+ # @see VectorMCP::Server#setup_default_handlers
13
+ module Core
14
+ # --- Request Handlers ---
15
+
16
+ # Handles the `ping` request.
17
+ #
18
+ # @param _params [Hash] The request parameters (ignored).
19
+ # @param _session [VectorMCP::Session] The current session (ignored).
20
+ # @param _server [VectorMCP::Server] The server instance (ignored).
21
+ # @return [Hash] An empty hash, as per MCP spec for ping.
22
+ def self.ping(_params, _session, _server)
23
+ VectorMCP.logger.debug("Handling ping request")
24
+ {}
25
+ end
26
+
27
+ # Handles the `tools/list` request.
28
+ #
29
+ # @param _params [Hash] The request parameters (ignored).
30
+ # @param _session [VectorMCP::Session] The current session (ignored).
31
+ # @param server [VectorMCP::Server] The server instance.
32
+ # @return [Hash] A hash containing an array of tool definitions.
33
+ # Example: `{ tools: [ { name: "my_tool", ... } ] }`
34
+ def self.list_tools(_params, _session, server)
35
+ {
36
+ tools: server.tools.values.map(&:as_mcp_definition)
37
+ }
38
+ end
39
+
40
+ # Handles the `tools/call` request.
41
+ #
42
+ # @param params [Hash] The request parameters.
43
+ # Expected keys: "name" (String), "arguments" (Hash, optional).
44
+ # @param _session [VectorMCP::Session] The current session (ignored).
45
+ # @param server [VectorMCP::Server] The server instance.
46
+ # @return [Hash] A hash containing the tool call result or an error indication.
47
+ # Example success: `{ isError: false, content: [{ type: "text", ... }] }`
48
+ # @raise [VectorMCP::NotFoundError] if the requested tool is not found.
49
+ def self.call_tool(params, _session, server)
50
+ tool_name = params["name"]
51
+ arguments = params["arguments"] || {}
52
+
53
+ tool = server.tools[tool_name]
54
+ raise VectorMCP::NotFoundError.new("Not Found", details: "Tool not found: #{tool_name}") unless tool
55
+
56
+ # Let StandardError propagate to Server#handle_request
57
+ result = tool.handler.call(arguments)
58
+ {
59
+ isError: false,
60
+ content: VectorMCP::Util.convert_to_mcp_content(result)
61
+ }
62
+ end
63
+
64
+ # Handles the `resources/list` request.
65
+ #
66
+ # @param _params [Hash] The request parameters (ignored).
67
+ # @param _session [VectorMCP::Session] The current session (ignored).
68
+ # @param server [VectorMCP::Server] The server instance.
69
+ # @return [Hash] A hash containing an array of resource definitions.
70
+ # Example: `{ resources: [ { uri: "memory://data", name: "My Data", ... } ] }`
71
+ def self.list_resources(_params, _session, server)
72
+ {
73
+ resources: server.resources.values.map(&:as_mcp_definition)
74
+ }
75
+ end
76
+
77
+ # Handles the `resources/read` request.
78
+ #
79
+ # @param params [Hash] The request parameters.
80
+ # Expected key: "uri" (String).
81
+ # @param _session [VectorMCP::Session] The current session (ignored).
82
+ # @param server [VectorMCP::Server] The server instance.
83
+ # @return [Hash] A hash containing an array of content items from the resource.
84
+ # Example: `{ contents: [{ type: "text", text: "...", uri: "memory://data" }] }`
85
+ # @raise [VectorMCP::NotFoundError] if the requested resource URI is not found.
86
+ def self.read_resource(params, _session, server)
87
+ uri_s = params["uri"]
88
+ raise VectorMCP::NotFoundError.new("Not Found", details: "Resource not found: #{uri_s}") unless server.resources[uri_s]
89
+
90
+ resource = server.resources[uri_s]
91
+ # Let StandardError propagate to Server#handle_request
92
+ content_raw = resource.handler.call(params)
93
+ contents = VectorMCP::Util.convert_to_mcp_content(content_raw, mime_type: resource.mime_type)
94
+ contents.each do |item|
95
+ # Add URI to each content item if not already present
96
+ item[:uri] ||= uri_s
97
+ end
98
+ { contents: contents }
99
+ end
100
+
101
+ # Handles the `prompts/list` request.
102
+ # If the server supports dynamic prompt lists, this clears the `listChanged` flag.
103
+ #
104
+ # @param _params [Hash] The request parameters (ignored).
105
+ # @param _session [VectorMCP::Session] The current session (ignored).
106
+ # @param server [VectorMCP::Server] The server instance.
107
+ # @return [Hash] A hash containing an array of prompt definitions.
108
+ # Example: `{ prompts: [ { name: "my_prompt", ... } ] }`
109
+ def self.list_prompts(_params, _session, server)
110
+ # Once the list is supplied, clear the listChanged flag
111
+ result = {
112
+ prompts: server.prompts.values.map(&:as_mcp_definition)
113
+ }
114
+ server.clear_prompts_list_changed if server.respond_to?(:clear_prompts_list_changed)
115
+ result
116
+ end
117
+
118
+ # Handles the `prompts/subscribe` request (placeholder).
119
+ # This implementation is a simple acknowledgement.
120
+ #
121
+ # @param _params [Hash] The request parameters (ignored).
122
+ # @param session [VectorMCP::Session] The current session.
123
+ # @param server [VectorMCP::Server] The server instance.
124
+ # @return [Hash] An empty hash.
125
+ def self.subscribe_prompts(_params, session, server)
126
+ # Use private helper via send to avoid making it public
127
+ server.send(:subscribe_prompts, session) if server.respond_to?(:send)
128
+ {}
129
+ end
130
+
131
+ # Handles the `prompts/get` request.
132
+ # Validates arguments and the structure of the prompt handler's response.
133
+ #
134
+ # @param params [Hash] The request parameters.
135
+ # Expected keys: "name" (String), "arguments" (Hash, optional).
136
+ # @param _session [VectorMCP::Session] The current session (ignored).
137
+ # @param server [VectorMCP::Server] The server instance.
138
+ # @return [Hash] The result from the prompt's handler, which should conform to MCP's GetPromptResult.
139
+ # @raise [VectorMCP::NotFoundError] if the prompt name is not found.
140
+ # @raise [VectorMCP::InvalidParamsError] if arguments are invalid.
141
+ # @raise [VectorMCP::InternalError] if the prompt handler returns an invalid data structure.
142
+ def self.get_prompt(params, _session, server)
143
+ prompt_name = params["name"]
144
+ prompt = fetch_prompt(prompt_name, server)
145
+
146
+ arguments = params["arguments"] || {}
147
+ validate_arguments!(prompt_name, prompt, arguments)
148
+
149
+ # Call the registered handler after arguments were validated
150
+ result_data = prompt.handler.call(arguments)
151
+
152
+ validate_prompt_response!(prompt_name, result_data, server)
153
+
154
+ # Return the handler response directly (must match GetPromptResult schema)
155
+ result_data
156
+ end
157
+
158
+ # --- Notification Handlers ---
159
+
160
+ # Handles the `initialized` notification from the client.
161
+ #
162
+ # @param _params [Hash] The notification parameters (ignored).
163
+ # @param _session [VectorMCP::Session] The current session (ignored, but state is on server).
164
+ # @param server [VectorMCP::Server] The server instance.
165
+ # @return [void]
166
+ def self.initialized_notification(_params, _session, server)
167
+ server.logger.info("Session initialized")
168
+ end
169
+
170
+ # Handles the `$/cancelRequest` notification from the client.
171
+ #
172
+ # @param params [Hash] The notification parameters. Expected key: "id".
173
+ # @param _session [VectorMCP::Session] The current session (ignored).
174
+ # @param server [VectorMCP::Server] The server instance.
175
+ # @return [void]
176
+ def self.cancel_request_notification(params, _session, server)
177
+ request_id = params["id"]
178
+ server.logger.info("Received cancellation request for ID: #{request_id}")
179
+ # Application-specific cancellation logic would go here
180
+ # Access in-flight requests via server.in_flight_requests[request_id]
181
+ end
182
+
183
+ # --- Helper methods (internal) ---
184
+
185
+ # Fetches a prompt by its name from the server.
186
+ # @api private
187
+ # @param prompt_name [String] The name of the prompt to fetch.
188
+ # @param server [VectorMCP::Server] The server instance.
189
+ # @return [VectorMCP::Definitions::Prompt] The prompt definition.
190
+ # @raise [VectorMCP::NotFoundError] if the prompt is not found.
191
+ def self.fetch_prompt(prompt_name, server)
192
+ prompt = server.prompts[prompt_name]
193
+ return prompt if prompt
194
+
195
+ raise VectorMCP::NotFoundError.new("Not Found", details: "Prompt not found: #{prompt_name}")
196
+ end
197
+ private_class_method :fetch_prompt
198
+
199
+ # Validates arguments provided for a prompt against its definition.
200
+ # @api private
201
+ # @param prompt_name [String] The name of the prompt.
202
+ # @param prompt [VectorMCP::Definitions::Prompt] The prompt definition.
203
+ # @param arguments [Hash] The arguments supplied by the client.
204
+ # @return [void]
205
+ # @raise [VectorMCP::InvalidParamsError] if arguments are invalid (e.g., missing, unknown, wrong type).
206
+ def self.validate_arguments!(prompt_name, prompt, arguments)
207
+ ensure_hash!(prompt_name, arguments, "arguments")
208
+
209
+ arg_defs = prompt.respond_to?(:arguments) ? (prompt.arguments || []) : []
210
+ missing, unknown = argument_diffs(arg_defs, arguments)
211
+
212
+ return if missing.empty? && unknown.empty?
213
+
214
+ raise VectorMCP::InvalidParamsError.new("Invalid arguments",
215
+ details: build_invalid_arg_details(prompt_name, missing, unknown))
216
+ end
217
+ private_class_method :validate_arguments!
218
+
219
+ # Ensures a given value is a Hash.
220
+ # @api private
221
+ # @param prompt_name [String] The name of the prompt (for error reporting).
222
+ # @param value [Object] The value to check.
223
+ # @param field_name [String] The name of the field being checked (for error reporting).
224
+ # @return [void]
225
+ # @raise [VectorMCP::InvalidParamsError] if the value is not a Hash.
226
+ def self.ensure_hash!(prompt_name, value, field_name)
227
+ return if value.is_a?(Hash)
228
+
229
+ raise VectorMCP::InvalidParamsError.new("#{field_name} must be an object", details: { prompt: prompt_name })
230
+ end
231
+ private_class_method :ensure_hash!
232
+
233
+ # Calculates the difference between required/allowed arguments and supplied arguments.
234
+ # @api private
235
+ # @param arg_defs [Array<Hash>] The argument definitions for the prompt.
236
+ # @param arguments [Hash] The arguments supplied by the client.
237
+ # @return [Array(Array<String>, Array<String>)] A pair of arrays: [missing_keys, unknown_keys].
238
+ def self.argument_diffs(arg_defs, arguments)
239
+ required = arg_defs.select { |a| a[:required] }.map { |a| a[:name].to_s }
240
+ allowed = arg_defs.map { |a| a[:name].to_s }
241
+
242
+ supplied_keys = arguments.keys.map(&:to_s)
243
+
244
+ [required - supplied_keys, supplied_keys - allowed]
245
+ end
246
+ private_class_method :argument_diffs
247
+
248
+ # Builds the details hash for an InvalidParamsError related to prompt arguments.
249
+ # @api private
250
+ # @param prompt_name [String] The name of the prompt.
251
+ # @param missing [Array<String>] List of missing required argument names.
252
+ # @param unknown [Array<String>] List of supplied argument names that are not allowed.
253
+ # @return [Hash] The error details hash.
254
+ def self.build_invalid_arg_details(prompt_name, missing, unknown)
255
+ {}.tap do |details|
256
+ details[:prompt] = prompt_name
257
+ details[:missing] = missing unless missing.empty?
258
+ details[:unknown] = unknown unless unknown.empty?
259
+ end
260
+ end
261
+ private_class_method :build_invalid_arg_details
262
+
263
+ # Validates the structure of a prompt handler's response.
264
+ # @api private
265
+ # @param prompt_name [String] The name of the prompt (for error reporting).
266
+ # @param result_data [Object] The data returned by the prompt handler.
267
+ # @param server [VectorMCP::Server] The server instance (for logging).
268
+ # @return [void]
269
+ # @raise [VectorMCP::InternalError] if the response structure is invalid.
270
+ def self.validate_prompt_response!(prompt_name, result_data, server)
271
+ unless result_data.is_a?(Hash) && result_data[:messages].is_a?(Array)
272
+ server.logger.error("Prompt handler for '#{prompt_name}' returned invalid data structure: #{result_data.inspect}")
273
+ raise VectorMCP::InternalError.new("Prompt handler returned invalid data structure",
274
+ details: { prompt: prompt_name, error: "Handler must return a Hash with a :messages Array" })
275
+ end
276
+
277
+ result_data[:messages].each do |msg|
278
+ next if msg.is_a?(Hash) && msg[:role] && msg[:content].is_a?(Hash) && msg[:content][:type]
279
+
280
+ server.logger.error("Prompt handler for '#{prompt_name}' returned invalid message structure: #{msg.inspect}")
281
+ raise VectorMCP::InternalError.new("Prompt handler returned invalid message structure",
282
+ details: { prompt: prompt_name,
283
+ error: "Messages must be Hashes with :role and :content Hash (with :type)" })
284
+ end
285
+ end
286
+ private_class_method :validate_prompt_response!
287
+ end
288
+ end
289
+ end