rubydex 0.1.0.beta11 → 0.1.0.beta13

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +23 -23
  3. data/README.md +125 -125
  4. data/THIRD_PARTY_LICENSES.html +2018 -945
  5. data/exe/rdx +47 -47
  6. data/ext/rubydex/declaration.c +453 -388
  7. data/ext/rubydex/declaration.h +23 -23
  8. data/ext/rubydex/definition.c +284 -197
  9. data/ext/rubydex/definition.h +28 -28
  10. data/ext/rubydex/diagnostic.c +6 -6
  11. data/ext/rubydex/diagnostic.h +11 -11
  12. data/ext/rubydex/document.c +97 -98
  13. data/ext/rubydex/document.h +10 -10
  14. data/ext/rubydex/extconf.rb +146 -127
  15. data/ext/rubydex/graph.c +701 -512
  16. data/ext/rubydex/graph.h +10 -10
  17. data/ext/rubydex/handle.h +44 -44
  18. data/ext/rubydex/location.c +22 -22
  19. data/ext/rubydex/location.h +15 -15
  20. data/ext/rubydex/reference.c +123 -104
  21. data/ext/rubydex/reference.h +15 -16
  22. data/ext/rubydex/rubydex.c +22 -22
  23. data/ext/rubydex/utils.c +108 -86
  24. data/ext/rubydex/utils.h +34 -28
  25. data/lib/rubydex/comment.rb +17 -17
  26. data/lib/rubydex/declaration.rb +11 -0
  27. data/lib/rubydex/diagnostic.rb +21 -21
  28. data/lib/rubydex/failures.rb +15 -15
  29. data/lib/rubydex/graph.rb +98 -92
  30. data/lib/rubydex/keyword.rb +17 -0
  31. data/lib/rubydex/keyword_parameter.rb +13 -0
  32. data/lib/rubydex/location.rb +90 -90
  33. data/lib/rubydex/mixin.rb +22 -0
  34. data/lib/rubydex/version.rb +5 -5
  35. data/lib/rubydex.rb +24 -20
  36. data/rbi/rubydex.rbi +425 -310
  37. data/rust/Cargo.lock +1851 -1851
  38. data/rust/Cargo.toml +29 -29
  39. data/rust/about.toml +10 -10
  40. data/rust/{about.hbs → about_templates/about.hbs} +81 -78
  41. data/rust/about_templates/mingw_licenses.hbs +1071 -0
  42. data/rust/rubydex/Cargo.toml +42 -42
  43. data/rust/rubydex/src/compile_assertions.rs +13 -13
  44. data/rust/rubydex/src/diagnostic.rs +110 -109
  45. data/rust/rubydex/src/errors.rs +28 -28
  46. data/rust/rubydex/src/indexing/local_graph.rs +224 -224
  47. data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -1554
  48. data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -6753
  49. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
  50. data/rust/rubydex/src/indexing.rs +210 -210
  51. data/rust/rubydex/src/integrity.rs +279 -278
  52. data/rust/rubydex/src/job_queue.rs +199 -205
  53. data/rust/rubydex/src/lib.rs +17 -17
  54. data/rust/rubydex/src/listing.rs +371 -272
  55. data/rust/rubydex/src/main.rs +160 -160
  56. data/rust/rubydex/src/model/built_in.rs +83 -0
  57. data/rust/rubydex/src/model/comment.rs +24 -24
  58. data/rust/rubydex/src/model/declaration.rs +679 -588
  59. data/rust/rubydex/src/model/definitions.rs +1682 -1602
  60. data/rust/rubydex/src/model/document.rs +222 -252
  61. data/rust/rubydex/src/model/encoding.rs +22 -22
  62. data/rust/rubydex/src/model/graph.rs +3782 -3556
  63. data/rust/rubydex/src/model/id.rs +110 -110
  64. data/rust/rubydex/src/model/identity_maps.rs +58 -58
  65. data/rust/rubydex/src/model/ids.rs +60 -38
  66. data/rust/rubydex/src/model/keywords.rs +256 -256
  67. data/rust/rubydex/src/model/name.rs +298 -298
  68. data/rust/rubydex/src/model/references.rs +111 -111
  69. data/rust/rubydex/src/model/string_ref.rs +50 -50
  70. data/rust/rubydex/src/model/visibility.rs +41 -41
  71. data/rust/rubydex/src/model.rs +15 -14
  72. data/rust/rubydex/src/offset.rs +147 -147
  73. data/rust/rubydex/src/position.rs +6 -6
  74. data/rust/rubydex/src/query.rs +1841 -1700
  75. data/rust/rubydex/src/resolution.rs +1852 -5895
  76. data/rust/rubydex/src/resolution_tests.rs +4701 -0
  77. data/rust/rubydex/src/stats/memory.rs +71 -71
  78. data/rust/rubydex/src/stats/orphan_report.rs +264 -263
  79. data/rust/rubydex/src/stats/timer.rs +127 -127
  80. data/rust/rubydex/src/stats.rs +11 -11
  81. data/rust/rubydex/src/test_utils/context.rs +226 -226
  82. data/rust/rubydex/src/test_utils/graph_test.rs +730 -679
  83. data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -602
  84. data/rust/rubydex/src/test_utils.rs +52 -52
  85. data/rust/rubydex/src/visualization/dot.rs +192 -176
  86. data/rust/rubydex/src/visualization.rs +6 -6
  87. data/rust/rubydex/tests/cli.rs +185 -167
  88. data/rust/rubydex-mcp/Cargo.toml +28 -28
  89. data/rust/rubydex-mcp/src/main.rs +48 -48
  90. data/rust/rubydex-mcp/src/server.rs +1145 -1145
  91. data/rust/rubydex-mcp/src/tools.rs +49 -49
  92. data/rust/rubydex-mcp/tests/mcp.rs +302 -302
  93. data/rust/rubydex-sys/Cargo.toml +20 -20
  94. data/rust/rubydex-sys/build.rs +14 -14
  95. data/rust/rubydex-sys/cbindgen.toml +12 -12
  96. data/rust/rubydex-sys/src/declaration_api.rs +485 -469
  97. data/rust/rubydex-sys/src/definition_api.rs +443 -352
  98. data/rust/rubydex-sys/src/diagnostic_api.rs +99 -99
  99. data/rust/rubydex-sys/src/document_api.rs +85 -54
  100. data/rust/rubydex-sys/src/graph_api.rs +1017 -700
  101. data/rust/rubydex-sys/src/lib.rs +79 -9
  102. data/rust/rubydex-sys/src/location_api.rs +79 -79
  103. data/rust/rubydex-sys/src/name_api.rs +187 -135
  104. data/rust/rubydex-sys/src/reference_api.rs +267 -195
  105. data/rust/rubydex-sys/src/utils.rs +70 -70
  106. data/rust/rustfmt.toml +2 -2
  107. metadata +16 -9
  108. data/lib/rubydex/librubydex_sys.so +0 -0
