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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -3
  3. data/THIRD_PARTY_LICENSES.html +168 -1381
  4. data/exe/rdx +136 -34
  5. data/ext/rubydex/declaration.c +1 -1
  6. data/ext/rubydex/definition.c +32 -4
  7. data/ext/rubydex/extconf.rb +0 -8
  8. data/ext/rubydex/graph.c +29 -18
  9. data/ext/rubydex/query.c +105 -0
  10. data/ext/rubydex/query.h +8 -0
  11. data/ext/rubydex/reference.c +60 -0
  12. data/ext/rubydex/rubydex.c +2 -0
  13. data/ext/rubydex/utils.c +12 -0
  14. data/ext/rubydex/utils.h +5 -0
  15. data/lib/rubydex/mcp_server/protocol.rb +156 -0
  16. data/lib/rubydex/mcp_server/tools/base_tool.rb +109 -0
  17. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +30 -0
  18. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +46 -0
  19. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +68 -0
  20. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +46 -0
  21. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +55 -0
  22. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +55 -0
  23. data/lib/rubydex/mcp_server.rb +219 -0
  24. data/lib/rubydex/version.rb +1 -1
  25. data/rbi/rubydex.rbi +26 -4
  26. data/rust/Cargo.lock +5 -552
  27. data/rust/Cargo.toml +0 -1
  28. data/rust/rubydex/Cargo.toml +1 -0
  29. data/rust/rubydex/benches/graph_memory.rs +22 -4
  30. data/rust/rubydex/src/compile_assertions.rs +15 -0
  31. data/rust/rubydex/src/config.rs +54 -34
  32. data/rust/rubydex/src/diagnostic.rs +1 -1
  33. data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
  34. data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
  35. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
  36. data/rust/rubydex/src/listing.rs +159 -102
  37. data/rust/rubydex/src/main.rs +3 -125
  38. data/rust/rubydex/src/model/declaration.rs +0 -11
  39. data/rust/rubydex/src/model/definitions.rs +27 -26
  40. data/rust/rubydex/src/model/document.rs +43 -7
  41. data/rust/rubydex/src/model/graph.rs +47 -35
  42. data/rust/rubydex/src/model/id.rs +55 -0
  43. data/rust/rubydex/src/model/ids.rs +21 -9
  44. data/rust/rubydex/src/model/name.rs +35 -7
  45. data/rust/rubydex/src/model/references.rs +16 -13
  46. data/rust/rubydex/src/operation/applier.rs +0 -2
  47. data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
  48. data/rust/rubydex/src/query/cypher/schema.rs +790 -0
  49. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  50. data/rust/rubydex/src/query/cypher/tests.rs +228 -0
  51. data/rust/rubydex/src/query/cypher.rs +57 -0
  52. data/rust/rubydex/src/query.rs +2 -0
  53. data/rust/rubydex/src/resolution.rs +248 -227
  54. data/rust/rubydex/src/resolution_tests.rs +263 -65
  55. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  56. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  57. data/rust/rubydex-sys/src/graph_api.rs +175 -27
  58. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  59. metadata +17 -10
  60. data/exe/rubydex_mcp +0 -17
  61. data/lib/rubydex/bin/rubydex_mcp.exe +0 -0
  62. data/rust/rubydex-mcp/Cargo.toml +0 -34
  63. data/rust/rubydex-mcp/src/main.rs +0 -48
  64. data/rust/rubydex-mcp/src/server.rs +0 -1148
  65. data/rust/rubydex-mcp/src/tools.rs +0 -49
  66. data/rust/rubydex-mcp/tests/mcp.rs +0 -302
