rubydex 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -3
  3. data/THIRD_PARTY_LICENSES.html +168 -1381
  4. data/exe/rdx +136 -34
  5. data/ext/rubydex/declaration.c +1 -1
  6. data/ext/rubydex/definition.c +32 -4
  7. data/ext/rubydex/extconf.rb +0 -8
  8. data/ext/rubydex/graph.c +29 -18
  9. data/ext/rubydex/query.c +105 -0
  10. data/ext/rubydex/query.h +8 -0
  11. data/ext/rubydex/reference.c +60 -0
  12. data/ext/rubydex/rubydex.c +2 -0
  13. data/ext/rubydex/utils.c +12 -0
  14. data/ext/rubydex/utils.h +5 -0
  15. data/lib/rubydex/mcp_server/protocol.rb +156 -0
  16. data/lib/rubydex/mcp_server/tools/base_tool.rb +109 -0
  17. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +30 -0
  18. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +46 -0
  19. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +68 -0
  20. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +46 -0
  21. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +55 -0
  22. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +55 -0
  23. data/lib/rubydex/mcp_server.rb +219 -0
  24. data/lib/rubydex/version.rb +1 -1
  25. data/rbi/rubydex.rbi +26 -4
  26. data/rust/Cargo.lock +5 -552
  27. data/rust/Cargo.toml +0 -1
  28. data/rust/rubydex/Cargo.toml +1 -0
  29. data/rust/rubydex/benches/graph_memory.rs +22 -4
  30. data/rust/rubydex/src/compile_assertions.rs +15 -0
  31. data/rust/rubydex/src/config.rs +54 -34
  32. data/rust/rubydex/src/diagnostic.rs +1 -1
  33. data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
  34. data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
  35. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
  36. data/rust/rubydex/src/listing.rs +159 -102
  37. data/rust/rubydex/src/main.rs +3 -125
  38. data/rust/rubydex/src/model/declaration.rs +0 -11
  39. data/rust/rubydex/src/model/definitions.rs +27 -26
  40. data/rust/rubydex/src/model/document.rs +43 -7
  41. data/rust/rubydex/src/model/graph.rs +47 -35
  42. data/rust/rubydex/src/model/id.rs +55 -0
  43. data/rust/rubydex/src/model/ids.rs +21 -9
  44. data/rust/rubydex/src/model/name.rs +35 -7
  45. data/rust/rubydex/src/model/references.rs +16 -13
  46. data/rust/rubydex/src/operation/applier.rs +0 -2
  47. data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
  48. data/rust/rubydex/src/query/cypher/schema.rs +790 -0
  49. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  50. data/rust/rubydex/src/query/cypher/tests.rs +228 -0
  51. data/rust/rubydex/src/query/cypher.rs +57 -0
  52. data/rust/rubydex/src/query.rs +2 -0
  53. data/rust/rubydex/src/resolution.rs +248 -227
  54. data/rust/rubydex/src/resolution_tests.rs +263 -65
  55. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  56. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  57. data/rust/rubydex-sys/src/graph_api.rs +175 -27
  58. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  59. metadata +17 -10
  60. data/exe/rubydex_mcp +0 -17
  61. data/lib/rubydex/bin/rubydex_mcp.exe +0 -0
  62. data/rust/rubydex-mcp/Cargo.toml +0 -34
  63. data/rust/rubydex-mcp/src/main.rs +0 -48
  64. data/rust/rubydex-mcp/src/server.rs +0 -1148
  65. data/rust/rubydex-mcp/src/tools.rs +0 -49
  66. data/rust/rubydex-mcp/tests/mcp.rs +0 -302
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require "rubydex"
6
+ require "rubydex/mcp_server/protocol"
7
+
8
+ Dir[File.join(__dir__, "mcp_server", "tools", "*_tool.rb")].sort.each { |file| require file }
9
+
10
+ module Rubydex
11
+ module MCPServer
12
+ SERVER_INSTRUCTIONS = <<~TEXT
13
+ Rubydex provides semantic Ruby code intelligence.
14
+
15
+ Use these tools for Ruby source files (.rb, .rbi, .rbs) when you need structural information about declarations, locations, hierarchy, references, or codebase composition.
16
+
17
+ Use text search instead for literal strings, comments, log messages, non-Ruby files, or content search rather than structural queries.
18
+
19
+ Fully qualified name format: "Foo::Bar" for classes/modules/constants, "Foo::Bar#method_name" for instance methods.
20
+
21
+ Pagination: tools that may return a high number of results include `total` for pagination. When `total` exceeds the number of returned items, use `offset` to fetch the next page.
22
+ TEXT
23
+
24
+ class Server
25
+ WORKER_COUNT = 4
26
+
27
+ #: (root_path: String, ?transport: StdioTransport) -> void
28
+ def initialize(root_path:, transport: nil)
29
+ @root_path = root_path
30
+ @transport = transport
31
+ @graph = Graph.new(workspace_path: @root_path)
32
+ @graph.load_config
33
+ @index_finished = false
34
+ @incoming_queue = Thread::Queue.new
35
+ @outgoing_queue = Thread::Queue.new
36
+ @workers = []
37
+ @outgoing_dispatcher = nil
38
+ end
39
+
40
+ attr_reader :root_path
41
+
42
+ #: -> Thread
43
+ def spawn_indexer
44
+ Thread.new do
45
+ @graph.index_workspace
46
+ @graph.resolve
47
+ @index_finished = true
48
+ end
49
+ end
50
+
51
+ #: -> void
52
+ def main_loop
53
+ @workers = Array.new(WORKER_COUNT) { new_worker }
54
+ @outgoing_dispatcher = Thread.new do
55
+ while (response = @outgoing_queue.pop)
56
+ @transport.write(response)
57
+ end
58
+ end
59
+
60
+ @transport.open do |request, parse_error|
61
+ if parse_error
62
+ send_message(parse_error)
63
+ else
64
+ @incoming_queue << request
65
+ end
66
+ end
67
+ ensure
68
+ run_shutdown
69
+ @transport.close
70
+ end
71
+
72
+ #: (Hash | Array | untyped) -> Hash | Array[Hash]?
73
+ def handle(request)
74
+ if request.is_a?(Array)
75
+ return JSONRPC.error_response(nil, JSONRPC::INVALID_REQUEST, "Invalid Request", data: "Request is an empty array") if request.empty?
76
+
77
+ responses = request.filter_map { |entry| handle(entry) }
78
+ return responses if responses.any?
79
+
80
+ return
81
+ end
82
+
83
+ unless request.is_a?(Hash)
84
+ return JSONRPC.error_response(nil, JSONRPC::INVALID_REQUEST, "Invalid Request", data: "Request must be a hash")
85
+ end
86
+
87
+ has_id = request.key?(:id)
88
+ id = request[:id]
89
+ method = request[:method]
90
+ params = request[:params]
91
+
92
+ unless request[:jsonrpc] == "2.0"
93
+ return JSONRPC.error_response(nil, JSONRPC::INVALID_REQUEST, "Invalid Request", data: "JSON-RPC version must be 2.0")
94
+ end
95
+
96
+ unless !has_id || id.is_a?(Integer) || (id.is_a?(String) && id.match?(/\A[a-zA-Z0-9_-]+\z/))
97
+ return JSONRPC.error_response(nil, JSONRPC::INVALID_REQUEST, "Invalid Request", data: "Request ID must be a string or integer")
98
+ end
99
+
100
+ unless method.is_a?(String) && !method.start_with?("rpc.")
101
+ return JSONRPC.error_response(nil, JSONRPC::INVALID_REQUEST, "Invalid Request", data: 'Method name must be a string and not start with "rpc."')
102
+ end
103
+
104
+ unless params.nil? || params.is_a?(Hash)
105
+ return JSONRPC.error_response(id, JSONRPC::INVALID_PARAMS, "Invalid params", data: "Method parameters must be an object or null")
106
+ end
107
+
108
+ result = case method
109
+ when "initialize"
110
+ {
111
+ protocolVersion: "2025-03-26",
112
+ capabilities: { tools: {} },
113
+ serverInfo: {
114
+ name: "rubydex_mcp",
115
+ version: Rubydex::VERSION,
116
+ },
117
+ instructions: SERVER_INSTRUCTIONS,
118
+ }
119
+ when "tools/list"
120
+ { tools: Tool.tools.map(&:to_h) }
121
+ when "tools/call"
122
+ call_tool(params || {})
123
+ when "ping"
124
+ {}
125
+ when "notifications/initialized"
126
+ return
127
+ else
128
+ return has_id ? JSONRPC.error_response(id, JSONRPC::METHOD_NOT_FOUND, "Method not found", data: method) : nil
129
+ end
130
+
131
+ has_id ? { jsonrpc: "2.0", id: id, result: result } : nil
132
+ rescue KeyError => e
133
+ has_id ? JSONRPC.error_response(id, JSONRPC::INVALID_PARAMS, "Invalid params", data: e.message) : nil
134
+ rescue StandardError => e
135
+ has_id ? JSONRPC.error_response(id, JSONRPC::INTERNAL_ERROR, "Internal error", data: e.message) : nil
136
+ end
137
+
138
+ #: -> Graph | Error
139
+ def graph_or_error
140
+ return @graph if @index_finished
141
+
142
+ Error.new(
143
+ "indexing",
144
+ "Rubydex is still indexing the codebase",
145
+ "The server is starting up. Please retry in a few seconds.",
146
+ )
147
+ end
148
+
149
+ private
150
+
151
+ #: -> void
152
+ def run_shutdown
153
+ @incoming_queue.close unless @incoming_queue.closed?
154
+ @workers.each(&:join)
155
+ @outgoing_queue.close unless @outgoing_queue.closed?
156
+ @outgoing_dispatcher&.join
157
+ end
158
+
159
+ #: -> Thread
160
+ def new_worker
161
+ Thread.new do
162
+ while (request = @incoming_queue.pop)
163
+ send_message(handle(request))
164
+ end
165
+ end
166
+ end
167
+
168
+ #: (Hash | Array[Hash]?) -> void
169
+ def send_message(response)
170
+ return unless response
171
+ return if @outgoing_queue.closed?
172
+
173
+ @outgoing_queue << response
174
+ end
175
+
176
+ #: (Hash) -> Hash
177
+ def call_tool(params)
178
+ tool_name = params.fetch(:name)
179
+ tool = Tool.tools_by_name.fetch(tool_name, nil)
180
+ raise KeyError, "Tool not found: #{tool_name}" unless tool
181
+
182
+ arguments = params[:arguments] || {}
183
+ known_arguments = tool.input_schema.fetch(:properties).keys.map(&:to_s)
184
+ unknown_arguments = arguments.keys.map(&:to_s) - known_arguments
185
+ unless unknown_arguments.empty?
186
+ return Tool::Response.new(
187
+ [{ type: "text", text: "Unknown arguments: #{unknown_arguments.join(", ")}" }],
188
+ error: true,
189
+ ).to_h
190
+ end
191
+
192
+ missing_arguments = Array(tool.input_schema[:required]) - arguments.keys.map(&:to_s)
193
+ unless missing_arguments.empty?
194
+ return Tool::Response.new(
195
+ [{ type: "text", text: "Missing required arguments: #{missing_arguments.join(", ")}" }],
196
+ error: true,
197
+ ).to_h
198
+ end
199
+
200
+ graph = graph_or_error
201
+ return Tool::Response.new([{ type: "text", text: JSON.generate(graph) }]).to_h if graph.is_a?(Error)
202
+
203
+ response = tool.new(graph).call(**arguments.transform_keys(&:to_sym))
204
+ response.to_h
205
+ end
206
+ end
207
+
208
+ class << self
209
+ #: (?String) -> void
210
+ def run(path = ".")
211
+ root = File.realpath(path)
212
+ server = Server.new(root_path: root, transport: StdioTransport.new)
213
+ server.spawn_indexer
214
+
215
+ server.main_loop
216
+ end
217
+ end
218
+ end
219
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubydex
4
- VERSION = "0.2.8"
4
+ VERSION = "0.3.0"
5
5
  end
