Skip to content

Commit cb6ca8c

Browse files
committed
feat: expose baseline table reads over mcp
1 parent f6cacc4 commit cb6ca8c

5 files changed

Lines changed: 358 additions & 15 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ automatically. `--config` overrides that path.
8585
`raysense mcp` runs a stdio MCP server for agents. It exposes tools to read and
8686
write config, run health, inspect scan facts, list dependency edges, read
8787
hotspots, read rule findings, read DSM module edges, and materialize memory
88-
table summaries. It can also save and diff baselines.
88+
table summaries. It can also save/diff baselines and read saved baseline
89+
tables.
8990

9091
Baselines are stored under `<path>/.raysense/baseline` by default. The manifest
9192
is JSON for fast agent diffs, and baseline tables are written under `tables/`
@@ -141,7 +142,8 @@ codebases:
141142
- Rule thresholds can be configured with TOML.
142143
- Forbidden top-level module dependencies can be configured with TOML.
143144
- Config read/write, health runs, scan facts, edges, hotspots, rule findings,
144-
module edges, and memory summaries are exposed through the MCP interface.
145+
module edges, memory summaries, and saved baseline tables are exposed through
146+
the MCP interface.
145147
- Baseline save/diff is available through the CLI and MCP, with Rayforce
146148
splayed-table storage for baseline tables.
147149
- Rayforce table materialization for scan facts, call facts, call edges,

crates/rayforce-sys/src/lib.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,20 @@ use std::os::raw::{c_char, c_int};
2828

2929
pub const RAY_I32: i8 = 4;
3030
pub const RAY_I64: i8 = 5;
31+
pub const RAY_F64: i8 = 8;
3132
pub const RAY_STR: i8 = 13;
3233
pub const RAY_TABLE: i8 = 98;
34+
pub const RAY_ERROR: i8 = 127;
3335

3436
#[repr(C)]
3537
pub struct ray_t {
36-
_private: [u8; 0],
38+
pub header: [u8; 16],
39+
pub mmod: u8,
40+
pub order: u8,
41+
pub type_: i8,
42+
pub attrs: u8,
43+
pub rc: u32,
44+
pub len: i64,
3745
}
3846

3947
unsafe extern "C" {
@@ -43,17 +51,24 @@ unsafe extern "C" {
4351
pub fn ray_version_string() -> *const c_char;
4452

4553
pub fn ray_release(v: *mut ray_t);
54+
pub fn ray_err_code(err: *mut ray_t) -> *const c_char;
4655

4756
pub fn ray_sym_init() -> ray_err_t;
4857
pub fn ray_sym_destroy();
4958
pub fn ray_sym_intern(str: *const c_char, len: usize) -> i64;
59+
pub fn ray_sym_str(id: i64) -> *mut ray_t;
5060

5161
pub fn ray_vec_new(type_: i8, capacity: i64) -> *mut ray_t;
5262
pub fn ray_vec_append(vec: *mut ray_t, elem: *const std::ffi::c_void) -> *mut ray_t;
5363
pub fn ray_str_vec_append(vec: *mut ray_t, s: *const c_char, len: usize) -> *mut ray_t;
64+
pub fn ray_str_vec_get(vec: *mut ray_t, idx: i64, out_len: *mut usize) -> *const c_char;
65+
pub fn ray_str_ptr(s: *mut ray_t) -> *const c_char;
66+
pub fn ray_str_len(s: *mut ray_t) -> usize;
5467

5568
pub fn ray_table_new(ncols: i64) -> *mut ray_t;
5669
pub fn ray_table_add_col(tbl: *mut ray_t, name_id: i64, col_vec: *mut ray_t) -> *mut ray_t;
70+
pub fn ray_table_get_col_idx(tbl: *mut ray_t, idx: i64) -> *mut ray_t;
71+
pub fn ray_table_col_name(tbl: *mut ray_t, idx: i64) -> i64;
5772
pub fn ray_table_ncols(tbl: *mut ray_t) -> i64;
5873
pub fn ray_table_nrows(tbl: *mut ray_t) -> i64;
5974

crates/raysense-cli/src/mcp.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,16 @@ fn tools_list() -> Value {
191191
"name": "raysense_baseline_diff",
192192
"description": "Diff the current project health against a saved baseline.",
193193
"inputSchema": baseline_schema("Baseline directory. Defaults to <path>/.raysense/baseline.")
194+
},
195+
{
196+
"name": "raysense_baseline_tables",
197+
"description": "List Rayforce splayed tables saved in a Raysense baseline.",
198+
"inputSchema": baseline_table_schema(false)
199+
},
200+
{
201+
"name": "raysense_baseline_table_read",
202+
"description": "Read rows from a Rayforce splayed table saved in a Raysense baseline.",
203+
"inputSchema": baseline_table_schema(true)
194204
}
195205
]
196206
})
@@ -218,6 +228,8 @@ fn call_tool(params: &Value) -> Result<Value> {
218228
"raysense_memory_summary" => memory_summary_tool(&args),
219229
"raysense_baseline_save" => baseline_save_tool(&args),
220230
"raysense_baseline_diff" => baseline_diff_tool(&args),
231+
"raysense_baseline_tables" => baseline_tables_tool(&args),
232+
"raysense_baseline_table_read" => baseline_table_read_tool(&args),
221233
_ => Err(anyhow!("unknown tool {name}")),
222234
}
223235
}
@@ -429,6 +441,49 @@ fn baseline_diff_tool(args: &Value) -> Result<Value> {
429441
}))
430442
}
431443

