rubydex 0.2.7-aarch64-linux → 0.2.9-aarch64-linux

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/THIRD_PARTY_LICENSES.html +132 -1378
  4. data/exe/rdx +28 -2
  5. data/ext/rubydex/extconf.rb +0 -8
  6. data/ext/rubydex/graph.c +43 -29
  7. data/lib/rubydex/3.2/rubydex.so +0 -0
  8. data/lib/rubydex/3.3/rubydex.so +0 -0
  9. data/lib/rubydex/3.4/rubydex.so +0 -0
  10. data/lib/rubydex/4.0/rubydex.so +0 -0
  11. data/lib/rubydex/librubydex_sys.so +0 -0
  12. data/lib/rubydex/mcp_server/protocol.rb +156 -0
  13. data/lib/rubydex/mcp_server/tools/base_tool.rb +109 -0
  14. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +30 -0
  15. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +46 -0
  16. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +68 -0
  17. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +46 -0
  18. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +55 -0
  19. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +55 -0
  20. data/lib/rubydex/mcp_server.rb +219 -0
  21. data/lib/rubydex/version.rb +1 -1
  22. data/rbi/rubydex.rbi +8 -8
  23. data/rust/Cargo.lock +1 -555
  24. data/rust/Cargo.toml +0 -1
  25. data/rust/rubydex/src/config.rs +54 -34
  26. data/rust/rubydex/src/listing.rs +159 -102
  27. data/rust/rubydex/src/main.rs +148 -5
  28. data/rust/rubydex/src/model/declaration.rs +14 -14
  29. data/rust/rubydex/src/model/graph.rs +92 -9
  30. data/rust/rubydex/src/model/ids.rs +30 -0
  31. data/rust/rubydex/src/model/string_ref.rs +12 -4
  32. data/rust/rubydex/src/operation/applier.rs +0 -2
  33. data/rust/rubydex/src/query.rs +83 -15
  34. data/rust/rubydex/src/resolution.rs +196 -139
  35. data/rust/rubydex/tests/cli.rs +66 -0
  36. data/rust/rubydex-sys/src/declaration_api.rs +2 -2
  37. data/rust/rubydex-sys/src/graph_api.rs +71 -56
  38. metadata +11 -10
  39. data/exe/rubydex_mcp +0 -17
  40. data/lib/rubydex/bin/rubydex_mcp +0 -0
  41. data/rust/rubydex-mcp/Cargo.toml +0 -34
  42. data/rust/rubydex-mcp/src/main.rs +0 -48
  43. data/rust/rubydex-mcp/src/server.rs +0 -1148
  44. data/rust/rubydex-mcp/src/tools.rs +0 -49
  45. 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
- end.parse!
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)
@@ -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(query) -> Enumerator[Rubydex::Declaration]
199
+ * search(*queries) -> Enumerator[Rubydex::Declaration]
200
200
  *
201
- * Returns an enumerator that yields declarations matching the query exactly by substring.
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 self, VALUE query) {
204
- Check_Type(query, T_STRING);
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"), 1, &query);
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
- return rdxr_graph_yield_search_results(self, rdx_graph_declarations_search(graph, StringValueCStr(query)));
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(query) -> Enumerator[Rubydex::Declaration]
225
+ * fuzzy_search(*queries) -> Enumerator[Rubydex::Declaration]
219
226
  *
220
- * Returns an enumerator that yields declarations matching the query fuzzily.
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 self, VALUE query) {
223
- Check_Type(query, T_STRING);
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"), 1, &query);
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
- return rdxr_graph_yield_search_results(self, rdx_graph_declarations_fuzzy_search(graph, StringValueCStr(query)));
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
- * exclude_paths(paths) -> nil
799
+ * exclude_patterns(patterns) -> nil
786
800
  *
787
- * Excludes the paths from file discovery during indexing.
801
+ * Excludes files matching the given glob patterns from discovery during indexing.
788
802
  */
789
- static VALUE rdxr_graph_exclude_paths(VALUE self, VALUE paths) {
790
- Check_Type(paths, T_ARRAY);
791
- rdxi_check_array_of_strings(paths);
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(paths);
794
- char **converted_paths = rdxi_str_array_to_char(paths, length);
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
- rdx_graph_exclude_paths(graph, (const char **)converted_paths, length);
800
- rdxi_free_str_array(converted_paths, length);
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
- * excluded_paths -> Array[String]
821
+ * excluded_patterns -> Array[String]
808
822
  *
809
- * Returns the paths currently excluded from file discovery.
823
+ * Returns the glob patterns currently excluded from file discovery.
810
824
  */
811
- static VALUE rdxr_graph_excluded_paths(VALUE self) {
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 = rdx_graph_excluded_paths(graph, &out_count);
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, "exclude_paths", rdxr_graph_exclude_paths, 1);
955
- rb_define_method(cGraph, "excluded_paths", rdxr_graph_excluded_paths, 0);
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);
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -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