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_typesArray<String>

Returns the full set of allowed file type extensions for search filtering. Merges DEFAULT_ALLOWED_FILE_TYPES with configured extras.

Returns:

  • (Array<String>)

    lowercase extension strings without leading dots



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

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) ⇒ MCP::Tool::Response

Searches the Rails codebase for a pattern using ripgrep or a Ruby fallback.

Parameters:

  • pattern (String)

    search pattern (regex supported)

  • path (String, nil) (defaults to: nil)

    subdirectory to search in (defaults to app root)

  • file_type (String, nil) (defaults to: nil)

    filter by allowed file extension

  • max_results (Integer) (defaults to: 30)

    maximum number of results (capped at MAX_RESULTS_CAP)

Returns:

  • (MCP::Tool::Response)

    search results formatted as text, or an error response if validation fails



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/rails_ai_bridge/tools/search_code.rb', line 45

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) ⇒ Integer

Normalizes the max_results parameter, capping at MAX_RESULTS_CAP and defaulting to 30 for invalid values.

Parameters:

  • max_results (Integer, nil)

    requested result limit

Returns:

  • (Integer)

    normalized result limit (1..MAX_RESULTS_CAP)



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

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

.ripgrep_available?Boolean

Checks whether ripgrep (+rg+) is available on the system. Result is memoized for the process lifetime.

Returns:

  • (Boolean)

    true if ripgrep is installed and callable



99
100
101
102
103
104
105
# File 'lib/rails_ai_bridge/tools/search_code.rb', line 99

def self.ripgrep_available?
  return @ripgrep_available if instance_variable_defined?(:@ripgrep_available)

  @ripgrep_available = Open3.capture2('rg', '--version').last.success?
rescue Errno::ENOENT
  @ripgrep_available = false
end

.search_engine(search_params) ⇒ Array<Hash>

Selects and runs the appropriate search engine (ripgrep or Ruby fallback).

Parameters:

  • search_params (Hash)

    validated search parameters

Returns:

  • (Array<Hash>)

    search results with :file, :line_number, :content



87
88
89
90
91
92
93
# File 'lib/rails_ai_bridge/tools/search_code.rb', line 87

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

.with_search_timeout { ... } ⇒ Object

Executes the search block with an optional wall-clock timeout. When search_code_timeout_seconds is 0 or negative, no timeout is applied.

Yields:

  • block to execute with timeout protection

Returns:

  • (Object)

    the block's result, or a timeout error array on timeout



112
113
114
115
116
117
118
119
# File 'lib/rails_ai_bridge/tools/search_code.rb', line 112

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