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.
- checksums.yaml +4 -4
- data/README.md +69 -3
- data/THIRD_PARTY_LICENSES.html +168 -1381
- data/exe/rdx +136 -34
- data/ext/rubydex/declaration.c +1 -1
- data/ext/rubydex/definition.c +32 -4
- data/ext/rubydex/extconf.rb +0 -8
- data/ext/rubydex/graph.c +29 -18
- data/ext/rubydex/query.c +105 -0
- data/ext/rubydex/query.h +8 -0
- data/ext/rubydex/reference.c +60 -0
- data/ext/rubydex/rubydex.c +2 -0
- data/ext/rubydex/utils.c +12 -0
- data/ext/rubydex/utils.h +5 -0
- data/lib/rubydex/mcp_server/protocol.rb +156 -0
- data/lib/rubydex/mcp_server/tools/base_tool.rb +109 -0
- data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +30 -0
- data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +46 -0
- data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +68 -0
- data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +46 -0
- data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +55 -0
- data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +55 -0
- data/lib/rubydex/mcp_server.rb +219 -0
- data/lib/rubydex/version.rb +1 -1
- data/rbi/rubydex.rbi +26 -4
- data/rust/Cargo.lock +5 -552
- data/rust/Cargo.toml +0 -1
- data/rust/rubydex/Cargo.toml +1 -0
- data/rust/rubydex/benches/graph_memory.rs +22 -4
- data/rust/rubydex/src/compile_assertions.rs +15 -0
- data/rust/rubydex/src/config.rs +54 -34
- data/rust/rubydex/src/diagnostic.rs +1 -1
- data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
- data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
- data/rust/rubydex/src/listing.rs +159 -102
- data/rust/rubydex/src/main.rs +3 -125
- data/rust/rubydex/src/model/declaration.rs +0 -11
- data/rust/rubydex/src/model/definitions.rs +27 -26
- data/rust/rubydex/src/model/document.rs +43 -7
- data/rust/rubydex/src/model/graph.rs +47 -35
- data/rust/rubydex/src/model/id.rs +55 -0
- data/rust/rubydex/src/model/ids.rs +21 -9
- data/rust/rubydex/src/model/name.rs +35 -7
- data/rust/rubydex/src/model/references.rs +16 -13
- data/rust/rubydex/src/operation/applier.rs +0 -2
- data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
- data/rust/rubydex/src/query/cypher/schema.rs +790 -0
- data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
- data/rust/rubydex/src/query/cypher/tests.rs +228 -0
- data/rust/rubydex/src/query/cypher.rs +57 -0
- data/rust/rubydex/src/query.rs +2 -0
- data/rust/rubydex/src/resolution.rs +248 -227
- data/rust/rubydex/src/resolution_tests.rs +263 -65
- data/rust/rubydex-sys/src/declaration_api.rs +6 -3
- data/rust/rubydex-sys/src/definition_api.rs +27 -7
- data/rust/rubydex-sys/src/graph_api.rs +175 -27
- data/rust/rubydex-sys/src/reference_api.rs +58 -12
- metadata +17 -10
- data/exe/rubydex_mcp +0 -17
- data/lib/rubydex/bin/rubydex_mcp.exe +0 -0
- data/rust/rubydex-mcp/Cargo.toml +0 -34
- data/rust/rubydex-mcp/src/main.rs +0 -48
- data/rust/rubydex-mcp/src/server.rs +0 -1148
- data/rust/rubydex-mcp/src/tools.rs +0 -49
- 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
|
data/lib/rubydex/version.rb
CHANGED
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
|
|
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(
|
|
422
|
-
def
|
|
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
|
|
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
|