use super::types::{TraceFile, TraceRange};
use serde_json::Value;
const FILE_EDIT_TOOLS: &[&str] = &[
"Read",
"Write",
"Edit",
"MultiEdit",
"NotebookRead",
"NotebookEdit",
];
pub fn extract_files_from_tool_call(tool_name: &str, params: &Value) -> Vec<TraceFile> {
let base_tool_name = normalize_tool_name(tool_name);
if !FILE_EDIT_TOOLS.contains(&base_tool_name.as_str()) {
return Vec::new();
}
let mut files = Vec::new();
match base_tool_name.as_str() {
"Read" | "Write" => {
if let Some(file_path) = extract_file_path(params) {
files.push(TraceFile {
path: file_path,
ranges: Vec::new(),
operation: Some(
if base_tool_name == "Read" {
"read"
} else {
"write"
}
.to_string(),
),
content_hash: None,
});
}
}
"Edit" => {
if let Some(file_path) = extract_file_path(params) {
let ranges = extract_ranges_from_edit(params);
files.push(TraceFile {
path: file_path,
ranges: ranges.unwrap_or_default(),
operation: Some("edit".to_string()),
content_hash: None,
});
}
}
"MultiEdit" => {
if let Some(edits) = params.get("edits").and_then(|v| v.as_array()) {
for edit in edits {
if let Some(edit_path) = extract_file_path(edit) {
let ranges = extract_ranges_from_edit(edit);
files.push(TraceFile {
path: edit_path,
ranges: ranges.unwrap_or_default(),
operation: Some("edit".to_string()),
content_hash: None,
});
}
}
}
}
"NotebookRead" | "NotebookEdit" => {
if let Some(file_path) = extract_file_path(params) {
files.push(TraceFile {
path: file_path,
ranges: Vec::new(),
operation: Some(
if base_tool_name == "NotebookRead" {
"read"
} else {
"edit"
}
.to_string(),
),
content_hash: None,
});
}
}
_ => {}
}
files
}
fn extract_file_path(params: &Value) -> Option<String> {
params
.get("file_path")
.or_else(|| params.get("path"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
fn extract_ranges_from_edit(params: &Value) -> Option<Vec<TraceRange>> {
let mut ranges = Vec::new();
if let (Some(start), Some(end)) = (
params.get("startLine").and_then(|v| v.as_u64()),
params.get("endLine").and_then(|v| v.as_u64()),
) {
ranges.push(TraceRange {
start_line: start as u32,
end_line: end as u32,
start_column: None,
end_column: None,
});
}
if let (Some(old), Some(new)) = (
params.get("oldLine").and_then(|v| v.as_u64()),
params.get("newLine").and_then(|v| v.as_u64()),
) {
ranges.push(TraceRange {
start_line: old as u32,
end_line: new as u32,
start_column: None,
end_column: None,
});
}
if ranges.is_empty() {
None
} else {
Some(ranges)
}
}
fn normalize_tool_name(tool_name: &str) -> String {
if let Some(rest) = tool_name.strip_prefix("mcp__") {
if let Some(pos) = rest.rfind("__") {
rest[pos + 2..].to_string()
} else {
tool_name.to_string()
}
} else {
tool_name.to_string()
}
}
pub fn compute_content_hash(file_path: &str, content: Option<&str>) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
file_path.hash(&mut hasher);
if let Some(content) = content {
content.hash(&mut hasher);
}
format!("{:x}", hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_extract_read_tool() {
let params = json!({
"file_path": "/path/to/file.ts",
"offset": 1,
"limit": 100
});
let files = extract_files_from_tool_call("Read", ¶ms);
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, "/path/to/file.ts");
assert_eq!(files[0].operation, Some("read".to_string()));
}
#[test]
fn test_extract_edit_tool_with_ranges() {
let params = json!({
"file_path": "/path/to/file.ts",
"startLine": 10,
"endLine": 20,
"oldStr": "old",
"newStr": "new"
});
let files = extract_files_from_tool_call("Edit", ¶ms);
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, "/path/to/file.ts");
assert_eq!(files[0].ranges.len(), 1);
assert_eq!(files[0].ranges[0].start_line, 10);
assert_eq!(files[0].ranges[0].end_line, 20);
}
#[test]
fn test_normalize_tool_name() {
assert_eq!(normalize_tool_name("Read"), "Read");
assert_eq!(normalize_tool_name("mcp__server__Read"), "Read");
assert_eq!(normalize_tool_name("mcp__my-server__my_tool"), "my_tool");
}
#[test]
fn test_content_hash() {
let hash1 = compute_content_hash("test.ts", Some("content"));
let hash2 = compute_content_hash("test.ts", Some("content"));
let hash3 = compute_content_hash("test.ts", Some("different"));
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
}
}