rubydex 0.1.0.beta12-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 (109) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +23 -0
  3. data/README.md +125 -0
  4. data/THIRD_PARTY_LICENSES.html +4562 -0
  5. data/exe/rdx +47 -0
  6. data/ext/rubydex/declaration.c +453 -0
  7. data/ext/rubydex/declaration.h +23 -0
  8. data/ext/rubydex/definition.c +284 -0
  9. data/ext/rubydex/definition.h +28 -0
  10. data/ext/rubydex/diagnostic.c +6 -0
  11. data/ext/rubydex/diagnostic.h +11 -0
  12. data/ext/rubydex/document.c +97 -0
  13. data/ext/rubydex/document.h +10 -0
  14. data/ext/rubydex/extconf.rb +138 -0
  15. data/ext/rubydex/graph.c +681 -0
  16. data/ext/rubydex/graph.h +10 -0
  17. data/ext/rubydex/handle.h +44 -0
  18. data/ext/rubydex/location.c +22 -0
  19. data/ext/rubydex/location.h +15 -0
  20. data/ext/rubydex/reference.c +123 -0
  21. data/ext/rubydex/reference.h +15 -0
  22. data/ext/rubydex/rubydex.c +22 -0
  23. data/ext/rubydex/utils.c +108 -0
  24. data/ext/rubydex/utils.h +34 -0
  25. data/lib/rubydex/3.2/rubydex.so +0 -0
  26. data/lib/rubydex/3.3/rubydex.so +0 -0
  27. data/lib/rubydex/3.4/rubydex.so +0 -0
  28. data/lib/rubydex/4.0/rubydex.so +0 -0
  29. data/lib/rubydex/comment.rb +17 -0
  30. data/lib/rubydex/diagnostic.rb +21 -0
  31. data/lib/rubydex/failures.rb +15 -0
  32. data/lib/rubydex/graph.rb +98 -0
  33. data/lib/rubydex/keyword.rb +17 -0
  34. data/lib/rubydex/keyword_parameter.rb +13 -0
  35. data/lib/rubydex/librubydex_sys.so +0 -0
  36. data/lib/rubydex/location.rb +90 -0
  37. data/lib/rubydex/mixin.rb +22 -0
  38. data/lib/rubydex/version.rb +5 -0
  39. data/lib/rubydex.rb +23 -0
  40. data/rbi/rubydex.rbi +422 -0
  41. data/rust/Cargo.lock +1851 -0
  42. data/rust/Cargo.toml +29 -0
  43. data/rust/about.hbs +78 -0
  44. data/rust/about.toml +10 -0
  45. data/rust/rubydex/Cargo.toml +42 -0
  46. data/rust/rubydex/src/compile_assertions.rs +13 -0
  47. data/rust/rubydex/src/diagnostic.rs +110 -0
  48. data/rust/rubydex/src/errors.rs +28 -0
  49. data/rust/rubydex/src/indexing/local_graph.rs +224 -0
  50. data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -0
  51. data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -0
  52. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
  53. data/rust/rubydex/src/indexing.rs +210 -0
  54. data/rust/rubydex/src/integrity.rs +279 -0
  55. data/rust/rubydex/src/job_queue.rs +205 -0
  56. data/rust/rubydex/src/lib.rs +17 -0
  57. data/rust/rubydex/src/listing.rs +371 -0
  58. data/rust/rubydex/src/main.rs +160 -0
  59. data/rust/rubydex/src/model/built_in.rs +83 -0
  60. data/rust/rubydex/src/model/comment.rs +24 -0
  61. data/rust/rubydex/src/model/declaration.rs +671 -0
  62. data/rust/rubydex/src/model/definitions.rs +1682 -0
  63. data/rust/rubydex/src/model/document.rs +222 -0
  64. data/rust/rubydex/src/model/encoding.rs +22 -0
  65. data/rust/rubydex/src/model/graph.rs +3754 -0
  66. data/rust/rubydex/src/model/id.rs +110 -0
  67. data/rust/rubydex/src/model/identity_maps.rs +58 -0
  68. data/rust/rubydex/src/model/ids.rs +60 -0
  69. data/rust/rubydex/src/model/keywords.rs +256 -0
  70. data/rust/rubydex/src/model/name.rs +298 -0
  71. data/rust/rubydex/src/model/references.rs +111 -0
  72. data/rust/rubydex/src/model/string_ref.rs +50 -0
  73. data/rust/rubydex/src/model/visibility.rs +41 -0
  74. data/rust/rubydex/src/model.rs +15 -0
  75. data/rust/rubydex/src/offset.rs +147 -0
  76. data/rust/rubydex/src/position.rs +6 -0
  77. data/rust/rubydex/src/query.rs +1841 -0
  78. data/rust/rubydex/src/resolution.rs +6517 -0
  79. data/rust/rubydex/src/stats/memory.rs +71 -0
  80. data/rust/rubydex/src/stats/orphan_report.rs +264 -0
  81. data/rust/rubydex/src/stats/timer.rs +127 -0
  82. data/rust/rubydex/src/stats.rs +11 -0
  83. data/rust/rubydex/src/test_utils/context.rs +226 -0
  84. data/rust/rubydex/src/test_utils/graph_test.rs +730 -0
  85. data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -0
  86. data/rust/rubydex/src/test_utils.rs +52 -0
  87. data/rust/rubydex/src/visualization/dot.rs +192 -0
  88. data/rust/rubydex/src/visualization.rs +6 -0
  89. data/rust/rubydex/tests/cli.rs +185 -0
  90. data/rust/rubydex-mcp/Cargo.toml +28 -0
  91. data/rust/rubydex-mcp/src/main.rs +48 -0
  92. data/rust/rubydex-mcp/src/server.rs +1145 -0
  93. data/rust/rubydex-mcp/src/tools.rs +49 -0
  94. data/rust/rubydex-mcp/tests/mcp.rs +302 -0
  95. data/rust/rubydex-sys/Cargo.toml +20 -0
  96. data/rust/rubydex-sys/build.rs +14 -0
  97. data/rust/rubydex-sys/cbindgen.toml +12 -0
  98. data/rust/rubydex-sys/src/declaration_api.rs +485 -0
  99. data/rust/rubydex-sys/src/definition_api.rs +443 -0
  100. data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
  101. data/rust/rubydex-sys/src/document_api.rs +85 -0
  102. data/rust/rubydex-sys/src/graph_api.rs +948 -0
  103. data/rust/rubydex-sys/src/lib.rs +79 -0
  104. data/rust/rubydex-sys/src/location_api.rs +79 -0
  105. data/rust/rubydex-sys/src/name_api.rs +135 -0
  106. data/rust/rubydex-sys/src/reference_api.rs +267 -0
  107. data/rust/rubydex-sys/src/utils.rs +70 -0
  108. data/rust/rustfmt.toml +2 -0
  109. metadata +159 -0