444+
fn baseline_tables_tool(args: &Value) -> Result<Value> {
445+
let root = root_arg(args)?;
446+
let baseline_dir = baseline_dir_arg(args, &root)?;
447+
let tables_dir = baseline_dir.join("tables");
448+
let tables = raysense_memory::list_baseline_tables(&tables_dir)
449+
.with_context(|| format!("failed to list baseline tables {}", tables_dir.display()))?;
450+
451+
Ok(json!({
452+
"baseline_path": baseline_dir,
453+
"tables_path": tables_dir,
454+
"tables": tables
455+
}))
456+
}
457+
458+
fn baseline_table_read_tool(args: &Value) -> Result<Value> {
459+
let root = root_arg(args)?;
460+
let baseline_dir = baseline_dir_arg(args, &root)?;
461+
let tables_dir = baseline_dir.join("tables");
462+
let table = args
463+
.get("table")
464+
.and_then(Value::as_str)
465+
.ok_or_else(|| anyhow!("table must be a string"))?;
466+
let offset = args
467+
.get("offset")
468+
.map(|value| {
469+
value
470+
.as_u64()
471+
.map(|value| value as usize)
472+
.ok_or_else(|| anyhow!("offset must be a non-negative integer"))
473+
})
474+
.transpose()?
475+
.unwrap_or(0);
476+
let limit = limit_arg(args, 100)?;
477+
let table_rows = raysense_memory::read_baseline_table(&tables_dir, table, offset, limit)
478+
.with_context(|| format!("failed to read baseline table {}", tables_dir.display()))?;
479+
480+
Ok(json!({
481+
"baseline_path": baseline_dir,
482+
"tables_path": tables_dir,
483+
"table": table_rows
484+
}))
485+
}
486+
432487
fn health_from_args(args: &Value) -> Result<(PathBuf, raysense_core::HealthSummary)> {
433488
let root = root_arg(args)?;
434489
let config = effective_config(args, &root)?;
@@ -660,6 +715,23 @@ fn baseline_schema(path_description: &str) -> Value {
660715
})
661716
}
662717

718+
fn baseline_table_schema(require_table: bool) -> Value {
719+
let mut schema = json!({
720+
"type": "object",
721+
"properties": {
722+
"path": {"type": "string", "description": "Project root. Defaults to the current directory."},
723+
"baseline_path": {"type": "string", "description": "Baseline directory. Defaults to <path>/.raysense/baseline."},
724+
"table": {"type": "string", "description": "Baseline table name, such as files, functions, imports, calls, call_edges, health, hotspots, rules, module_edges, or changed_files."},
725+
"offset": {"type": "integer", "minimum": 0, "description": "First row offset. Defaults to 0."},
726+
"limit": {"type": "integer", "minimum": 1, "description": "Maximum rows to return. Defaults to 100."}
727+
}
728+
});
729+
if require_table {
730+
schema["required"] = json!(["table"]);
731+
}
732+
schema
733+
}
734+
663735
fn limited<T: serde::Serialize>(items: &[T], limit: usize) -> Vec<Value> {
664736
items
665737
.iter()
@@ -695,6 +767,8 @@ mod tests {
695767
assert!(names.contains(&"raysense_memory_summary"));
696768
assert!(names.contains(&"raysense_baseline_save"));
697769
assert!(names.contains(&"raysense_baseline_diff"));
770+
assert!(names.contains(&"raysense_baseline_tables"));
771+
assert!(names.contains(&"raysense_baseline_table_read"));
698772
}
699773

700774
#[test]

crates/raysense-memory/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ license.workspace = true
2929
rayforce-sys = { path = "../rayforce-sys" }
3030
raysense-core = { path = "../raysense-core" }
3131
serde.workspace = true
32+
serde_json.workspace = true
3233
thiserror.workspace = true

0 commit comments

Comments
 (0)