Class: Aspera::Cli::McpTool
- Inherits:
-
MCP::Tool
- Object
- MCP::Tool
- Aspera::Cli::McpTool
- Defined in:
- lib/aspera/cli/mcp_tool.rb
Overview
MCP Tool: executes an ascli command in-process.
Constant Summary collapse
- DEFAULT_MAX_TEXT_BYTES =
Default maximum byte size of the JSON text content returned for list results. Items are appended whole until the limit is reached; the full list is always available in structuredContent.
100_000- DEFAULT_EXTRA_ARGS =
Default extra arguments automatically prepended to every ascli call. Keeps the AI from having to remember mandatory flags on every invocation.
['--interactive=no', '--transfer.asynchronous=true'].freeze
Class Attribute Summary collapse
-
.extra_args ⇒ Object
Returns the value of attribute extra_args.
-
.max_text_bytes ⇒ Object
Returns the value of attribute max_text_bytes.
Class Method Summary collapse
- .call(args:, server_context: nil) ⇒ Object
-
.dedupe_lines(msg) ⇒ Object
Collapse consecutive duplicate lines in a multi-line message.
-
.schema_to_rows(reader) ⇒ Object
Delegate to Schema::Reader#to_rows — semantic fields, no ANSI, MCP/JSON-ready.
-
.truncate_items_by_bytes(items, max_bytes) ⇒ Object
Returns the largest prefix of
itemswhose JSON serialization fits withinmax_bytes.
Class Attribute Details
.extra_args ⇒ Object
Returns the value of attribute extra_args.
148 149 150 |
# File 'lib/aspera/cli/mcp_tool.rb', line 148 def extra_args @extra_args end |
.max_text_bytes ⇒ Object
Returns the value of attribute max_text_bytes.
148 149 150 |
# File 'lib/aspera/cli/mcp_tool.rb', line 148 def max_text_bytes @max_text_bytes end |
Class Method Details
.call(args:, server_context: nil) ⇒ Object
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/aspera/cli/mcp_tool.rb', line 150 def call(args:, server_context: nil) effective_args = Array(extra_args || DEFAULT_EXTRA_ARGS) + args Log.dump(:mcp_execute, effective_args) runner = Runner.new(effective_args) result = runner.run_with_result case result when Result::Nothing, Result::Empty, NilClass MCP::Tool::Response.new([{type: 'text', text: ''}]) when Result::SingleObject, Result::ObjectList, Result::ValueList # Apply --select filter in place (affects both text and structuredContent). runner.context.formatter.filter_columns_on_select(result.data) if result.data.is_a?(Array) # MCP spec requires structuredContent to be a JSON object (not an array). structured = result.data.is_a?(Array) ? {items: result.data} : result.data content = if result.data.is_a?(Array) text_limit = max_text_bytes || DEFAULT_MAX_TEXT_BYTES truncated_items = truncate_items_by_bytes(result.data, text_limit) total = result.data.size if truncated_items.size < total [ {type: 'text', text: JSON.generate(truncated_items)}, {type: 'text', text: "WARNING: result truncated to #{truncated_items.size} of #{total} items. Full dataset available in structuredContent."} ] else [{type: 'text', text: JSON.generate(result.data)}] end else [{type: 'text', text: JSON.generate(result.data)}] end MCP::Tool::Response.new(content, structured_content: structured) else MCP::Tool::Response.new([{type: 'text', text: result.data.to_s}]) end rescue Cli::SchemaRequest => e schema_path = e.path if schema_path.nil? MCP::Tool::Response.new([{type: 'text', text: "#{e.class}: #{e.} (no schema available)"}], error: true) else rows = schema_to_rows(Schema::Registry.instance.reader(schema_path)) structured = {items: rows} MCP::Tool::Response.new([{type: 'text', text: JSON.generate(rows)}], structured_content: structured) end rescue SystemExit => e MCP::Tool::Response.new([{type: 'text', text: "exited with status #{e.status}"}], error: !e.status.zero?) rescue => e MCP::Tool::Response.new([{type: 'text', text: "#{e.class}: #{dedupe_lines(e.)}"}], error: true) end |
.dedupe_lines(msg) ⇒ Object
Collapse consecutive duplicate lines in a multi-line message. Each run of identical lines is replaced by one line + "(×N)" suffix when N > 1.
204 205 206 207 208 209 |
# File 'lib/aspera/cli/mcp_tool.rb', line 204 def dedupe_lines(msg) return msg unless msg.include?("\n") msg.split("\n").chunk_while { |a, b| a == b }.map do |group| group.size > 1 ? "#{group.first} (x#{group.size})" : group.first end.join("\n") end |
.schema_to_rows(reader) ⇒ Object
Delegate to Schema::Reader#to_rows — semantic fields, no ANSI, MCP/JSON-ready.
198 199 200 |
# File 'lib/aspera/cli/mcp_tool.rb', line 198 def schema_to_rows(reader) reader.to_rows end |
.truncate_items_by_bytes(items, max_bytes) ⇒ Object
Returns the largest prefix of items whose JSON serialization fits within max_bytes.
Items are appended whole — no item is ever split mid-JSON.
213 214 215 216 217 218 219 220 221 |
# File 'lib/aspera/cli/mcp_tool.rb', line 213 def truncate_items_by_bytes(items, max_bytes) buf = +'' items.each_with_index do |item, i| fragment = (i.zero? ? '[' : ',') + JSON.generate(item) break if buf.bytesize + fragment.bytesize + 1 > max_bytes # +1 for closing ']' buf << fragment end buf.empty? ? [] : JSON.parse("#{buf}]") end |