@@ -0,0 +1,192 @@
1
+ //! DOT format generator for Graphviz visualization of the graph structure.
2
+
3
+ use std::fmt::Write;
4
+
5
+ use crate::model::graph::Graph;
6
+
7
+ const NAME_NODE_SHAPE: &str = "hexagon";
8
+ const DEFINITION_NODE_SHAPE: &str = "ellipse";
9
+ const URI_NODE_SHAPE: &str = "box";
10
+
11
+ /// Escapes a string for use in DOT format labels and identifiers.
12
+ fn escape_dot_string(s: &str) -> String {
13
+ if !s.contains('"') {
14
+ return s.to_string();
15
+ }
16
+
17
+ let mut result = String::with_capacity(s.len());
18
+ for c in s.chars() {
19
+ match c {
20
+ '"' => result.push_str("\\\""),
21
+ _ => result.push(c),
22
+ }
23
+ }
24
+ result
25
+ }
26
+
27
+ #[must_use]
28
+ pub fn generate(graph: &Graph) -> String {
29
+ let mut output = String::new();
30
+ output.push_str("digraph {\n");
31
+ output.push_str(" rankdir=TB;\n\n");
32
+
33
+ write_declaration_nodes(&mut output, graph);
34
+ write_definition_nodes(&mut output, graph);
35
+ write_document_nodes(&mut output, graph);
36
+
37
+ output.push_str("}\n");
38
+ output
39
+ }
40
+
41
+ fn write_declaration_nodes(output: &mut String, graph: &Graph) {
42
+ let mut declarations: Vec<_> = graph.declarations().values().collect();
43
+ declarations.sort_by(|a, b| a.name().cmp(b.name()));
44
+
45
+ for declaration in declarations {
46
+ let name = declaration.name();
47
+ let escaped_name = escape_dot_string(name);
48
+ let node_id = format!("Name:{name}");
49
+ let _ = writeln!(
50
+ output,
51
+ " \"{node_id}\" [label=\"{escaped_name}\",shape={NAME_NODE_SHAPE}];"
52
+ );
53
+
54
+ for def_id in declaration.definitions() {
55
+ let _ = writeln!(output, " \"{node_id}\" -> \"def_{def_id}\" [dir=both];");
56
+ }
57
+ }
58
+
59
+ output.push('\n');
60
+ }
61
+
62
+ fn write_definition_nodes(output: &mut String, graph: &Graph) {
63
+ let mut definitions: Vec<_> = graph
64
+ .definitions()
65
+ .iter()
66
+ .filter_map(|(def_id, definition)| {
67
+ graph
68
+ .declarations()
69
+ .get(graph.definition_to_declaration_id(definition).unwrap())
70
+ .map(|declaration| {
71
+ let def_type = definition.kind();
72
+ let escaped_name = escape_dot_string(declaration.name());
73
+ let label = format!("{def_type}({escaped_name})");
74
+ let line = format!(" \"def_{def_id}\" [label=\"{label}\",shape={DEFINITION_NODE_SHAPE}];\n");
75
+ (label, line)
76
+ })
77
+ })
78
+ .collect();
79
+
80
+ definitions.sort_by(|a, b| a.0.cmp(&b.0));
81
+
82
+ for (_, line) in definitions {
83
+ output.push_str(&line);
84
+ }
85
+ output.push('\n');
86
+ }
87
+
88
+ fn write_document_nodes(output: &mut String, graph: &Graph) {
89
+ let mut documents: Vec<_> = graph.documents().values().collect();
90
+ documents.sort_by(|a, b| a.uri().cmp(b.uri()));
91
+
92
+ for document in documents {
93
+ let uri = document.uri();
94
+ let label = uri.rsplit('/').next().unwrap_or(uri);
95
+ let escaped_uri = escape_dot_string(uri);
96
+ let escaped_label = escape_dot_string(label);
97
+ let _ = writeln!(
98
+ output,
99
+ " \"{escaped_uri}\" [label=\"{escaped_label}\",shape={URI_NODE_SHAPE}];"
100
+ );
101
+
102
+ for def_id in document.definitions() {
103
+ let _ = writeln!(output, " \"def_{def_id}\" -> \"{escaped_uri}\";");
104
+ }
105
+ }
106
+ output.push('\n');
107
+ }
108
+
109
+ #[cfg(test)]
110
+ mod tests {
111
+ use super::*;
112
+ use crate::{model::ids::DeclarationId, test_utils::GraphTest};
113
+
114
+ fn create_test_graph() -> GraphTest {
115
+ let mut graph_test = GraphTest::new();
116
+ graph_test.index_uri(
117
+ "file:///test.rb",
118
+ "
119
+ class TestClass
120
+ end
121
+
122
+ module TestModule
123
+ end
124
+ ",
125
+ );
126
+ graph_test.resolve();
127
+ graph_test
128
+ }
129
+
130
+ /// Finds the first definition ID for the declaration with the given name.
131
+ fn def_id_for(graph: &Graph, name: &str) -> String {
132
+ let decl = graph.declarations().get(&DeclarationId::from(name)).unwrap();
133
+ decl.definitions().first().unwrap().to_string()
134
+ }
135
+
136
+ #[test]
137
+ fn test_dot_generation() {
138
+ let context = create_test_graph();
139
+ let dot_output = generate(context.graph());
140
+
141
+ let basic_object_def = def_id_for(context.graph(), "BasicObject");
142
+ let class_def = def_id_for(context.graph(), "Class");
143
+ let kernel_def = def_id_for(context.graph(), "Kernel");
144
+ let module_def = def_id_for(context.graph(), "Module");
145
+ let object_def = def_id_for(context.graph(), "Object");
146
+ let test_class_def = def_id_for(context.graph(), "TestClass");
147
+ let test_module_def = def_id_for(context.graph(), "TestModule");
148
+
149
+ let expected = format!(
150
+ r#"digraph {{
151
+ rankdir=TB;
152
+
153
+ "Name:BasicObject" [label="BasicObject",shape=hexagon];
154
+ "Name:BasicObject" -> "def_{basic_object_def}" [dir=both];
155
+ "Name:Class" [label="Class",shape=hexagon];
156
+ "Name:Class" -> "def_{class_def}" [dir=both];
157
+ "Name:Kernel" [label="Kernel",shape=hexagon];
158
+ "Name:Kernel" -> "def_{kernel_def}" [dir=both];
159
+ "Name:Module" [label="Module",shape=hexagon];
160
+ "Name:Module" -> "def_{module_def}" [dir=both];
161
+ "Name:Object" [label="Object",shape=hexagon];
162
+ "Name:Object" -> "def_{object_def}" [dir=both];
163
+ "Name:TestClass" [label="TestClass",shape=hexagon];
164
+ "Name:TestClass" -> "def_{test_class_def}" [dir=both];
165
+ "Name:TestModule" [label="TestModule",shape=hexagon];
166
+ "Name:TestModule" -> "def_{test_module_def}" [dir=both];
167
+
168
+ "def_{basic_object_def}" [label="Class(BasicObject)",shape=ellipse];
169
+ "def_{class_def}" [label="Class(Class)",shape=ellipse];
170
+ "def_{module_def}" [label="Class(Module)",shape=ellipse];
171
+ "def_{object_def}" [label="Class(Object)",shape=ellipse];
172
+ "def_{test_class_def}" [label="Class(TestClass)",shape=ellipse];
173
+ "def_{kernel_def}" [label="Module(Kernel)",shape=ellipse];
174
+ "def_{test_module_def}" [label="Module(TestModule)",shape=ellipse];
175
+
176
+ "file:///test.rb" [label="test.rb",shape=box];
177
+ "def_{test_class_def}" -> "file:///test.rb";
178
+ "def_{test_module_def}" -> "file:///test.rb";
179
+ "rubydex:built-in" [label="rubydex:built-in",shape=box];
180
+ "def_{basic_object_def}" -> "rubydex:built-in";
181
+ "def_{kernel_def}" -> "rubydex:built-in";
182
+ "def_{object_def}" -> "rubydex:built-in";
183
+ "def_{module_def}" -> "rubydex:built-in";
184
+ "def_{class_def}" -> "rubydex:built-in";
185
+
186
+ }}
187
+ "#
188
+ );
189
+
190
+ assert_eq!(dot_output, expected);
191
+ }
192
+ }
@@ -0,0 +1,6 @@
1
+ //! Graph visualization module for the Index structure.
2
+ //!
3
+ //! This module provides functionality to generate visual representations of the Index's
4
+ //! internal graph structure, showing the relationships between Names, Definitions, and URIs.
5
+
6
+ pub mod dot;
@@ -0,0 +1,185 @@
1
+ use assert_cmd::{assert::Assert, prelude::*};
2
+ use predicates::prelude::*;
3
+ use regex::Regex;
4
+ use rubydex::test_utils::{normalize_indentation, with_context};
5
+ use std::process::Command;
6
+
7
+ fn rdx_cmd(args: &[&str]) -> Command {
8
+ let mut cmd = Command::cargo_bin("rubydex_cli").unwrap();
9
+ cmd.args(args);
10
+ cmd
11
+ }
12
+
13
+ fn rdx(args: &[&str]) -> Assert {
14
+ rdx_cmd(args).assert()
15
+ }
16
+
17
+ #[test]
18
+ fn prints_help() {
19
+ rdx(&["--help"])
20
+ .success()
21
+ .stdout(predicate::str::contains("A Static Analysis Toolkit for Ruby"))
22
+ .stdout(predicate::str::contains("Usage:"))
23
+ .stdout(predicate::str::contains("--stats"))
24
+ .stdout(predicate::str::contains("--visualize"))
25
+ .stdout(predicate::str::contains("--stop-after"));
26
+ }
27
+
28
+ #[test]
29
+ fn paths_argument_variants() {
30
+ rdx(&[])
31
+ .success()
32
+ .stderr(predicate::str::is_empty())
33
+ .stdout(predicate::str::contains("Indexed 1 files"));
34
+
35
+ rdx(&["."])
36
+ .success()
37
+ .stderr(predicate::str::is_empty())
38
+ .stdout(predicate::str::contains("Indexed 1 files"));
39
+
40
+ with_context(|context| {
41
+ context.write("dir1/file1.rb", "class Class1\nend\n");
42
+ context.write("dir1/file2.rb", "class Class2\nend\n");
43
+ context.write("dir2/file1.rb", "class Class3\nend\n");
44
+ context.write("dir2/file2.rb", "class Class4\nend\n"); // not indexed
45
+
46
+ rdx(&[
47
+ context.absolute_path_to("dir1").to_str().unwrap(),
48
+ context.absolute_path_to("dir2/file1.rb").to_str().unwrap(),
49
+ ])
50
+ .success()
51
+ .stderr(predicate::str::is_empty())
52
+ .stdout(predicate::str::contains("Indexed 4 files"));
53
+ });
54
+ }
55
+
56
+ #[test]
57
+ fn prints_index_metrics() {
58
+ with_context(|context| {
59
+ context.write("file1.rb", "class FirstClass\nend\n");
60
+ context.write("file2.rb", "module SecondModule\nend\n");
61
+
62
+ rdx(&[context.absolute_path().to_str().unwrap()])
63
+ .success()
64
+ .stderr(predicate::str::is_empty())
65
+ .stdout(predicate::str::contains("Indexed 3 files"))
66
+ .stdout(predicate::str::contains("Found 7 names"))
67
+ .stdout(predicate::str::contains("Found 7 definitions"));
68
+ });
69
+ }
70
+
71
+ fn normalize_visualization_output(output: &str) -> String {
72
+ let def_re = Regex::new(r"def_-?[a-f0-9]+").unwrap();
73
+ let uri_re = Regex::new(r#"file://[^"]+/([^/"]+\.rb)"#).unwrap();
74
+
75
+ let normalized = def_re.replace_all(output, "def_<ID>");
76
+ uri_re.replace_all(&normalized, "file://<PATH>/$1").to_string()
77
+ }
78
+
79
+ #[test]
80
+ fn visualize_simple_class() {
81
+ with_context(|context| {
82
+ context.write("simple.rb", "class SimpleClass\nend\n");
83
+
84
+ let output = rdx_cmd(&[context.absolute_path().to_str().unwrap(), "--visualize"])
85
+ .output()
86
+ .unwrap();
87
+
88
+ assert!(output.status.success());
89
+
90
+ let stdout = String::from_utf8_lossy(&output.stdout);
91
+ let normalized = normalize_visualization_output(&stdout);
92
+
93
+ let expected = normalize_indentation({
94
+ r#"
95
+ digraph {
96
+ rankdir=TB;
97
+
98
+ "Name:BasicObject" [label="BasicObject",shape=hexagon];
99
+ "Name:BasicObject" -> "def_<ID>" [dir=both];
100
+ "Name:Class" [label="Class",shape=hexagon];
101
+ "Name:Class" -> "def_<ID>" [dir=both];
102
+ "Name:Kernel" [label="Kernel",shape=hexagon];
103
+ "Name:Kernel" -> "def_<ID>" [dir=both];
104
+ "Name:Module" [label="Module",shape=hexagon];
105
+ "Name:Module" -> "def_<ID>" [dir=both];
106
+ "Name:Object" [label="Object",shape=hexagon];
107
+ "Name:Object" -> "def_<ID>" [dir=both];
108
+ "Name:SimpleClass" [label="SimpleClass",shape=hexagon];
109
+ "Name:SimpleClass" -> "def_<ID>" [dir=both];
110
+
111
+ "def_<ID>" [label="Class(BasicObject)",shape=ellipse];
112
+ "def_<ID>" [label="Class(Class)",shape=ellipse];
113
+ "def_<ID>" [label="Class(Module)",shape=ellipse];
114
+ "def_<ID>" [label="Class(Object)",shape=ellipse];
115
+ "def_<ID>" [label="Class(SimpleClass)",shape=ellipse];
116
+ "def_<ID>" [label="Module(Kernel)",shape=ellipse];
117
+
118
+ "file://<PATH>/simple.rb" [label="simple.rb",shape=box];
119
+ "def_<ID>" -> "file://<PATH>/simple.rb";
120
+ "rubydex:built-in" [label="rubydex:built-in",shape=box];
121
+ "def_<ID>" -> "rubydex:built-in";
122
+ "def_<ID>" -> "rubydex:built-in";
123
+ "def_<ID>" -> "rubydex:built-in";
124
+ "def_<ID>" -> "rubydex:built-in";
125
+ "def_<ID>" -> "rubydex:built-in";
126
+
127
+ }
128
+
129
+ "#
130
+ });
131
+
132
+ assert_eq!(normalized, expected);
133
+ });
134
+ }
135
+
136
+ #[test]
137
+ fn stop_after() {
138
+ with_context(|context| {
139
+ context.write("file1.rb", "class Class1\nend\n");
140
+ context.write("file2.rb", "class Class2\nend\n");
141
+
142
+ rdx(&[
143
+ context.absolute_path().to_str().unwrap(),
144
+ "--stop-after",
145
+ "listing",
146
+ "--stats",
147
+ ])
148
+ .success()
149
+ .stdout(predicate::str::contains("Listing"))
150
+ .stdout(predicate::str::contains("Indexing").not())
151
+ .stdout(predicate::str::contains("Resolution").not())
152
+ .stdout(predicate::str::contains("Querying").not());
153
+
154
+ rdx(&[
155
+ context.absolute_path().to_str().unwrap(),
156
+ "--stop-after",
157
+ "indexing",
158
+ "--stats",
159
+ ])
160
+ .success()
161
+ .stdout(predicate::str::contains("Listing"))
162
+ .stdout(predicate::str::contains("Indexing"))
163
+ .stdout(predicate::str::contains("Resolution").not())
164
+ .stdout(predicate::str::contains("Querying").not());
165
+
166
+ rdx(&[
167
+ context.absolute_path().to_str().unwrap(),
168
+ "--stop-after",
169
+ "resolution",
170
+ "--stats",
171
+ ])
172
+ .success()
173
+ .stdout(predicate::str::contains("Listing"))
174
+ .stdout(predicate::str::contains("Indexing"))
175
+ .stdout(predicate::str::contains("Resolution"))
176
+ .stdout(predicate::str::contains("Querying").not());
177
+
178
+ rdx(&[context.absolute_path().to_str().unwrap(), "--stats"])
179
+ .success()
180
+ .stdout(predicate::str::contains("Listing"))
181
+ .stdout(predicate::str::contains("Indexing"))
182
+ .stdout(predicate::str::contains("Resolution"))
183
+ .stdout(predicate::str::contains("Querying"));
184
+ });
185
+ }
@@ -0,0 +1,28 @@
1
+ [package]
2
+ name = "rubydex-mcp"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ rust-version = "1.89.0"
6
+ license = "MIT"
7
+
8
+ [[bin]]
9
+ name = "rubydex_mcp"
10
+ path = "src/main.rs"
11
+
12
+ [dependencies]
13
+ rubydex = { path = "../rubydex" }
14
+ clap = { version = "4.5.16", features = ["derive"] }
15
+ rmcp = { version = "0.15", features = ["server", "macros", "transport-io", "schemars"] }
16
+ tokio = { version = "1", features = ["macros", "rt", "io-std"] }
17
+ serde = { version = "1", features = ["derive"] }
18
+ serde_json = "1"
19
+ schemars = "1"
20
+ url = "2"
21
+
22
+ [dev-dependencies]
23
+ rubydex = { path = "../rubydex", features = ["test_utils"] }
24
+ assert_cmd = "2.0"
25
+ serde_json = "1"
26
+
27
+ [lints]
28
+ workspace = true
@@ -0,0 +1,48 @@
1
+ use clap::Parser;
2
+
3
+ mod server;
4
+ mod tools;
5
+
6
+ #[derive(Parser, Debug)]
7
+ #[command(
8
+ name = "rubydex_mcp",
9
+ about = "Rubydex MCP server for AI-assisted Ruby code intelligence",
10
+ version
11
+ )]
12
+ struct Args {
13
+ #[arg(value_name = "PATH", default_value = ".")]
14
+ path: String,
15
+ }
16
+
17
+ fn main() {
18
+ let args = Args::parse();
19
+
20
+ let root = match std::fs::canonicalize(&args.path) {
21
+ Ok(p) => p
22
+ .into_os_string()
23
+ .into_string()
24
+ .expect("Project path is not valid UTF-8"),
25
+ Err(e) => {
26
+ eprintln!("Warning: failed to canonicalize '{}': {e}", args.path);
27
+ args.path
28
+ }
29
+ };
30
+
31
+ // Create the server and start indexing in the background.
32
+ let server = server::RubydexServer::new(root.clone());
33
+ server.spawn_indexer(root);
34
+
35
+ // Serve MCP over stdio immediately while indexing runs.
36
+ // We need to do this because Claude Code's default MCP server timeout is 30 seconds,
37
+ // And in big codebases it's possible to exceed that and Claude Code would just consider
38
+ // the server fail to connect.
39
+ let rt = tokio::runtime::Builder::new_current_thread()
40
+ .enable_all()
41
+ .build()
42
+ .expect("Failed to build tokio runtime");
43
+
44
+ if let Err(e) = rt.block_on(server.serve()) {
45
+ eprintln!("MCP server error: {e}");
46
+ std::process::exit(1);
47
+ }
48
+ }