data/rbi/rubydex.rbi CHANGED
@@ -20,6 +20,9 @@ class Rubydex::ConstantReference < Rubydex::Reference
20
20
  sig { returns(Rubydex::Location) }
21
21
  def location; end
22
22
 
23
+ sig { returns(Rubydex::Document) }
24
+ def document; end
25
+
23
26
  class << self
24
27
  private
25
28
 
@@ -158,6 +161,9 @@ class Rubydex::Definition
158
161
  sig { returns(T::Array[Rubydex::Definition]) }
159
162
  def lexical_nesting; end
160
163
 
164
+ sig { returns(Rubydex::Document) }
165
+ def document; end
166
+
161
167
  class << self
162
168
  private
163
169
 
@@ -278,6 +284,19 @@ end
278
284
 
279
285
  class Rubydex::IntegrityFailure < Rubydex::Failure; end
280
286
 
287
+ class Rubydex::Query
288
+ class << self
289
+ sig { params(query: String).returns(Rubydex::Query) }
290
+ def parse(query); end
291
+
292
+ sig { params(format: T.any(String, Symbol)).returns(String) }
293
+ def schema(format = :table); end
294
+ end
295
+
296
+ sig { params(graph: Rubydex::Graph, format: T.any(String, Symbol)).returns(String) }
297
+ def render(graph, format = :table); end
298
+ end
299
+
281
300
  class Rubydex::Graph
