actionmcp 0.102.0 → 0.104.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 +46 -3
- data/app/models/action_mcp/session.rb +6 -5
- data/lib/action_mcp/configuration.rb +44 -8
- data/lib/action_mcp/server/base_session.rb +5 -1
- data/lib/action_mcp/test_helper/session_store_assertions.rb +0 -70
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +0 -1
- data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +4 -4
- data/lib/generators/action_mcp/install/templates/mcp.yml +11 -1
- data/lib/generators/action_mcp/tool/templates/tool.rb.erb +2 -2
- metadata +1 -26
- data/lib/action_mcp/client/active_record_session_store.rb +0 -57
- data/lib/action_mcp/client/base.rb +0 -225
- data/lib/action_mcp/client/blueprint.rb +0 -163
- data/lib/action_mcp/client/catalog.rb +0 -164
- data/lib/action_mcp/client/collection.rb +0 -168
- data/lib/action_mcp/client/elicitation.rb +0 -34
- data/lib/action_mcp/client/json_rpc_handler.rb +0 -202
- data/lib/action_mcp/client/logging.rb +0 -19
- data/lib/action_mcp/client/messaging.rb +0 -28
- data/lib/action_mcp/client/prompt_book.rb +0 -117
- data/lib/action_mcp/client/prompts.rb +0 -47
- data/lib/action_mcp/client/request_timeouts.rb +0 -74
- data/lib/action_mcp/client/resources.rb +0 -100
- data/lib/action_mcp/client/roots.rb +0 -13
- data/lib/action_mcp/client/server.rb +0 -60
- data/lib/action_mcp/client/session_store.rb +0 -39
- data/lib/action_mcp/client/session_store_factory.rb +0 -27
- data/lib/action_mcp/client/streamable_client.rb +0 -264
- data/lib/action_mcp/client/streamable_http_transport.rb +0 -306
- data/lib/action_mcp/client/test_session_store.rb +0 -84
- data/lib/action_mcp/client/toolbox.rb +0 -199
- data/lib/action_mcp/client/tools.rb +0 -47
- data/lib/action_mcp/client/transport.rb +0 -137
- data/lib/action_mcp/client/volatile_session_store.rb +0 -38
- data/lib/action_mcp/client.rb +0 -71
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
# Base collection class for MCP client collections
|
|
6
|
-
class Collection
|
|
7
|
-
include RequestTimeouts
|
|
8
|
-
|
|
9
|
-
attr_reader :client, :loaded, :next_cursor, :total
|
|
10
|
-
|
|
11
|
-
def initialize(items, client, silence_sql: true)
|
|
12
|
-
@collection_data = items || []
|
|
13
|
-
@client = client
|
|
14
|
-
@loaded = !@collection_data.empty?
|
|
15
|
-
@silence_sql = silence_sql
|
|
16
|
-
@next_cursor = nil
|
|
17
|
-
@total = items&.size || 0
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def all(limit: nil)
|
|
21
|
-
if limit
|
|
22
|
-
# If a limit is provided, use pagination
|
|
23
|
-
result = []
|
|
24
|
-
each_page(limit: limit) { |page| result.concat(page) }
|
|
25
|
-
result
|
|
26
|
-
else
|
|
27
|
-
# Otherwise, maintain the old behavior
|
|
28
|
-
silence_logs { load_items unless @loaded }
|
|
29
|
-
@collection_data
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def all!(timeout: DEFAULT_TIMEOUT)
|
|
34
|
-
silence_logs { load_items(force: true, timeout: timeout) }
|
|
35
|
-
@collection_data
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Fetch a single page of results
|
|
39
|
-
#
|
|
40
|
-
# @param cursor [String, nil] Optional cursor for pagination
|
|
41
|
-
# @param limit [Integer, nil] Optional limit for page size
|
|
42
|
-
# @return [Array<Object>] The page of items
|
|
43
|
-
def page(cursor: nil, limit: nil)
|
|
44
|
-
silence_logs { load_page(cursor: cursor, limit: limit) }
|
|
45
|
-
@collection_data
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Check if there are more pages available
|
|
49
|
-
#
|
|
50
|
-
# @return [Boolean] true if there are more pages to fetch
|
|
51
|
-
def has_more_pages?
|
|
52
|
-
!@next_cursor.nil?
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Fetch the next page of results
|
|
56
|
-
#
|
|
57
|
-
# @param limit [Integer, nil] Optional limit for page size
|
|
58
|
-
# @return [Array<Object>] The next page of items, or empty array if no more pages
|
|
59
|
-
def next_page(limit: nil)
|
|
60
|
-
return [] unless has_more_pages?
|
|
61
|
-
|
|
62
|
-
page(cursor: @next_cursor, limit: limit)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Iterate through all pages of results
|
|
66
|
-
#
|
|
67
|
-
# @param limit [Integer, nil] Optional limit for page size
|
|
68
|
-
# @yield [page] Block to process each page
|
|
69
|
-
# @yieldparam page [Array<Object>] A page of items
|
|
70
|
-
def each_page(limit: nil)
|
|
71
|
-
return unless block_given?
|
|
72
|
-
|
|
73
|
-
current_page = page(limit: limit)
|
|
74
|
-
yield current_page
|
|
75
|
-
|
|
76
|
-
while has_more_pages?
|
|
77
|
-
current_page = next_page(limit: limit)
|
|
78
|
-
yield current_page unless current_page.empty?
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Filter items based on a given block
|
|
83
|
-
#
|
|
84
|
-
# @yield [item] Block that determines whether to include an item
|
|
85
|
-
# @yieldparam item [Object] An item from the collection
|
|
86
|
-
# @yieldreturn [Boolean] true to include the item, false to exclude it
|
|
87
|
-
# @return [Array<Object>] Items that match the filter criteria
|
|
88
|
-
def filter(&block)
|
|
89
|
-
all.select(&block)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Number of items in the collection
|
|
93
|
-
#
|
|
94
|
-
# @return [Integer] The number of items
|
|
95
|
-
def size
|
|
96
|
-
all.size
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Implements enumerable functionality
|
|
100
|
-
include Enumerable
|
|
101
|
-
|
|
102
|
-
def each(&block)
|
|
103
|
-
all.each(&block)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
alias loaded? loaded
|
|
107
|
-
|
|
108
|
-
protected
|
|
109
|
-
|
|
110
|
-
def load_items(force: false, timeout: DEFAULT_TIMEOUT)
|
|
111
|
-
return if @loaded && !force
|
|
112
|
-
|
|
113
|
-
# Make sure @load_method is defined in the subclass
|
|
114
|
-
raise NotImplementedError, "Subclass must define @load_method" unless defined?(@load_method)
|
|
115
|
-
|
|
116
|
-
# Use the RequestTimeouts module to handle the request
|
|
117
|
-
load_with_timeout(@load_method, force: force, timeout: timeout)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def load_page(cursor: nil, limit: nil, timeout: DEFAULT_TIMEOUT)
|
|
121
|
-
# Make sure @load_method is defined in the subclass
|
|
122
|
-
raise NotImplementedError, "Subclass must define @load_method" unless defined?(@load_method)
|
|
123
|
-
|
|
124
|
-
# Use the RequestTimeouts module to handle the request with pagination params
|
|
125
|
-
params = {}
|
|
126
|
-
params[:cursor] = cursor if cursor
|
|
127
|
-
params[:limit] = limit if limit
|
|
128
|
-
|
|
129
|
-
client.send(@load_method, params)
|
|
130
|
-
|
|
131
|
-
start_time = Time.now
|
|
132
|
-
sleep(0.1) while !@loaded && (Time.now - start_time) < timeout
|
|
133
|
-
|
|
134
|
-
# Update @loaded status even if we timed out
|
|
135
|
-
@loaded = true
|
|
136
|
-
|
|
137
|
-
# Return the loaded data
|
|
138
|
-
@collection_data
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
private
|
|
142
|
-
|
|
143
|
-
def silence_logs
|
|
144
|
-
return yield unless @silence_sql
|
|
145
|
-
|
|
146
|
-
original_log_level = ActionMCP::Session.logger&.level
|
|
147
|
-
begin
|
|
148
|
-
# Temporarily increase log level to suppress SQL queries
|
|
149
|
-
ActionMCP::Session.logger.level = Logger::WARN if ActionMCP::Session.logger
|
|
150
|
-
yield
|
|
151
|
-
ensure
|
|
152
|
-
# Restore original log level
|
|
153
|
-
ActionMCP::Session.logger.level = original_log_level if ActionMCP::Session.logger && original_log_level
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def log_error(message)
|
|
158
|
-
# Safely handle logging - don't assume Rails.logger exists
|
|
159
|
-
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
160
|
-
Rails.logger.error("[#{self.class.name}] #{message}")
|
|
161
|
-
else
|
|
162
|
-
# Fall back to puts if Rails.logger is not available
|
|
163
|
-
puts "[ERROR] [#{self.class.name}] #{message}"
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
# Handles elicitation requests from servers
|
|
6
|
-
module Elicitation
|
|
7
|
-
# Process elicitation request from server
|
|
8
|
-
# @param id [String, Integer] The request ID
|
|
9
|
-
# @param params [Hash] The elicitation parameters
|
|
10
|
-
def process_elicitation_request(id, params)
|
|
11
|
-
params["message"]
|
|
12
|
-
params["requestedSchema"]
|
|
13
|
-
|
|
14
|
-
# In a real implementation, this would prompt the user
|
|
15
|
-
# For now, we'll just return a decline response
|
|
16
|
-
# Actual implementations should override this method
|
|
17
|
-
send_jsonrpc_response(id, result: {
|
|
18
|
-
action: "decline"
|
|
19
|
-
})
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Send elicitation response
|
|
23
|
-
# @param id [String, Integer] The request ID
|
|
24
|
-
# @param action [String] The action taken ("accept", "decline", "cancel")
|
|
25
|
-
# @param content [Hash, nil] The form data if action is "accept"
|
|
26
|
-
def send_elicitation_response(id, action:, content: nil)
|
|
27
|
-
result = { action: action }
|
|
28
|
-
result[:content] = content if action == "accept" && content
|
|
29
|
-
|
|
30
|
-
send_jsonrpc_response(id, result: result)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
class JsonRpcHandler < JsonRpcHandlerBase
|
|
6
|
-
attr_reader :client
|
|
7
|
-
|
|
8
|
-
def initialize(transport, client)
|
|
9
|
-
super(transport)
|
|
10
|
-
@client = client
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Handle client-side JSON-RPC requests/responses
|
|
14
|
-
# @param request [JSON_RPC::Request, JSON_RPC::Notification, JSON_RPC::Response]
|
|
15
|
-
def call(request)
|
|
16
|
-
case request
|
|
17
|
-
when JSON_RPC::Request
|
|
18
|
-
handle_request(request)
|
|
19
|
-
when JSON_RPC::Notification
|
|
20
|
-
handle_notification(request)
|
|
21
|
-
when JSON_RPC::Response
|
|
22
|
-
handle_response(request)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
def handle_request(request)
|
|
29
|
-
id = request.id
|
|
30
|
-
rpc_method = request.method
|
|
31
|
-
params = request.params
|
|
32
|
-
|
|
33
|
-
handle_method(rpc_method, id, params)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def handle_notification(notification)
|
|
37
|
-
# Handle server notifications to client
|
|
38
|
-
client.log_debug("Received notification: #{notification.method}")
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def handle_response(response)
|
|
42
|
-
# Handle server responses to client requests
|
|
43
|
-
client.log_debug("Received response: #{response.id} - #{response.result ? 'success' : 'error'}")
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
protected
|
|
47
|
-
|
|
48
|
-
# Handle client-specific methods
|
|
49
|
-
# @param rpc_method [String]
|
|
50
|
-
# @param id [String, Integer]
|
|
51
|
-
# @param params [Hash]
|
|
52
|
-
def handle_method(rpc_method, id, params)
|
|
53
|
-
case rpc_method
|
|
54
|
-
when Methods::ELICITATION_CREATE
|
|
55
|
-
client.process_elicitation_request(id, params)
|
|
56
|
-
when %r{^roots/}
|
|
57
|
-
process_roots(rpc_method, id)
|
|
58
|
-
when %r{^sampling/}
|
|
59
|
-
process_sampling(rpc_method, id, params)
|
|
60
|
-
else
|
|
61
|
-
common_result = handle_common_methods(rpc_method, id, params)
|
|
62
|
-
client.log_warn("Unknown server method: #{rpc_method} #{id} #{params}") if common_result.nil?
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# @param rpc_method [String]
|
|
67
|
-
# @param id [String]
|
|
68
|
-
def process_roots(rpc_method, id)
|
|
69
|
-
case rpc_method
|
|
70
|
-
when "roots/list" # List available roots
|
|
71
|
-
transport.send_roots_list(id)
|
|
72
|
-
else
|
|
73
|
-
Rails.logger.warn("Unknown roots method: #{rpc_method}")
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# @param rpc_method [String]
|
|
78
|
-
# @param id [String]
|
|
79
|
-
# @param params [Hash]
|
|
80
|
-
def process_sampling(rpc_method, id, params)
|
|
81
|
-
case rpc_method
|
|
82
|
-
when "sampling/createMessage" # Create a message using AI
|
|
83
|
-
# @param id [String]
|
|
84
|
-
# @param params [SamplingRequest]
|
|
85
|
-
transport.send_sampling_create_message(id, params)
|
|
86
|
-
else
|
|
87
|
-
Rails.logger.warn("Unknown sampling method: #{rpc_method}")
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# @param rpc_method [String]
|
|
92
|
-
def process_notifications(rpc_method, params)
|
|
93
|
-
case rpc_method
|
|
94
|
-
when "notifications/resources/updated" # Resource update notification
|
|
95
|
-
client.log_debug("Resource #{params['uri']} was updated")
|
|
96
|
-
# Handle resource update notification
|
|
97
|
-
# TODO: fetch updated resource or mark it as stale
|
|
98
|
-
when "notifications/tools/list_changed" # Tool list change notification
|
|
99
|
-
client.log_debug("Tool list has changed")
|
|
100
|
-
# Handle tool list change notification
|
|
101
|
-
# TODO: fetch new tools or mark them as stale
|
|
102
|
-
when "notifications/prompts/list_changed" # Prompt list change notification
|
|
103
|
-
client.log_debug("Prompt list has changed")
|
|
104
|
-
# Handle prompt list change notification
|
|
105
|
-
# TODO: fetch new prompts or mark them as stale
|
|
106
|
-
when "notifications/resources/list_changed" # Resource list change notification
|
|
107
|
-
client.log_debug("Resource list has changed")
|
|
108
|
-
# Handle resource list change notification
|
|
109
|
-
# TODO: fetch new resources or mark them as stale
|
|
110
|
-
else
|
|
111
|
-
super
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def process_response(id, result)
|
|
116
|
-
# Check if this is a response to an initialize request
|
|
117
|
-
# We need to check the actual request method, not just compare IDs
|
|
118
|
-
request = client.session ? transport.messages.requests.find_by(jsonrpc_id: id) : nil
|
|
119
|
-
|
|
120
|
-
# If no session yet, this might be the initialize response
|
|
121
|
-
if !client.session && result["serverInfo"]
|
|
122
|
-
handle_initialize_response(id, result)
|
|
123
|
-
return send_initialized_notification
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
return unless request
|
|
127
|
-
|
|
128
|
-
# Mark the request as acknowledged
|
|
129
|
-
request.update(request_acknowledged: true)
|
|
130
|
-
|
|
131
|
-
case request.rpc_method
|
|
132
|
-
when "tools/list"
|
|
133
|
-
client.toolbox.tools = result["tools"]
|
|
134
|
-
client.toolbox.instance_variable_set(:@next_cursor, result["nextCursor"])
|
|
135
|
-
client.toolbox.instance_variable_set(:@total, result["tools"]&.size || 0)
|
|
136
|
-
return true
|
|
137
|
-
when "prompts/list"
|
|
138
|
-
client.prompt_book.prompts = result["prompts"]
|
|
139
|
-
client.prompt_book.instance_variable_set(:@next_cursor, result["nextCursor"])
|
|
140
|
-
client.prompt_book.instance_variable_set(:@total, result["prompts"]&.size || 0)
|
|
141
|
-
return true
|
|
142
|
-
when "resources/list"
|
|
143
|
-
client.catalog.resources = result["resources"]
|
|
144
|
-
client.catalog.instance_variable_set(:@next_cursor, result["nextCursor"])
|
|
145
|
-
client.catalog.instance_variable_set(:@total, result["resources"]&.size || 0)
|
|
146
|
-
return true
|
|
147
|
-
when "resources/templates/list"
|
|
148
|
-
client.blueprint.templates = result["resourceTemplates"]
|
|
149
|
-
client.blueprint.instance_variable_set(:@next_cursor, result["nextCursor"])
|
|
150
|
-
client.blueprint.instance_variable_set(:@total, result["resourceTemplates"]&.size || 0)
|
|
151
|
-
return true
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
client.log_warn("Unknown response: #{id} #{result}")
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def process_error(id, error)
|
|
158
|
-
# Do something ?
|
|
159
|
-
client.log_error("Unknown error: #{id} #{error}")
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def handle_initialize_response(_request_id, result)
|
|
163
|
-
# Session ID comes from HTTP headers, not the response body
|
|
164
|
-
# The transport should have already extracted it
|
|
165
|
-
session_id = transport.instance_variable_get(:@session_id)
|
|
166
|
-
|
|
167
|
-
if session_id.nil?
|
|
168
|
-
client.log_error("No session ID received from server")
|
|
169
|
-
return
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Check if we're resuming an existing session
|
|
173
|
-
if client.instance_variable_get(:@session_id) && session_id == client.instance_variable_get(:@session_id)
|
|
174
|
-
# We're resuming an existing session
|
|
175
|
-
client.instance_variable_set(:@session, ActionMCP::Session.find(session_id))
|
|
176
|
-
client.log_info("Resumed existing session: #{session_id}")
|
|
177
|
-
else
|
|
178
|
-
# Create a new session with the server-provided ID
|
|
179
|
-
client.instance_variable_set(:@session, ActionMCP::Session.from_client.new(
|
|
180
|
-
id: session_id,
|
|
181
|
-
protocol_version: result["protocolVersion"] || ActionMCP::DEFAULT_PROTOCOL_VERSION,
|
|
182
|
-
client_info: client.client_info,
|
|
183
|
-
client_capabilities: client.client_capabilities,
|
|
184
|
-
server_info: result["serverInfo"],
|
|
185
|
-
server_capabilities: result["capabilities"]
|
|
186
|
-
))
|
|
187
|
-
client.session.save
|
|
188
|
-
client.log_info("Created new session: #{session_id}")
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# Set the server info
|
|
192
|
-
client.server = Client::Server.new(result)
|
|
193
|
-
client.instance_variable_set(:@initialized, true)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def send_initialized_notification
|
|
197
|
-
transport.initialize! if transport.respond_to?(:initialize!)
|
|
198
|
-
client.send_jsonrpc_notification("notifications/initialized")
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
module Logging
|
|
6
|
-
# Set the client's logging level
|
|
7
|
-
# @param level [String] Logging level (debug, info, warning, error, etc.)
|
|
8
|
-
# @return [Boolean] Success status
|
|
9
|
-
def set_logging_level(level)
|
|
10
|
-
request_id = SecureRandom.uuid_v7
|
|
11
|
-
|
|
12
|
-
# Send request
|
|
13
|
-
send_jsonrpc_request("client/setLoggingLevel",
|
|
14
|
-
params: { level: level },
|
|
15
|
-
id: request_id)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
module Messaging
|
|
6
|
-
def send_jsonrpc_request(method, params: nil, id: SecureRandom.uuid_v7)
|
|
7
|
-
request = JSON_RPC::Request.new(id: id, method: method, params: params)
|
|
8
|
-
write_message(request)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def send_jsonrpc_response(request_id, result: nil, error: nil)
|
|
12
|
-
response = JSON_RPC::Response.new(id: request_id, result: result, error: error)
|
|
13
|
-
write_message(response)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def send_jsonrpc_notification(method, params = nil)
|
|
17
|
-
notification = JSON_RPC::Notification.new(method: method, params: params)
|
|
18
|
-
write_message(notification)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def send_jsonrpc_error(request_id, symbol, message, data = nil)
|
|
22
|
-
error = JSON_RPC::JsonRpcError.new(symbol, message:, data:)
|
|
23
|
-
response = JSON_RPC::Response.new(id: request_id, error:)
|
|
24
|
-
write_message(response)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
# PromptBook
|
|
6
|
-
#
|
|
7
|
-
# A collection that manages and provides access to prompt templates from the MCP server.
|
|
8
|
-
# The class stores prompt definitions along with their arguments and provides methods
|
|
9
|
-
# for retrieving, filtering, and accessing prompts. It supports lazy loading of prompts
|
|
10
|
-
# when initialized with a client.
|
|
11
|
-
#
|
|
12
|
-
# Example usage:
|
|
13
|
-
# # Eager loading
|
|
14
|
-
# prompts_data = client.list_prompts # Returns array of prompt definitions
|
|
15
|
-
# book = PromptBook.new(prompts_data)
|
|
16
|
-
#
|
|
17
|
-
# # Lazy loading
|
|
18
|
-
# book = PromptBook.new([], client)
|
|
19
|
-
# prompts = book.all # Prompts are loaded here
|
|
20
|
-
#
|
|
21
|
-
# # Access a specific prompt by name
|
|
22
|
-
# summary_prompt = book.find("summarize_text")
|
|
23
|
-
#
|
|
24
|
-
# # Get all prompts matching a criteria
|
|
25
|
-
# text_prompts = book.filter { |p| p.name.include?("text") }
|
|
26
|
-
#
|
|
27
|
-
class PromptBook < Collection
|
|
28
|
-
# Initialize a new PromptBook with prompt definitions
|
|
29
|
-
#
|
|
30
|
-
# @param prompts [Array<Hash>] Array of prompt definition hashes, each containing
|
|
31
|
-
# name, description, and arguments keys
|
|
32
|
-
# @param client [Object, nil] Optional client for lazy loading of prompts
|
|
33
|
-
def initialize(prompts, client)
|
|
34
|
-
super(prompts, client)
|
|
35
|
-
self.prompts = @collection_data
|
|
36
|
-
@load_method = :list_prompts
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Find a prompt by name
|
|
40
|
-
#
|
|
41
|
-
# @param name [String] Name of the prompt to find
|
|
42
|
-
# @return [Prompt, nil] The prompt with the given name, or nil if not found
|
|
43
|
-
def find(name)
|
|
44
|
-
all.find { |prompt| prompt.name == name }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Get a list of all prompt names
|
|
48
|
-
#
|
|
49
|
-
# @return [Array<String>] Names of all prompts in the collection
|
|
50
|
-
def names
|
|
51
|
-
all.map(&:name)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Check if the collection contains a prompt with the given name
|
|
55
|
-
#
|
|
56
|
-
# @param name [String] The prompt name to check for
|
|
57
|
-
# @return [Boolean] true if a prompt with the name exists
|
|
58
|
-
def contains?(name)
|
|
59
|
-
all.any? { |prompt| prompt.name == name }
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Convert raw prompt data into Prompt objects
|
|
63
|
-
#
|
|
64
|
-
# @param prompts [Array<Hash>] Array of prompt definition hashes
|
|
65
|
-
def prompts=(prompts)
|
|
66
|
-
@collection_data = prompts.map { |data| Prompt.new(data) }
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Internal Prompt class to represent individual prompts
|
|
70
|
-
class Prompt
|
|
71
|
-
attr_reader :name, :description, :arguments
|
|
72
|
-
|
|
73
|
-
# Initialize a new Prompt instance
|
|
74
|
-
#
|
|
75
|
-
# @param data [Hash] Prompt definition hash containing name, description, and arguments
|
|
76
|
-
def initialize(data)
|
|
77
|
-
@name = data["name"]
|
|
78
|
-
@description = data["description"]
|
|
79
|
-
@arguments = data["arguments"] || []
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Get all required arguments for this prompt
|
|
83
|
-
#
|
|
84
|
-
# @return [Array<Hash>] Array of argument hashes that are required
|
|
85
|
-
def required_arguments
|
|
86
|
-
@arguments.select { |arg| arg["required"] }
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Get all optional arguments for this prompt
|
|
90
|
-
#
|
|
91
|
-
# @return [Array<Hash>] Array of argument hashes that are optional
|
|
92
|
-
def optional_arguments
|
|
93
|
-
@arguments.reject { |arg| arg["required"] }
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Check if the prompt has a specific argument
|
|
97
|
-
#
|
|
98
|
-
# @param name [String] Name of the argument to check for
|
|
99
|
-
# @return [Boolean] true if the argument exists
|
|
100
|
-
def has_argument?(name)
|
|
101
|
-
@arguments.any? { |arg| arg["name"] == name }
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Generate a hash representation of the prompt
|
|
105
|
-
#
|
|
106
|
-
# @return [Hash] Hash containing prompt details
|
|
107
|
-
def to_h
|
|
108
|
-
{
|
|
109
|
-
"name" => @name,
|
|
110
|
-
"description" => @description,
|
|
111
|
-
"arguments" => @arguments
|
|
112
|
-
}
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
module Prompts
|
|
6
|
-
# List all available prompts from the server
|
|
7
|
-
# @param params [Hash] Optional parameters for pagination
|
|
8
|
-
# @option params [String] :cursor Pagination cursor for fetching next page
|
|
9
|
-
# @option params [Integer] :limit Maximum number of items to return
|
|
10
|
-
# @return [String] Request ID for tracking the request
|
|
11
|
-
def list_prompts(params = {})
|
|
12
|
-
request_id = SecureRandom.uuid_v7
|
|
13
|
-
|
|
14
|
-
# Send request with pagination parameters if provided
|
|
15
|
-
request_params = {}
|
|
16
|
-
request_params[:cursor] = params[:cursor] if params[:cursor]
|
|
17
|
-
request_params[:limit] = params[:limit] if params[:limit]
|
|
18
|
-
|
|
19
|
-
send_jsonrpc_request("prompts/list",
|
|
20
|
-
params: request_params.empty? ? nil : request_params,
|
|
21
|
-
id: request_id)
|
|
22
|
-
|
|
23
|
-
# Return request ID for tracking the request
|
|
24
|
-
request_id
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Get a specific prompt with arguments
|
|
28
|
-
# @param name [String] Name of the prompt to get
|
|
29
|
-
# @param arguments [Hash] Arguments to pass to the prompt
|
|
30
|
-
# @return [String] Request ID for tracking the request
|
|
31
|
-
def get_prompt(name, arguments = {})
|
|
32
|
-
request_id = SecureRandom.uuid_v7
|
|
33
|
-
|
|
34
|
-
# Send request
|
|
35
|
-
send_jsonrpc_request("prompts/get",
|
|
36
|
-
params: {
|
|
37
|
-
name: name,
|
|
38
|
-
arguments: arguments
|
|
39
|
-
},
|
|
40
|
-
id: request_id)
|
|
41
|
-
|
|
42
|
-
# Return request ID for tracking the request
|
|
43
|
-
request_id
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
module RequestTimeouts
|
|
6
|
-
# Default timeout in seconds
|
|
7
|
-
DEFAULT_TIMEOUT = 1.0
|
|
8
|
-
|
|
9
|
-
# Load resources with timeout support - blocking until response or timeout
|
|
10
|
-
# @param method_name [Symbol] The method to call for loading (e.g., :list_resources)
|
|
11
|
-
# @param force [Boolean] Whether to force reload even if already loaded
|
|
12
|
-
# @param timeout [Float] Timeout in seconds
|
|
13
|
-
# @return [Boolean] Success status
|
|
14
|
-
def load_with_timeout(method_name, force: false, timeout: DEFAULT_TIMEOUT)
|
|
15
|
-
return true if @loaded && !force
|
|
16
|
-
|
|
17
|
-
# Make the request and store its ID
|
|
18
|
-
request_id = client.send(method_name)
|
|
19
|
-
|
|
20
|
-
start_time = Time.now
|
|
21
|
-
|
|
22
|
-
# Wait until either:
|
|
23
|
-
# 1. The collection is loaded (@loaded becomes true from JsonRpcHandler)
|
|
24
|
-
# 2. The timeout is reached
|
|
25
|
-
sleep(0.1) while !@loaded && (Time.now - start_time) < timeout
|
|
26
|
-
|
|
27
|
-
# If we timed out
|
|
28
|
-
unless @loaded
|
|
29
|
-
request = client.session.messages.requests.find_by(jsonrpc_id: request_id)
|
|
30
|
-
|
|
31
|
-
if request && !request.request_acknowledged?
|
|
32
|
-
# Send cancel notification
|
|
33
|
-
client.send_jsonrpc_notification("notifications/cancelled", {
|
|
34
|
-
requestId: request_id,
|
|
35
|
-
reason: "Request timed out after #{timeout} seconds"
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
# Mark as cancelled in the database
|
|
39
|
-
request.update(request_cancelled: true)
|
|
40
|
-
|
|
41
|
-
log_error("Request #{method_name} timed out after #{timeout} seconds")
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Mark as loaded even though we timed out
|
|
45
|
-
@loaded = true
|
|
46
|
-
return false
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Collection was successfully loaded
|
|
50
|
-
true
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
def handle_timeout(request_id, method_name, timeout)
|
|
56
|
-
# Find the request
|
|
57
|
-
request = client.session.messages.requests.find_by(jsonrpc_id: request_id)
|
|
58
|
-
|
|
59
|
-
return unless request && !request.request_acknowledged?
|
|
60
|
-
|
|
61
|
-
# Send cancel notification
|
|
62
|
-
client.send_jsonrpc_notification("notifications/cancelled", {
|
|
63
|
-
requestId: request_id,
|
|
64
|
-
reason: "Request timed out after #{timeout} seconds"
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
# Mark as cancelled in the database
|
|
68
|
-
request.update(request_cancelled: true)
|
|
69
|
-
|
|
70
|
-
log_error("Request #{method_name} timed out after #{timeout} seconds")
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|