actionmcp 0.20.0 → 0.22.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/app/controllers/action_mcp/messages_controller.rb +2 -2
- data/app/models/action_mcp/session/message.rb +12 -1
- data/app/models/action_mcp/session.rb +8 -4
- data/lib/action_mcp/capability.rb +2 -3
- data/lib/action_mcp/client/base.rb +222 -0
- data/lib/action_mcp/client/blueprint.rb +227 -0
- data/lib/action_mcp/client/catalog.rb +226 -0
- data/lib/action_mcp/client/json_rpc_handler.rb +109 -0
- data/lib/action_mcp/client/logging.rb +20 -0
- data/lib/action_mcp/{transport → client}/messaging.rb +1 -1
- data/lib/action_mcp/client/prompt_book.rb +183 -0
- data/lib/action_mcp/client/prompts.rb +33 -0
- data/lib/action_mcp/client/resources.rb +70 -0
- data/lib/action_mcp/client/roots.rb +13 -0
- data/lib/action_mcp/client/server.rb +60 -0
- data/lib/action_mcp/{transport → client}/sse_client.rb +70 -111
- data/lib/action_mcp/{transport → client}/stdio_client.rb +38 -38
- data/lib/action_mcp/client/toolbox.rb +236 -0
- data/lib/action_mcp/client/tools.rb +33 -0
- data/lib/action_mcp/client.rb +20 -231
- data/lib/action_mcp/engine.rb +1 -3
- data/lib/action_mcp/instrumentation/controller_runtime.rb +1 -1
- data/lib/action_mcp/instrumentation/instrumentation.rb +2 -0
- data/lib/action_mcp/instrumentation/resource_instrumentation.rb +1 -0
- data/lib/action_mcp/json_rpc_handler_base.rb +106 -0
- data/lib/action_mcp/log_subscriber.rb +2 -0
- data/lib/action_mcp/logging.rb +1 -1
- data/lib/action_mcp/{transport → server}/capabilities.rb +2 -2
- data/lib/action_mcp/server/json_rpc_handler.rb +121 -0
- data/lib/action_mcp/server/messaging.rb +28 -0
- data/lib/action_mcp/{transport → server}/notifications.rb +1 -1
- data/lib/action_mcp/{transport → server}/prompts.rb +1 -1
- data/lib/action_mcp/{transport → server}/resources.rb +1 -18
- data/lib/action_mcp/{transport → server}/roots.rb +1 -1
- data/lib/action_mcp/{transport → server}/sampling.rb +1 -1
- data/lib/action_mcp/server/sampling_request.rb +115 -0
- data/lib/action_mcp/{transport → server}/tools.rb +1 -1
- data/lib/action_mcp/server/transport_handler.rb +41 -0
- data/lib/action_mcp/uri_ambiguity_checker.rb +6 -10
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +2 -1
- metadata +29 -33
- data/lib/action_mcp/base_json_rpc_handler.rb +0 -97
- data/lib/action_mcp/client_json_rpc_handler.rb +0 -69
- data/lib/action_mcp/json_rpc_handler.rb +0 -229
- data/lib/action_mcp/sampling_request.rb +0 -113
- data/lib/action_mcp/server_json_rpc_handler.rb +0 -90
- data/lib/action_mcp/transport/transport_base.rb +0 -126
- data/lib/action_mcp/transport_handler.rb +0 -39
@@ -0,0 +1,226 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Client
|
5
|
+
# Catalog
|
6
|
+
#
|
7
|
+
# A collection that manages and provides access to resources.
|
8
|
+
# This class stores resource definitions and provides methods for
|
9
|
+
# retrieving, filtering, and accessing resources by URI or other attributes.
|
10
|
+
# It supports lazy loading of resources when initialized with a client.
|
11
|
+
#
|
12
|
+
# Example usage:
|
13
|
+
# # Eager loading
|
14
|
+
# resources_data = client.list_resources # Returns array of resource definitions
|
15
|
+
# catalog = Catalog.new(resources_data)
|
16
|
+
#
|
17
|
+
# # Lazy loading
|
18
|
+
# catalog = Catalog.new([], client)
|
19
|
+
# resources = catalog.all # Resources are loaded here
|
20
|
+
#
|
21
|
+
# # Access a specific resource by URI
|
22
|
+
# main_file = catalog.find_by_uri("file:///project/src/main.rs")
|
23
|
+
#
|
24
|
+
# # Get all resources matching a criteria
|
25
|
+
# rust_files = catalog.filter { |r| r.mime_type == "text/x-rust" }
|
26
|
+
#
|
27
|
+
class Catalog
|
28
|
+
# Initialize a new Catalog with resource definitions
|
29
|
+
#
|
30
|
+
# @param resources [Array<Hash>] Array of resource definition hashes, each containing
|
31
|
+
# uri, name, description, and mimeType keys
|
32
|
+
# @param client [Object, nil] Optional client for lazy loading of resources
|
33
|
+
attr_reader :client
|
34
|
+
|
35
|
+
def initialize(resources, client)
|
36
|
+
self.resources = resources
|
37
|
+
@client = client
|
38
|
+
@loaded = !resources.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return all resources in the collection. If initialized with a client and resources
|
42
|
+
# haven't been loaded yet, this will trigger lazy loading from the client.
|
43
|
+
#
|
44
|
+
# @return [Array<Resource>] All resource objects in the collection
|
45
|
+
def all
|
46
|
+
load_resources unless @loaded
|
47
|
+
@resources
|
48
|
+
end
|
49
|
+
|
50
|
+
# Force reload all resources from the client and return them
|
51
|
+
#
|
52
|
+
# @return [Array<Resource>] All resource objects in the collection
|
53
|
+
def all!
|
54
|
+
load_resources(force: true)
|
55
|
+
@resources
|
56
|
+
end
|
57
|
+
|
58
|
+
# Find a resource by URI
|
59
|
+
#
|
60
|
+
# @param uri [String] URI of the resource to find
|
61
|
+
# @return [Resource, nil] The resource with the given URI, or nil if not found
|
62
|
+
def find_by_uri(uri)
|
63
|
+
all.find { |resource| resource.uri == uri }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Find resources by name
|
67
|
+
#
|
68
|
+
# @param name [String] Name of the resources to find
|
69
|
+
# @return [Array<Resource>] Resources with the given name
|
70
|
+
def find_by_name(name)
|
71
|
+
all.select { |resource| resource.name == name }
|
72
|
+
end
|
73
|
+
|
74
|
+
# Find resources by MIME type
|
75
|
+
#
|
76
|
+
# @param mime_type [String] MIME type to search for
|
77
|
+
# @return [Array<Resource>] Resources with the given MIME type
|
78
|
+
def find_by_mime_type(mime_type)
|
79
|
+
all.select { |resource| resource.mime_type == mime_type }
|
80
|
+
end
|
81
|
+
|
82
|
+
# Filter resources based on a given block
|
83
|
+
#
|
84
|
+
# @yield [resource] Block that determines whether to include a resource
|
85
|
+
# @yieldparam resource [Resource] A resource from the collection
|
86
|
+
# @yieldreturn [Boolean] true to include the resource, false to exclude it
|
87
|
+
# @return [Array<Resource>] Resources that match the filter criteria
|
88
|
+
def filter(&block)
|
89
|
+
all.select(&block)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Get a list of all resource URIs
|
93
|
+
#
|
94
|
+
# @return [Array<String>] URIs of all resources in the collection
|
95
|
+
def uris
|
96
|
+
all.map(&:uri)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Number of resources in the collection
|
100
|
+
#
|
101
|
+
# @return [Integer] The number of resources
|
102
|
+
def size
|
103
|
+
all.size
|
104
|
+
end
|
105
|
+
|
106
|
+
# Check if the collection contains a resource with the given URI
|
107
|
+
#
|
108
|
+
# @param uri [String] The resource URI to check for
|
109
|
+
# @return [Boolean] true if a resource with the URI exists
|
110
|
+
def contains_uri?(uri)
|
111
|
+
all.any? { |resource| resource.uri == uri }
|
112
|
+
end
|
113
|
+
|
114
|
+
# Group resources by MIME type
|
115
|
+
#
|
116
|
+
# @return [Hash<String, Array<Resource>>] Hash mapping MIME types to arrays of resources
|
117
|
+
def group_by_mime_type
|
118
|
+
all.group_by(&:mime_type)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Search resources by keyword in name or description
|
122
|
+
#
|
123
|
+
# @param keyword [String] Keyword to search for
|
124
|
+
# @return [Array<Resource>] Resources matching the search term
|
125
|
+
def search(keyword)
|
126
|
+
keyword = keyword.downcase
|
127
|
+
all.select do |resource|
|
128
|
+
resource.name.downcase.include?(keyword) ||
|
129
|
+
(resource.description && resource.description.downcase.include?(keyword))
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Implements enumerable functionality for the collection
|
134
|
+
include Enumerable
|
135
|
+
|
136
|
+
# Yield each resource in the collection to the given block
|
137
|
+
#
|
138
|
+
# @yield [resource] Block to execute for each resource
|
139
|
+
# @yieldparam resource [Resource] A resource from the collection
|
140
|
+
# @return [Enumerator] If no block is given
|
141
|
+
def each(&block)
|
142
|
+
all.each(&block)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Convert raw resource data into Resource objects
|
146
|
+
#
|
147
|
+
# @param raw_resources [Array<Hash>] Array of resource definition hashes
|
148
|
+
def resources=(raw_resources)
|
149
|
+
@resources = raw_resources.map { |resource_data| Resource.new(resource_data) }
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
# Load or reload resources using the client
|
155
|
+
#
|
156
|
+
# @param force [Boolean] Whether to force reload even if resources are already loaded
|
157
|
+
# @return [void]
|
158
|
+
def load_resources(force: false)
|
159
|
+
return if @loaded && !force
|
160
|
+
|
161
|
+
begin
|
162
|
+
@client.list_resources
|
163
|
+
@loaded = true
|
164
|
+
rescue StandardError => e
|
165
|
+
Rails.logger.error("Failed to load resources: #{e.message}")
|
166
|
+
@loaded = true unless @resources.empty?
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Internal Resource class to represent individual resources
|
171
|
+
class Resource
|
172
|
+
attr_reader :uri, :name, :description, :mime_type
|
173
|
+
|
174
|
+
# Initialize a new Resource instance
|
175
|
+
#
|
176
|
+
# @param data [Hash] Resource definition hash containing uri, name, description, and mimeType
|
177
|
+
def initialize(data = [])
|
178
|
+
@uri = data["uri"]
|
179
|
+
@name = data["name"]
|
180
|
+
@description = data["description"]
|
181
|
+
@mime_type = data["mimeType"]
|
182
|
+
end
|
183
|
+
|
184
|
+
# Get the file extension from the resource name
|
185
|
+
#
|
186
|
+
# @return [String, nil] The file extension or nil if no extension
|
187
|
+
def extension
|
188
|
+
File.extname(@name)[1..-1] if @name.include?(".")
|
189
|
+
end
|
190
|
+
|
191
|
+
# Check if this resource is a text file based on MIME type
|
192
|
+
#
|
193
|
+
# @return [Boolean] true if the resource is a text file
|
194
|
+
def text?
|
195
|
+
@mime_type&.start_with?("text/")
|
196
|
+
end
|
197
|
+
|
198
|
+
# Check if this resource is an image based on MIME type
|
199
|
+
#
|
200
|
+
# @return [Boolean] true if the resource is an image
|
201
|
+
def image?
|
202
|
+
@mime_type&.start_with?("image/")
|
203
|
+
end
|
204
|
+
|
205
|
+
# Get the path portion of the URI
|
206
|
+
#
|
207
|
+
# @return [String, nil] The path component of the URI
|
208
|
+
def path
|
209
|
+
URI(@uri).path rescue nil
|
210
|
+
end
|
211
|
+
|
212
|
+
# Generate a hash representation of the resource
|
213
|
+
#
|
214
|
+
# @return [Hash] Hash containing resource details
|
215
|
+
def to_h
|
216
|
+
{
|
217
|
+
"uri" => @uri,
|
218
|
+
"name" => @name,
|
219
|
+
"description" => @description,
|
220
|
+
"mimeType" => @mime_type
|
221
|
+
}
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Client
|
5
|
+
class JsonRpcHandler < JsonRpcHandlerBase
|
6
|
+
attr_reader :client
|
7
|
+
def initialize(transport, client)
|
8
|
+
super(transport)
|
9
|
+
@client = client
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
# Handle client-specific methods
|
15
|
+
# @param rpc_method [String]
|
16
|
+
# @param id [String, Integer]
|
17
|
+
# @param params [Hash]
|
18
|
+
def handle_method(rpc_method, id, params)
|
19
|
+
puts "\e[31mUnknown server method: #{rpc_method} #{id} #{params}\e[0m"
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param rpc_method [String]
|
23
|
+
# @param id [String]
|
24
|
+
def process_roots(rpc_method, id)
|
25
|
+
case rpc_method
|
26
|
+
when "roots/list" # List available roots
|
27
|
+
transport.send_roots_list(id)
|
28
|
+
else
|
29
|
+
Rails.logger.warn("Unknown roots method: #{rpc_method}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param rpc_method [String]
|
34
|
+
# @param id [String]
|
35
|
+
# @param params [Hash]
|
36
|
+
def process_sampling(rpc_method, id, params)
|
37
|
+
case rpc_method
|
38
|
+
when "sampling/createMessage" # Create a message using AI
|
39
|
+
# @param id [String]
|
40
|
+
# @param params [SamplingRequest]
|
41
|
+
transport.send_sampling_create_message(id, params)
|
42
|
+
else
|
43
|
+
Rails.logger.warn("Unknown sampling method: #{rpc_method}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param rpc_method [String]
|
48
|
+
def process_notifications(rpc_method, params)
|
49
|
+
case rpc_method
|
50
|
+
when "notifications/resources/updated" # Resource update notification
|
51
|
+
puts "\e[31m Resource #{params['uri']} was updated\e[0m"
|
52
|
+
# Handle resource update notification
|
53
|
+
# TODO: fetch updated resource or mark it as stale
|
54
|
+
when "notifications/tools/list_changed" # Tool list change notification
|
55
|
+
puts "\e[31m Tool list has changed\e[0m"
|
56
|
+
# Handle tool list change notification
|
57
|
+
# TODO: fetch new tools or mark them as stale
|
58
|
+
when "notifications/prompts/list_changed" # Prompt list change notification
|
59
|
+
puts "\e[31m Prompt list has changed\e[0m"
|
60
|
+
# Handle prompt list change notification
|
61
|
+
# TODO: fetch new prompts or mark them as stale
|
62
|
+
when "notifications/resources/list_changed" # Resource list change notification
|
63
|
+
puts "\e[31m Resource list has changed\e[0m"
|
64
|
+
# Handle resource list change notification
|
65
|
+
# TODO: fetch new resources or mark them as stale
|
66
|
+
else
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def process_response(id, result)
|
72
|
+
if transport.id == id
|
73
|
+
## This initialize the transport
|
74
|
+
client.server = Client::Server.new(result)
|
75
|
+
return send_initialized_notification
|
76
|
+
end
|
77
|
+
request = transport.messages.requests.find_by(jsonrpc_id: id)
|
78
|
+
return unless request
|
79
|
+
case request.rpc_method
|
80
|
+
when "tools/list"
|
81
|
+
client.toolbox.tools = result["tools"]
|
82
|
+
return client.toolbox.all
|
83
|
+
when "prompts/list"
|
84
|
+
client.prompt_book.prompts = result["prompts"]
|
85
|
+
return client.prompt_book.all
|
86
|
+
when "resources/list"
|
87
|
+
client.catalog.resources = result["resources"]
|
88
|
+
return client.catalog.all
|
89
|
+
when "resources/templates/list"
|
90
|
+
client.blueprint.templates = result["resourceTemplates"]
|
91
|
+
return client.blueprint.all
|
92
|
+
end
|
93
|
+
|
94
|
+
puts "\e[31mUnknown response: #{id} #{result}\e[0m"
|
95
|
+
end
|
96
|
+
|
97
|
+
def process_error(id, error)
|
98
|
+
# Do something ?
|
99
|
+
puts "\e[31mUnknown error: #{id} #{error}\e[0m"
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
def send_initialized_notification
|
104
|
+
transport.initialize!
|
105
|
+
client.send_jsonrpc_notification("notifications/initialized")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,20 @@
|
|
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
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,183 @@
|
|
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
|
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
|
+
attr_reader :client
|
34
|
+
|
35
|
+
def initialize(prompts, client)
|
36
|
+
self.prompts = prompts
|
37
|
+
@client = client
|
38
|
+
@loaded = !prompts.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return all prompts in the collection. If initialized with a client and prompts
|
42
|
+
# haven't been loaded yet, this will trigger lazy loading from the client.
|
43
|
+
#
|
44
|
+
# @return [Array<Prompt>] All prompt objects in the collection
|
45
|
+
def all
|
46
|
+
load_prompts unless @loaded
|
47
|
+
@prompts
|
48
|
+
end
|
49
|
+
|
50
|
+
# Find a prompt by name
|
51
|
+
#
|
52
|
+
# @param name [String] Name of the prompt to find
|
53
|
+
# @return [Prompt, nil] The prompt with the given name, or nil if not found
|
54
|
+
def find(name)
|
55
|
+
all.find { |prompt| prompt.name == name }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Filter prompts based on a given block
|
59
|
+
#
|
60
|
+
# @yield [prompt] Block that determines whether to include a prompt
|
61
|
+
# @yieldparam prompt [Prompt] A prompt from the collection
|
62
|
+
# @yieldreturn [Boolean] true to include the prompt, false to exclude it
|
63
|
+
# @return [Array<Prompt>] Prompts that match the filter criteria
|
64
|
+
def filter(&block)
|
65
|
+
all.select(&block)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get a list of all prompt names
|
69
|
+
#
|
70
|
+
# @return [Array<String>] Names of all prompts in the collection
|
71
|
+
def names
|
72
|
+
all.map(&:name)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Number of prompts in the collection
|
76
|
+
#
|
77
|
+
# @return [Integer] The number of prompts
|
78
|
+
def size
|
79
|
+
all.size
|
80
|
+
end
|
81
|
+
|
82
|
+
# Check if the collection contains a prompt with the given name
|
83
|
+
#
|
84
|
+
# @param name [String] The prompt name to check for
|
85
|
+
# @return [Boolean] true if a prompt with the name exists
|
86
|
+
def contains?(name)
|
87
|
+
all.any? { |prompt| prompt.name == name }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Implements enumerable functionality for the collection
|
91
|
+
include Enumerable
|
92
|
+
|
93
|
+
# Yield each prompt in the collection to the given block
|
94
|
+
#
|
95
|
+
# @yield [prompt] Block to execute for each prompt
|
96
|
+
# @yieldparam prompt [Prompt] A prompt from the collection
|
97
|
+
# @return [Enumerator] If no block is given
|
98
|
+
def each(&block)
|
99
|
+
all.each(&block)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Force reload all prompts from the client and return them
|
103
|
+
#
|
104
|
+
# @return [Array<Prompt>] All prompt objects in the collection
|
105
|
+
def all!
|
106
|
+
load_prompts(force: true)
|
107
|
+
all
|
108
|
+
end
|
109
|
+
|
110
|
+
# Convert raw prompt data into Prompt objects
|
111
|
+
#
|
112
|
+
# @param prompts [Array<Hash>] Array of prompt definition hashes
|
113
|
+
def prompts=(prompts)
|
114
|
+
@prompts = prompts.map { |data| Prompt.new(data) }
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# Load or reload prompts using the client
|
120
|
+
#
|
121
|
+
# @param force [Boolean] Whether to force reload even if prompts are already loaded
|
122
|
+
# @return [void]
|
123
|
+
def load_prompts(force: false)
|
124
|
+
return if @loaded && !force
|
125
|
+
|
126
|
+
begin
|
127
|
+
@client.list_prompts
|
128
|
+
@loaded = true
|
129
|
+
rescue StandardError => e
|
130
|
+
Rails.logger.error("Failed to load prompts: #{e.message}")
|
131
|
+
@loaded = true unless @prompts.empty?
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Internal Prompt class to represent individual prompts
|
136
|
+
class Prompt
|
137
|
+
attr_reader :name, :description, :arguments
|
138
|
+
|
139
|
+
# Initialize a new Prompt instance
|
140
|
+
#
|
141
|
+
# @param data [Hash] Prompt definition hash containing name, description, and arguments
|
142
|
+
def initialize(data)
|
143
|
+
@name = data["name"]
|
144
|
+
@description = data["description"]
|
145
|
+
@arguments = data["arguments"] || []
|
146
|
+
end
|
147
|
+
|
148
|
+
# Get all required arguments for this prompt
|
149
|
+
#
|
150
|
+
# @return [Array<Hash>] Array of argument hashes that are required
|
151
|
+
def required_arguments
|
152
|
+
@arguments.select { |arg| arg["required"] }
|
153
|
+
end
|
154
|
+
|
155
|
+
# Get all optional arguments for this prompt
|
156
|
+
#
|
157
|
+
# @return [Array<Hash>] Array of argument hashes that are optional
|
158
|
+
def optional_arguments
|
159
|
+
@arguments.reject { |arg| arg["required"] }
|
160
|
+
end
|
161
|
+
|
162
|
+
# Check if the prompt has a specific argument
|
163
|
+
#
|
164
|
+
# @param name [String] Name of the argument to check for
|
165
|
+
# @return [Boolean] true if the argument exists
|
166
|
+
def has_argument?(name)
|
167
|
+
@arguments.any? { |arg| arg["name"] == name }
|
168
|
+
end
|
169
|
+
|
170
|
+
# Generate a hash representation of the prompt
|
171
|
+
#
|
172
|
+
# @return [Hash] Hash containing prompt details
|
173
|
+
def to_h
|
174
|
+
{
|
175
|
+
"name" => @name,
|
176
|
+
"description" => @description,
|
177
|
+
"arguments" => @arguments
|
178
|
+
}
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Client
|
5
|
+
module Prompts
|
6
|
+
# List all available prompts from the server
|
7
|
+
# @return [Array<Hash>] List of available prompts with their metadata
|
8
|
+
def list_prompts
|
9
|
+
request_id = SecureRandom.uuid_v7
|
10
|
+
|
11
|
+
# Send request
|
12
|
+
send_jsonrpc_request("prompts/list", id: request_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get a specific prompt with arguments
|
16
|
+
# @param name [String] Name of the prompt to get
|
17
|
+
# @param arguments [Hash] Arguments to pass to the prompt
|
18
|
+
# @return [Hash] Prompt content with messages
|
19
|
+
def get_prompt(name, arguments = {})
|
20
|
+
request_id = SecureRandom.uuid_v7
|
21
|
+
|
22
|
+
# Send request
|
23
|
+
send_jsonrpc_request("prompts/get",
|
24
|
+
params: {
|
25
|
+
name: name,
|
26
|
+
arguments: arguments
|
27
|
+
},
|
28
|
+
id: request_id
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Client
|
5
|
+
module Resources
|
6
|
+
# List all available resources from the server
|
7
|
+
# @return [Array<Hash>] List of available resources with their metadata
|
8
|
+
def list_resources
|
9
|
+
request_id = SecureRandom.uuid_v7
|
10
|
+
|
11
|
+
# Send request
|
12
|
+
send_jsonrpc_request("resources/list", id: request_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
# List resource templates from the server
|
16
|
+
# @return [Array<Hash>] List of resource templates
|
17
|
+
def list_resource_templates
|
18
|
+
request_id = SecureRandom.uuid_v7
|
19
|
+
|
20
|
+
# Send request
|
21
|
+
send_jsonrpc_request("resources/templates/list", id: request_id)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Read a specific resource
|
25
|
+
# @param uri [String] URI of the resource to read
|
26
|
+
# @return [Hash] Resource content
|
27
|
+
def read_resource(uri)
|
28
|
+
request_id = SecureRandom.uuid_v7
|
29
|
+
|
30
|
+
# Send request
|
31
|
+
send_jsonrpc_request("resources/read",
|
32
|
+
params: { uri: uri },
|
33
|
+
id: request_id
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Subscribe to updates for a specific resource
|
38
|
+
# @param uri [String] URI of the resource to subscribe to
|
39
|
+
# @param update_callback [Proc] Callback for resource updates
|
40
|
+
# @return [Boolean] Success status
|
41
|
+
def subscribe_resource(uri, update_callback)
|
42
|
+
@resource_subscriptions ||= {}
|
43
|
+
@resource_subscriptions[uri] = update_callback
|
44
|
+
|
45
|
+
request_id = SecureRandom.uuid_v7
|
46
|
+
|
47
|
+
# Send request
|
48
|
+
send_jsonrpc_request("resources/subscribe",
|
49
|
+
params: { uri: uri },
|
50
|
+
id: request_id
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Unsubscribe from updates for a specific resource
|
55
|
+
# @param uri [String] URI of the resource to unsubscribe from
|
56
|
+
# @return [Boolean] Success status
|
57
|
+
def unsubscribe_resource(uri)
|
58
|
+
@resource_subscriptions&.delete(uri)
|
59
|
+
|
60
|
+
request_id = SecureRandom.uuid_v7
|
61
|
+
|
62
|
+
# Send request
|
63
|
+
send_jsonrpc_request("resources/unsubscribe",
|
64
|
+
params: { uri: uri },
|
65
|
+
id: request_id
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Client
|
5
|
+
module Roots
|
6
|
+
# Notify the server that the roots list has changed
|
7
|
+
def roots_list_changed_notification
|
8
|
+
send_jsonrpc_notification("notifications/roots/list_changed")
|
9
|
+
true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|