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
data/ext/rubydex/utils.h
CHANGED
|
@@ -17,6 +17,11 @@ void rdxi_check_array_of_strings(VALUE array);
|
|
|
17
17
|
// Returns nil when the Rust side returned NULL.
|
|
18
18
|
VALUE rdxi_owned_c_string_to_ruby(const char *string);
|
|
19
19
|
|
|
20
|
+
// Coerce an optional String, Symbol, or nil to a C string, returning `default_value` when the value
|
|
21
|
+
// is nil. A non-nil value must be a String or Symbol, otherwise a `TypeError` is raised. The
|
|
22
|
+
// returned pointer is owned by the Ruby VALUE and is only valid while it stays live.
|
|
23
|
+
const char *rdxi_symbol_or_string_cstr(VALUE value, const char *default_value);
|
|
24
|
+
|
|
20
25
|
// Yield body for iterating over declarations
|
|
21
26
|
VALUE rdxi_declarations_yield(VALUE args);
|
|
22
27
|
|
|
@@ -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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubydex
|
|
4
|
+
module MCPServer
|
|
5
|
+
class SearchDeclarationsTool < BaseTool
|
|
6
|
+
tool_name "search_declarations"
|
|
7
|
+
description 'Search for Ruby classes, modules, methods, or constants by name. Use this INSTEAD OF Grep when you know part of a Ruby identifier name and want to find its definition. Returns fully qualified names, kinds, and file locations. Use the `kind` filter ("Class", "Module", "Method", "Constant") to narrow results. Set `match_mode` to "exact" for precise substring matching or "fuzzy" for LSP-style workspace symbol search (default). Results are paginated: the response includes `total` (the full count of matches). If `total` exceeds the number of returned results, use `offset` to fetch subsequent pages.'
|
|
8
|
+
input_schema(
|
|
9
|
+
properties: {
|
|
10
|
+
query: { type: "string", description: "Search query to match against declaration names" },
|
|
11
|
+
kind: { type: "string", description: "Filter by declaration kind: Class, Module, Method, Constant, etc." },
|
|
12
|
+
match_mode: { type: "string", description: 'Matching mode: "fuzzy" (default) for LSP-style workspace symbol search, or "exact" for precise substring matching' },
|
|
13
|
+
limit: { type: "integer", description: "Maximum number of results to return (default 50, max 100)" },
|
|
14
|
+
offset: { type: "integer", description: "Number of results to skip for pagination (default 0)" },
|
|
15
|
+
},
|
|
16
|
+
required: ["query"],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
#: (query: String, ?kind: String, ?match_mode: String, ?limit: Integer, ?offset: Integer) -> Tool::Response
|
|
20
|
+
def call(query:, kind: nil, match_mode: nil, limit: nil, offset: nil)
|
|
21
|
+
declarations = case match_mode
|
|
22
|
+
when nil, "fuzzy"
|
|
23
|
+
@graph.fuzzy_search(query)
|
|
24
|
+
when "exact"
|
|
25
|
+
@graph.search(query)
|
|
26
|
+
else
|
|
27
|
+
return response(
|
|
28
|
+
Error.new(
|
|
29
|
+
"invalid_match_mode",
|
|
30
|
+
"Invalid match_mode '#{match_mode}'",
|
|
31
|
+
'Use "fuzzy" or "exact"',
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if kind
|
|
37
|
+
declarations = declarations.lazy.select { |declaration| declaration_kind(declaration).casecmp?(kind) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
page, total = paginate(declarations, offset, limit, 100)
|
|
41
|
+
results = page.map do |declaration|
|
|
42
|
+
{
|
|
43
|
+
name: declaration.name,
|
|
44
|
+
kind: declaration_kind(declaration),
|
|
45
|
+
locations: declaration.definitions.map do |definition|
|
|
46
|
+
display_location(definition.location)
|
|
47
|
+
end,
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
response(results: results, total: total)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|