Class: RailsMcpCodeSearch::Tools::SearchTool

Inherits:
BaseTool
  • Object
show all
Defined in:
lib/rails_mcp_code_search/tools/search_tool.rb

Class Method Summary collapse

Class Method Details

.call(query:, server_context:, limit: nil, file_pattern: nil) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/rails_mcp_code_search/tools/search_tool.rb', line 28

def self.call(query:, server_context:, limit: nil, file_pattern: nil)
  runtime = runtime_for(server_context:)
  limit = (limit || 10).clamp(1, 50)

  if Chunk.count == 0
    worker_state = runtime.worker.state
    if worker_state == :indexing
      return error_response(error: "indexing_in_progress", message: "Index is still building. Try again in a moment.", recoverable: true, suggested_action: "status")
    else
      return error_response(error: "index_empty", message: "No files indexed yet. Call reindex first.", recoverable: true, suggested_action: "reindex")
    end
  end

  # Smart reindex: enqueue changed files and wait briefly
  trigger_smart_reindex(runtime)

  # Generate query embedding
  query_vector = runtime.embedding_adapter.embed([ query ]).first

  # KNN search — over-fetch if filtering by file pattern
  fetch_limit = file_pattern ? limit * 5 : limit
  raw_results = Chunk.nearest_neighbors(:embedding, query_vector, distance: "cosine").first(fetch_limit)

  # Filter by file pattern
  filtered_out = 0
  if file_pattern
    before_count = raw_results.size
    raw_results = raw_results.select { File.fnmatch?(file_pattern, _1.file_path, File::FNM_PATHNAME) }
    filtered_out = before_count - raw_results.size
  end

  # Dedup overlapping results from same file
  results = dedup_overlapping(raw_results)
  results = results.first(limit)

  # Track metrics
  runtime.worker.enqueue_hit_counts(results.map(&:id))
  runtime.worker.increment_search_count

  stale = runtime.worker.state == :indexing

  text_response({
    results: results.map { format_result(_1) },
    metadata: {
      query:,
      limit:,
      count: results.size,
      has_more: raw_results.size > limit,
      index_state: runtime.worker.state.to_s,
      index_completeness: runtime.worker.state == :idle ? 1.0 : runtime.worker.progress,
      results_may_be_stale: stale,
      total_indexed_chunks: Chunk.count,
      filtered_out_count: filtered_out
    }
  })
rescue => e
  error_response(error: "search_error", message: e.message, recoverable: true)
end