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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +210 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/vector_mcp/definitions.rb +81 -0
- data/lib/vector_mcp/errors.rb +138 -0
- data/lib/vector_mcp/handlers/core.rb +289 -0
- data/lib/vector_mcp/server.rb +521 -0
- data/lib/vector_mcp/session.rb +67 -0
- data/lib/vector_mcp/transport/sse.rb +663 -0
- data/lib/vector_mcp/transport/stdio.rb +258 -0
- data/lib/vector_mcp/util.rb +113 -0
- data/lib/vector_mcp/version.rb +6 -0
- data/lib/vector_mcp.rb +65 -0
- metadata +131 -0
@@ -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
|