@@ -1,167 +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 0 files"));
34
-
35
- rdx(&["."])
36
- .success()
37
- .stderr(predicate::str::is_empty())
38
- .stdout(predicate::str::contains("Indexed 0 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 3 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 2 files"))
66
- .stdout(predicate::str::contains("Found 5 names"))
67
- .stdout(predicate::str::contains("Found 2 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:Class" [label="Class",shape=hexagon];
99
- "Name:Module" [label="Module",shape=hexagon];
100
- "Name:Object" [label="Object",shape=hexagon];
101
- "Name:SimpleClass" [label="SimpleClass",shape=hexagon];
102
- "Name:SimpleClass" -> "def_<ID>" [dir=both];
103
-
104
- "def_<ID>" [label="Class(SimpleClass)",shape=ellipse];
105
-
106
- "file://<PATH>/simple.rb" [label="simple.rb",shape=box];
107
- "def_<ID>" -> "file://<PATH>/simple.rb";
108
-
109
- }
110
-
111
- "#
112
- });
113
-
114
- assert_eq!(normalized, expected);
115
- });
116
- }
117
-
118
- #[test]
119
- fn stop_after() {
120
- with_context(|context| {
121
- context.write("file1.rb", "class Class1\nend\n");
122
- context.write("file2.rb", "class Class2\nend\n");
123
-
124
- rdx(&[
125
- context.absolute_path().to_str().unwrap(),
126
- "--stop-after",
127
- "listing",
128
- "--stats",
129
- ])
130
- .success()
131
- .stdout(predicate::str::contains("Listing"))
132
- .stdout(predicate::str::contains("Indexing").not())
133
- .stdout(predicate::str::contains("Resolution").not())
134
- .stdout(predicate::str::contains("Querying").not());
135
-
136
- rdx(&[
137
- context.absolute_path().to_str().unwrap(),
138
- "--stop-after",
139
- "indexing",
140
- "--stats",
141
- ])
142
- .success()
143
- .stdout(predicate::str::contains("Listing"))
144
- .stdout(predicate::str::contains("Indexing"))
145
- .stdout(predicate::str::contains("Resolution").not())
146
- .stdout(predicate::str::contains("Querying").not());
147
-
148
- rdx(&[
149
- context.absolute_path().to_str().unwrap(),
150
- "--stop-after",
151
- "resolution",
152
- "--stats",
153
- ])
154
- .success()
155
- .stdout(predicate::str::contains("Listing"))
156
- .stdout(predicate::str::contains("Indexing"))
157
- .stdout(predicate::str::contains("Resolution"))
158
- .stdout(predicate::str::contains("Querying").not());
159
-
160
- rdx(&[context.absolute_path().to_str().unwrap(), "--stats"])
161
- .success()
162
- .stdout(predicate::str::contains("Listing"))
163
- .stdout(predicate::str::contains("Indexing"))
164
- .stdout(predicate::str::contains("Resolution"))
165
- .stdout(predicate::str::contains("Querying"));
166
- });
167
- }
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
+ }
@@ -1,28 +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
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
@@ -1,48 +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
- }
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
+ }