use serde_json::{Value, json};
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, Command, Stdio};
struct ChildGuard(Child);
impl std::ops::Deref for ChildGuard {
type Target = Child;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for ChildGuard {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Drop for ChildGuard {
fn drop(&mut self) {
if matches!(self.0.try_wait(), Ok(None)) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
}
#[test]
fn test_mcp_stdio_handshake_and_tools() {
let temp_dir = code_kb_core::safe_tempdir();
let root = temp_dir.path().to_path_buf();
let db_dir = root.join(".code-kb");
std::fs::create_dir_all(&db_dir).unwrap();
let db_path = db_dir.join("artifact.db");
let src_dir = root.join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let content = "pub struct Workspace {\n pub root: String,\n}\n";
std::fs::write(src_dir.join("workspace.rs"), content).unwrap();
let bytes = content.len() as i64;
let hash = format!("blake3:{}", blake3::hash(content.as_bytes()).to_hex());
let conn = code_kb_core::open_read_write(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE files (
file_id TEXT PRIMARY KEY, path TEXT, language TEXT, content_hash TEXT,
content_bytes INTEGER, line_count INTEGER, indexed_at TEXT
);
CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
CREATE TABLE relationships (
relationship_id TEXT PRIMARY KEY, from_symbol_id TEXT, to_symbol_id TEXT,
kind TEXT, path TEXT, start_line INTEGER, start_column INTEGER
);
CREATE TABLE pending_relationships (
from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT,
path TEXT, start_line INTEGER, start_column INTEGER
);
CREATE TABLE structural_facts (
fact_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT,
pattern_id TEXT, kind TEXT, name TEXT, receiver TEXT, symbol_id TEXT,
scope_symbol_id TEXT, parent_fact_id TEXT, start_line INTEGER,
start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, confidence REAL, payload TEXT
);
CREATE TABLE literals (
literal_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT,
kind TEXT, value TEXT, scope_symbol_id TEXT, start_line INTEGER,
start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER
);",
)
.unwrap();
conn.execute(
"INSERT INTO files VALUES ('f1', 'src/workspace.rs', 'rust', ?1, ?2, 3, '2026-01-01')",
rusqlite::params![hash, bytes],
)
.unwrap();
conn.execute(
"INSERT INTO symbols VALUES (
's1', 'f1', 'src/workspace.rs', 'rust', 'Workspace', 'struct',
'pub struct Workspace', 'Workspace representation for code-kb workspace discovery root', 'pub', NULL,
1, 0, 3, 1, 0, ?1, 1, 21, 3, 1, 21, ?1, 'b3:hash',
NULL, 0, 0
)",
rusqlite::params![bytes],
)
.unwrap();
conn.execute(
"INSERT INTO pending_relationships VALUES ('s1', 'println', 'call', 'src/workspace.rs', 2, 4)",
[],
)
.unwrap();
code_kb_core::db::ensure_fts_index(&conn).unwrap();
drop(conn);
let mut child = ChildGuard(
Command::new(env!("CARGO_BIN_EXE_code-kb"))
.env("CODE_KB_TELEMETRY_DIR", root.join(".telemetry_test"))
.arg("serve")
.arg("--root")
.arg(&root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn code-kb serve"),
);
let mut stdin = child.stdin.take().expect("Failed to open stdin");
let stdout = child.stdout.take().expect("Failed to open stdout");
let mut reader = std::io::BufReader::new(stdout);
let init_req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "test-agent",
"version": "1.0"
}
}
});
let mut line = serde_json::to_string(&init_req).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line = String::new();
use std::io::BufRead;
reader.read_line(&mut response_line).unwrap();
let resp: Value = serde_json::from_str(&response_line).expect("Failed to parse JSON response");
assert_eq!(resp["id"], 1);
assert_eq!(resp["result"]["serverInfo"]["name"], "code-kb");
assert_eq!(
resp["result"]["serverInfo"]["version"],
env!("CARGO_PKG_VERSION")
);
assert!(
resp["result"]["instructions"]
.as_str()
.is_some_and(|instructions| !instructions.is_empty())
);
let list_req = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
});
let mut line2 = serde_json::to_string(&list_req).unwrap();
line2.push('\n');
stdin.write_all(line2.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line2 = String::new();
reader.read_line(&mut response_line2).unwrap();
let resp2: Value =
serde_json::from_str(&response_line2).expect("Failed to parse JSON response");
assert_eq!(resp2["id"], 2);
let tools = resp2["result"]["tools"]
.as_array()
.expect("Expected tools array");
assert_eq!(tools.len(), 11);
let tool_names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
assert!(tool_names.contains(&"codebase_outline"));
assert!(tool_names.contains(&"file_skeleton"));
assert!(tool_names.contains(&"lookup_symbol"));
assert!(tool_names.contains(&"search_symbols"));
assert!(tool_names.contains(&"get_symbol_body"));
assert!(tool_names.contains(&"get_symbol_context"));
assert!(tool_names.contains(&"find_references"));
assert!(tool_names.contains(&"find_structural_facts"));
assert!(tool_names.contains(&"blast_radius"));
assert!(tool_names.contains(&"replace_symbol_body"));
assert!(tool_names.contains(&"telemetry_summary"));
for tool in tools {
let schema = &tool["inputSchema"];
let props = &schema["properties"];
assert!(
props.get("workspace").is_none()
&& props.get("workspace_id").is_none()
&& props.get("repo_path").is_none()
&& props.get("root_dir").is_none(),
"Tool '{}' should NOT expose workspace parameters in schema",
tool["name"]
);
}
let call_req = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "lookup_symbol",
"arguments": {
"query": "Workspace",
"kind": "struct"
}
}
});
let mut line3 = serde_json::to_string(&call_req).unwrap();
line3.push('\n');
stdin.write_all(line3.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line3 = String::new();
reader.read_line(&mut response_line3).unwrap();
let resp3: Value =
serde_json::from_str(&response_line3).expect("Failed to parse JSON response");
assert_eq!(resp3["id"], 3);
let content_text = resp3["result"]["content"][0]["text"].as_str().unwrap();
assert!(content_text.contains("Workspace"));
let search_req = json!({
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "search_symbols",
"arguments": {
"query": "workspace discovery root"
}
}
});
let mut line4 = serde_json::to_string(&search_req).unwrap();
line4.push('\n');
stdin.write_all(line4.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line4 = String::new();
reader.read_line(&mut response_line4).unwrap();
let resp4: Value =
serde_json::from_str(&response_line4).expect("Failed to parse JSON response");
assert_eq!(resp4["id"], 4);
let search_text = resp4["result"]["content"][0]["text"].as_str().unwrap();
assert!(search_text.contains("Workspace"));
let refs_req = json!({
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "find_references",
"arguments": {
"symbol": "Workspace"
}
}
});
let mut line5 = serde_json::to_string(&refs_req).unwrap();
line5.push('\n');
stdin.write_all(line5.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line5 = String::new();
reader.read_line(&mut response_line5).unwrap();
let resp5: Value =
serde_json::from_str(&response_line5).expect("Failed to parse JSON response");
assert_eq!(resp5["id"], 5);
assert!(resp5["error"].is_null());
assert_ne!(resp5["result"]["isError"], true);
let refs_text = resp5["result"]["content"][0]["text"].as_str().unwrap();
assert!(
refs_text.contains("Callers")
|| refs_text.contains("No callers found")
|| refs_text.contains("Workspace")
);
let facts_req = json!({
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "find_structural_facts",
"arguments": {}
}
});
let mut line6 = serde_json::to_string(&facts_req).unwrap();
line6.push('\n');
stdin.write_all(line6.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line6 = String::new();
reader.read_line(&mut response_line6).unwrap();
let resp6: Value =
serde_json::from_str(&response_line6).expect("Failed to parse JSON response");
assert_eq!(resp6["id"], 6);
assert!(resp6["error"].is_null());
assert_ne!(resp6["result"]["isError"], true);
let facts_text = resp6["result"]["content"][0]["text"].as_str().unwrap();
assert!(
facts_text.contains("Available structural fact categories")
|| facts_text.contains("No structural facts")
);
let skeleton_req = json!({
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "file_skeleton",
"arguments": {
"file": "src/workspace.rs"
}
}
});
let mut line7 = serde_json::to_string(&skeleton_req).unwrap();
line7.push('\n');
stdin.write_all(line7.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line7 = String::new();
reader.read_line(&mut response_line7).unwrap();
let resp7: Value =
serde_json::from_str(&response_line7).expect("Failed to parse JSON response");
assert_eq!(resp7["id"], 7);
let skeleton_text = resp7["result"]["content"][0]["text"].as_str().unwrap();
assert!(skeleton_text.contains("pub struct Workspace"));
let blast_req = json!({
"jsonrpc": "2.0",
"id": 8,
"method": "tools/call",
"params": {
"name": "blast_radius",
"arguments": {
"symbol": "Workspace"
}
}
});
let mut line8 = serde_json::to_string(&blast_req).unwrap();
line8.push('\n');
stdin.write_all(line8.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line8 = String::new();
reader.read_line(&mut response_line8).unwrap();
let resp8: Value =
serde_json::from_str(&response_line8).expect("Failed to parse JSON response");
assert_eq!(resp8["id"], 8);
let blast_text = resp8["result"]["content"][0]["text"].as_str().unwrap();
assert!(blast_text.contains("Blast Radius"));
let slice_req = json!({
"jsonrpc": "2.0",
"id": 9,
"method": "tools/call",
"params": {
"name": "get_symbol_context",
"arguments": {
"symbol_name": "Workspace",
"include_external": false
}
}
});
let mut line9 = serde_json::to_string(&slice_req).unwrap();
line9.push('\n');
stdin.write_all(line9.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line9 = String::new();
reader.read_line(&mut response_line9).unwrap();
let resp9: Value =
serde_json::from_str(&response_line9).expect("Failed to parse JSON response");
assert_eq!(resp9["id"], 9);
assert_ne!(resp9["result"]["isError"], true);
let slice_text = resp9["result"]["content"][0]["text"].as_str().unwrap();
assert!(slice_text.contains("Workspace"));
assert!(
!slice_text.contains("println"),
"Default get_symbol_context should not contain external callee println"
);
let slice_req10 = json!({
"jsonrpc": "2.0",
"id": 10,
"method": "tools/call",
"params": {
"name": "get_symbol_context",
"arguments": {
"symbol_name": "Workspace",
"include_external": true
}
}
});
let mut line10 = serde_json::to_string(&slice_req10).unwrap();
line10.push('\n');
stdin.write_all(line10.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line10 = String::new();
reader.read_line(&mut response_line10).unwrap();
let resp10: Value =
serde_json::from_str(&response_line10).expect("Failed to parse JSON response");
assert_eq!(resp10["id"], 10);
assert_ne!(resp10["result"]["isError"], true);
let slice_text10 = resp10["result"]["content"][0]["text"].as_str().unwrap();
assert!(slice_text10.contains("Workspace"));
assert!(
slice_text10.contains("println"),
"Extended get_context_slice must contain external callee println"
);
let telem_req = json!({
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "telemetry_summary",
"arguments": {
"time_window": "month"
}
}
});
let mut telem_line = serde_json::to_string(&telem_req).unwrap();
telem_line.push('\n');
stdin.write_all(telem_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line_telem = String::new();
reader.read_line(&mut response_line_telem).unwrap();
let resp_telem: Value =
serde_json::from_str(&response_line_telem).expect("Failed to parse JSON response");
assert_eq!(resp_telem["id"], 11);
assert_ne!(resp_telem["result"]["isError"], true);
let telem_text = resp_telem["result"]["content"][0]["text"].as_str().unwrap();
assert!(telem_text.contains("Telemetry Summary"));
let notification = json!({
"jsonrpc": "2.0",
"method": "notifications/roots/list_changed"
});
let mut notification_line = serde_json::to_string(¬ification).unwrap();
notification_line.push('\n');
stdin.write_all(notification_line.as_bytes()).unwrap();
let ping_req = json!({
"jsonrpc": "2.0",
"id": 12,
"method": "ping"
});
let mut ping_line = serde_json::to_string(&ping_req).unwrap();
ping_line.push('\n');
stdin.write_all(ping_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line12 = String::new();
reader.read_line(&mut response_line12).unwrap();
let resp12: Value =
serde_json::from_str(&response_line12).expect("Failed to parse JSON response");
assert_eq!(resp12["id"], 12);
drop(stdin);
let _ = child.wait();
}
#[test]
fn test_mcp_invalid_path_does_not_poison_session() {
let temp_dir = code_kb_core::safe_tempdir();
let root = temp_dir.path().to_path_buf();
let db_dir = root.join(".code-kb");
std::fs::create_dir_all(&db_dir).unwrap();
let db_path = db_dir.join("artifact.db");
let src_dir = root.join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let content = "pub fn valid_func() {}\n";
let file_path = src_dir.join("lib.rs");
std::fs::write(&file_path, content).unwrap();
let conn = code_kb_core::open_read_write(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE files (
file_id TEXT PRIMARY KEY, path TEXT, language TEXT, content_hash TEXT,
content_bytes INTEGER, line_count INTEGER, indexed_at TEXT
);
CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
CREATE TABLE relationships (
relationship_id TEXT PRIMARY KEY, from_symbol_id TEXT, to_symbol_id TEXT,
kind TEXT, path TEXT, start_line INTEGER, start_column INTEGER
);
CREATE TABLE pending_relationships (
from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT,
path TEXT, start_line INTEGER, start_column INTEGER
);",
)
.unwrap();
let bytes = content.len() as i64;
let hash = format!("blake3:{}", blake3::hash(content.as_bytes()).to_hex());
conn.execute(
"INSERT INTO files VALUES ('f1', 'src/lib.rs', 'rust', ?1, ?2, 1, '2026-01-01')",
rusqlite::params![hash, bytes],
)
.unwrap();
conn.execute(
"INSERT INTO symbols VALUES (
's1', 'f1', 'src/lib.rs', 'rust', 'valid_func', 'function',
'pub fn valid_func()', NULL, 'pub', NULL,
1, 0, 1, 22, 0, ?1, 1, 0, 1, 22, 0, ?1, 'b3:hash',
NULL, 0, 0
)",
rusqlite::params![bytes],
)
.unwrap();
code_kb_core::db::ensure_fts_index(&conn).unwrap();
drop(conn);
let mut child = ChildGuard(
Command::new(env!("CARGO_BIN_EXE_code-kb"))
.env("CODE_KB_TELEMETRY_DIR", root.join(".telemetry_test"))
.arg("serve")
.arg("--root")
.arg(&root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn code-kb serve"),
);
let mut stdin = child.stdin.take().expect("Failed to open stdin");
let stdout = child.stdout.take().expect("Failed to open stdout");
let mut reader = std::io::BufReader::new(stdout);
use std::io::BufRead;
let init_req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "test-agent", "version": "1.0" }
}
});
let mut line = serde_json::to_string(&init_req).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line = String::new();
reader.read_line(&mut resp_line).unwrap();
let invalid_call = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "file_skeleton",
"arguments": {
"file": "/nonexistent_abs_path/nowhere/does_not_exist.rs"
}
}
});
let mut line2 = serde_json::to_string(&invalid_call).unwrap();
line2.push('\n');
stdin.write_all(line2.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line2 = String::new();
reader.read_line(&mut resp_line2).unwrap();
let resp2: Value = serde_json::from_str(&resp_line2).unwrap();
assert_eq!(resp2["id"], 2);
assert!(resp2["result"]["isError"] == true || resp2["error"].is_object());
let valid_call = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "file_skeleton",
"arguments": {
"file": "src/lib.rs"
}
}
});
let mut line3 = serde_json::to_string(&valid_call).unwrap();
line3.push('\n');
stdin.write_all(line3.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line3 = String::new();
reader.read_line(&mut resp_line3).unwrap();
let resp3: Value = serde_json::from_str(&resp_line3).unwrap();
assert_eq!(resp3["id"], 3);
assert_ne!(resp3["result"]["isError"], true);
let text = resp3["result"]["content"][0]["text"].as_str().unwrap();
assert!(
text.contains("valid_func"),
"Must retrieve valid_func from original workspace"
);
drop(stdin);
let _ = child.wait();
}
#[test]
fn test_mcp_worktree_rebind() {
let temp_dir = code_kb_core::safe_tempdir();
let main_root = temp_dir.path().join("main_repo");
let wt_root = main_root.join(".worktrees").join("feature-x");
std::fs::create_dir_all(main_root.join(".git")).unwrap();
std::fs::create_dir_all(main_root.join(".code-kb")).unwrap();
std::fs::create_dir_all(main_root.join("src")).unwrap();
let main_file = main_root.join("src").join("main.rs");
let main_content = "pub fn main_fn() {}\n";
std::fs::write(&main_file, main_content).unwrap();
let main_db = main_root.join(".code-kb").join("artifact.db");
let conn = code_kb_core::open_read_write(&main_db).unwrap();
conn.execute_batch(
"CREATE TABLE files (
file_id TEXT PRIMARY KEY, path TEXT, language TEXT, content_hash TEXT,
content_bytes INTEGER, line_count INTEGER, indexed_at TEXT
);
CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
CREATE TABLE relationships (
relationship_id TEXT PRIMARY KEY, from_symbol_id TEXT, to_symbol_id TEXT,
kind TEXT, path TEXT, start_line INTEGER, start_column INTEGER
);
CREATE TABLE pending_relationships (
from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT,
path TEXT, start_line INTEGER, start_column INTEGER
);",
)
.unwrap();
let bytes = main_content.len() as i64;
let hash = format!("blake3:{}", blake3::hash(main_content.as_bytes()).to_hex());
conn.execute(
"INSERT INTO files VALUES ('f1', 'src/main.rs', 'rust', ?1, ?2, 1, '2026-01-01')",
rusqlite::params![hash, bytes],
)
.unwrap();
conn.execute(
"INSERT INTO symbols VALUES (
's1', 'f1', 'src/main.rs', 'rust', 'main_fn', 'function',
'pub fn main_fn()', NULL, 'pub', NULL,
1, 0, 1, 19, 0, ?1, 1, 0, 1, 19, 0, ?1, 'b3:hash',
NULL, 0, 0
)",
rusqlite::params![bytes],
)
.unwrap();
code_kb_core::db::ensure_fts_index(&conn).unwrap();
drop(conn);
std::fs::create_dir_all(wt_root.join(".code-kb")).unwrap();
std::fs::create_dir_all(wt_root.join("src")).unwrap();
let gitdir_path = main_root.join(".git").join("worktrees").join("feature-x");
std::fs::create_dir_all(&gitdir_path).unwrap();
std::fs::write(
wt_root.join(".git"),
format!("gitdir: {}\n", gitdir_path.display()),
)
.unwrap();
let wt_file = wt_root.join("src").join("feature.rs");
let wt_content = "pub fn feature_fn() {}\n";
std::fs::write(&wt_file, wt_content).unwrap();
let wt_db = wt_root.join(".code-kb").join("artifact.db");
let conn_wt = code_kb_core::open_read_write(&wt_db).unwrap();
conn_wt.execute_batch(
"CREATE TABLE files (
file_id TEXT PRIMARY KEY, path TEXT, language TEXT, content_hash TEXT,
content_bytes INTEGER, line_count INTEGER, indexed_at TEXT
);
CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
CREATE TABLE relationships (
relationship_id TEXT PRIMARY KEY, from_symbol_id TEXT, to_symbol_id TEXT,
kind TEXT, path TEXT, start_line INTEGER, start_column INTEGER
);
CREATE TABLE pending_relationships (
from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT,
path TEXT, start_line INTEGER, start_column INTEGER
);",
)
.unwrap();
let wt_bytes = wt_content.len() as i64;
let wt_hash = format!("blake3:{}", blake3::hash(wt_content.as_bytes()).to_hex());
conn_wt
.execute(
"INSERT INTO files VALUES ('f2', 'src/feature.rs', 'rust', ?1, ?2, 1, '2026-01-01')",
rusqlite::params![wt_hash, wt_bytes],
)
.unwrap();
conn_wt
.execute(
"INSERT INTO symbols VALUES (
's2', 'f2', 'src/feature.rs', 'rust', 'feature_fn', 'function',
'pub fn feature_fn()', NULL, 'pub', NULL,
1, 0, 1, 22, 0, ?1, 1, 0, 1, 22, 0, ?1, 'b3:hash',
NULL, 0, 0
)",
rusqlite::params![wt_bytes],
)
.unwrap();
code_kb_core::db::ensure_fts_index(&conn_wt).unwrap();
drop(conn_wt);
let mut child = ChildGuard(
Command::new(env!("CARGO_BIN_EXE_code-kb"))
.env("CODE_KB_TELEMETRY_DIR", main_root.join(".telemetry_test"))
.arg("serve")
.arg("--root")
.arg(&main_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn code-kb serve"),
);
let mut stdin = child.stdin.take().expect("Failed to open stdin");
let stdout = child.stdout.take().expect("Failed to open stdout");
let mut reader = std::io::BufReader::new(stdout);
use std::io::BufRead;
let init_req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "test-agent", "version": "1.0" }
}
});
let mut line = serde_json::to_string(&init_req).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line = String::new();
reader.read_line(&mut resp_line).unwrap();
let main_call = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "lookup_symbol",
"arguments": { "query": "main_fn" }
}
});
let mut line2 = serde_json::to_string(&main_call).unwrap();
line2.push('\n');
stdin.write_all(line2.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line2 = String::new();
reader.read_line(&mut resp_line2).unwrap();
let resp2: Value = serde_json::from_str(&resp_line2).unwrap();
assert_eq!(resp2["id"], 2);
assert!(
resp2["result"]["content"][0]["text"]
.as_str()
.unwrap()
.contains("main_fn")
);
let wt_call = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "file_skeleton",
"arguments": { "file": wt_file.to_string_lossy().to_string() }
}
});
let mut line3 = serde_json::to_string(&wt_call).unwrap();
line3.push('\n');
stdin.write_all(line3.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line3 = String::new();
reader.read_line(&mut resp_line3).unwrap();
let resp3: Value = serde_json::from_str(&resp_line3).unwrap();
assert_eq!(resp3["id"], 3);
assert!(
resp3["result"]["content"][0]["text"]
.as_str()
.unwrap()
.contains("feature_fn")
);
let back_call = json!({
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "file_skeleton",
"arguments": { "file": main_file.to_string_lossy().to_string() }
}
});
let mut line4 = serde_json::to_string(&back_call).unwrap();
line4.push('\n');
stdin.write_all(line4.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line4 = String::new();
reader.read_line(&mut resp_line4).unwrap();
let resp4: Value = serde_json::from_str(&resp_line4).unwrap();
assert_eq!(resp4["id"], 4);
assert!(
resp4["result"]["content"][0]["text"]
.as_str()
.unwrap()
.contains("main_fn")
);
drop(stdin);
let _ = child.wait();
}
#[test]
fn test_mcp_worktree_auto_copy_fast_path() {
let temp_dir = code_kb_core::safe_tempdir();
let main_root = temp_dir.path().join("main_repo");
let wt_root = main_root.join(".worktrees").join("feature-y");
std::fs::create_dir_all(main_root.join(".git")).unwrap();
std::fs::create_dir_all(main_root.join(".code-kb")).unwrap();
std::fs::create_dir_all(main_root.join("src")).unwrap();
let main_file = main_root.join("src").join("main.rs");
let main_content = "pub fn shared_fn() {}\n";
std::fs::write(&main_file, main_content).unwrap();
let main_db = main_root.join(".code-kb").join("artifact.db");
let conn = code_kb_core::open_read_write(&main_db).unwrap();
conn.execute_batch(
"CREATE TABLE files (
file_id TEXT PRIMARY KEY, path TEXT, language TEXT, content_hash TEXT,
content_bytes INTEGER, line_count INTEGER, indexed_at TEXT
);
CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
CREATE TABLE relationships (
relationship_id TEXT PRIMARY KEY, from_symbol_id TEXT, to_symbol_id TEXT,
kind TEXT, path TEXT, start_line INTEGER, start_column INTEGER
);
CREATE TABLE pending_relationships (
from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT,
path TEXT, start_line INTEGER, start_column INTEGER
);",
)
.unwrap();
let bytes = main_content.len() as i64;
let hash = format!("blake3:{}", blake3::hash(main_content.as_bytes()).to_hex());
conn.execute(
"INSERT INTO files VALUES ('f1', 'src/main.rs', 'rust', ?1, ?2, 1, '2026-01-01')",
rusqlite::params![hash, bytes],
)
.unwrap();
conn.execute(
"INSERT INTO symbols VALUES (
's1', 'f1', 'src/main.rs', 'rust', 'shared_fn', 'function',
'pub fn shared_fn()', NULL, 'pub', NULL,
1, 0, 1, 21, 0, ?1, 1, 0, 1, 21, 0, ?1, 'b3:hash',
NULL, 0, 0
)",
rusqlite::params![bytes],
)
.unwrap();
conn.execute_batch(
"CREATE TABLE artifact_metadata (
key TEXT PRIMARY KEY, value TEXT NOT NULL
);",
)
.unwrap();
conn.execute(
"INSERT INTO artifact_metadata VALUES ('root_path', ?1)",
rusqlite::params![code_kb_core::to_forward_slash(&main_root)],
)
.unwrap();
code_kb_core::db::ensure_fts_index(&conn).unwrap();
drop(conn);
std::fs::create_dir_all(wt_root.join("src")).unwrap();
let gitdir_path = main_root.join(".git").join("worktrees").join("feature-y");
std::fs::create_dir_all(&gitdir_path).unwrap();
std::fs::write(
wt_root.join(".git"),
format!("gitdir: {}\n", gitdir_path.display()),
)
.unwrap();
let wt_file = wt_root.join("src").join("main.rs");
std::fs::write(&wt_file, main_content).unwrap();
let wt_db = wt_root.join(".code-kb").join("artifact.db");
assert!(!wt_db.exists(), "Worktree DB must NOT exist initially");
let mut child = ChildGuard(
Command::new(env!("CARGO_BIN_EXE_code-kb"))
.env("CODE_KB_TELEMETRY_DIR", main_root.join(".telemetry_test"))
.arg("serve")
.arg("--root")
.arg(&main_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn code-kb serve"),
);
let mut stdin = child.stdin.take().expect("Failed to open stdin");
let stdout = child.stdout.take().expect("Failed to open stdout");
let mut reader = std::io::BufReader::new(stdout);
use std::io::BufRead;
let init_req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "test-agent", "version": "1.0" }
}
});
let mut line = serde_json::to_string(&init_req).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line = String::new();
reader.read_line(&mut resp_line).unwrap();
let wt_call = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "file_skeleton",
"arguments": { "file": wt_file.to_string_lossy().to_string() }
}
});
let mut line2 = serde_json::to_string(&wt_call).unwrap();
line2.push('\n');
stdin.write_all(line2.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line2 = String::new();
reader.read_line(&mut resp_line2).unwrap();
let resp2: Value = serde_json::from_str(&resp_line2).unwrap();
assert_eq!(resp2["id"], 2);
assert!(
resp2["result"]["content"][0]["text"]
.as_str()
.unwrap()
.contains("shared_fn"),
"Worktree query should succeed using copied parent DB"
);
assert!(wt_db.exists(), "Worktree DB should now exist on disk");
{
let wt_conn = code_kb_core::open_read_only(&wt_db).unwrap();
let retargeted_root: String = wt_conn
.query_row(
"SELECT value FROM artifact_metadata WHERE key = 'root_path'",
[],
|r| r.get(0),
)
.unwrap();
assert!(
code_kb_core::workspace::paths_equal(std::path::Path::new(&retargeted_root), &wt_root),
"artifact_metadata root_path must be retargeted to worktree root: got {retargeted_root}, expected {}",
wt_root.display()
);
}
drop(stdin);
let _ = child.wait();
}
fn setup_test_repo() -> tempfile::TempDir {
let temp_dir = code_kb_core::safe_tempdir();
let root = temp_dir.path().to_path_buf();
let db_dir = root.join(".code-kb");
std::fs::create_dir_all(&db_dir).unwrap();
let db_path = db_dir.join("artifact.db");
let src_dir = root.join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let content = "pub struct Workspace {\n pub root: String,\n}\n";
std::fs::write(src_dir.join("workspace.rs"), content).unwrap();
let bytes = content.len() as i64;
let hash = format!("blake3:{}", blake3::hash(content.as_bytes()).to_hex());
let conn = code_kb_core::open_read_write(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE files (
file_id TEXT PRIMARY KEY, path TEXT, language TEXT, content_hash TEXT,
content_bytes INTEGER, line_count INTEGER, indexed_at TEXT
);
CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
CREATE TABLE relationships (
relationship_id TEXT PRIMARY KEY, from_symbol_id TEXT, to_symbol_id TEXT,
kind TEXT, path TEXT, start_line INTEGER, start_column INTEGER
);
CREATE TABLE pending_relationships (
from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT,
path TEXT, start_line INTEGER, start_column INTEGER
);
CREATE TABLE structural_facts (
structural_fact_id TEXT PRIMARY KEY, file_id TEXT, path TEXT NOT NULL, language TEXT,
pattern_id TEXT, capture_name TEXT, node_kind TEXT, containing_symbol_id TEXT,
start_line INTEGER, end_line INTEGER, confidence REAL, metadata_json TEXT
);
CREATE TABLE literals (
literal_id TEXT PRIMARY KEY, file_id TEXT, path TEXT NOT NULL, language TEXT,
kind TEXT, literal_text TEXT, carrier TEXT, containing_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER
);",
)
.unwrap();
conn.execute(
"INSERT INTO files VALUES ('f1', 'src/workspace.rs', 'rust', ?1, ?2, 3, '2026-01-01')",
rusqlite::params![hash, bytes],
)
.unwrap();
conn.execute(
"INSERT INTO symbols VALUES (
's1', 'f1', 'src/workspace.rs', 'rust', 'Workspace', 'struct',
'pub struct Workspace', 'Workspace representation for code-kb workspace discovery root', 'pub', NULL,
1, 0, 3, 1, 0, ?1, 1, 21, 3, 1, 21, ?1, 'b3:hash',
NULL, 0, 0
)",
rusqlite::params![bytes],
)
.unwrap();
conn.execute(
"INSERT INTO pending_relationships VALUES ('s1', 'println', 'call', 'src/workspace.rs', 2, 4)",
[],
)
.unwrap();
code_kb_core::db::ensure_fts_index(&conn).unwrap();
drop(conn);
temp_dir
}
#[test]
fn test_mcp_initialize_roots_file_uris() {
use std::io::BufRead;
let repo = setup_test_repo();
let root = repo.path();
let root_str = root.to_string_lossy().replace('\\', "/");
let three_slash_uri = format!("file:///{root_str}");
let two_slash_uri = format!("file://{root_str}");
{
let mut child = ChildGuard(
Command::new(env!("CARGO_BIN_EXE_code-kb"))
.env("CODE_KB_TELEMETRY_DIR", root.join(".telemetry_test"))
.current_dir(root)
.arg("serve")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn code-kb serve without --root"),
);
let mut stdin = child.stdin.take().expect("Failed to open stdin");
let stdout = child.stdout.take().expect("Failed to open stdout");
let mut reader = std::io::BufReader::new(stdout);
let init = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "uri-test", "version": "1.0" },
"roots": [{ "uri": three_slash_uri }]
}
});
let mut line = serde_json::to_string(&init).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut init_resp = String::new();
reader.read_line(&mut init_resp).unwrap();
let resp: Value = serde_json::from_str(&init_resp).unwrap();
assert_eq!(resp["id"], 1);
let call = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "lookup_symbol",
"arguments": { "query": "Workspace" }
}
});
let mut call_line = serde_json::to_string(&call).unwrap();
call_line.push('\n');
stdin.write_all(call_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut call_resp = String::new();
reader.read_line(&mut call_resp).unwrap();
let call_val: Value = serde_json::from_str(&call_resp).unwrap();
assert_eq!(call_val["id"], 2);
assert_ne!(call_val["result"]["isError"], true);
assert!(
call_val["result"]["content"][0]["text"]
.as_str()
.unwrap()
.contains("Workspace")
);
drop(stdin);
let _ = child.wait();
}
{
let mut child = ChildGuard(
Command::new(env!("CARGO_BIN_EXE_code-kb"))
.env("CODE_KB_TELEMETRY_DIR", root.join(".telemetry_test"))
.current_dir(root)
.arg("serve")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn code-kb serve without --root"),
);
let mut stdin = child.stdin.take().expect("Failed to open stdin");
let stdout = child.stdout.take().expect("Failed to open stdout");
let mut reader = std::io::BufReader::new(stdout);
let init = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "uri-test", "version": "1.0" },
"roots": [{ "uri": two_slash_uri }]
}
});
let mut line = serde_json::to_string(&init).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut init_resp = String::new();
reader.read_line(&mut init_resp).unwrap();
let resp: Value = serde_json::from_str(&init_resp).unwrap();
assert_eq!(resp["id"], 1);
let call = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "lookup_symbol",
"arguments": { "query": "Workspace" }
}
});
let mut call_line = serde_json::to_string(&call).unwrap();
call_line.push('\n');
stdin.write_all(call_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut call_resp = String::new();
reader.read_line(&mut call_resp).unwrap();
let call_val: Value = serde_json::from_str(&call_resp).unwrap();
assert_eq!(call_val["id"], 2);
assert_ne!(call_val["result"]["isError"], true);
assert!(
call_val["result"]["content"][0]["text"]
.as_str()
.unwrap()
.contains("Workspace")
);
drop(stdin);
let _ = child.wait();
}
}
#[test]
fn test_mcp_rebinding_drive_casing_insensitivity() {
use std::io::BufRead;
let repo = setup_test_repo();
let root = repo.path();
let mut child = ChildGuard(
Command::new(env!("CARGO_BIN_EXE_code-kb"))
.env("CODE_KB_TELEMETRY_DIR", root.join(".telemetry_test"))
.arg("serve")
.arg("--root")
.arg(root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn code-kb serve"),
);
let mut stdin = child.stdin.take().expect("Failed to open stdin");
let stdout = child.stdout.take().expect("Failed to open stdout");
let mut reader = std::io::BufReader::new(stdout);
let init_req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "casing-test", "version": "1.0" }
}
});
let mut line = serde_json::to_string(&init_req).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut init_resp = String::new();
reader.read_line(&mut init_resp).unwrap();
let resp: Value = serde_json::from_str(&init_resp).unwrap();
assert_eq!(resp["id"], 1);
let abs_file = root
.join("src")
.join("workspace.rs")
.to_string_lossy()
.to_string();
let inverted_abs_file = if abs_file.len() >= 2 && abs_file.as_bytes()[1] == b':' {
let first_char = abs_file.chars().next().unwrap();
let toggled = if first_char.is_ascii_uppercase() {
first_char.to_ascii_lowercase()
} else {
first_char.to_ascii_uppercase()
};
format!("{}{}", toggled, &abs_file[1..])
} else {
abs_file.clone()
};
let call1 = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "file_skeleton",
"arguments": { "file": inverted_abs_file }
}
});
let mut call1_line = serde_json::to_string(&call1).unwrap();
call1_line.push('\n');
stdin.write_all(call1_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line1 = String::new();
reader.read_line(&mut resp_line1).unwrap();
let resp1: Value = serde_json::from_str(&resp_line1).unwrap();
assert_eq!(resp1["id"], 2);
assert_ne!(
resp1["result"]["isError"], true,
"file_skeleton should not error on inverted drive casing: {resp1:?}"
);
assert!(
resp1["result"]["content"][0]["text"]
.as_str()
.unwrap()
.contains("pub struct Workspace"),
"file_skeleton should return symbol signatures"
);
let call2 = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_symbol_body",
"arguments": {
"symbol": "Workspace",
"file": inverted_abs_file
}
}
});
let mut call2_line = serde_json::to_string(&call2).unwrap();
call2_line.push('\n');
stdin.write_all(call2_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line2 = String::new();
reader.read_line(&mut resp_line2).unwrap();
let resp2: Value = serde_json::from_str(&resp_line2).unwrap();
assert_eq!(resp2["id"], 3);
assert_ne!(
resp2["result"]["isError"], true,
"get_symbol_body should not error on inverted drive casing: {resp2:?}"
);
assert!(
resp2["result"]["content"][0]["text"]
.as_str()
.unwrap()
.contains("pub struct Workspace"),
"get_symbol_body should return symbol body"
);
drop(stdin);
let _ = child.wait();
}
#[test]
fn test_mcp_telemetry_summary_unindexed_repo_no_autoscan() {
let temp_dir = tempfile::tempdir().unwrap();
let root = temp_dir.path();
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"unindexed\"\nversion = \"0.1.0\"\n",
)
.unwrap();
let src = root.join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("lib.rs"), "pub fn unindexed_func() {}\n").unwrap();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_code-kb"));
cmd.env("CODE_KB_TELEMETRY_DIR", root.join(".telemetry_test"))
.arg("serve")
.arg("--root")
.arg(root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = ChildGuard(cmd.spawn().expect("Failed to spawn code-kb mcp"));
let mut stdin = child.stdin.take().expect("Failed to open stdin");
let mut reader = BufReader::new(child.stdout.take().expect("Failed to open stdout"));
let init_req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"clientInfo": { "name": "test-client", "version": "1.0" },
"capabilities": {}
}
});
let mut line = serde_json::to_string(&init_req).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response_line = String::new();
reader.read_line(&mut response_line).unwrap();
let call_req = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "telemetry_summary",
"arguments": {
"time_window": "all",
"workspace_only": true
}
}
});
let mut call_line = serde_json::to_string(&call_req).unwrap();
call_line.push('\n');
stdin.write_all(call_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut call_resp_line = String::new();
reader.read_line(&mut call_resp_line).unwrap();
let resp: Value = serde_json::from_str(&call_resp_line).expect("Failed to parse JSON response");
assert_eq!(resp["id"], 2);
assert_ne!(resp["result"]["isError"], true);
let summary_text = resp["result"]["content"][0]["text"].as_str().unwrap();
assert!(summary_text.contains("Telemetry Summary"));
assert!(
!root.join(".code-kb").join("artifact.db").exists(),
"telemetry_summary must not trigger auto-scan on an unindexed repository"
);
drop(stdin);
let _ = child.wait();
}
#[test]
fn test_mcp_telemetry_summary_scoped_errors_no_cross_workspace_leak() {
let telem_dir = code_kb_core::safe_tempdir();
let telem_db_path = telem_dir.path().join("telemetry.db");
let telem_conn = code_kb_core::Connection::open(&telem_db_path).unwrap();
telem_conn
.execute_batch(
"PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS tool_telemetry (
id TEXT PRIMARY KEY,
timestamp TEXT NOT NULL,
workspace_root TEXT NOT NULL,
workspace_name TEXT NOT NULL,
tool TEXT NOT NULL,
duration_ms INTEGER NOT NULL,
outcome TEXT NOT NULL,
error_message TEXT,
result_count INTEGER NOT NULL DEFAULT 0,
bytes_returned INTEGER NOT NULL DEFAULT 0,
est_tokens INTEGER NOT NULL DEFAULT 0,
est_tokens_saved INTEGER NOT NULL DEFAULT 0,
code_kb_version TEXT NOT NULL
);",
)
.unwrap();
let ws_b_dir = code_kb_core::safe_tempdir();
let ws_b_root = code_kb_core::to_forward_slash(
&code_kb_core::Workspace::new(ws_b_dir.path().to_path_buf()).canonical_root,
);
let secret_error = "SECRET_PATH_EXPOSURE: failed to parse /secret/unrelated/project/token.key";
telem_conn
.execute(
"INSERT INTO tool_telemetry VALUES (
'err-b-1', datetime('now'), ?1, 'unrelated-repo', 'get_symbol_body',
10, 'error', ?2, 0, 100, 25, 0, '0.7.0'
)",
rusqlite::params![ws_b_root, secret_error],
)
.unwrap();
let ws_a_dir = code_kb_core::safe_tempdir();
let root = ws_a_dir.path().to_path_buf();
let ws_a_root =
code_kb_core::to_forward_slash(&code_kb_core::Workspace::new(root.clone()).canonical_root);
let local_error = "Active repo local error: symbol MissingSymbol not found";
telem_conn
.execute(
"INSERT INTO tool_telemetry VALUES (
'err-a-1', datetime('now'), ?1, 'active-repo', 'get_symbol_body',
10, 'error', ?2, 0, 100, 25, 0, '0.7.0'
)",
rusqlite::params![ws_a_root, local_error],
)
.unwrap();
drop(telem_conn);
let db_dir = root.join(".code-kb");
std::fs::create_dir_all(&db_dir).unwrap();
let db_path = db_dir.join("artifact.db");
let src_dir = root.join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let content = "// 40 bytes line of source code text!\n".repeat(10);
let file_bytes = content.len() as i64;
let hash = format!("blake3:{}", blake3::hash(content.as_bytes()).to_hex());
std::fs::write(src_dir.join("lib.rs"), &content).unwrap();
let conn = code_kb_core::open_read_write(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE files (
file_id TEXT PRIMARY KEY, path TEXT, language TEXT, content_hash TEXT,
content_bytes INTEGER, line_count INTEGER, indexed_at TEXT
);
CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER
);",
)
.unwrap();
conn.execute(
"INSERT INTO files VALUES ('f1', 'src/lib.rs', 'rust', ?1, ?2, 10, '2026-01-01')",
rusqlite::params![hash, file_bytes],
)
.unwrap();
conn.execute(
"INSERT INTO symbols VALUES (
's1', 'f1', 'src/lib.rs', 'rust', 'sample_func', 'function',
'pub fn sample_func()', NULL, 'pub', NULL,
1, 0, 2, 1, 0, ?1, 1, 20, 2, 1, 20, ?1, 'b3:hash',
NULL, 0, 0
)",
rusqlite::params![file_bytes],
)
.unwrap();
drop(conn);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_code-kb"));
cmd.arg("serve")
.arg("--root")
.arg(&root)
.env("CODE_KB_TELEMETRY_DIR", telem_dir.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = ChildGuard(cmd.spawn().expect("Failed to spawn code-kb serve"));
let mut stdin = child.stdin.take().expect("Failed to open stdin");
let mut reader = BufReader::new(child.stdout.take().expect("Failed to open stdout"));
let init_req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "test-client", "version": "1.0" }
}
});
let mut line = serde_json::to_string(&init_req).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut init_resp_line = String::new();
reader.read_line(&mut init_resp_line).unwrap();
let call_telem_text = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "telemetry_summary",
"arguments": {
"workspace_only": false
}
}
});
let mut telem_line = serde_json::to_string(&call_telem_text).unwrap();
telem_line.push('\n');
stdin.write_all(telem_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut telem_resp_line = String::new();
reader.read_line(&mut telem_resp_line).unwrap();
let resp: Value = serde_json::from_str(&telem_resp_line).unwrap();
assert_eq!(resp["id"], 2);
let summary_text = resp["result"]["content"][0]["text"].as_str().unwrap();
assert!(summary_text.contains("Total Tool Calls: 2"));
assert!(summary_text.contains(local_error));
assert!(
!summary_text.contains(secret_error),
"Global telemetry summary must not leak error messages from other workspaces"
);
let call_telem_json = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "telemetry_summary",
"arguments": {
"workspace_only": false,
"json": true
}
}
});
let mut json_line = serde_json::to_string(&call_telem_json).unwrap();
json_line.push('\n');
stdin.write_all(json_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut json_resp_line = String::new();
reader.read_line(&mut json_resp_line).unwrap();
let resp_json: Value = serde_json::from_str(&json_resp_line).unwrap();
assert_eq!(resp_json["id"], 3);
let summary_json_str = resp_json["result"]["content"][0]["text"].as_str().unwrap();
let summary_data: Value = serde_json::from_str(summary_json_str).unwrap();
assert_eq!(summary_data["total_calls"], 3);
let recent_errors = summary_data["recent_errors"].as_array().unwrap();
assert_eq!(recent_errors.len(), 1);
assert_eq!(recent_errors[0]["error_message"], local_error);
let skeleton_req = json!({
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "file_skeleton",
"arguments": {
"file_path": "src/lib.rs"
}
}
});
let mut skel_line = serde_json::to_string(&skeleton_req).unwrap();
skel_line.push('\n');
stdin.write_all(skel_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut skel_resp_line = String::new();
reader.read_line(&mut skel_resp_line).unwrap();
let resp_skel: Value = serde_json::from_str(&skel_resp_line).unwrap();
assert_eq!(resp_skel["id"], 4);
assert_ne!(resp_skel["result"]["isError"], true);
let skel_text = resp_skel["result"]["content"][0]["text"].as_str().unwrap();
let skel_tokens = skel_text.len() / 4;
let expected_saved = ((file_bytes as usize) / 4).saturating_sub(skel_tokens);
let stats_req = json!({
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "telemetry_summary",
"arguments": {
"workspace_only": true,
"json": true
}
}
});
let mut stats_line = serde_json::to_string(&stats_req).unwrap();
stats_line.push('\n');
stdin.write_all(stats_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut stats_resp_line = String::new();
reader.read_line(&mut stats_resp_line).unwrap();
let resp_stats: Value = serde_json::from_str(&stats_resp_line).unwrap();
let stats_json: Value =
serde_json::from_str(resp_stats["result"]["content"][0]["text"].as_str().unwrap()).unwrap();
let skel_stat = stats_json["tool_stats"]
.as_array()
.unwrap()
.iter()
.find(|s| s["tool"] == "file_skeleton")
.expect("file_skeleton stat should be present");
assert_eq!(skel_stat["tokens_saved"], expected_saved);
drop(stdin);
let _ = child.wait();
}
#[test]
fn test_mcp_telemetry_summary_does_not_rebind_workspace() {
use std::io::BufRead;
let repo1 = setup_test_repo();
let root1 = repo1.path();
let repo2 = setup_test_repo();
let root2 = repo2.path();
let telem_dir = code_kb_core::safe_tempdir();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_code-kb"));
cmd.env("CODE_KB_TELEMETRY_DIR", telem_dir.path());
cmd.arg("serve")
.arg("--root")
.arg(root1)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = ChildGuard(cmd.spawn().expect("Failed to spawn code-kb serve"));
let mut stdin = child.stdin.take().expect("Failed to open stdin");
let stdout = child.stdout.take().expect("Failed to open stdout");
let mut reader = std::io::BufReader::new(stdout);
let init_req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "rebind-test", "version": "1.0" }
}
});
let mut init_line = serde_json::to_string(&init_req).unwrap();
init_line.push('\n');
stdin.write_all(init_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut init_resp_line = String::new();
reader.read_line(&mut init_resp_line).unwrap();
let stats_req = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "telemetry_summary",
"arguments": {
"workspace": root2.to_str().unwrap(),
"file_path": root2.join("src/lib.rs").to_str().unwrap()
}
}
});
let mut stats_line = serde_json::to_string(&stats_req).unwrap();
stats_line.push('\n');
stdin.write_all(stats_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut stats_resp_line = String::new();
reader.read_line(&mut stats_resp_line).unwrap();
let stats_resp: Value = serde_json::from_str(&stats_resp_line).unwrap();
assert_ne!(stats_resp["result"]["isError"], true);
let outline_req = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "codebase_outline",
"arguments": {}
}
});
let mut outline_line = serde_json::to_string(&outline_req).unwrap();
outline_line.push('\n');
stdin.write_all(outline_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut outline_resp_line = String::new();
reader.read_line(&mut outline_resp_line).unwrap();
let outline_resp: Value = serde_json::from_str(&outline_resp_line).unwrap();
assert_ne!(outline_resp["result"]["isError"], true);
let invalid_req = json!({
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "telemetry_summary",
"arguments": {
"time_window": "invalid_window_123"
}
}
});
let mut inv_line = serde_json::to_string(&invalid_req).unwrap();
inv_line.push('\n');
stdin.write_all(inv_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut inv_resp_line = String::new();
reader.read_line(&mut inv_resp_line).unwrap();
let inv_resp: Value = serde_json::from_str(&inv_resp_line).unwrap();
assert_eq!(inv_resp["result"]["isError"], true);
drop(stdin);
let _ = child.wait();
}
#[test]
fn test_mcp_lookup_and_search_symbols_accept_symbol_name_and_symbol_aliases() {
let repo = setup_test_repo();
let root = repo.path();
let mut child = ChildGuard(
Command::new(env!("CARGO_BIN_EXE_code-kb"))
.env("CODE_KB_TELEMETRY_DIR", root.join(".telemetry_test"))
.current_dir(root)
.arg("serve")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn code-kb serve"),
);
let mut stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let mut reader = BufReader::new(stdout);
let init_req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "test-client", "version": "1.0" },
"rootUri": format!("file://{}", root.display())
}
});
let mut init_line = serde_json::to_string(&init_req).unwrap();
init_line.push('\n');
stdin.write_all(init_line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut init_resp_line = String::new();
reader.read_line(&mut init_resp_line).unwrap();
let init_resp: Value = serde_json::from_str(&init_resp_line).unwrap();
assert_eq!(init_resp["id"], 1);
let lookup_symbol_name = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "lookup_symbol",
"arguments": {
"symbol_name": "Workspace"
}
}
});
let mut line = serde_json::to_string(&lookup_symbol_name).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line = String::new();
reader.read_line(&mut resp_line).unwrap();
let resp: Value = serde_json::from_str(&resp_line).unwrap();
assert_ne!(resp["result"]["isError"], true);
let lookup_symbol = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "lookup_symbol",
"arguments": {
"symbol": "Workspace"
}
}
});
let mut line = serde_json::to_string(&lookup_symbol).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line = String::new();
reader.read_line(&mut resp_line).unwrap();
let resp: Value = serde_json::from_str(&resp_line).unwrap();
assert_ne!(resp["result"]["isError"], true);
let search_symbol_name = json!({
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "search_symbols",
"arguments": {
"symbol_name": "Workspace"
}
}
});
let mut line = serde_json::to_string(&search_symbol_name).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line = String::new();
reader.read_line(&mut resp_line).unwrap();
let resp: Value = serde_json::from_str(&resp_line).unwrap();
assert_ne!(resp["result"]["isError"], true);
let search_symbol = json!({
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "search_symbols",
"arguments": {
"symbol": "Workspace"
}
}
});
let mut line = serde_json::to_string(&search_symbol).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut resp_line = String::new();
reader.read_line(&mut resp_line).unwrap();
let resp: Value = serde_json::from_str(&resp_line).unwrap();
assert_ne!(resp["result"]["isError"], true);
drop(stdin);
let _ = child.wait();
}
#[test]
fn test_first_tool_call_sees_files_changed_while_no_server_ran() {
let temp_dir = code_kb_core::safe_tempdir();
let root = temp_dir.path().to_path_buf();
std::fs::create_dir_all(root.join(".code-kb")).unwrap();
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/lib.rs"), "pub fn indexed_by_scan() {}\n").unwrap();
let workspace = code_kb_core::Workspace::new(root.clone());
let db_path = root.join(".code-kb/artifact.db");
code_kb_core::scan_workspace(&workspace, &db_path, false).unwrap();
std::fs::write(
root.join("src/offline.rs"),
"pub fn added_while_no_server_ran() {}\n",
)
.unwrap();
let mut child = ChildGuard(
Command::new(env!("CARGO_BIN_EXE_code-kb"))
.env("CODE_KB_TELEMETRY_DIR", root.join(".telemetry_test"))
.arg("serve")
.arg("--root")
.arg(&root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn code-kb serve"),
);
let mut stdin = child.stdin.take().unwrap();
let mut reader = BufReader::new(child.stdout.take().unwrap());
let mut rpc = |request: Value| -> Value {
let mut line = serde_json::to_string(&request).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
let mut response = String::new();
reader.read_line(&mut response).unwrap();
serde_json::from_str(&response).unwrap()
};
rpc(json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "t", "version": "0"}}
}));
let response = rpc(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "lookup_symbol", "arguments": {"query": "added_while_no_server_ran"}}
}));
let text = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(text.contains("added_while_no_server_ran"), "{text}");
}