282
301
  sig { params(workspace_path: T.nilable(String)).void }
283
302
  def initialize(workspace_path: nil); end
@@ -313,7 +332,7 @@ class Rubydex::Graph
313
332
  sig { returns(T::Array[String]) }
314
333
  def index_workspace; end
315
334
 
316
- # Loads configuration, merging its excluded paths into the graph's configuration (the workspace path is never
335
+ # Loads configuration, merging its exclusion patterns into the graph's configuration (the workspace path is never
317
336
  # overridden). With `config_path` (resolved relative to the workspace path), an explicitly named file that does not
318
337
  # exist raises `Rubydex::ConfigError`. With no argument, the default `.rubydex` is loaded if present and ignored if
319
338
  # missing. Raises `Rubydex::ConfigError` if a file cannot be read or is malformed.
@@ -418,11 +437,11 @@ class Rubydex::Graph
418
437
  sig { params(paths: T::Array[String]).void }
419
438
  def add_workspace_dependency_paths(paths); end
420
439
 
421
- sig { params(paths: T::Array[String]).void }
422
- def exclude_paths(paths); end
440
+ sig { params(patterns: T::Array[String]).void }
441
+ def exclude_patterns(patterns); end
423
442
 
424
443
  sig { returns(T::Array[String]) }
425
- def excluded_paths; end
444
+ def excluded_patterns; end
426
445
  end
427
446
 
428
447
  class Rubydex::DisplayLocation < Rubydex::Location
@@ -502,6 +521,9 @@ class Rubydex::MethodReference < Rubydex::Reference
502
521
 
503
522
  sig { returns(T.nilable(Rubydex::Declaration)) }
504
523
  def receiver; end
524
+
525
+ sig { returns(Rubydex::Document) }
526
+ def document; end
505
527
  end
506
528
 
507
529
  class Rubydex::Reference