use std::fs;
use crate::db::Database;
use crate::mcp::constants::{DEFAULT_CONTEXT_LINES, MAX_REFERENCES_PER_KIND};
use crate::mcp::types::{DefinitionRequest, SymbolRequest};
use crate::security::safe_join;
use crate::types::EdgeKind;
pub fn handle_node(db: &Database, req: &SymbolRequest) -> Result<String, String> {
let node = match db.find_node_by_name(&req.symbol) {
Ok(Some(n)) => n,
Ok(None) => return Ok(format!("Symbol '{}' not found", req.symbol)),
Err(e) => return Err(e.to_string()),
};
let mut output = format!("## {}: `{}`\n\n", node.kind.as_str(), node.name);
output.push_str(&format!(
"**File:** {}:{}-{}\n",
node.file_path, node.start_line, node.end_line
));
output.push_str(&format!("**Language:** {}\n", node.language.as_str()));
output.push_str(&format!("**Visibility:** {}\n", node.visibility.as_str()));
if node.is_async {
output.push_str("**Async:** yes\n");
}
if node.is_static {
output.push_str("**Static:** yes\n");
}
if node.is_exported {
output.push_str("**Exported:** yes\n");
}
if let Some(ref sig) = node.signature {
output.push_str(&format!("\n**Signature:**\n```\n{}\n```\n", sig));
}
if let Some(ref doc) = node.docstring {
output.push_str(&format!("\n**Documentation:**\n{}\n", doc));
}
Ok(output)
}
pub fn handle_definition(
db: &Database,
project_root: &str,
req: &DefinitionRequest,
) -> Result<String, String> {
let node = match db.find_node_by_name(&req.symbol) {
Ok(Some(n)) => n,
Ok(None) => return Ok(format!("Symbol '{}' not found", req.symbol)),
Err(e) => return Err(e.to_string()),
};
let context_lines = req.context_lines.unwrap_or(DEFAULT_CONTEXT_LINES) as usize;
let file_path = safe_join(project_root, &node.file_path).map_err(|e| e.to_string())?;
let content = fs::read_to_string(&file_path)
.map_err(|e| format!("reading file {}: {}", node.file_path, e))?;
let lines: Vec<&str> = content.lines().collect();
let start = (node.start_line as usize).saturating_sub(1);
let end = (node.end_line as usize).min(lines.len());
if start >= lines.len() {
return Err(format!(
"line range {}-{} out of bounds",
node.start_line, node.end_line
));
}
let mut output = format!(
"## {} `{}`\n\n**File:** {}:{}-{}\n**Language:** {}\n\n",
node.kind.as_str(),
node.name,
node.file_path,
node.start_line,
node.end_line,
node.language.as_str()
);
if let Some(ref sig) = node.signature {
output.push_str(&format!("**Signature:** `{}`\n\n", sig));
}
let ctx_start = start.saturating_sub(context_lines);
if ctx_start < start {
output.push_str("```");
output.push_str(node.language.as_str());
output.push_str("\n// ... context before\n");
for (i, line) in lines[ctx_start..start].iter().enumerate() {
output.push_str(&format!("{:4} │ {}\n", ctx_start + i + 1, line));
}
output.push_str("// --- definition starts ---\n");
} else {
output.push_str("```");
output.push_str(node.language.as_str());
output.push('\n');
}
for (i, line) in lines[start..end].iter().enumerate() {
output.push_str(&format!("{:4} │ {}\n", start + i + 1, line));
}
let ctx_end = (end + context_lines).min(lines.len());
if ctx_end > end {
output.push_str("// --- definition ends ---\n");
for (i, line) in lines[end..ctx_end].iter().enumerate() {
output.push_str(&format!("{:4} │ {}\n", end + i + 1, line));
}
output.push_str("// ... context after\n");
}
output.push_str("```\n");
Ok(output)
}
pub fn handle_references(db: &Database, req: &SymbolRequest) -> Result<String, String> {
let node = match db.find_node_by_name(&req.symbol) {
Ok(Some(n)) => n,
Ok(None) => return Ok(format!("Symbol '{}' not found", req.symbol)),
Err(e) => return Err(e.to_string()),
};
let edges = db.get_incoming_edges(node.id).map_err(|e| e.to_string())?;
if edges.is_empty() {
return Ok(format!("No references found for '{}'", req.symbol));
}
let mut output = format!(
"## References to `{}`\n\n**Location:** {}:{}-{}\n\n",
node.name, node.file_path, node.start_line, node.end_line
);
let mut by_kind: std::collections::HashMap<EdgeKind, Vec<_>> = std::collections::HashMap::new();
for edge in &edges {
by_kind.entry(edge.kind).or_default().push(edge);
}
let mut total = 0;
for kind in [
EdgeKind::Calls,
EdgeKind::Imports,
EdgeKind::Extends,
EdgeKind::Implements,
EdgeKind::Contains,
EdgeKind::References,
EdgeKind::Exports,
] {
if let Some(edges) = by_kind.get(&kind) {
output.push_str(&format!("### {} ({}):\n\n", kind.as_str(), edges.len()));
total += edges.len();
for edge in edges.iter().take(MAX_REFERENCES_PER_KIND) {
if let Ok(Some(source)) = db.get_node(edge.source_id) {
output.push_str(&format!(
"- `{}` ({}) - {}",
source.name,
source.kind.as_str(),
source.file_path
));
if let Some(line) = edge.line {
output.push_str(&format!(":{}", line));
}
output.push('\n');
}
}
if edges.len() > MAX_REFERENCES_PER_KIND {
output.push_str(&format!(
" ... and {} more\n",
edges.len() - MAX_REFERENCES_PER_KIND
));
}
output.push('\n');
}
}
output.push_str(&format!("**Total references:** {}\n", total));
Ok(output)
}
#[cfg(test)]
mod security_tests {
use super::*;
use crate::types::{Language, Node, NodeKind, Visibility};
fn seed(db: &Database, file_path: &str) {
let file = crate::types::FileRecord {
path: file_path.to_string(),
content_hash: "h".into(),
language: Language::Rust,
size: 0,
modified_at: 0,
indexed_at: 0,
node_count: 1,
};
db.insert_or_update_file(&file).unwrap();
let node = Node {
id: 0,
kind: NodeKind::Function,
name: "victim".to_string(),
qualified_name: None,
file_path: file_path.to_string(),
start_line: 1,
end_line: 2,
start_column: 0,
end_column: 0,
signature: None,
visibility: Visibility::Public,
docstring: None,
is_async: false,
is_static: false,
is_exported: false,
is_test: false,
is_generated: false,
language: Language::Rust,
};
db.insert_node(&node).unwrap();
}
#[test]
fn definition_rejects_traversal_file_path_in_db() {
let tmp = tempfile::tempdir().unwrap();
let db = Database::in_memory().unwrap();
seed(&db, "../../../etc/passwd");
let req = DefinitionRequest {
symbol: "victim".into(),
context_lines: None,
};
let err = handle_definition(&db, tmp.path().to_str().unwrap(), &req).unwrap_err();
assert!(
err.contains("path security") || err.contains("traversal"),
"got: {err}"
);
}
}