rubydex 0.2.8 → 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 +15 -15
- 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 +4 -4
- 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 +1 -1
- data/rust/rubydex/src/model/graph.rs +7 -7
- data/rust/rubydex/src/operation/applier.rs +0 -2
- data/rust/rubydex-sys/src/graph_api.rs +17 -13
- 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
|
@@ -796,38 +796,38 @@ static VALUE rdxr_graph_complete_method_argument(int argc, VALUE *argv, VALUE se
|
|
|
796
796
|
|
|
797
797
|
/*
|
|
798
798
|
* call-seq:
|
|
799
|
-
*
|
|
799
|
+
* exclude_patterns(patterns) -> nil
|
|
800
800
|
*
|
|
801
|
-
* Excludes the
|
|
801
|
+
* Excludes files matching the given glob patterns from discovery during indexing.
|
|
802
802
|
*/
|
|
803
|
-
static VALUE
|
|
804
|
-
Check_Type(
|
|
805
|
-
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);
|
|
806
806
|
|
|
807
|
-
size_t length = RARRAY_LEN(
|
|
808
|
-
char **
|
|
807
|
+
size_t length = RARRAY_LEN(patterns);
|
|
808
|
+
char **converted_patterns = rdxi_str_array_to_char(patterns, length);
|
|
809
809
|
|
|
810
810
|
void *graph;
|
|
811
811
|
TypedData_Get_Struct(self, void*, &graph_type, graph);
|
|
812
812
|
|
|
813
|
-
|
|
814
|
-
rdxi_free_str_array(
|
|
813
|
+
rdx_graph_exclude_patterns(graph, (const char **)converted_patterns, length);
|
|
814
|
+
rdxi_free_str_array(converted_patterns, length);
|
|
815
815
|
|
|
816
816
|
return Qnil;
|
|
817
817
|
}
|
|
818
818
|
|
|
819
819
|
/*
|
|
820
820
|
* call-seq:
|
|
821
|
-
*
|
|
821
|
+
* excluded_patterns -> Array[String]
|
|
822
822
|
*
|
|
823
|
-
* Returns the
|
|
823
|
+
* Returns the glob patterns currently excluded from file discovery.
|
|
824
824
|
*/
|
|
825
|
-
static VALUE
|
|
825
|
+
static VALUE rdxr_graph_excluded_patterns(VALUE self) {
|
|
826
826
|
void *graph;
|
|
827
827
|
TypedData_Get_Struct(self, void*, &graph_type, graph);
|
|
828
828
|
|
|
829
829
|
size_t out_count = 0;
|
|
830
|
-
const char *const *results =
|
|
830
|
+
const char *const *results = rdx_graph_excluded_patterns(graph, &out_count);
|
|
831
831
|
|
|
832
832
|
if (results == NULL) {
|
|
833
833
|
return rb_ary_new();
|
|
@@ -965,8 +965,8 @@ void rdxi_initialize_graph(VALUE moduleRubydex) {
|
|
|
965
965
|
rb_define_method(cGraph, "complete_namespace_access", rdxr_graph_complete_namespace_access, -1);
|
|
966
966
|
rb_define_method(cGraph, "complete_method_call", rdxr_graph_complete_method_call, -1);
|
|
967
967
|
rb_define_method(cGraph, "complete_method_argument", rdxr_graph_complete_method_argument, -1);
|
|
968
|
-
rb_define_method(cGraph, "
|
|
969
|
-
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);
|
|
970
970
|
rb_define_method(cGraph, "workspace_path", rdxr_graph_workspace_path, 0);
|
|
971
971
|
rb_define_method(cGraph, "workspace_path=", rdxr_graph_set_workspace_path, 1);
|
|
972
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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubydex
|
|
4
|
+
module MCPServer
|
|
5
|
+
class GetFileDeclarationsTool < BaseTool
|
|
6
|
+
tool_name "get_file_declarations"
|
|
7
|
+
description "List all Ruby classes, modules, methods, and constants defined in a specific file. Returns a structural overview with names, kinds, and line numbers. Use this to understand a file's structure before reading it, or to see what a file contributes to the codebase. Accepts relative or absolute paths."
|
|
8
|
+
input_schema(
|
|
9
|
+
properties: {
|
|
10
|
+
file_path: { type: "string", description: "File path (relative or absolute) to list declarations for" },
|
|
11
|
+
},
|
|
12
|
+
required: ["file_path"],
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
#: (file_path: String) -> Tool::Response
|
|
16
|
+
def call(file_path:)
|
|
17
|
+
absolute_target = if Pathname.new(file_path).absolute?
|
|
18
|
+
file_path
|
|
19
|
+
else
|
|
20
|
+
File.join(@root_path, file_path)
|
|
21
|
+
end
|
|
22
|
+
return file_not_found_response(file_path) unless File.exist?(absolute_target)
|
|
23
|
+
|
|
24
|
+
document = document_for_path(file_path)
|
|
25
|
+
return file_not_found_response(file_path) unless document
|
|
26
|
+
|
|
27
|
+
declarations = document.definitions.filter_map do |definition|
|
|
28
|
+
declaration = definition.declaration
|
|
29
|
+
next unless declaration
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
name: declaration.name,
|
|
33
|
+
kind: declaration_kind(declaration),
|
|
34
|
+
line: definition.location.to_display.start_line,
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
response(file: format_path(document.uri), declarations: declarations)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
#: (String) -> Tool::Response
|
|
44
|
+
def file_not_found_response(file_path)
|
|
45
|
+
response(
|
|
46
|
+
Error.new(
|
|
47
|
+
"not_found",
|
|
48
|
+
"File '#{file_path}' not found in the index",
|
|
49
|
+
"Use a relative path like 'app/models/user.rb' or an absolute path matching the indexed project",
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|