Skip to content

Commit 01f956e

Browse files
authored
fix: directory tree tool result (#26)
* fix: directory tree result * chore: fix typo * chore: clippy warnings
1 parent c6d5a15 commit 01f956e

File tree

6 files changed

+113
-26
lines changed

6 files changed

+113
-26
lines changed

rust-toolchain.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[toolchain]
2+
channel = "1.88.0"
3+
components = ["rustfmt", "clippy"]

src/fs_service.rs

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use grep::{
66
regex::RegexMatcherBuilder,
77
searcher::{sinks::UTF8, BinaryDetection, Searcher},
88
};
9+
use serde_json::{json, Value};
910

1011
use std::{
1112
env,
@@ -67,7 +68,7 @@ impl FileSystemService {
6768
.map_while(|dir| {
6869
let expand_result = expand_home(dir.into());
6970
if !expand_result.is_dir() {
70-
panic!("{}", format!("Error: {} is not a directory", dir));
71+
panic!("{}", format!("Error: {dir} is not a directory"));
7172
}
7273
Some(expand_result)
7374
})
@@ -179,7 +180,7 @@ impl FileSystemService {
179180
if target_path.exists() {
180181
return Err(std::io::Error::new(
181182
std::io::ErrorKind::AlreadyExists,
182-
format!("'{}' already exists!", target_zip_file),
183+
format!("'{target_zip_file}' already exists!"),
183184
)
184185
.into());
185186
}
@@ -269,7 +270,7 @@ impl FileSystemService {
269270
if target_path.exists() {
270271
return Err(std::io::Error::new(
271272
std::io::ErrorKind::AlreadyExists,
272-
format!("'{}' already exists!", target_zip_file),
273+
format!("'{target_zip_file}' already exists!"),
273274
)
274275
.into());
275276
}
@@ -326,7 +327,7 @@ impl FileSystemService {
326327
if target_dir_path.exists() {
327328
return Err(std::io::Error::new(
328329
std::io::ErrorKind::AlreadyExists,
329-
format!("'{}' directory already exists!", target_dir),
330+
format!("'{target_dir}' directory already exists!"),
330331
)
331332
.into());
332333
}
@@ -473,7 +474,7 @@ impl FileSystemService {
473474
let glob_pattern = if pattern.contains('*') {
474475
pattern.clone()
475476
} else {
476-
format!("*{}*", pattern)
477+
format!("*{pattern}*")
477478
};
478479

479480
Pattern::new(&glob_pattern)
@@ -501,6 +502,85 @@ impl FileSystemService {
501502
Ok(result)
502503
}
503504

505+
/// Generates a JSON representation of a directory tree starting at the given path.
506+
///
507+
/// This function recursively builds a JSON array object representing the directory structure,
508+
/// where each entry includes a `name` (file or directory name), `type` ("file" or "directory"),
509+
/// and for directories, a `children` array containing their contents. Files do not have a
510+
/// `children` field.
511+
///
512+
/// The function supports optional constraints to limit the tree size:
513+
/// - `max_depth`: Limits the depth of directory traversal.
514+
/// - `max_files`: Limits the total number of entries (files and directories).
515+
///
516+
/// # IMPORTANT NOTE
517+
///
518+
/// use max_depth or max_files could lead to partial or skewed representations of actual directory tree
519+
pub fn directory_tree<P: AsRef<Path>>(
520+
&self,
521+
root_path: P,
522+
max_depth: Option<usize>,
523+
max_files: Option<usize>,
524+
current_count: &mut usize,
525+
) -> ServiceResult<Value> {
526+
let valid_path = self.validate_path(root_path.as_ref())?;
527+
528+
let metadata = fs::metadata(&valid_path)?;
529+
if !metadata.is_dir() {
530+
return Err(ServiceError::FromString(
531+
"Root path must be a directory".into(),
532+
));
533+
}
534+
535+
let mut children = Vec::new();
536+
537+
if max_depth != Some(0) {
538+
for entry in WalkDir::new(valid_path)
539+
.min_depth(1)
540+
.max_depth(1)
541+
.follow_links(true)
542+
.into_iter()
543+
.filter_map(|e| e.ok())
544+
{
545+
let child_path = entry.path();
546+
let metadata = fs::metadata(child_path)?;
547+
548+
let entry_name = child_path
549+
.file_name()
550+
.ok_or(ServiceError::FromString("Invalid path".to_string()))?
551+
.to_string_lossy()
552+
.into_owned();
553+
554+
// Increment the count for this entry
555+
*current_count += 1;
556+
557+
// Check if we've exceeded max_files (if set)
558+
if let Some(max) = max_files {
559+
if *current_count > max {
560+
continue; // Skip this entry but continue processing others
561+
}
562+
}
563+
564+
let mut json_entry = json!({
565+
"name": entry_name,
566+
"type": if metadata.is_dir() { "directory" } else { "file" }
567+
});
568+
569+
if metadata.is_dir() {
570+
let next_depth = max_depth.map(|d| d - 1);
571+
let child_children =
572+
self.directory_tree(child_path, next_depth, max_files, current_count)?;
573+
json_entry
574+
.as_object_mut()
575+
.unwrap()
576+
.insert("children".to_string(), child_children);
577+
}
578+
children.push(json_entry);
579+
}
580+
}
581+
Ok(Value::Array(children))
582+
}
583+
504584
pub fn create_unified_diff(
505585
&self,
506586
original_content: &str,
@@ -519,8 +599,8 @@ impl FileSystemService {
519599
let patch = diff
520600
.unified_diff()
521601
.header(
522-
format!("{}\toriginal", file_name).as_str(),
523-
format!("{}\tmodified", file_name).as_str(),
602+
format!("{file_name}\toriginal").as_str(),
603+
format!("{file_name}\tmodified").as_str(),
524604
)
525605
.context_radius(4)
526606
.to_string();

src/fs_service/utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ pub fn format_bytes(bytes: u64) -> String {
8282
return format!("{:.2} {}", bytes as f64 / threshold as f64, unit);
8383
}
8484
}
85-
format!("{} bytes", bytes)
85+
format!("{bytes} bytes")
8686
}
8787

8888
pub async fn write_zip_entry(

src/handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ impl ServerHandler for MyServerHandler {
7777
) -> std::result::Result<InitializeResult, RpcError> {
7878
runtime
7979
.set_client_details(initialize_request.params.clone())
80-
.map_err(|err| RpcError::internal_error().with_message(format!("{}", err)))?;
80+
.map_err(|err| RpcError::internal_error().with_message(format!("{err}")))?;
8181

8282
let mut server_info = runtime.server_info().to_owned();
8383
// Provide compatibility for clients using older MCP protocol versions.

src/tools/directory_tree.rs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
use std::path::Path;
2-
31
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
42
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
53
use serde_json::json;
64

5+
use crate::error::ServiceError;
76
use crate::fs_service::FileSystemService;
87

98
#[mcp_tool(
109
name = "directory_tree",
1110
description = concat!("Get a recursive tree view of files and directories as a JSON structure. ",
1211
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. ",
1312
"Files have no children array, while directories always have a children array (which may be empty). ",
13+
"If the 'max_depth' parameter is provided, the traversal will be limited to the specified depth. ",
14+
"As a result, the returned directory structure may be incomplete or provide a skewed representation of the full directory tree, since deeper-level files and subdirectories beyond the specified depth will be excluded. ",
1415
"The output is formatted with 2-space indentation for readability. Only works within allowed directories."),
1516
destructive_hint = false,
1617
idempotent_hint = false,
@@ -21,28 +22,31 @@ use crate::fs_service::FileSystemService;
2122
pub struct DirectoryTreeTool {
2223
/// The root path of the directory tree to generate.
2324
pub path: String,
25+
/// Limits the depth of directory traversal
26+
pub max_depth: Option<u64>,
2427
}
2528
impl DirectoryTreeTool {
2629
pub async fn run_tool(
2730
params: Self,
2831
context: &FileSystemService,
2932
) -> std::result::Result<CallToolResult, CallToolError> {
33+
let mut entry_counter: usize = 0;
3034
let entries = context
31-
.list_directory(Path::new(&params.path))
32-
.await
35+
.directory_tree(
36+
params.path,
37+
params.max_depth.map(|v| v as usize),
38+
None,
39+
&mut entry_counter,
40+
)
3341
.map_err(CallToolError::new)?;
3442

35-
let json_tree: Vec<serde_json::Value> = entries
36-
.iter()
37-
.map(|entry| {
38-
json!({
39-
"name": entry.file_name().to_str().unwrap_or_default(),
40-
"type": if entry.path().is_dir(){"directory"}else{"file"}
41-
})
42-
})
43-
.collect();
44-
let json_str =
45-
serde_json::to_string_pretty(&json!(json_tree)).map_err(CallToolError::new)?;
43+
if entry_counter == 0 {
44+
return Err(CallToolError::new(ServiceError::FromString(
45+
"Could not find any entries".to_string(),
46+
)));
47+
}
48+
49+
let json_str = serde_json::to_string_pretty(&json!(entries)).map_err(CallToolError::new)?;
4650
Ok(CallToolResult::text_content(json_str, None))
4751
}
4852
}

src/tools/read_multiple_files.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ impl ReadMultipleFilesTool {
4040
.map_err(CallToolError::new);
4141

4242
content.map_or_else(
43-
|err| format!("{}: Error - {}", path, err),
44-
|value| format!("{}:\n{}\n", path, value),
43+
|err| format!("{path}: Error - {err}"),
44+
|value| format!("{path}:\n{value}\n"),
4545
)
4646
}
4747
})

0 commit comments

Comments
 (0)