rubydex 0.2.7 → 0.2.9
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 +3 -3
- data/THIRD_PARTY_LICENSES.html +132 -1378
- data/exe/rdx +28 -2
- data/ext/rubydex/extconf.rb +0 -8
- data/ext/rubydex/graph.c +43 -29
- 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 +8 -8
- data/rust/Cargo.lock +1 -555
- data/rust/Cargo.toml +0 -1
- data/rust/rubydex/src/config.rs +54 -34
- data/rust/rubydex/src/listing.rs +159 -102
- data/rust/rubydex/src/main.rs +148 -5
- data/rust/rubydex/src/model/declaration.rs +14 -14
- data/rust/rubydex/src/model/graph.rs +92 -9
- data/rust/rubydex/src/model/ids.rs +30 -0
- data/rust/rubydex/src/model/string_ref.rs +12 -4
- data/rust/rubydex/src/operation/applier.rs +0 -2
- data/rust/rubydex/src/query.rs +83 -15
- data/rust/rubydex/src/resolution.rs +196 -139
- data/rust/rubydex/tests/cli.rs +66 -0
- data/rust/rubydex-sys/src/declaration_api.rs +2 -2
- data/rust/rubydex-sys/src/graph_api.rs +71 -56
- metadata +11 -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
data/exe/rdx
CHANGED
|
@@ -7,7 +7,7 @@ require "optparse"
|
|
|
7
7
|
|
|
8
8
|
options = {}
|
|
9
9
|
|
|
10
|
-
OptionParser.new do |parser|
|
|
10
|
+
option_parser = OptionParser.new do |parser|
|
|
11
11
|
parser.on("--version", "Print the gem's version") do
|
|
12
12
|
require "rubydex/version"
|
|
13
13
|
puts "v#{Rubydex::VERSION}"
|
|
@@ -22,10 +22,36 @@ OptionParser.new do |parser|
|
|
|
22
22
|
parser.on("-i", "--interactive", "Open an interactive session with a populated graph for the current workspace") do
|
|
23
23
|
options[:interactive] = true
|
|
24
24
|
end
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
parser.on("--mcp", "Run the MCP server for AI assistants") do
|
|
27
|
+
options[:mcp] = true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
arguments = option_parser.parse(ARGV)
|
|
33
|
+
rescue OptionParser::ParseError => e
|
|
34
|
+
warn("error: #{e.message}")
|
|
35
|
+
warn
|
|
36
|
+
warn(option_parser)
|
|
37
|
+
exit(2)
|
|
38
|
+
end
|
|
26
39
|
|
|
27
40
|
require "rubydex"
|
|
28
41
|
|
|
42
|
+
if options[:mcp]
|
|
43
|
+
if arguments.length > 1
|
|
44
|
+
warn("error: unexpected argument '#{arguments[1]}' found")
|
|
45
|
+
warn
|
|
46
|
+
warn(option_parser)
|
|
47
|
+
exit(2)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
require "rubydex/mcp_server"
|
|
51
|
+
Rubydex::MCPServer.run(arguments.fetch(0, Dir.pwd))
|
|
52
|
+
exit
|
|
53
|
+
end
|
|
54
|
+
|
|
29
55
|
def __with_timer(message, &block)
|
|
30
56
|
print(message)
|
|
31
57
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
data/ext/rubydex/extconf.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "mkmf"
|
|
4
|
-
require "fileutils"
|
|
5
4
|
require "pathname"
|
|
6
5
|
|
|
7
6
|
unless system("cargo", "--version", out: File::NULL, err: File::NULL)
|
|
@@ -82,12 +81,6 @@ else
|
|
|
82
81
|
end
|
|
83
82
|
|
|
84
83
|
lib_dir = gem_dir.join("lib").join("rubydex")
|
|
85
|
-
mcp_bin_dir = lib_dir.join("bin")
|
|
86
|
-
FileUtils.mkdir_p(mcp_bin_dir)
|
|
87
|
-
|
|
88
|
-
mcp_executable = Gem.win_platform? ? "rubydex_mcp.exe" : "rubydex_mcp"
|
|
89
|
-
mcp_src = target_dir.join(mcp_executable)
|
|
90
|
-
mcp_dst = mcp_bin_dir.join(mcp_executable)
|
|
91
84
|
|
|
92
85
|
copy_dylib_commands = if Gem.win_platform?
|
|
93
86
|
""
|
|
@@ -114,7 +107,6 @@ new_makefile = makefile.gsub("$(OBJS): $(HDRS) $(ruby_headers)", <<~MAKEFILE.cho
|
|
|
114
107
|
.rust_built: $(RUST_SRCS)
|
|
115
108
|
\t#{cargo_command} || (echo "Compiling Rust failed" && exit 1)
|
|
116
109
|
\t$(COPY) #{bindings_path} #{__dir__}
|
|
117
|
-
\t$(COPY) #{mcp_src} #{mcp_dst}
|
|
118
110
|
\ttouch $@
|
|
119
111
|
|
|
120
112
|
compile_rust: .rust_built
|
data/ext/rubydex/graph.c
CHANGED
|
@@ -196,40 +196,54 @@ static VALUE rdxr_graph_yield_search_results(VALUE self, void *iter) {
|
|
|
196
196
|
|
|
197
197
|
/*
|
|
198
198
|
* call-seq:
|
|
199
|
-
* search(
|
|
199
|
+
* search(*queries) -> Enumerator[Rubydex::Declaration]
|
|
200
200
|
*
|
|
201
|
-
* Returns an enumerator that yields declarations
|
|
201
|
+
* Returns an enumerator that yields declarations whose name matches any of the queries exactly by substring.
|
|
202
202
|
*/
|
|
203
|
-
static VALUE rdxr_graph_search(VALUE
|
|
204
|
-
|
|
203
|
+
static VALUE rdxr_graph_search(int argc, VALUE *argv, VALUE self) {
|
|
204
|
+
rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
|
|
205
|
+
VALUE queries = rb_ary_new_from_values(argc, argv);
|
|
206
|
+
rdxi_check_array_of_strings(queries);
|
|
205
207
|
|
|
206
208
|
if (!rb_block_given_p()) {
|
|
207
|
-
return rb_enumeratorize(self, rb_str_new2("search"),
|
|
209
|
+
return rb_enumeratorize(self, rb_str_new2("search"), argc, argv);
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
void *graph;
|
|
211
213
|
TypedData_Get_Struct(self, void *, &graph_type, graph);
|
|
212
214
|
|
|
213
|
-
|
|
215
|
+
size_t length = (size_t)argc;
|
|
216
|
+
char **converted = rdxi_str_array_to_char(queries, length);
|
|
217
|
+
void *iter = rdx_graph_declarations_search(graph, (const char *const *)converted, length);
|
|
218
|
+
rdxi_free_str_array(converted, length);
|
|
219
|
+
|
|
220
|
+
return rdxr_graph_yield_search_results(self, iter);
|
|
214
221
|
}
|
|
215
222
|
|
|
216
223
|
/*
|
|
217
224
|
* call-seq:
|
|
218
|
-
* fuzzy_search(
|
|
225
|
+
* fuzzy_search(*queries) -> Enumerator[Rubydex::Declaration]
|
|
219
226
|
*
|
|
220
|
-
* Returns an enumerator that yields declarations
|
|
227
|
+
* Returns an enumerator that yields declarations whose name matches any of the queries fuzzily.
|
|
221
228
|
*/
|
|
222
|
-
static VALUE rdxr_graph_fuzzy_search(VALUE
|
|
223
|
-
|
|
229
|
+
static VALUE rdxr_graph_fuzzy_search(int argc, VALUE *argv, VALUE self) {
|
|
230
|
+
rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
|
|
231
|
+
VALUE queries = rb_ary_new_from_values(argc, argv);
|
|
232
|
+
rdxi_check_array_of_strings(queries);
|
|
224
233
|
|
|
225
234
|
if (!rb_block_given_p()) {
|
|
226
|
-
return rb_enumeratorize(self, rb_str_new2("fuzzy_search"),
|
|
235
|
+
return rb_enumeratorize(self, rb_str_new2("fuzzy_search"), argc, argv);
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
void *graph;
|
|
230
239
|
TypedData_Get_Struct(self, void *, &graph_type, graph);
|
|
231
240
|
|
|
232
|
-
|
|
241
|
+
size_t length = (size_t)argc;
|
|
242
|
+
char **converted = rdxi_str_array_to_char(queries, length);
|
|
243
|
+
void *iter = rdx_graph_declarations_fuzzy_search(graph, (const char *const *)converted, length);
|
|
244
|
+
rdxi_free_str_array(converted, length);
|
|
245
|
+
|
|
246
|
+
return rdxr_graph_yield_search_results(self, iter);
|
|
233
247
|
}
|
|
234
248
|
|
|
235
249
|
// Body function for rb_ensure in Graph#documents
|
|
@@ -782,38 +796,38 @@ static VALUE rdxr_graph_complete_method_argument(int argc, VALUE *argv, VALUE se
|
|
|
782
796
|
|
|
783
797
|
/*
|
|
784
798
|
* call-seq:
|
|
785
|
-
*
|
|
799
|
+
* exclude_patterns(patterns) -> nil
|
|
786
800
|
*
|
|
787
|
-
* Excludes the
|
|
801
|
+
* Excludes files matching the given glob patterns from discovery during indexing.
|
|
788
802
|
*/
|
|
789
|
-
static VALUE
|
|
790
|
-
Check_Type(
|
|
791
|
-
rdxi_check_array_of_strings(
|
|
803
|
+
static VALUE rdxr_graph_exclude_patterns(VALUE self, VALUE patterns) {
|
|
804
|
+
Check_Type(patterns, T_ARRAY);
|
|
805
|
+
rdxi_check_array_of_strings(patterns);
|
|
792
806
|
|
|
793
|
-
size_t length = RARRAY_LEN(
|
|
794
|
-
char **
|
|
807
|
+
size_t length = RARRAY_LEN(patterns);
|
|
808
|
+
char **converted_patterns = rdxi_str_array_to_char(patterns, length);
|
|
795
809
|
|
|
796
810
|
void *graph;
|
|
797
811
|
TypedData_Get_Struct(self, void*, &graph_type, graph);
|
|
798
812
|
|
|
799
|
-
|
|
800
|
-
rdxi_free_str_array(
|
|
813
|
+
rdx_graph_exclude_patterns(graph, (const char **)converted_patterns, length);
|
|
814
|
+
rdxi_free_str_array(converted_patterns, length);
|
|
801
815
|
|
|
802
816
|
return Qnil;
|
|
803
817
|
}
|
|
804
818
|
|
|
805
819
|
/*
|
|
806
820
|
* call-seq:
|
|
807
|
-
*
|
|
821
|
+
* excluded_patterns -> Array[String]
|
|
808
822
|
*
|
|
809
|
-
* Returns the
|
|
823
|
+
* Returns the glob patterns currently excluded from file discovery.
|
|
810
824
|
*/
|
|
811
|
-
static VALUE
|
|
825
|
+
static VALUE rdxr_graph_excluded_patterns(VALUE self) {
|
|
812
826
|
void *graph;
|
|
813
827
|
TypedData_Get_Struct(self, void*, &graph_type, graph);
|
|
814
828
|
|
|
815
829
|
size_t out_count = 0;
|
|
816
|
-
const char *const *results =
|
|
830
|
+
const char *const *results = rdx_graph_excluded_patterns(graph, &out_count);
|
|
817
831
|
|
|
818
832
|
if (results == NULL) {
|
|
819
833
|
return rb_ary_new();
|
|
@@ -942,8 +956,8 @@ void rdxi_initialize_graph(VALUE moduleRubydex) {
|
|
|
942
956
|
rb_define_method(cGraph, "diagnostics", rdxr_graph_diagnostics, 0);
|
|
943
957
|
rb_define_method(cGraph, "check_integrity", rdxr_graph_check_integrity, 0);
|
|
944
958
|
rb_define_method(cGraph, "[]", rdxr_graph_aref, 1);
|
|
945
|
-
rb_define_method(cGraph, "search", rdxr_graph_search, 1);
|
|
946
|
-
rb_define_method(cGraph, "fuzzy_search", rdxr_graph_fuzzy_search, 1);
|
|
959
|
+
rb_define_method(cGraph, "search", rdxr_graph_search, -1);
|
|
960
|
+
rb_define_method(cGraph, "fuzzy_search", rdxr_graph_fuzzy_search, -1);
|
|
947
961
|
rb_define_method(cGraph, "encoding=", rdxr_graph_set_encoding, 1);
|
|
948
962
|
rb_define_method(cGraph, "resolve_require_path", rdxr_graph_resolve_require_path, 2);
|
|
949
963
|
rb_define_method(cGraph, "require_paths", rdxr_graph_require_paths, 1);
|
|
@@ -951,8 +965,8 @@ void rdxi_initialize_graph(VALUE moduleRubydex) {
|
|
|
951
965
|
rb_define_method(cGraph, "complete_namespace_access", rdxr_graph_complete_namespace_access, -1);
|
|
952
966
|
rb_define_method(cGraph, "complete_method_call", rdxr_graph_complete_method_call, -1);
|
|
953
967
|
rb_define_method(cGraph, "complete_method_argument", rdxr_graph_complete_method_argument, -1);
|
|
954
|
-
rb_define_method(cGraph, "
|
|
955
|
-
rb_define_method(cGraph, "
|
|
968
|
+
rb_define_method(cGraph, "exclude_patterns", rdxr_graph_exclude_patterns, 1);
|
|
969
|
+
rb_define_method(cGraph, "excluded_patterns", rdxr_graph_excluded_patterns, 0);
|
|
956
970
|
rb_define_method(cGraph, "workspace_path", rdxr_graph_workspace_path, 0);
|
|
957
971
|
rb_define_method(cGraph, "workspace_path=", rdxr_graph_set_workspace_path, 1);
|
|
958
972
|
rb_define_method(cGraph, "load_config", rdxr_graph_load_config, -1);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rubydex
|
|
6
|
+
module MCPServer
|
|
7
|
+
module JSONRPC
|
|
8
|
+
PARSE_ERROR = -32_700
|
|
9
|
+
INVALID_REQUEST = -32_600
|
|
10
|
+
METHOD_NOT_FOUND = -32_601
|
|
11
|
+
INVALID_PARAMS = -32_602
|
|
12
|
+
INTERNAL_ERROR = -32_603
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
#: (untyped, Integer, String, ?data: untyped) -> Hash
|
|
16
|
+
def error_response(id, code, message, data: nil)
|
|
17
|
+
{
|
|
18
|
+
jsonrpc: "2.0",
|
|
19
|
+
id: id,
|
|
20
|
+
error: {
|
|
21
|
+
code: code,
|
|
22
|
+
message: message,
|
|
23
|
+
data: data,
|
|
24
|
+
}.compact,
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Error
|
|
31
|
+
#: (String, ?String, ?String) -> void
|
|
32
|
+
def initialize(error, message = nil, suggestion = nil)
|
|
33
|
+
@error = error
|
|
34
|
+
@message = message
|
|
35
|
+
@suggestion = suggestion
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#: (*untyped) -> String
|
|
39
|
+
def to_json(*args)
|
|
40
|
+
payload = { error: @error }
|
|
41
|
+
payload[:message] = @message if @message
|
|
42
|
+
payload[:suggestion] = @suggestion if @suggestion
|
|
43
|
+
payload.to_json(*args)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Tool
|
|
48
|
+
@tools = []
|
|
49
|
+
|
|
50
|
+
class Response
|
|
51
|
+
#: (Array[Hash], ?bool) -> void
|
|
52
|
+
def initialize(content, error: false)
|
|
53
|
+
@content = content
|
|
54
|
+
@error = error
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
attr_reader :content
|
|
58
|
+
|
|
59
|
+
#: -> bool
|
|
60
|
+
def error?
|
|
61
|
+
@error
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#: -> Hash
|
|
65
|
+
def to_h
|
|
66
|
+
{
|
|
67
|
+
content: content,
|
|
68
|
+
isError: error?,
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class << self
|
|
74
|
+
attr_reader :tools
|
|
75
|
+
|
|
76
|
+
#: (Class) -> void
|
|
77
|
+
def inherited(tool)
|
|
78
|
+
Tool.tools << tool unless tool.name&.end_with?("::BaseTool")
|
|
79
|
+
super
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
#: -> Hash[String, Class]
|
|
83
|
+
def tools_by_name
|
|
84
|
+
tools.to_h { |tool| [tool.tool_name, tool] }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
#: (?String) -> String
|
|
88
|
+
def tool_name(value = nil)
|
|
89
|
+
@tool_name = value if value
|
|
90
|
+
@tool_name || raise(NotImplementedError, "#{name} must define tool_name")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
#: (?String) -> String
|
|
94
|
+
def description(value = nil)
|
|
95
|
+
@description = value if value
|
|
96
|
+
@description || raise(NotImplementedError, "#{name} must define description")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
#: (?Hash, ?Array[String]) -> Hash
|
|
100
|
+
def input_schema(properties: nil, required: nil)
|
|
101
|
+
if properties
|
|
102
|
+
@input_schema = {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: properties,
|
|
105
|
+
}
|
|
106
|
+
@input_schema[:required] = required if required
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@input_schema || { type: "object", properties: {} }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
#: -> Hash
|
|
113
|
+
def to_h
|
|
114
|
+
{
|
|
115
|
+
name: tool_name,
|
|
116
|
+
description: description,
|
|
117
|
+
inputSchema: input_schema,
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class StdioTransport
|
|
124
|
+
#: -> void
|
|
125
|
+
def initialize
|
|
126
|
+
@input = $stdin
|
|
127
|
+
@output = $stdout
|
|
128
|
+
@input.binmode
|
|
129
|
+
@input.sync = true
|
|
130
|
+
@output.binmode
|
|
131
|
+
@output.sync = true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
#: { (Hash | Array?, Hash?) -> void } -> void
|
|
135
|
+
def open
|
|
136
|
+
@input.each_line do |line|
|
|
137
|
+
yield JSON.parse(line, symbolize_names: true)
|
|
138
|
+
rescue JSON::ParserError
|
|
139
|
+
yield nil, JSONRPC.error_response(nil, JSONRPC::PARSE_ERROR, "Parse error", data: "Invalid JSON")
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
#: (Hash | Array[Hash]?) -> void
|
|
144
|
+
def write(response)
|
|
145
|
+
return unless response
|
|
146
|
+
|
|
147
|
+
@output.puts(JSON.generate(response))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
#: -> void
|
|
151
|
+
def close
|
|
152
|
+
@output.flush
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Rubydex
|
|
8
|
+
module MCPServer
|
|
9
|
+
class BaseTool < Tool
|
|
10
|
+
DEFAULT_LIMIT = 50
|
|
11
|
+
|
|
12
|
+
#: (Graph) -> void
|
|
13
|
+
def initialize(graph)
|
|
14
|
+
super()
|
|
15
|
+
@graph = graph
|
|
16
|
+
@root_path = graph.workspace_path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
#: (Hash | Error) -> Tool::Response
|
|
20
|
+
def response(payload)
|
|
21
|
+
Tool::Response.new([{ type: "text", text: JSON.generate(payload) }])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
#: (Declaration) -> String
|
|
25
|
+
def declaration_kind(declaration)
|
|
26
|
+
return "<TODO>" if declaration.is_a?(Rubydex::Todo)
|
|
27
|
+
|
|
28
|
+
declaration.class.name.delete_prefix("Rubydex::")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#: (String) -> String
|
|
32
|
+
def format_path(uri)
|
|
33
|
+
path = file_path_for_uri(uri)
|
|
34
|
+
return uri unless path
|
|
35
|
+
|
|
36
|
+
absolute_path = File.expand_path(path)
|
|
37
|
+
absolute_root = File.expand_path(@root_path)
|
|
38
|
+
relative_path = Pathname.new(absolute_path).relative_path_from(Pathname.new(absolute_root)).to_s
|
|
39
|
+
|
|
40
|
+
relative_path.start_with?("..") ? absolute_path : relative_path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
#: (String) -> String?
|
|
44
|
+
def file_path_for_uri(uri)
|
|
45
|
+
parsed = URI.parse(uri)
|
|
46
|
+
return unless parsed.scheme == "file"
|
|
47
|
+
|
|
48
|
+
path = URI.decode_uri_component(parsed.path)
|
|
49
|
+
path.delete_prefix!("/") if Gem.win_platform?
|
|
50
|
+
path
|
|
51
|
+
rescue URI::InvalidURIError, ArgumentError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#: (String) -> Document?
|
|
56
|
+
def document_for_path(file_path)
|
|
57
|
+
absolute_target = if Pathname.new(file_path).absolute?
|
|
58
|
+
file_path
|
|
59
|
+
else
|
|
60
|
+
File.join(@root_path, file_path)
|
|
61
|
+
end
|
|
62
|
+
canonical_target = File.realpath(absolute_target)
|
|
63
|
+
@graph.documents.find do |document|
|
|
64
|
+
path = file_path_for_uri(document.uri)
|
|
65
|
+
path && File.expand_path(path) == canonical_target
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
#: (Location) -> Hash
|
|
70
|
+
def display_location(location)
|
|
71
|
+
display = location.to_display
|
|
72
|
+
{
|
|
73
|
+
path: format_path(display.uri),
|
|
74
|
+
line: display.start_line,
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
#: (Enumerable, Integer?, Integer?, Integer) -> [Array, Integer]
|
|
79
|
+
def paginate(items, offset, limit, max_limit)
|
|
80
|
+
offset = (offset || 0).to_i
|
|
81
|
+
offset = 0 unless offset.positive?
|
|
82
|
+
limit = (limit || DEFAULT_LIMIT).to_i
|
|
83
|
+
limit = DEFAULT_LIMIT unless limit.positive?
|
|
84
|
+
limit = [limit, max_limit].min
|
|
85
|
+
|
|
86
|
+
page = []
|
|
87
|
+
index = 0
|
|
88
|
+
items.each do |item|
|
|
89
|
+
page << item if index >= offset && page.length < limit
|
|
90
|
+
index += 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
[page, index]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
#: (String) -> Declaration | Error
|
|
97
|
+
def lookup_declaration(name)
|
|
98
|
+
declaration = @graph[name]
|
|
99
|
+
return declaration if declaration
|
|
100
|
+
|
|
101
|
+
Error.new(
|
|
102
|
+
"not_found",
|
|
103
|
+
"Declaration '#{name}' not found",
|
|
104
|
+
"Try search_declarations with a partial name to find the correct FQN",
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubydex
|
|
4
|
+
module MCPServer
|
|
5
|
+
class CodebaseStatsTool < BaseTool
|
|
6
|
+
tool_name "codebase_stats"
|
|
7
|
+
description "Get an overview of the indexed Ruby codebase: total file count, declaration counts, and breakdown by kind (classes, modules, methods, constants). Use this to understand codebase size and composition, or to verify that indexing completed successfully."
|
|
8
|
+
input_schema(properties: {})
|
|
9
|
+
|
|
10
|
+
#: -> Tool::Response
|
|
11
|
+
def call
|
|
12
|
+
declaration_count = 0
|
|
13
|
+
breakdown = Hash.new(0)
|
|
14
|
+
@graph.declarations.each do |declaration|
|
|
15
|
+
declaration_count += 1
|
|
16
|
+
breakdown[declaration_kind(declaration)] += 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
response(
|
|
20
|
+
files: @graph.documents.count,
|
|
21
|
+
declarations: declaration_count,
|
|
22
|
+
definitions: @graph.documents.sum { |document| document.definitions.count },
|
|
23
|
+
constant_references: @graph.constant_references.count,
|
|
24
|
+
method_references: @graph.method_references.count,
|
|
25
|
+
breakdown_by_kind: breakdown,
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubydex
|
|
4
|
+
module MCPServer
|
|
5
|
+
class FindConstantReferencesTool < BaseTool
|
|
6
|
+
tool_name "find_constant_references"
|
|
7
|
+
description "Find all resolved references to a Ruby class, module, or constant across the codebase. Returns file paths, line numbers, and columns for each usage. Results are paginated: the response includes `total`. If `total` exceeds the number of returned results, use `offset` to fetch subsequent pages."
|
|
8
|
+
input_schema(
|
|
9
|
+
properties: {
|
|
10
|
+
name: { type: "string", description: "Fully qualified name of the class, module, or constant to find references for" },
|
|
11
|
+
limit: { type: "integer", description: "Maximum number of references to return (default 50, max 200)" },
|
|
12
|
+
offset: { type: "integer", description: "Number of references to skip for pagination (default 0)" },
|
|
13
|
+
},
|
|
14
|
+
required: ["name"],
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
#: (name: String, ?limit: Integer, ?offset: Integer) -> Tool::Response
|
|
18
|
+
def call(name:, limit: nil, offset: nil)
|
|
19
|
+
declaration = lookup_declaration(name)
|
|
20
|
+
|
|
21
|
+
case declaration
|
|
22
|
+
when Error
|
|
23
|
+
response(declaration)
|
|
24
|
+
else
|
|
25
|
+
references = case declaration
|
|
26
|
+
when Rubydex::Namespace, Rubydex::Constant, Rubydex::ConstantAlias
|
|
27
|
+
declaration.references
|
|
28
|
+
else
|
|
29
|
+
[]
|
|
30
|
+
end
|
|
31
|
+
page, total = paginate(references, offset, limit, 200)
|
|
32
|
+
payload = page.map do |reference|
|
|
33
|
+
display = reference.location.to_display
|
|
34
|
+
{
|
|
35
|
+
path: format_path(display.uri),
|
|
36
|
+
line: display.start_line,
|
|
37
|
+
column: display.start_column,
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
response(name: name, references: payload, total: total)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubydex
|
|
4
|
+
module MCPServer
|
|
5
|
+
class GetDeclarationTool < BaseTool
|
|
6
|
+
tool_name "get_declaration"
|
|
7
|
+
description 'Get complete information about a Ruby class, module, method, or constant by its exact fully qualified name. Returns file locations, documentation comments, ancestor chain, and members with locations. FQN format: "Foo::Bar" for classes/modules/constants, "Foo::Bar#method_name" for instance methods, "Foo::Bar::<Bar>" for singleton classes, and "Foo::Bar::<Bar>#method_name" for class methods.'
|
|
8
|
+
input_schema(
|
|
9
|
+
properties: {
|
|
10
|
+
name: { type: "string", description: "Fully qualified name of the declaration (e.g. 'Foo::Bar', 'Foo::Bar#baz')" },
|
|
11
|
+
},
|
|
12
|
+
required: ["name"],
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
#: (name: String) -> Tool::Response
|
|
16
|
+
def call(name:)
|
|
17
|
+
declaration = lookup_declaration(name)
|
|
18
|
+
|
|
19
|
+
case declaration
|
|
20
|
+
when Error
|
|
21
|
+
response(declaration)
|
|
22
|
+
else
|
|
23
|
+
definitions = declaration.definitions.map do |definition|
|
|
24
|
+
display_location(definition.location).merge(
|
|
25
|
+
comments: definition.comments.map do |comment|
|
|
26
|
+
comment.string.delete_prefix("# ")
|
|
27
|
+
end,
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
ancestors = if declaration.is_a?(Rubydex::Namespace)
|
|
32
|
+
declaration.ancestors.map do |ancestor|
|
|
33
|
+
{
|
|
34
|
+
name: ancestor.name,
|
|
35
|
+
kind: declaration_kind(ancestor),
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
[]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
members = if declaration.is_a?(Rubydex::Namespace)
|
|
43
|
+
declaration.members.map do |member|
|
|
44
|
+
payload = {
|
|
45
|
+
name: member.name,
|
|
46
|
+
kind: declaration_kind(member),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
definition = member.definitions.first
|
|
50
|
+
payload[:location] = display_location(definition.location) if definition
|
|
51
|
+
payload
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
[]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
response(
|
|
58
|
+
name: declaration.name,
|
|
59
|
+
kind: declaration_kind(declaration),
|
|
60
|
+
definitions: definitions,
|
|
61
|
+
ancestors: ancestors,
|
|
62
|
+
members: members,
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubydex
|
|
4
|
+
module MCPServer
|
|
5
|
+
class GetDescendantsTool < BaseTool
|
|
6
|
+
tool_name "get_descendants"
|
|
7
|
+
description "Returns all known descendants for the given namespace including itself and all transitive descendants. Can be used to understand how a module/class is used across the codebase. Results are paginated: the response includes `total`. If `total` exceeds the number of returned results, use `offset` to fetch subsequent pages."
|
|
8
|
+
input_schema(
|
|
9
|
+
properties: {
|
|
10
|
+
name: { type: "string", description: "Fully qualified name of the class or module" },
|
|
11
|
+
limit: { type: "integer", description: "Maximum number of descendants to return (default 50, max 500)" },
|
|
12
|
+
offset: { type: "integer", description: "Number of descendants to skip for pagination (default 0)" },
|
|
13
|
+
},
|
|
14
|
+
required: ["name"],
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
#: (name: String, ?limit: Integer, ?offset: Integer) -> Tool::Response
|
|
18
|
+
def call(name:, limit: nil, offset: nil)
|
|
19
|
+
declaration = lookup_declaration(name)
|
|
20
|
+
|
|
21
|
+
case declaration
|
|
22
|
+
when Error
|
|
23
|
+
response(declaration)
|
|
24
|
+
when Rubydex::Namespace
|
|
25
|
+
page, total = paginate(declaration.descendants, offset, limit, 500)
|
|
26
|
+
descendants = page.map do |descendant|
|
|
27
|
+
{
|
|
28
|
+
name: descendant.name,
|
|
29
|
+
kind: declaration_kind(descendant),
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
response(name: declaration.name, descendants: descendants, total: total)
|
|
34
|
+
else
|
|
35
|
+
response(
|
|
36
|
+
Error.new(
|
|
37
|
+
"invalid_kind",
|
|
38
|
+
"'#{name}' is not a class or module (it is a #{declaration_kind(declaration)})",
|
|
39
|
+
"get_descendants only works on classes and modules, not methods or constants",
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|