@@ -15,47 +15,51 @@ use rubydex::model::ids::{DeclarationId, NameId, UriId, declaration_id_from_look
15
15
  use rubydex::model::keywords;
16
16
  use rubydex::model::name::NameRef;
17
17
  use rubydex::model::visibility::Visibility;
18
+ use rubydex::query::cypher::{self, OutputFormat};
18
19
  use rubydex::query::{CompletionCandidate, CompletionContext, CompletionReceiver};
19
20
  use rubydex::resolution::Resolver;
20
21
  use rubydex::{indexing, integrity, listing, query};
21
22
  use std::ffi::CString;
22
23
  use std::path::{Path, PathBuf};
23
- use std::{mem, ptr};
24
+ use std::{mem, ptr, sync::RwLock};
24
25
 
25
26
  pub type GraphPointer = *mut c_void;
26
27
 
27
- /// Creates a new graph within a mutex. This is meant to be used when creating new Graph objects in Ruby
28
+ /// Creates a new graph wrapped in a `Box<RwLock<Graph>>` for thread-safe shared access across Ractors.
28
29
  #[unsafe(no_mangle)]
29
30
  pub extern "C" fn rdx_graph_new() -> GraphPointer {
30
- Box::into_raw(Box::new(Graph::new())) as GraphPointer
31
+ Box::into_raw(Box::new(RwLock::new(Graph::new()))) as GraphPointer
31
32
  }
32
33
 
33
34
  /// Frees a Graph through its pointer
34
35
  #[unsafe(no_mangle)]
35
36
  pub extern "C" fn rdx_graph_free(pointer: GraphPointer) {
36
37
  unsafe {
37
- let _ = Box::from_raw(pointer.cast::<Graph>());
38
+ let _ = Box::from_raw(pointer.cast::<RwLock<Graph>>());
38
39
  }
39
40
  }
40
41
 
42
+ /// Runs `action` against the graph referenced by `pointer` under a read lock.
43
+ ///
44
+ /// # Panics
45
+ ///
46
+ /// Panics if the `RwLock` is poisoned (a writer panicked while holding the lock).
41
47
  pub fn with_graph<F, T>(pointer: GraphPointer, action: F) -> T
42
48
  where
43
49
  F: FnOnce(&Graph) -> T,
44
50
  {
45
- let mut graph = unsafe { Box::from_raw(pointer.cast::<Graph>()) };
46
- let result = action(&mut graph);
47
- mem::forget(graph);
48
- result
51
+ let rwlock = unsafe { &*pointer.cast::<RwLock<Graph>>() };
52
+ let guard = rwlock.read().unwrap();
53
+ action(&guard)
49
54
  }
50
55
 
51
56
  fn with_mut_graph<F, T>(pointer: GraphPointer, action: F) -> T
52
57
  where
53
58
  F: FnOnce(&mut Graph) -> T,
54
59
  {
55
- let mut graph = unsafe { Box::from_raw(pointer.cast::<Graph>()) };
56
- let result = action(&mut graph);
57
- mem::forget(graph);
58
- result
60
+ let rwlock = unsafe { &*pointer.cast::<RwLock<Graph>>() };
61
+ let mut guard = rwlock.write().unwrap();
62
+ action(&mut guard)
59
63
  }
60
64
 
61
65
  /// Searches the graph using exact substring matching, returning every declaration whose name matches any of the
@@ -169,37 +173,41 @@ pub unsafe extern "C" fn rdx_graph_resolve_constant(
169
173
  })
170
174
  }
171
175
 
172
- /// Adds paths to exclude from file discovery during indexing.
176
+ /// Adds glob patterns to exclude from file discovery during indexing.
173
177
  ///
174
178
  /// # Panics
175
179
  ///
176
- /// Will panic if the given array of C string paths cannot be converted to a `Vec<String>`.
180
+ /// Will panic if the given array of C string patterns cannot be converted to a `Vec<String>`.
177
181
  ///
178
182
  /// # Safety
179
183
  ///
180
184
  /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
181
- /// - `paths` must be an array of `count` valid, null-terminated UTF-8 strings.
185
+ /// - `patterns` must be an array of `count` valid, null-terminated UTF-8 strings.
182
186
  #[unsafe(no_mangle)]
