aspera-cli 4.26.2 → 4.27.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
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +281 -21
- data/CONTRIBUTING.md +98 -7
- data/bin/ascli +3 -3
- data/docs/README.md +10757 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +20 -6
- data/lib/aspera/agent/connect.rb +53 -16
- data/lib/aspera/agent/desktop.rb +52 -17
- data/lib/aspera/agent/direct.rb +26 -24
- data/lib/aspera/agent/httpgw.rb +1 -1
- data/lib/aspera/agent/node.rb +32 -0
- data/lib/aspera/agent/transferd.rb +39 -3
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +14 -16
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +15 -14
- data/lib/aspera/api/httpgw.rb +4 -4
- data/lib/aspera/api/node.rb +26 -23
- data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
- data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
- data/lib/aspera/ascmd.rb +21 -19
- data/lib/aspera/ascp/installation.rb +37 -38
- data/lib/aspera/ascp/management.rb +2 -3
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +81 -0
- data/lib/aspera/cli/bootstrapper.rb +197 -0
- data/lib/aspera/cli/command_registry.rb +145 -0
- data/lib/aspera/cli/command_spec.rb +138 -0
- data/lib/aspera/cli/context.rb +16 -3
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +2 -1
- data/lib/aspera/cli/formatter.rb +41 -27
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +2 -2
- data/lib/aspera/cli/http.rb +70 -19
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +198 -0
- data/lib/aspera/cli/options.schema.yaml +364 -7
- data/lib/aspera/cli/{manager.rb → parser.rb} +334 -142
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1166 -853
- data/lib/aspera/cli/plugins/ats.rb +200 -161
- data/lib/aspera/cli/plugins/base.rb +457 -100
- data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
- data/lib/aspera/cli/plugins/config.rb +428 -754
- data/lib/aspera/cli/plugins/console.rb +106 -64
- data/lib/aspera/cli/plugins/cos.rb +44 -32
- data/lib/aspera/cli/plugins/factory.rb +4 -4
- data/lib/aspera/cli/plugins/faspex.rb +294 -257
- data/lib/aspera/cli/plugins/faspex5.rb +585 -357
- data/lib/aspera/cli/plugins/faspio.rb +49 -51
- data/lib/aspera/cli/plugins/httpgw.rb +18 -25
- data/lib/aspera/cli/plugins/mcp.rb +279 -0
- data/lib/aspera/cli/plugins/node.rb +998 -794
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +111 -134
- data/lib/aspera/cli/plugins/preview.rb +180 -139
- data/lib/aspera/cli/plugins/server.rb +124 -79
- data/lib/aspera/cli/plugins/shares.rb +301 -107
- data/lib/aspera/cli/preset_actions.rb +139 -0
- data/lib/aspera/cli/preset_manager.rb +13 -12
- data/lib/aspera/cli/result.rb +61 -11
- data/lib/aspera/cli/runner.rb +275 -141
- data/lib/aspera/cli/secret_finder.rb +40 -0
- data/lib/aspera/cli/special_values.rb +1 -0
- data/lib/aspera/cli/sync_actions.rb +83 -59
- data/lib/aspera/cli/terminal_formatter.rb +1 -1
- data/lib/aspera/cli/transfer_actions.rb +83 -0
- data/lib/aspera/cli/transfer_agent.rb +116 -51
- data/lib/aspera/cli/transfer_progress.rb +3 -3
- data/lib/aspera/cli/vault_manager.rb +57 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +21 -20
- data/lib/aspera/command_line_builder.rb +22 -22
- data/lib/aspera/data_repository.rb +2 -1
- data/lib/aspera/dot_container.rb +4 -4
- data/lib/aspera/environment.rb +18 -11
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/faspex_postproc.rb +4 -3
- data/lib/aspera/graphql.rb +35 -0
- data/lib/aspera/hash_ext.rb +6 -0
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/encrypted_hash.rb +1 -1
- data/lib/aspera/keychain/factory.rb +3 -3
- data/lib/aspera/keychain/macos_security.rb +3 -3
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +17 -3
- data/lib/aspera/markdown.rb +85 -2
- data/lib/aspera/node_simulator.rb +3 -1
- data/lib/aspera/oauth/base.rb +5 -5
- data/lib/aspera/oauth/boot.rb +2 -2
- data/lib/aspera/oauth/factory.rb +7 -5
- data/lib/aspera/oauth/jwt.rb +4 -4
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/persistency_action_once.rb +8 -8
- data/lib/aspera/persistency_folder.rb +10 -3
- data/lib/aspera/preview/file_types.rb +3 -3
- data/lib/aspera/preview/generator.rb +3 -3
- data/lib/aspera/preview/utils.rb +1 -1
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/products/other.rb +1 -1
- data/lib/aspera/products/transferd.rb +1 -1
- data/lib/aspera/proxy_auto_config.rb +7 -6
- data/lib/aspera/rest.rb +10 -36
- data/lib/aspera/rest_error_analyzer.rb +3 -3
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +3 -3
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
- data/lib/aspera/schema/documentation.rb +57 -26
- data/lib/aspera/schema/reader.rb +59 -10
- data/lib/aspera/schema/registry.rb +39 -6
- data/lib/aspera/secret_hider.rb +7 -2
- data/lib/aspera/ssl.rb +4 -4
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/operations.rb +10 -13
- data/lib/aspera/temp_file_manager.rb +1 -1
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +23 -21
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +6 -6
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +12 -3
- data/lib/aspera/uri_reader.rb +2 -2
- data/lib/aspera/web_auth.rb +2 -2
- data/lib/aspera/web_server_simple.rb +8 -5
- data/lib/aspera/yaml.rb +2 -1
- data.tar.gz.sig +0 -0
- metadata +25 -3
- metadata.gz.sig +0 -0
- data/lib/aspera/json_rpc.rb +0 -52
data/lib/aspera/cli/http.rb
CHANGED
|
@@ -4,6 +4,10 @@ require 'aspera/cli/special_values'
|
|
|
4
4
|
require 'aspera/log'
|
|
5
5
|
require 'aspera/assert'
|
|
6
6
|
require 'aspera/line_logger'
|
|
7
|
+
require 'aspera/schema/registry'
|
|
8
|
+
require 'aspera/rest'
|
|
9
|
+
require 'aspera/oauth'
|
|
10
|
+
require 'aspera/ssl'
|
|
7
11
|
require 'openssl'
|
|
8
12
|
|
|
9
13
|
module Aspera
|
|
@@ -29,19 +33,64 @@ module Aspera
|
|
|
29
33
|
@certificate_paths = nil
|
|
30
34
|
end
|
|
31
35
|
|
|
32
|
-
attr_accessor :insecure, :warn_insecure
|
|
33
|
-
attr_reader :ignore_cert_host_port
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
options
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
attr_accessor :insecure, :warn_insecure
|
|
37
|
+
attr_reader :ignore_cert_host_port, :http_options
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
# Declare all HTTP/S CLI options (metadata only - no handler binding yet).
|
|
41
|
+
# Called once from Config#initialize before this instance is available as a target.
|
|
42
|
+
# Handlers are bound in a second pass via bind_options once the instance exists.
|
|
43
|
+
# @param options [Aspera::Cli::Parser] CLI options manager to declare options into
|
|
44
|
+
# @return [nil]
|
|
45
|
+
def declare_options(options)
|
|
46
|
+
options.declare(:insecure, description: 'HTTP/S: Do not validate any certificate', allowed: Allowed::TYPES_BOOLEAN, default: false)
|
|
47
|
+
options.declare(:ignore_certificate, description: 'HTTP/S: Do not validate certificate for these URLs', allowed: [Array, NilClass])
|
|
48
|
+
options.declare(:warn_insecure, description: 'HTTP/S: Issue a warning if certificate is ignored', allowed: Allowed::TYPES_BOOLEAN, default: true)
|
|
49
|
+
options.declare(:cert_stores, description: 'HTTP/S: List of folder with trusted certificates', allowed: Allowed::TYPES_STRING_ARRAY)
|
|
50
|
+
options.declare(:http_options, schema: Schema::Registry::HTTP_OPTIONS)
|
|
51
|
+
options.declare(:http_proxy, description: 'HTTP/S: URL for proxy with optional credentials')
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Bind all HTTP options to this instance using set_handler.
|
|
57
|
+
# Called from Config#initialize immediately after Http.new.
|
|
58
|
+
# @param options [Aspera::Cli::Parser]
|
|
59
|
+
# @return [nil]
|
|
60
|
+
def bind_options(options)
|
|
61
|
+
options.set_handler(:insecure, object: self, method: :insecure)
|
|
62
|
+
options.set_handler(:ignore_certificate, object: self, method: :ignore_cert_host_port)
|
|
63
|
+
options.set_handler(:warn_insecure, object: self, method: :warn_insecure)
|
|
64
|
+
options.set_handler(:cert_stores, object: self, method: :trusted_cert_locations)
|
|
65
|
+
options.set_handler(:http_options, object: self, method: :http_options)
|
|
66
|
+
options.set_handler(:http_proxy, object: self, method: :http_proxy)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Setter for http_options: dispatch each key to its target singleton immediately.
|
|
70
|
+
# Keys matching RestParameters setters go to RestParameters, 'ssl_options' goes to SSL,
|
|
71
|
+
# keys matching OAuth::Factory.instance.parameters go to OAuth, and the rest are kept
|
|
72
|
+
# in @http_options for Net::HTTP session configuration in update_session.
|
|
73
|
+
# This runs on every assignment (JSON hash, dotted notation, preset merge) so timing
|
|
74
|
+
# of option parsing never matters.
|
|
75
|
+
# @param new_options [Hash] merged http_options hash
|
|
76
|
+
# @return [nil]
|
|
77
|
+
def http_options=(new_options)
|
|
78
|
+
Aspera.assert_type(new_options, Hash)
|
|
79
|
+
kept = {}
|
|
80
|
+
new_options.each do |k, v|
|
|
81
|
+
method = "#{k}=".to_sym
|
|
82
|
+
if RestParameters.instance.respond_to?(method)
|
|
83
|
+
RestParameters.instance.send(method, v)
|
|
84
|
+
elsif k.to_s.eql?('ssl_options')
|
|
85
|
+
Aspera::SSL.option_list = v
|
|
86
|
+
elsif OAuth::Factory.instance.parameters.key?(k.to_sym)
|
|
87
|
+
OAuth::Factory.instance.parameters[k.to_sym] = v
|
|
88
|
+
else
|
|
89
|
+
kept[k] = v
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
@http_options = kept
|
|
93
|
+
nil
|
|
45
94
|
end
|
|
46
95
|
|
|
47
96
|
# ------------------------------------------------------------------
|
|
@@ -64,7 +113,7 @@ module Aspera
|
|
|
64
113
|
def ignore_cert_host_port=(url_list)
|
|
65
114
|
url_list.each do |url|
|
|
66
115
|
uri = URI.parse(url)
|
|
67
|
-
|
|
116
|
+
Aspera.assert(uri.scheme.eql?('https')){"Expecting https scheme: #{url}"}
|
|
68
117
|
@ignore_cert_host_port.push([uri.host, uri.port].freeze)
|
|
69
118
|
end
|
|
70
119
|
end
|
|
@@ -90,7 +139,8 @@ module Aspera
|
|
|
90
139
|
# ------------------------------------------------------------------
|
|
91
140
|
|
|
92
141
|
# Add files, folders or the default OS locations to the cert store.
|
|
93
|
-
# @param path_list [Array<String>]
|
|
142
|
+
# @param path_list [Array<String>] list of file/folder paths to add to the certificate store
|
|
143
|
+
# @return [nil]
|
|
94
144
|
def trusted_cert_locations=(path_list)
|
|
95
145
|
Aspera.assert_type(path_list, Array){'cert locations'}
|
|
96
146
|
if @certificate_store.nil?
|
|
@@ -126,6 +176,7 @@ module Aspera
|
|
|
126
176
|
end
|
|
127
177
|
end
|
|
128
178
|
@certificate_paths.uniq!
|
|
179
|
+
nil
|
|
129
180
|
end
|
|
130
181
|
|
|
131
182
|
# Return cert file paths (computes OS defaults lazily if never set).
|
|
@@ -145,12 +196,11 @@ module Aspera
|
|
|
145
196
|
# Called every time a new Net::HTTP session is opened.
|
|
146
197
|
# ------------------------------------------------------------------
|
|
147
198
|
|
|
148
|
-
# @param http_session [Net::HTTP]
|
|
199
|
+
# @param http_session [Net::HTTP] HTTP session to configure
|
|
200
|
+
# @return [nil]
|
|
149
201
|
def update_session(http_session)
|
|
150
202
|
http_session.set_debug_output(LineLogger.new(:trace2)) if Log.instance.logger.trace2?
|
|
151
|
-
if http_session.use_ssl? && ignore_cert?(http_session.address, http_session.port)
|
|
152
|
-
http_session.verify_mode = SELF_SIGNED_CERT
|
|
153
|
-
end
|
|
203
|
+
http_session.verify_mode = SELF_SIGNED_CERT if http_session.use_ssl? && ignore_cert?(http_session.address, http_session.port)
|
|
154
204
|
http_session.cert_store = @certificate_store if @certificate_store
|
|
155
205
|
Log.log.debug{"Using cert store #{http_session.cert_store} (#{@certificate_store})"} unless http_session.cert_store.nil?
|
|
156
206
|
@http_options.each do |k, v|
|
|
@@ -161,6 +211,7 @@ module Aspera
|
|
|
161
211
|
Log.log.error{"Unknown HTTP session attribute: #{k}"}
|
|
162
212
|
end
|
|
163
213
|
end
|
|
214
|
+
nil
|
|
164
215
|
end
|
|
165
216
|
end
|
|
166
217
|
end
|
data/lib/aspera/cli/info.rb
CHANGED
|
@@ -15,6 +15,8 @@ module Aspera
|
|
|
15
15
|
# Set this to warn in advance when minimum required ruby version will increase
|
|
16
16
|
# See also required_ruby_version in gemspec file
|
|
17
17
|
RUBY_FUTURE_MINIMUM_VERSION = '3.2'
|
|
18
|
+
# Version with which this version of CLI was tested
|
|
19
|
+
SDK_VERSION = '1.1.9'
|
|
18
20
|
end
|
|
19
21
|
end
|
|
20
22
|
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/environment'
|
|
4
|
+
require 'aspera/log'
|
|
5
|
+
require 'aspera/assert'
|
|
6
|
+
require 'erb'
|
|
7
|
+
|
|
8
|
+
module Aspera
|
|
9
|
+
module Cli
|
|
10
|
+
# Email service injected into Context as :mailer so that any component
|
|
11
|
+
# (TransferAgent, plugins, ...) can send emails via SMTP and ERB templates.
|
|
12
|
+
# Provides :smtp, :notify_to, :notify_template options via the Options manager.
|
|
13
|
+
class Mailer
|
|
14
|
+
SMTP_CONF_PARAMS = %i[server tls ssl port domain username password from_name from_email].freeze
|
|
15
|
+
SMTP_BOOL_PARAMS = %i[tls ssl].freeze
|
|
16
|
+
SMTP_INT_PARAMS = %i[port].freeze
|
|
17
|
+
SMTP_STR_PARAMS = %i[server domain username password from_email from_name].freeze
|
|
18
|
+
|
|
19
|
+
# @param options [Parser] CLI options manager (provides :smtp, :notify_to, :notify_template)
|
|
20
|
+
# @param main_folder [String] application main folder (unused directly but kept for symmetry)
|
|
21
|
+
def initialize(options, main_folder)
|
|
22
|
+
@options = options
|
|
23
|
+
@main_folder = main_folder
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Parser]
|
|
27
|
+
attr_reader :options
|
|
28
|
+
|
|
29
|
+
# @return [Hash] email server settings with defaults applied
|
|
30
|
+
def email_settings
|
|
31
|
+
smtp = options.get_option(:smtp, mandatory: true)
|
|
32
|
+
# Change keys from string into symbol
|
|
33
|
+
smtp = smtp.symbolize_keys
|
|
34
|
+
unsupported = smtp.keys - SMTP_CONF_PARAMS
|
|
35
|
+
Aspera.assert(unsupported.empty?, type: Cli::Error){"Unsupported SMTP parameter: #{unsupported.join(', ')}, use: #{SMTP_CONF_PARAMS.join(', ')}"}
|
|
36
|
+
# Boolean fields must be actual booleans, not strings like "false"
|
|
37
|
+
SMTP_BOOL_PARAMS.each do |k|
|
|
38
|
+
Aspera.assert_values(smtp[k], [true, false], type: Cli::Error){"smtp.#{k}"} if smtp.key?(k)
|
|
39
|
+
end
|
|
40
|
+
SMTP_INT_PARAMS.each do |k|
|
|
41
|
+
Aspera.assert_type(smtp[k], Integer, type: Cli::Error){"smtp.#{k}"} if smtp.key?(k)
|
|
42
|
+
end
|
|
43
|
+
SMTP_STR_PARAMS.each do |k|
|
|
44
|
+
Aspera.assert_type(smtp[k], String, type: Cli::Error){"smtp.#{k}"} if smtp.key?(k)
|
|
45
|
+
end
|
|
46
|
+
# smtp[:ssl] = nil (false)
|
|
47
|
+
smtp[:tls] = !smtp[:ssl] unless smtp.key?(:tls)
|
|
48
|
+
smtp[:port] ||= if smtp[:tls]
|
|
49
|
+
587
|
|
50
|
+
elsif smtp[:ssl]
|
|
51
|
+
465
|
|
52
|
+
else
|
|
53
|
+
25
|
|
54
|
+
end
|
|
55
|
+
smtp[:from_email] ||= smtp[:username] if smtp.key?(:username)
|
|
56
|
+
smtp[:from_name] ||= smtp[:from_email].sub(/@.*$/, '').gsub(/[^a-zA-Z]/, ' ').capitalize if smtp.key?(:username)
|
|
57
|
+
smtp[:domain] ||= smtp[:from_email].sub(/^.*@/, '') if smtp.key?(:from_email)
|
|
58
|
+
%i[server port domain].each do |n|
|
|
59
|
+
Aspera.assert(smtp.key?(n)){"Missing mandatory smtp parameter: #{n}"}
|
|
60
|
+
end
|
|
61
|
+
Log.log.debug{"smtp=#{smtp}"}
|
|
62
|
+
return smtp
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Send email using ERB template
|
|
66
|
+
# @param email_template_default [String] default template, can be overridden by option
|
|
67
|
+
# @param values [Hash] values to be used in template, keys with default: to, from_name, from_email
|
|
68
|
+
def send_email_template(email_template_default: nil, values: {})
|
|
69
|
+
values[:to] ||= options.get_option(:notify_to, mandatory: true)
|
|
70
|
+
notify_template = options.get_option(:notify_template, mandatory: email_template_default.nil?) || email_template_default
|
|
71
|
+
mail_conf = email_settings
|
|
72
|
+
values[:from_name] ||= mail_conf[:from_name]
|
|
73
|
+
values[:from_email] ||= mail_conf[:from_email]
|
|
74
|
+
%i[to from_email].each do |n|
|
|
75
|
+
Aspera.assert_type(values[n], String){"Missing email parameter: #{n} in config"}
|
|
76
|
+
end
|
|
77
|
+
start_options = [mail_conf[:domain]]
|
|
78
|
+
start_options.push(mail_conf[:username], mail_conf[:password], :login) if mail_conf.key?(:username) && mail_conf.key?(:password)
|
|
79
|
+
template_binding = Environment.empty_binding
|
|
80
|
+
values.each do |k, v|
|
|
81
|
+
Aspera.assert_type(k, Symbol)
|
|
82
|
+
template_binding.local_variable_set(k, v)
|
|
83
|
+
end
|
|
84
|
+
msg_with_headers = ERB.new(notify_template).result(template_binding)
|
|
85
|
+
Log.dump(:msg_with_headers, msg_with_headers)
|
|
86
|
+
require 'net/smtp'
|
|
87
|
+
smtp = Net::SMTP.new(mail_conf[:server], mail_conf[:port])
|
|
88
|
+
smtp.enable_starttls if mail_conf[:tls]
|
|
89
|
+
smtp.enable_tls if mail_conf[:ssl]
|
|
90
|
+
smtp.start(*start_options) do |smtp_session|
|
|
91
|
+
smtp_session.send_message(msg_with_headers, values[:from_email], values[:to])
|
|
92
|
+
end
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# cspell:ignore ascli
|
|
4
|
+
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'aspera/log'
|
|
7
|
+
require 'aspera/cli/runner'
|
|
8
|
+
require 'aspera/cli/error'
|
|
9
|
+
unless defined?(MCP::Tool)
|
|
10
|
+
begin
|
|
11
|
+
require 'mcp'
|
|
12
|
+
rescue LoadError
|
|
13
|
+
raise Cli::Error, "The 'mcp' gem is required. Install it with: gem install mcp"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module Aspera
|
|
18
|
+
module Cli
|
|
19
|
+
# MCP Tool: executes an ascli command in-process.
|
|
20
|
+
class McpTool < MCP::Tool
|
|
21
|
+
# Default maximum byte size of the JSON text content returned for list results.
|
|
22
|
+
# Items are appended whole until the limit is reached; the full list is always
|
|
23
|
+
# available in structuredContent.
|
|
24
|
+
DEFAULT_MAX_TEXT_BYTES = 100_000
|
|
25
|
+
# Default extra arguments automatically prepended to every ascli call.
|
|
26
|
+
# Keeps the AI from having to remember mandatory flags on every invocation.
|
|
27
|
+
DEFAULT_EXTRA_ARGS = ['--interactive=no', '--transfer.asynchronous=true'].freeze
|
|
28
|
+
|
|
29
|
+
tool_name 'execute_ascli_command'
|
|
30
|
+
|
|
31
|
+
description <<~DESC.strip
|
|
32
|
+
Execute any ascli (Aspera CLI) command in-process and return its result.
|
|
33
|
+
|
|
34
|
+
SYNTAX
|
|
35
|
+
args is a JSON array of strings mirroring the CLI command line.
|
|
36
|
+
Element 0 : plugin name (aoc, faspex5, node, server, config, …).
|
|
37
|
+
Elements 1+: sub-commands, then --option=value flags in any order.
|
|
38
|
+
Passing structured values: use an extended-value prefix on the relevant element:
|
|
39
|
+
"@json:{...}" — inline JSON object or array (use for Hash/Array arguments)
|
|
40
|
+
"@preset:name" — expand a saved credential preset
|
|
41
|
+
"@env:VAR" — read value from environment variable
|
|
42
|
+
"@file:/path" — read value from a file
|
|
43
|
+
|
|
44
|
+
AUTOMATIC FLAGS
|
|
45
|
+
The server automatically prepends extra_args to every call (default:
|
|
46
|
+
--interactive=no and --transfer.asynchronous=true).
|
|
47
|
+
Do NOT repeat these flags in your args array — they are already injected.
|
|
48
|
+
If you explicitly need to override one (e.g. --interactive=yes), include it
|
|
49
|
+
in your args; your value will take precedence because it appears after the
|
|
50
|
+
injected ones.
|
|
51
|
+
If credentials are missing or incomplete, the command will return an error;
|
|
52
|
+
report it and stop, never wait for input.
|
|
53
|
+
|
|
54
|
+
DISCOVERY — recommended sequence
|
|
55
|
+
Step 1 — enumerate all commands:
|
|
56
|
+
["config", "commands"]
|
|
57
|
+
Returns { syntax, description } for every leaf command of every plugin.
|
|
58
|
+
Syntax notation: <arg> mandatory, [<arg>] optional, <a|b> enum, <arg...> variadic.
|
|
59
|
+
This single call covers all 800+ commands — no other discovery step is needed
|
|
60
|
+
unless you want details about a specific command or its Hash arguments.
|
|
61
|
+
IMPORTANT: never guess command names from training data. If you are unsure of
|
|
62
|
+
the exact subcommand name (e.g. shared_folders vs shared_inboxes), always call
|
|
63
|
+
["config", "commands"] first to find the correct name.
|
|
64
|
+
|
|
65
|
+
Step 2 — inspect a Hash argument schema BEFORE calling any command with <data>:
|
|
66
|
+
MANDATORY: whenever the command syntax shows a <data> argument, you MUST call
|
|
67
|
+
"help" offline first. Never infer field names from server error messages.
|
|
68
|
+
["<plugin>", "<cmd>", ..., "help"]
|
|
69
|
+
Replace the Hash positional argument with the literal string "help".
|
|
70
|
+
Returns a table of field names, types, and descriptions for that argument.
|
|
71
|
+
Example: ["aoc", "admin", "user", "create", "help"]
|
|
72
|
+
Note: only works for Hash-typed arguments, not for plain String arguments.
|
|
73
|
+
|
|
74
|
+
Step 2b — discover available query/filter parameters for list commands:
|
|
75
|
+
["<plugin>", "<cmd>", ..., "--query=help"]
|
|
76
|
+
Add --query=help to any list command to see all supported filter parameters
|
|
77
|
+
with their types and descriptions.
|
|
78
|
+
Example: ["aoc", "admin", "user", "list", "--query=help"]
|
|
79
|
+
Example: ["faspex5", "admin", "packages", "list", "--query=help"]
|
|
80
|
+
Note: only works on commands that support --query filtering (list/delete).
|
|
81
|
+
|
|
82
|
+
Step 3 — list all options for a plugin as structured data:
|
|
83
|
+
["config", "options", "<plugin>"]
|
|
84
|
+
Returns { option, description, allowed, deprecated } for every --flag
|
|
85
|
+
accepted by that plugin (global + plugin-specific, ~80 entries).
|
|
86
|
+
Use when you need to know the exact allowed values or find a specific flag.
|
|
87
|
+
|
|
88
|
+
Full documentation:
|
|
89
|
+
["config", "documentation", "toc"]
|
|
90
|
+
Returns the table of contents: { level, title, anchor } for every heading.
|
|
91
|
+
["config", "documentation", "local", "<anchor>"]
|
|
92
|
+
Returns only the section matching that anchor (same slugs as GitHub).
|
|
93
|
+
["config", "documentation", "local", "--ui=text"]
|
|
94
|
+
Returns the complete README (~300 KB). Use only when a specific section
|
|
95
|
+
is insufficient and you need broader narrative context.
|
|
96
|
+
|
|
97
|
+
RESULT FORMAT
|
|
98
|
+
Structured data is always in structuredContent (a JSON object).
|
|
99
|
+
For list results, text content is limited to #{DEFAULT_MAX_TEXT_BYTES} bytes (whole items only).
|
|
100
|
+
When truncated, a WARNING block is appended: "WARNING: result truncated to N of TOTAL items."
|
|
101
|
+
You MUST read structuredContent.items to obtain the full dataset — never report
|
|
102
|
+
counts, totals, or search results from a truncated text block.
|
|
103
|
+
|
|
104
|
+
FILE LIST FOR TRANSFERS
|
|
105
|
+
For all transfers (upload, download, package send, …), append source file paths
|
|
106
|
+
at the end of the args array — no --sources flag needed.
|
|
107
|
+
["server", "upload", "--to-folder=/dst", "/local/file1", "/local/file2"]
|
|
108
|
+
["aoc", "packages", "send", "@:", "name=pkg", "recipients.0=user@example.com", "END",
|
|
109
|
+
"/local/file1", "/local/file2"]
|
|
110
|
+
|
|
111
|
+
EXAMPLES
|
|
112
|
+
["config", "commands"] ← Step 1: full capability map
|
|
113
|
+
["aoc", "admin", "user", "create", "help"] ← Step 2: schema of <data> Hash
|
|
114
|
+
["config", "options", "aoc"] ← Step 3: all --flags for aoc plugin
|
|
115
|
+
["config", "documentation", "toc"] ← TOC of local README
|
|
116
|
+
["config", "documentation", "local",
|
|
117
|
+
"leveraging-ai-assistance"] ← single README section by anchor
|
|
118
|
+
["aoc", "admin", "user", "create",
|
|
119
|
+
'@json:{"email":"a@b.com","name":"Alice"}',
|
|
120
|
+
"--url=https://org.ibmaspera.com", "--username=admin@org.com",
|
|
121
|
+
"--password=secret"]
|
|
122
|
+
["server", "browse", "/",
|
|
123
|
+
"--url=https://host", "--username=user", "--password=secret"]
|
|
124
|
+
["server", "upload", "--url=https://host", "--username=user", "--password=secret",
|
|
125
|
+
"--to-folder=/uploads", "/local/file1.txt", "/local/file2.txt"]
|
|
126
|
+
["aoc", "packages", "list", "--workspace=MyWorkspace"]
|
|
127
|
+
["node", "info", "--url=https://node-host",
|
|
128
|
+
"--username=user", "--password=pass"]
|
|
129
|
+
DESC
|
|
130
|
+
|
|
131
|
+
input_schema(
|
|
132
|
+
properties: {
|
|
133
|
+
args: {
|
|
134
|
+
type: 'array',
|
|
135
|
+
items: {type: 'string'},
|
|
136
|
+
minItems: 1,
|
|
137
|
+
description: 'ascli arguments: first element is the plugin name, followed by action and --option=value flags'
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
required: ['args']
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
class << self
|
|
144
|
+
attr_accessor :max_text_bytes, :extra_args
|
|
145
|
+
|
|
146
|
+
def call(args:, server_context: nil)
|
|
147
|
+
effective_args = Array(extra_args || DEFAULT_EXTRA_ARGS) + args
|
|
148
|
+
Log.dump(:mcp_execute, effective_args)
|
|
149
|
+
runner = Runner.new(effective_args)
|
|
150
|
+
result = runner.run_with_result
|
|
151
|
+
case result
|
|
152
|
+
when Result::Nothing, Result::Empty, NilClass
|
|
153
|
+
MCP::Tool::Response.new([{type: 'text', text: ''}])
|
|
154
|
+
when Result::SingleObject, Result::ObjectList, Result::ValueList
|
|
155
|
+
# Apply --select filter in place (affects both text and structuredContent).
|
|
156
|
+
runner.context.formatter.filter_columns_on_select(result.data) if result.data.is_a?(Array)
|
|
157
|
+
# MCP spec requires structuredContent to be a JSON object (not an array).
|
|
158
|
+
structured = result.data.is_a?(Array) ? {items: result.data} : result.data
|
|
159
|
+
content = if result.data.is_a?(Array)
|
|
160
|
+
text_limit = max_text_bytes || DEFAULT_MAX_TEXT_BYTES
|
|
161
|
+
truncated_items = truncate_items_by_bytes(result.data, text_limit)
|
|
162
|
+
total = result.data.size
|
|
163
|
+
if truncated_items.size < total
|
|
164
|
+
[
|
|
165
|
+
{type: 'text', text: JSON.generate(truncated_items)},
|
|
166
|
+
{type: 'text', text: "WARNING: result truncated to #{truncated_items.size} of #{total} items. Full dataset available in structuredContent."}
|
|
167
|
+
]
|
|
168
|
+
else
|
|
169
|
+
[{type: 'text', text: JSON.generate(result.data)}]
|
|
170
|
+
end
|
|
171
|
+
else
|
|
172
|
+
[{type: 'text', text: JSON.generate(result.data)}]
|
|
173
|
+
end
|
|
174
|
+
MCP::Tool::Response.new(content, structured_content: structured)
|
|
175
|
+
else
|
|
176
|
+
MCP::Tool::Response.new([{type: 'text', text: result.data.to_s}])
|
|
177
|
+
end
|
|
178
|
+
rescue SystemExit => e
|
|
179
|
+
MCP::Tool::Response.new([{type: 'text', text: "exited with status #{e.status}"}], error: !e.status.zero?)
|
|
180
|
+
rescue => e
|
|
181
|
+
MCP::Tool::Response.new([{type: 'text', text: "#{e.class}: #{e.message}"}], error: true)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Returns the largest prefix of +items+ whose JSON serialization fits within +max_bytes+.
|
|
185
|
+
# Items are appended whole — no item is ever split mid-JSON.
|
|
186
|
+
def truncate_items_by_bytes(items, max_bytes)
|
|
187
|
+
buf = +''
|
|
188
|
+
items.each_with_index do |item, i|
|
|
189
|
+
fragment = (i.zero? ? '[' : ',') + JSON.generate(item)
|
|
190
|
+
break if buf.bytesize + fragment.bytesize + 1 > max_bytes # +1 for closing ']'
|
|
191
|
+
buf << fragment
|
|
192
|
+
end
|
|
193
|
+
buf.empty? ? [] : JSON.parse("#{buf}]")
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|