Class: Aspera::Cli::McpTool

Inherits:
MCP::Tool
  • Object
show all
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

Class Method Summary collapse

Class Attribute Details

.extra_argsObject

Returns the value of attribute extra_args.



144
145
146
# File 'lib/aspera/cli/mcp_tool.rb', line 144

def extra_args
  @extra_args
end

.max_text_bytesObject

Returns the value of attribute max_text_bytes.



144
145
146
# File 'lib/aspera/cli/mcp_tool.rb', line 144

def max_text_bytes
  @max_text_bytes
end

Class Method Details

.call(args:, server_context: nil) ⇒ Object



146
147
148
149
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
# File 'lib/aspera/cli/mcp_tool.rb', line 146

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 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}: #{e.message}"}], error: true)
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.



186
187
188
189
190
191
192
193
194
# File 'lib/aspera/cli/mcp_tool.rb', line 186

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