183
- pub unsafe extern "C" fn rdx_graph_exclude_paths(pointer: GraphPointer, paths: *const *const c_char, count: usize) {
184
- let paths: Vec<String> = unsafe { utils::convert_double_pointer_to_vec(paths, count).unwrap() };
185
- let path_bufs: Vec<PathBuf> = paths.into_iter().map(PathBuf::from).collect();
186
- with_mut_graph(pointer, |graph| graph.exclude_paths(path_bufs));
187
+ pub unsafe extern "C" fn rdx_graph_exclude_patterns(
188
+ pointer: GraphPointer,
189
+ patterns: *const *const c_char,
190
+ count: usize,
191
+ ) {
192
+ let patterns: Vec<String> = unsafe { utils::convert_double_pointer_to_vec(patterns, count).unwrap() };
193
+ let entries: Vec<Box<str>> = patterns.into_iter().map(String::into_boxed_str).collect();
194
+ with_mut_graph(pointer, |graph| graph.exclude_patterns(entries));
187
195
  }
188
196
 
189
- /// Returns the currently excluded paths as an array of C strings. Writes the count to `out_count`. Returns NULL if no
190
- /// paths are excluded. Caller must free with `free_c_string_array`.
197
+ /// Returns the currently excluded glob patterns as an array of C strings. Writes the count to `out_count`. Returns NULL
198
+ /// if no patterns are excluded. Caller must free with `free_c_string_array`.
191
199
  ///
192
200
  /// # Safety
193
201
  ///
194
202
  /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
195
203
  /// - `out_count` must be a valid, writable pointer.
196
204
  #[unsafe(no_mangle)]
197
- pub unsafe extern "C" fn rdx_graph_excluded_paths(
205
+ pub unsafe extern "C" fn rdx_graph_excluded_patterns(
198
206
  pointer: GraphPointer,
199
207
  out_count: *mut usize,
200
208
  ) -> *const *const c_char {
201
209
  with_graph(pointer, |graph| {
202
- let excluded = graph.excluded_paths();
210
+ let excluded = graph.excluded_patterns();
203
211
 
204
212
  if excluded.is_empty() {
205
213
  unsafe { *out_count = 0 };
@@ -213,7 +221,7 @@ pub unsafe extern "C" fn rdx_graph_excluded_paths(
213
221
  // on Windows if a configuration file is using forward slashes. For example:
214
222
  //
215
223
  // C:\project/vendor/bundle
216
- let normalized = path.to_string_lossy().replace(std::path::MAIN_SEPARATOR, "/");
224
+ let normalized = path.replace(std::path::MAIN_SEPARATOR, "/");
217
225
 
218
226
  CString::new(normalized)
219
227
  .ok()
@@ -313,7 +321,7 @@ pub unsafe extern "C" fn rdx_index_all(
313
321
  let file_paths: Vec<String> = unsafe { utils::convert_double_pointer_to_vec(file_paths, count).unwrap() };
314
322
 
315
323
  with_mut_graph(pointer, |graph| {
316
- let (file_paths, listing_errors) = listing::collect_file_paths(file_paths, &graph.excluded_paths());
324
+ let (file_paths, listing_errors) = listing::collect_file_paths(file_paths, &graph.excluded_patterns());
317
325
  let indexing_errors = indexing::index_files(graph, file_paths, indexing::IndexerBackend::RubyIndexer);
318
326
 
319
327
  let all_errors: Vec<String> = listing_errors
@@ -1058,6 +1066,145 @@ pub unsafe extern "C" fn rdx_keyword_get(name: *const c_char) -> *const CKeyword
1058
1066
  }
1059
1067
  }
1060
1068
 
1069
+ /// The result of running a Cypher query, carrying either the formatted output or an error message.
1070
+ #[repr(C)]
1071
+ pub struct CQueryResult {
1072
+ /// Non-null on success; null on error. Caller must free with `free_c_string`.
1073
+ pub output: *const c_char,
1074
+ /// Non-null on error; null on success. Caller must free with `free_c_string`.
1075
+ pub error: *const c_char,
1076
+ }
1077
+
1078
+ impl CQueryResult {
1079
+ fn success(output: &str) -> Self {
1080
+ match CString::new(output) {
1081
+ Ok(c_string) => Self {
1082
+ output: c_string.into_raw().cast_const(),
1083
+ error: ptr::null(),
1084
+ },
1085
+ Err(_) => Self::error("query output contained an interior NUL byte"),
1086
+ }
1087
+ }
1088
+
1089
+ fn error(message: &str) -> Self {
1090
+ Self {
1091
+ output: ptr::null(),
1092
+ error: CString::new(message).map_or(ptr::null(), |s| s.into_raw().cast_const()),
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ /// The result of parsing a Cypher query into an opaque, reusable parsed-query object.
1098
+ #[repr(C)]
1099
+ pub struct CParseResult {
1100
+ /// Non-null on success: a heap-allocated parsed query. Free with `rdx_cypher_query_free`.
1101
+ pub query: *mut c_void,
1102
+ /// Non-null on error; null on success. Caller must free with `free_c_string`.
1103
+ pub error: *const c_char,
1104
+ }
1105
+
1106
+ /// Parses a Cypher query string into an opaque parsed-query object, without needing a graph.
1107
+ ///
1108
+ /// On success, `query` is a heap-allocated parsed query that can be executed against a graph with
1109
+ /// `rdx_query_run` and must eventually be freed with `rdx_cypher_query_free`. On failure, `error`
1110
+ /// holds the message.
1111
+ ///
1112
+ /// # Safety
1113
+ ///
1114
+ /// - `query` must be a valid, null-terminated UTF-8 string.
1115
+ #[unsafe(no_mangle)]
1116
+ pub unsafe extern "C" fn rdx_cypher_parse(query: *const c_char) -> CParseResult {
1117
+ let Ok(query_str) = (unsafe { utils::convert_char_ptr_to_string(query) }) else {
1118
+ return CParseResult {
1119
+ query: ptr::null_mut(),
1120
+ error: CString::new("query is not valid UTF-8").map_or(ptr::null(), |s| s.into_raw().cast_const()),
1121
+ };
1122
+ };
1123
+
1124
+ match cypher::parse(&query_str) {
1125
+ Ok(parsed) => CParseResult {
1126
+ query: Box::into_raw(Box::new(parsed)).cast::<c_void>(),
1127
+ error: ptr::null(),
1128
+ },
1129
+ Err(error) => CParseResult {
1130
+ query: ptr::null_mut(),
1131
+ error: CString::new(error.to_string()).map_or(ptr::null(), |s| s.into_raw().cast_const()),
1132
+ },
1133
+ }
1134
+ }
1135
+
1136
+ /// Frees a parsed query previously returned by `rdx_cypher_parse`.
1137
+ ///
1138
+ /// # Safety
1139
+ ///
1140
+ /// - `query` must be a pointer returned by `rdx_cypher_parse`, or null. It must not be used after.
1141
+ #[unsafe(no_mangle)]
1142
+ pub unsafe extern "C" fn rdx_cypher_query_free(query: *mut c_void) {
1143
+ if query.is_null() {
1144
+ return;
1145
+ }
1146
+ let _ = unsafe { Box::from_raw(query.cast::<cypher::Query>()) };
1147
+ }
1148
+
1149
+ /// Executes a previously parsed query (from `rdx_cypher_parse`) against the graph and returns the
1150
+ /// formatted output or an error message. `format` must be `"table"` or `"json"`.
1151
+ ///
1152
+ /// # Safety
1153
+ ///
1154
+ /// - `query` must be a valid pointer returned by `rdx_cypher_parse`.
1155
+ /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
1156
+ /// - `format` must be a valid, null-terminated UTF-8 string.
1157
+ #[unsafe(no_mangle)]
1158
+ pub unsafe extern "C" fn rdx_query_run(
1159
+ query: *const c_void,
1160
+ pointer: GraphPointer,
1161
+ format: *const c_char,
1162
+ ) -> CQueryResult {
1163
+ if query.is_null() {
1164
+ return CQueryResult::error("query is null");
1165
+ }
1166
+
1167
+ let Ok(format_str) = (unsafe { utils::convert_char_ptr_to_string(format) }) else {
1168
+ return CQueryResult::error("format is not valid UTF-8");
1169
+ };
1170
+
1171
+ let output_format = match format_str.as_str() {
1172
+ "table" => OutputFormat::Table,
1173
+ "json" => OutputFormat::Json,
1174
+ other => {
1175
+ return CQueryResult::error(&format!("unknown query format `{other}` (expected `table` or `json`)"));
1176
+ }
1177
+ };
1178
+
1179
+ let parsed = unsafe { &*query.cast::<cypher::Query>() };
1180
+
1181
+ with_graph(pointer, |graph| {
1182
+ match cypher::run_parsed(graph, parsed, output_format) {
1183
+ Ok(output) => CQueryResult::success(&output),
1184
+ Err(error) => CQueryResult::error(&error.to_string()),
1185
+ }
1186
+ })
1187
+ }
1188
+
1189
+ /// Returns a description of the queryable Cypher schema (node labels, relationship types, and
1190
+ /// properties) in the given format (`"table"` or `"json"`). The schema is static and requires no
1191
+ /// graph. Caller must free the returned pointer with `free_c_string`.
1192
+ ///
1193
+ /// # Safety
1194
+ ///
1195
+ /// - `format` must be a valid, null-terminated UTF-8 string.
1196
+ #[unsafe(no_mangle)]
1197
+ pub unsafe extern "C" fn rdx_cypher_schema(format: *const c_char) -> *const c_char {
1198
+ let format_str = unsafe { utils::convert_char_ptr_to_string(format) }.unwrap_or_else(|_| "table".to_string());
1199
+ let output_format = if format_str == "json" {
1200
+ OutputFormat::Json
1201
+ } else {
1202
+ OutputFormat::Table
1203
+ };
1204
+
1205
+ CString::new(cypher::schema(output_format)).map_or(ptr::null(), |s| s.into_raw().cast_const())
1206
+ }
1207
+
1061
1208
  #[repr(u8)]
1062
1209
  #[derive(Debug, Clone, Copy)]
1063
1210
  pub enum CVisibility {
@@ -1166,7 +1313,7 @@ mod tests {
1166
1313
  .ref_count()
1167
1314
  );
1168
1315
 
1169
- let graph_ptr = Box::into_raw(Box::new(graph)) as GraphPointer;
1316
+ let graph_ptr = Box::into_raw(Box::new(RwLock::new(graph))) as GraphPointer;
1170
1317
 
1171
1318
  // Build the nesting array: ["Foo"] since BAR is inside class Foo
1172
1319
  let nesting_strings = [CString::new("Foo").unwrap()];
@@ -1182,7 +1329,8 @@ mod tests {
1182
1329
  assert_eq!((*decl).id(), *DeclarationId::from("Foo::BAR"));
1183
1330
  };
1184
1331
 
1185
- let graph = unsafe { Box::from_raw(graph_ptr.cast::<Graph>()) };
1332
+ let graph = unsafe { Box::from_raw(graph_ptr.cast::<RwLock<Graph>>()) };
1333
+ let graph = graph.read().unwrap();
1186
1334
 
1187
1335
  assert_eq!(
1188
1336
  1,
@@ -120,7 +120,7 @@ pub unsafe extern "C" fn rdx_method_references_iter_free(iter: *mut MethodRefere
120
120
  unsafe { MethodReferencesIter::free(iter) }
121
121
  }
122
122
 
123
- /// Returns the UTF-8 name string for a constant reference id.
123
+ /// Returns the UTF-8 name string for a constant reference id, or NULL if the reference cannot be found.
124
124
  /// Caller must free with `free_c_string`.
125
125
  ///
126
126
  /// # Safety
@@ -129,12 +129,14 @@ pub unsafe extern "C" fn rdx_method_references_iter_free(iter: *mut MethodRefere
129
129
  ///
130
130
  /// # Panics
131
131
  ///
132
- /// This function will panic if the reference cannot be found.
132
+ /// This function will panic if the reference's name cannot be found.
133
133
  #[unsafe(no_mangle)]
134
134
  pub unsafe extern "C" fn rdx_constant_reference_name(pointer: GraphPointer, reference_id: u64) -> *const c_char {
135
135
  with_graph(pointer, |graph| {
136
136
  let ref_id = ConstantReferenceId::new(reference_id);
137
- let reference = graph.constant_references().get(&ref_id).expect("Reference not found");
137
+ let Some(reference) = graph.constant_references().get(&ref_id) else {
138
+ return ptr::null();
139
+ };
138
140
  let name = graph.names().get(reference.name_id()).expect("Name ID should exist");
139
141
 
140
142
  let name_string = graph
@@ -146,7 +148,7 @@ pub unsafe extern "C" fn rdx_constant_reference_name(pointer: GraphPointer, refe
146
148
  })
147
149
  }
148
150
 
149
- /// Returns the UTF-8 name string for a method reference id.
151
+ /// Returns the UTF-8 name string for a method reference id, or NULL if the reference cannot be found.
150
152
  /// Caller must free with `free_c_string`.
151
153
  ///
152
154
  /// # Safety
@@ -155,12 +157,14 @@ pub unsafe extern "C" fn rdx_constant_reference_name(pointer: GraphPointer, refe
155
157
  ///
156
158
  /// # Panics
157
159
  ///
158
- /// This function will panic if the reference cannot be found.
160
+ /// This function will panic if the reference's name cannot be found.
159
161
  #[unsafe(no_mangle)]
160
162
  pub unsafe extern "C" fn rdx_method_reference_name(pointer: GraphPointer, reference_id: u64) -> *const c_char {
161
163
  with_graph(pointer, |graph| {
162
164
  let ref_id = MethodReferenceId::new(reference_id);
163
- let reference = graph.method_references().get(&ref_id).expect("Reference not found");
165
+ let Some(reference) = graph.method_references().get(&ref_id) else {
166
+ return ptr::null();
167
+ };
164
168
  let name = graph
165
169
  .strings()
166
170
  .get(reference.str())
@@ -170,7 +174,7 @@ pub unsafe extern "C" fn rdx_method_reference_name(pointer: GraphPointer, refere
170
174
  })
171
175
  }
172
176
 
173
- /// Returns a newly allocated `Location` for the given constant reference id.
177
+ /// Returns a newly allocated `Location` for the given constant reference id, or NULL if the reference cannot be found.
174
178
  /// Caller must free the returned pointer with `rdx_location_free`.
175
179
  ///
176
180
  /// # Safety
@@ -180,12 +184,14 @@ pub unsafe extern "C" fn rdx_method_reference_name(pointer: GraphPointer, refere
180
184
  ///
181
185
  /// # Panics
182
186
  ///
183
- /// This function will panic if a reference or document cannot be found.
187
+ /// This function will panic if the reference's document cannot be found.
184
188
  #[unsafe(no_mangle)]
185
189
  pub unsafe extern "C" fn rdx_constant_reference_location(pointer: GraphPointer, reference_id: u64) -> *mut Location {
186
190
  with_graph(pointer, |graph| {
187
191
  let ref_id = ConstantReferenceId::new(reference_id);
188
- let reference = graph.constant_references().get(&ref_id).expect("Reference not found");
192
+ let Some(reference) = graph.constant_references().get(&ref_id) else {
193
+ return ptr::null_mut();
194
+ };
189
195
  let document = graph
190
196
  .documents()
191
197
  .get(&reference.uri_id())
@@ -226,6 +232,25 @@ pub unsafe extern "C" fn rdx_resolved_constant_reference_declaration(
226
232
  })
227
233
  }
228
234
 
235
+ /// Returns a pointer to the URI ID of the document a constant reference belongs
236
+ /// to, or NULL if the reference cannot be found. Caller must free the returned
237
+ /// pointer with `free_u64`.
238
+ ///
239
+ /// # Safety
240
+ /// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`.
241
+ /// - `reference_id` must be a valid reference id.
242
+ #[unsafe(no_mangle)]
243
+ pub unsafe extern "C" fn rdx_constant_reference_document(pointer: GraphPointer, reference_id: u64) -> *const u64 {
244
+ with_graph(pointer, |graph| {
245
+ let ref_id = ConstantReferenceId::new(reference_id);
246
+ if let Some(reference) = graph.constant_references().get(&ref_id) {
247
+ Box::into_raw(Box::new(*reference.uri_id())).cast_const()
248
+ } else {
249
+ ptr::null()
250
+ }
251
+ })
252
+ }
253
+
229
254
  /// Returns the declaration of the resolved receiver for the given method reference. Returns NULL when the method
230
255
  /// reference has no tracked receiver or when the receiver could not be resolved. Caller must free with
231
256
  /// `free_c_declaration`.
@@ -263,7 +288,7 @@ pub unsafe extern "C" fn rdx_method_reference_receiver_declaration(
263
288
  })
264
289
  }
265
290
 
266
- /// Returns a newly allocated `Location` for the given method reference id.
291
+ /// Returns a newly allocated `Location` for the given method reference id, or NULL if the reference cannot be found.
267
292
  /// Caller must free the returned pointer with `rdx_location_free`.
268
293
  ///
269
294
  /// # Safety
@@ -273,12 +298,14 @@ pub unsafe extern "C" fn rdx_method_reference_receiver_declaration(
273
298
  ///
274
299
  /// # Panics
275
300
  ///
276
- /// This function will panic if a reference or document cannot be found.
301
+ /// This function will panic if the reference's document cannot be found.
277
302
  #[unsafe(no_mangle)]
278
303
  pub unsafe extern "C" fn rdx_method_reference_location(pointer: GraphPointer, reference_id: u64) -> *mut Location {
279
304
  with_graph(pointer, |graph| {
280
305
  let ref_id = MethodReferenceId::new(reference_id);
281
- let reference = graph.method_references().get(&ref_id).expect("Reference not found");
306
+ let Some(reference) = graph.method_references().get(&ref_id) else {
307
+ return ptr::null_mut();
308
+ };
282
309
  let document = graph
283
310
  .documents()
284
311
  .get(&reference.uri_id())
@@ -288,6 +315,25 @@ pub unsafe extern "C" fn rdx_method_reference_location(pointer: GraphPointer, re
288
315
  })
289
316
  }
290
317
 
318
+ /// Returns a pointer to the URI ID of the document a method reference belongs
319
+ /// to, or NULL if the reference cannot be found. Caller must free the returned
320
+ /// pointer with `free_u64`.
321
+ ///
322
+ /// # Safety
323
+ /// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`.
324
+ /// - `reference_id` must be a valid reference id.
325
+ #[unsafe(no_mangle)]
326
+ pub unsafe extern "C" fn rdx_method_reference_document(pointer: GraphPointer, reference_id: u64) -> *const u64 {
327
+ with_graph(pointer, |graph| {
328
+ let ref_id = MethodReferenceId::new(reference_id);
329
+ if let Some(reference) = graph.method_references().get(&ref_id) {
330
+ Box::into_raw(Box::new(*reference.uri_id())).cast_const()
331
+ } else {
332
+ ptr::null()
333
+ }
334
+ })
335
+ }
336
+
291
337
  /// Frees a `CConstantReference` previously returned by an FFI function.
292
338
  ///
293
339
  /// # Safety
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubydex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-08 00:00:00.000000000 Z
11
+ date: 2026-07-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A high-performance static analysis suite for Ruby, built in Rust with
14
14
  Ruby APIs
@@ -16,7 +16,6 @@ email:
16
16
  - ruby@shopify.com
17
17
  executables:
18
18
  - rdx
19
- - rubydex_mcp
20
19
  extensions:
21
20
  - ext/rubydex/extconf.rb
22
21
  extra_rdoc_files: []
@@ -25,7 +24,6 @@ files:
25
24
  - README.md
26
25
  - THIRD_PARTY_LICENSES.html
27
26
  - exe/rdx
28
- - exe/rubydex_mcp
29
27
  - ext/rubydex/declaration.c
30
28
  - ext/rubydex/declaration.h
31
29
  - ext/rubydex/definition.c
@@ -40,6 +38,8 @@ files:
40
38
  - ext/rubydex/handle.h
41
39
  - ext/rubydex/location.c
42
40
  - ext/rubydex/location.h
41
+ - ext/rubydex/query.c
42
+ - ext/rubydex/query.h
43
43
  - ext/rubydex/reference.c
44
44
  - ext/rubydex/reference.h
45
45
  - ext/rubydex/rubydex.c
@@ -48,7 +48,6 @@ files:
48
48
  - ext/rubydex/utils.c
49
49
  - ext/rubydex/utils.h
50
50
  - lib/rubydex.rb
51
- - lib/rubydex/bin/rubydex_mcp.exe
52
51
  - lib/rubydex/comment.rb
53
52
  - lib/rubydex/declaration.rb
54
53
  - lib/rubydex/diagnostic.rb
@@ -58,6 +57,15 @@ files:
58
57
  - lib/rubydex/keyword.rb
59
58
  - lib/rubydex/keyword_parameter.rb
60
59
  - lib/rubydex/location.rb
60
+ - lib/rubydex/mcp_server.rb
61
+ - lib/rubydex/mcp_server/protocol.rb
62
+ - lib/rubydex/mcp_server/tools/base_tool.rb
63
+ - lib/rubydex/mcp_server/tools/codebase_stats_tool.rb
64
+ - lib/rubydex/mcp_server/tools/find_constant_references_tool.rb
65
+ - lib/rubydex/mcp_server/tools/get_declaration_tool.rb
66
+ - lib/rubydex/mcp_server/tools/get_descendants_tool.rb
67
+ - lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb
68
+ - lib/rubydex/mcp_server/tools/search_declarations_tool.rb
61
69
  - lib/rubydex/mixin.rb
62
70
  - lib/rubydex/reference.rb
63
71
  - lib/rubydex/signature.rb
@@ -68,11 +76,6 @@ files:
68
76
  - rust/about.toml
69
77
  - rust/about_templates/about.hbs
70
78
  - rust/about_templates/mingw_licenses.hbs
71
- - rust/rubydex-mcp/Cargo.toml
72
- - rust/rubydex-mcp/src/main.rs
73
- - rust/rubydex-mcp/src/server.rs
74
- - rust/rubydex-mcp/src/tools.rs
75
- - rust/rubydex-mcp/tests/mcp.rs
76
79
  - rust/rubydex-sys/Cargo.toml
77
80
  - rust/rubydex-sys/build.rs
78
81
  - rust/rubydex-sys/cbindgen.toml
@@ -127,6 +130,10 @@ files:
127
130
  - rust/rubydex/src/operation/ruby_builder.rs
128
131
  - rust/rubydex/src/position.rs
129
132
  - rust/rubydex/src/query.rs
133
+ - rust/rubydex/src/query/cypher.rs
134
+ - rust/rubydex/src/query/cypher/schema.rs
135
+ - rust/rubydex/src/query/cypher/schema_info.rs
136
+ - rust/rubydex/src/query/cypher/tests.rs
130
137
  - rust/rubydex/src/resolution.rs
131
138
  - rust/rubydex/src/resolution_tests.rs
132
139
  - rust/rubydex/src/stats.rs
data/exe/rubydex_mcp DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require "rbconfig"
5
-
6
- host_os = RbConfig::CONFIG.fetch("host_os")
7
- executable = host_os.match?(/mswin|mingw|cygwin/) ? "rubydex_mcp.exe" : "rubydex_mcp"
8
- binary = File.expand_path("../lib/rubydex/bin/#{executable}", __dir__)
9
-
10
- unless File.executable?(binary)
11
- abort(<<~MESSAGE.chomp)
12
- rubydex_mcp is not available at #{binary}.
13
- Install a precompiled rubydex gem, or reinstall rubydex with Cargo available so the MCP executable can be built locally.
14
- MESSAGE
15
- end
16
-
17
- exec(binary, *ARGV)
Binary file
@@ -1,34 +0,0 @@
1
- [package]
2
- name = "rubydex-mcp"
3
- version = "0.2.6"
4
- edition = "2024"
5
- rust-version = "1.89.0"
6
- license = "MIT"
7
- description = "MCP server exposing Rubydex semantic Ruby code intelligence tools."
8
- homepage = "https://github.com/Shopify/rubydex"
9
- repository = "https://github.com/Shopify/rubydex"
10
- readme = "README.md"
11
- keywords = ["ruby", "mcp", "static-analysis"]
12
- categories = ["development-tools"]
13
-
14
- [[bin]]
15
- name = "rubydex_mcp"
16
- path = "src/main.rs"
17
-
18
- [dependencies]
19
- rubydex = { version = "0.2.6", path = "../rubydex" }
20
- clap = { version = "4.5.16", features = ["derive"] }
21
- rmcp = { version = "1.4", features = ["server", "macros", "transport-io", "schemars"] }
22
- tokio = { version = "1", features = ["macros", "rt", "io-std"] }
23
- serde = { version = "1", features = ["derive"] }
24
- serde_json = "1"
25
- schemars = "1"
26
- url = "2"
27
-
28
- [dev-dependencies]
29
- rubydex = { version = "0.2.6", path = "../rubydex", features = ["test_utils"] }
30
- assert_cmd = "2.0"
31
- serde_json = "1"
32
-
33
- [lints]
34
- workspace = true
@@ -1,48 +0,0 @@
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
- }