Class: RailsAiBridge::Tools::SearchCode

Inherits:
BaseTool
  • Object
show all
Defined in:
lib/rails_ai_bridge/tools/search_code.rb,
lib/rails_ai_bridge/tools/search_code/formatter.rb,
lib/rails_ai_bridge/tools/search_code/validator.rb,
lib/rails_ai_bridge/tools/search_code/ruby_search.rb,
lib/rails_ai_bridge/tools/search_code/ripgrep_search.rb

Overview

MCP tool searching the app tree with ripgrep (+rg+) or a Ruby fallback.

Pattern size is capped by Configuration#search_code_pattern_max_bytes (default 2048). Wall-clock limits use Configuration#search_code_timeout_seconds (+0+ disables).

Defined Under Namespace

Classes: Formatter, RipgrepSearch, RubySearch, Validator

Constant Summary collapse

MAX_RESULTS_CAP =

Hard upper bound for +max_results+ regardless of client input.

100
DEFAULT_ALLOWED_FILE_TYPES =

Default extensions when no +file_type+ is given, merged with Configuration#search_code_allowed_file_types.

%w[rb erb js ts jsx tsx yml yaml json].freeze

Class Method Summary collapse

Methods inherited from BaseTool

cached_context, cached_section, config, rails_app, reset_cache!, text_response

Class Method Details

.allowed_search_file_typesObject



52
53
54
55
56
57
58
# File 'lib/rails_ai_bridge/tools/search_code.rb', line 52

def self.allowed_search_file_types
  extras = RailsAiBridge.configuration.search_code_allowed_file_types.map do |x|
    x.to_s.downcase.strip.delete_prefix('.').gsub(/[^a-z0-9]/, '')
  end.reject(&:empty?)

  (DEFAULT_ALLOWED_FILE_TYPES + extras).uniq
end

.call(pattern:, path: nil, file_type: nil, max_results: 30) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/rails_ai_bridge/tools/search_code.rb', line 36

def self.call(pattern:, path: nil, file_type: nil, max_results: 30)
  root = Rails.root.to_s
  validator = Validator.new(pattern, file_type, root, path)
  validated = validator.validate
  return validated if validated.is_a?(MCP::Tool::Response)

  max_res = normalize_max_results(max_results)
  search_params = validated.merge(pattern: pattern, max_results: max_res, root: root)

  results = with_search_timeout do
    search_engine(search_params)
  end

  text_response(Formatter.new.call(results, pattern, path))
end

.normalize_max_results(max_results) ⇒ Object



60
61
62
63
# File 'lib/rails_ai_bridge/tools/search_code.rb', line 60

def self.normalize_max_results(max_results)
  normalized = [max_results.to_i, MAX_RESULTS_CAP].min
  normalized < 1 ? 30 : normalized
end

.ripgrep_available?Boolean

Returns:

  • (Boolean)


73
74
75
76
# File 'lib/rails_ai_bridge/tools/search_code.rb', line 73

def self.ripgrep_available?
  @ripgrep_available = system('which rg > /dev/null 2>&1') unless instance_variable_defined?(:@ripgrep_available)
  @ripgrep_available
end

.search_engine(search_params) ⇒ Object



65
66
67
68
69
70
71
# File 'lib/rails_ai_bridge/tools/search_code.rb', line 65

def self.search_engine(search_params)
  if ripgrep_available?
    RipgrepSearch.new(search_params).call
  else
    RubySearch.new(search_params).call
  end
end

.with_search_timeoutObject



78
79
80
81
82
83
84
85
# File 'lib/rails_ai_bridge/tools/search_code.rb', line 78

def self.with_search_timeout(&)
  sec = RailsAiBridge.configuration.search_code_timeout_seconds.to_f
  return yield if sec <= 0

  Timeout.timeout(sec, &)
rescue Timeout::Error
  [{ file: 'error', line_number: 0, content: "Search timed out after #{sec} seconds." }]
end