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,225 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "transport"
|
|
4
|
-
|
|
5
|
-
module ActionMCP
|
|
6
|
-
module Client
|
|
7
|
-
# Base client class containing common MCP functionality
|
|
8
|
-
class Base
|
|
9
|
-
# Include all transport protocol modules
|
|
10
|
-
include Messaging
|
|
11
|
-
include Tools
|
|
12
|
-
include Prompts
|
|
13
|
-
include Resources
|
|
14
|
-
include Roots
|
|
15
|
-
include Elicitation
|
|
16
|
-
|
|
17
|
-
attr_reader :logger, :transport,
|
|
18
|
-
:connection_error, :server,
|
|
19
|
-
:server_capabilities, :session,
|
|
20
|
-
:catalog, :blueprint,
|
|
21
|
-
:prompt_book, :toolbox
|
|
22
|
-
|
|
23
|
-
delegate :connected?, :ready?, to: :transport
|
|
24
|
-
|
|
25
|
-
def initialize(transport:, logger: ActionMCP.logger, protocol_version: nil, **options)
|
|
26
|
-
@logger = logger
|
|
27
|
-
@transport = transport
|
|
28
|
-
@session = nil # Session will be created/loaded based on server response
|
|
29
|
-
@session_id = options[:session_id] # Optional session ID for resumption
|
|
30
|
-
@protocol_version = protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION
|
|
31
|
-
@server_capabilities = nil
|
|
32
|
-
@connection_error = nil
|
|
33
|
-
@initialized = false
|
|
34
|
-
|
|
35
|
-
# Resource objects
|
|
36
|
-
@catalog = Catalog.new([], self)
|
|
37
|
-
# Resource template objects
|
|
38
|
-
@blueprint = Blueprint.new([], self)
|
|
39
|
-
# Prompt objects
|
|
40
|
-
@prompt_book = PromptBook.new([], self)
|
|
41
|
-
# Tool objects
|
|
42
|
-
@toolbox = Toolbox.new([], self)
|
|
43
|
-
|
|
44
|
-
setup_transport_callbacks
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Connect to the MCP server
|
|
48
|
-
def connect
|
|
49
|
-
return true if connected?
|
|
50
|
-
|
|
51
|
-
begin
|
|
52
|
-
log_debug("Connecting to MCP server via #{transport.class.name}...")
|
|
53
|
-
@connection_error = nil
|
|
54
|
-
|
|
55
|
-
success = @transport.connect
|
|
56
|
-
unless success
|
|
57
|
-
log_error("Failed to establish transport connection")
|
|
58
|
-
return false
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
log_debug("Connected to MCP server")
|
|
62
|
-
true
|
|
63
|
-
rescue StandardError => e
|
|
64
|
-
@connection_error = e.message
|
|
65
|
-
log_error("Failed to connect to MCP server: #{e.message}")
|
|
66
|
-
false
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Disconnect from the MCP server
|
|
71
|
-
def disconnect
|
|
72
|
-
return true unless connected?
|
|
73
|
-
|
|
74
|
-
begin
|
|
75
|
-
@transport.disconnect
|
|
76
|
-
log_debug("Disconnected from MCP server")
|
|
77
|
-
true
|
|
78
|
-
rescue StandardError => e
|
|
79
|
-
log_error("Error disconnecting from MCP server: #{e.message}")
|
|
80
|
-
false
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Send a request to the MCP server
|
|
85
|
-
def write_message(payload)
|
|
86
|
-
unless ready?
|
|
87
|
-
log_error("Cannot send request - transport not ready")
|
|
88
|
-
return false
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
begin
|
|
92
|
-
# Only write to session if it exists (after initialization)
|
|
93
|
-
session&.write(payload)
|
|
94
|
-
data = payload.to_json unless payload.is_a?(String)
|
|
95
|
-
@transport.send_message(data)
|
|
96
|
-
true
|
|
97
|
-
rescue StandardError => e
|
|
98
|
-
log_error("Failed to send request: #{e.message}")
|
|
99
|
-
false
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def server=(server)
|
|
104
|
-
@server = if server.is_a?(Client::Server)
|
|
105
|
-
server
|
|
106
|
-
else
|
|
107
|
-
Client::Server.new(server)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# Only update session if it exists
|
|
111
|
-
return unless @session
|
|
112
|
-
|
|
113
|
-
@session.server_capabilities = server.capabilities
|
|
114
|
-
@session.server_info = server.server_info
|
|
115
|
-
@session.save
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def initialized?
|
|
119
|
-
@initialized && @session&.initialized?
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def inspect
|
|
123
|
-
session_info = @session ? "session: #{@session.id}" : "session: none"
|
|
124
|
-
"#<#{self.class.name} transport: #{transport.class.name}, server: #{server}, client_name: #{client_info[:name]}, client_version: #{client_info[:version]}, capabilities: #{client_capabilities}, connected: #{connected?}, initialized: #{initialized?}, #{session_info}>"
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
protected
|
|
128
|
-
|
|
129
|
-
def setup_transport_callbacks
|
|
130
|
-
# Create JSON-RPC handler
|
|
131
|
-
@json_rpc_handler = JsonRpcHandler.new(session, self)
|
|
132
|
-
|
|
133
|
-
# Set up transport callbacks
|
|
134
|
-
@transport.on_message do |message|
|
|
135
|
-
handle_raw_message(message)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
@transport.on_error do |error|
|
|
139
|
-
handle_transport_error(error)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
@transport.on_connect do
|
|
143
|
-
handle_transport_connect
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
@transport.on_disconnect do
|
|
147
|
-
handle_transport_disconnect
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def handle_raw_message(raw)
|
|
152
|
-
@json_rpc_handler.call(raw)
|
|
153
|
-
rescue MultiJson::ParseError => e
|
|
154
|
-
log_error("JSON parse error: #{e} (raw: #{raw})")
|
|
155
|
-
rescue StandardError => e
|
|
156
|
-
log_error("Error handling message: #{e} (raw: #{raw})")
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def handle_transport_error(error)
|
|
160
|
-
@connection_error = error.message
|
|
161
|
-
log_error("Transport error: #{error.message}")
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def handle_transport_connect
|
|
165
|
-
log_debug("Transport connected")
|
|
166
|
-
# Send initial capabilities after connection
|
|
167
|
-
send_initial_capabilities
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def handle_transport_disconnect
|
|
171
|
-
log_debug("Transport disconnected")
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def send_initial_capabilities
|
|
175
|
-
log_debug("Sending client capabilities")
|
|
176
|
-
|
|
177
|
-
# If we have a session_id, we're trying to resume
|
|
178
|
-
log_debug("Attempting to resume session: #{@session_id}") if @session_id
|
|
179
|
-
|
|
180
|
-
params = {
|
|
181
|
-
protocolVersion: @protocol_version,
|
|
182
|
-
capabilities: client_capabilities,
|
|
183
|
-
clientInfo: client_info
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
# Include session_id if we're trying to resume
|
|
187
|
-
params[:sessionId] = @session_id if @session_id
|
|
188
|
-
|
|
189
|
-
# Use a unique request ID (not session ID since we don't have one yet)
|
|
190
|
-
request_id = SecureRandom.uuid_v7
|
|
191
|
-
send_jsonrpc_request("initialize", params: params, id: request_id)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def client_capabilities
|
|
195
|
-
{
|
|
196
|
-
# Base client capabilities can be defined here
|
|
197
|
-
# TODO
|
|
198
|
-
}
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def user_agent
|
|
202
|
-
"ActionMCP-Client"
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def client_info
|
|
206
|
-
{
|
|
207
|
-
name: user_agent,
|
|
208
|
-
version: ActionMCP.gem_version.to_s
|
|
209
|
-
}
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def log_debug(message)
|
|
213
|
-
logger.debug("[ActionMCP::#{self.class.name.split('::').last}] #{message}")
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def log_info(message)
|
|
217
|
-
logger.info("[ActionMCP::#{self.class.name.split('::').last}] #{message}")
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def log_error(message)
|
|
221
|
-
logger.error("[ActionMCP::#{self.class.name.split('::').last}] #{message}")
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
end
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
# Blueprints
|
|
6
|
-
#
|
|
7
|
-
# A collection that manages and provides access to URI templates (blueprints) for Model Context Protocol (MCP)
|
|
8
|
-
# resource discovery. These blueprints allow dynamic construction of resource URIs by filling in
|
|
9
|
-
# variable placeholders with specific values. The class supports lazy loading of templates when
|
|
10
|
-
# initialized with a client.
|
|
11
|
-
#
|
|
12
|
-
# Example usage:
|
|
13
|
-
# # Eager loading
|
|
14
|
-
# template_data = client.list_resource_templates # Returns array of URI template definitions
|
|
15
|
-
# blueprints = Blueprint.new(template_data)
|
|
16
|
-
#
|
|
17
|
-
# # Lazy loading
|
|
18
|
-
# blueprints = Blueprint.new([], client)
|
|
19
|
-
# templates = blueprints.all # Templates are loaded here
|
|
20
|
-
#
|
|
21
|
-
# # Access a specific blueprint by pattern
|
|
22
|
-
# file_blueprint = Blueprint.find_by_pattern("file://{path}")
|
|
23
|
-
#
|
|
24
|
-
# # Generate a concrete URI from a blueprint with parameters
|
|
25
|
-
# uri = Blueprint.construct("file://{path}", { path: "/logs/app.log" })
|
|
26
|
-
#
|
|
27
|
-
class Blueprint < Collection
|
|
28
|
-
# Initialize a new Blueprints collection with URI template definitions
|
|
29
|
-
#
|
|
30
|
-
# @param templates [Array<Hash>] Array of URI template definition hashes, each containing
|
|
31
|
-
# uriTemplate, name, description, and optionally mimeType keys
|
|
32
|
-
# @param client [Object, nil] Optional client for lazy loading of templates
|
|
33
|
-
def initialize(templates, client)
|
|
34
|
-
super(templates, client)
|
|
35
|
-
self.templates = @collection_data
|
|
36
|
-
@load_method = :list_resource_templates
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Find a blueprint by its URI pattern
|
|
40
|
-
#
|
|
41
|
-
# @param pattern [String] URI template pattern to find
|
|
42
|
-
# @return [Blueprint, nil] The blueprint with the given pattern, or nil if not found
|
|
43
|
-
def find_by_pattern(pattern)
|
|
44
|
-
all.find { |blueprint| blueprint.pattern == pattern }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Find blueprints by name
|
|
48
|
-
#
|
|
49
|
-
# @param name [String] Name of the blueprints to find
|
|
50
|
-
# @return [Array<Blueprint>] Blueprints with the given name
|
|
51
|
-
def find_by_name(name)
|
|
52
|
-
all.select { |blueprint| blueprint.name == name }
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Construct a concrete URI by applying parameters to a blueprint
|
|
56
|
-
#
|
|
57
|
-
# @param pattern [String] URI template pattern to use
|
|
58
|
-
# @param params [Hash] Parameters to substitute into the pattern
|
|
59
|
-
# @return [String] The constructed URI with parameters applied
|
|
60
|
-
# @raise [KeyError] If a required parameter is missing
|
|
61
|
-
def construct(pattern, params)
|
|
62
|
-
blueprint = find_by_pattern(pattern)
|
|
63
|
-
raise ArgumentError, "Unknown blueprint pattern: #{pattern}" unless blueprint
|
|
64
|
-
|
|
65
|
-
blueprint.construct(params)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Check if the collection contains a blueprint with the given pattern
|
|
69
|
-
#
|
|
70
|
-
# @param pattern [String] The blueprint pattern to check for
|
|
71
|
-
# @return [Boolean] true if a blueprint with the pattern exists
|
|
72
|
-
def contains?(pattern)
|
|
73
|
-
all.any? { |blueprint| blueprint.pattern == pattern }
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Group blueprints by their base protocol
|
|
77
|
-
#
|
|
78
|
-
# @return [Hash<String, Array<Blueprint>>] Hash mapping protocols to arrays of blueprints
|
|
79
|
-
def group_by_protocol
|
|
80
|
-
all.group_by(&:protocol)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Convert raw template data into ResourceTemplate objects
|
|
84
|
-
#
|
|
85
|
-
# @param templates [Array<Hash>] Array of template definition hashes
|
|
86
|
-
def templates=(templates)
|
|
87
|
-
@collection_data = templates.map { |template_data| ResourceTemplate.new(template_data) }
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Internal Blueprint class to represent individual URI templates
|
|
91
|
-
class ResourceTemplate
|
|
92
|
-
attr_reader :pattern, :name, :description, :mime_type, :annotations
|
|
93
|
-
|
|
94
|
-
# Initialize a new ResourceTemplate instance
|
|
95
|
-
#
|
|
96
|
-
# @param data [Hash] ResourceTemplate definition hash containing uriTemplate, name, description,
|
|
97
|
-
# and optionally mimeType, and annotations
|
|
98
|
-
def initialize(data)
|
|
99
|
-
@pattern = data["uriTemplate"]
|
|
100
|
-
@name = data["name"]
|
|
101
|
-
@description = data["description"]
|
|
102
|
-
@mime_type = data["mimeType"]
|
|
103
|
-
@variable_pattern = /{([^}]+)}/
|
|
104
|
-
@annotations = data["annotations"] || {}
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Extract variable names from the template pattern
|
|
108
|
-
#
|
|
109
|
-
# @return [Array<String>] List of variable names in the pattern
|
|
110
|
-
def variables
|
|
111
|
-
@pattern.scan(@variable_pattern).flatten
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Get the protocol part of the URI template
|
|
115
|
-
#
|
|
116
|
-
# @return [String] The protocol (scheme) of the URI template
|
|
117
|
-
def protocol
|
|
118
|
-
@pattern.split("://").first
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Construct a concrete URI by substituting parameters into the template pattern
|
|
122
|
-
#
|
|
123
|
-
# @param params [Hash] Parameters to substitute into the pattern
|
|
124
|
-
# @return [String] The constructed URI with parameters applied
|
|
125
|
-
# @raise [KeyError] If a required parameter is missing
|
|
126
|
-
def construct(params)
|
|
127
|
-
result = @pattern.dup
|
|
128
|
-
|
|
129
|
-
variables.each do |var|
|
|
130
|
-
raise KeyError, "Missing required parameter: #{var}" unless params.key?(var.to_sym) || params.key?(var)
|
|
131
|
-
|
|
132
|
-
value = params[var.to_sym] || params[var]
|
|
133
|
-
result.gsub!("{#{var}}", value.to_s)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
result
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Check if this template is compatible with a set of parameters
|
|
140
|
-
#
|
|
141
|
-
# @param params [Hash] Parameters to check
|
|
142
|
-
# @return [Boolean] true if all required variables have corresponding parameters
|
|
143
|
-
def compatible_with?(params)
|
|
144
|
-
symbolized_params = params.transform_keys(&:to_sym)
|
|
145
|
-
variables.all? { |var| symbolized_params.key?(var.to_sym) }
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# Generate a hash representation of the blueprint
|
|
149
|
-
#
|
|
150
|
-
# @return [Hash] Hash containing blueprint details
|
|
151
|
-
def to_h
|
|
152
|
-
{
|
|
153
|
-
"uriTemplate" => @pattern,
|
|
154
|
-
"name" => @name,
|
|
155
|
-
"description" => @description,
|
|
156
|
-
"mimeType" => @mime_type,
|
|
157
|
-
"annotations" => @annotations
|
|
158
|
-
}
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
end
|
|
@@ -1,164 +0,0 @@
|
|
|
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 < Collection
|
|
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
|
-
def initialize(resources, client)
|
|
34
|
-
super(resources, client)
|
|
35
|
-
self.resources = @collection_data
|
|
36
|
-
@load_method = :list_resources
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Find a resource by URI
|
|
40
|
-
#
|
|
41
|
-
# @param uri [String] URI of the resource to find
|
|
42
|
-
# @return [Resource, nil] The resource with the given URI, or nil if not found
|
|
43
|
-
def find_by_uri(uri)
|
|
44
|
-
all.find { |resource| resource.uri == uri }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Find resources by name
|
|
48
|
-
#
|
|
49
|
-
# @param name [String] Name of the resources to find
|
|
50
|
-
# @return [Array<Resource>] Resources with the given name
|
|
51
|
-
def find_by_name(name)
|
|
52
|
-
all.select { |resource| resource.name == name }
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Find resources by MIME type
|
|
56
|
-
#
|
|
57
|
-
# @param mime_type [String] MIME type to search for
|
|
58
|
-
# @return [Array<Resource>] Resources with the given MIME type
|
|
59
|
-
def find_by_mime_type(mime_type)
|
|
60
|
-
all.select { |resource| resource.mime_type == mime_type }
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Get a list of all resource URIs
|
|
64
|
-
#
|
|
65
|
-
# @return [Array<String>] URIs of all resources in the collection
|
|
66
|
-
def uris
|
|
67
|
-
all.map(&:uri)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Check if the collection contains a resource with the given URI
|
|
71
|
-
#
|
|
72
|
-
# @param uri [String] The resource URI to check for
|
|
73
|
-
# @return [Boolean] true if a resource with the URI exists
|
|
74
|
-
def contains_uri?(uri)
|
|
75
|
-
all.any? { |resource| resource.uri == uri }
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Group resources by MIME type
|
|
79
|
-
#
|
|
80
|
-
# @return [Hash<String, Array<Resource>>] Hash mapping MIME types to arrays of resources
|
|
81
|
-
def group_by_mime_type
|
|
82
|
-
all.group_by(&:mime_type)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Search resources by keyword in name or description
|
|
86
|
-
#
|
|
87
|
-
# @param keyword [String] Keyword to search for
|
|
88
|
-
# @return [Array<Resource>] Resources matching the search term
|
|
89
|
-
def search(keyword)
|
|
90
|
-
keyword = keyword.downcase
|
|
91
|
-
all.select do |resource|
|
|
92
|
-
resource.name.downcase.include?(keyword) ||
|
|
93
|
-
resource.description&.downcase&.include?(keyword)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Convert raw resource data into Resource objects
|
|
98
|
-
#
|
|
99
|
-
# @param raw_resources [Array<Hash>] Array of resource definition hashes
|
|
100
|
-
def resources=(raw_resources)
|
|
101
|
-
@collection_data = raw_resources.map { |resource_data| Resource.new(resource_data) }
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Internal Resource class to represent individual resources
|
|
105
|
-
class Resource
|
|
106
|
-
attr_reader :uri, :name, :description, :mime_type, :annotations
|
|
107
|
-
|
|
108
|
-
# Initialize a new Resource instance
|
|
109
|
-
#
|
|
110
|
-
# @param data [Hash] Resource definition hash containing uri, name, description, and mimeType
|
|
111
|
-
def initialize(data = [])
|
|
112
|
-
@uri = data["uri"]
|
|
113
|
-
@name = data["name"]
|
|
114
|
-
@description = data["description"]
|
|
115
|
-
@mime_type = data["mimeType"]
|
|
116
|
-
@annotations = data["annotations"] || {}
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Get the file extension from the resource name
|
|
120
|
-
#
|
|
121
|
-
# @return [String, nil] The file extension or nil if no extension
|
|
122
|
-
def extension
|
|
123
|
-
File.extname(@name)[1..] if @name.include?(".")
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# Check if this resource is a text file based on MIME type
|
|
127
|
-
#
|
|
128
|
-
# @return [Boolean] true if the resource is a text file
|
|
129
|
-
def text?
|
|
130
|
-
@mime_type&.start_with?("text/")
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Check if this resource is an image based on MIME type
|
|
134
|
-
#
|
|
135
|
-
# @return [Boolean] true if the resource is an image
|
|
136
|
-
def image?
|
|
137
|
-
@mime_type&.start_with?("image/")
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# Get the path portion of the URI
|
|
141
|
-
#
|
|
142
|
-
# @return [String, nil] The path component of the URI
|
|
143
|
-
def path
|
|
144
|
-
URI(@uri).path
|
|
145
|
-
rescue StandardError
|
|
146
|
-
nil
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Generate a hash representation of the resource
|
|
150
|
-
#
|
|
151
|
-
# @return [Hash] Hash containing resource details
|
|
152
|
-
def to_h
|
|
153
|
-
{
|
|
154
|
-
"uri" => @uri,
|
|
155
|
-
"name" => @name,
|
|
156
|
-
"description" => @description,
|
|
157
|
-
"mimeType" => @mime_type,
|
|
158
|
-
"annotations" => @annotations
|
|
159
|
-
}
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
end
|