@@ -24,7 +24,7 @@
| Create a new directory or ensure a directory exists. Can create multiple nested directories in one operation. If the directory already exists, this operation will succeed silently. Perfect for setting up directory structures for projects or ensuring required paths exist. Only works within allowed directories. |
- -
path : string
+ -
path : string
|
@@ -33,10 +33,11 @@
directory_tree
|
- Get a recursive tree view of files and directories as a JSON structure. Each entry includes name, type (file/directory), and children for directories. Files have no children array, while directories always have a children array (which may be empty). The output is formatted with 2-space indentation for readability. Only works within allowed directories. |
+ Get a recursive tree view of files and directories as a JSON structure. Each entry includes name, type (file/directory), and children for directories. Files have no children array, while directories always have a children array (which may be empty). If the max_depth parameter is provided, the traversal will be limited to the specified depth. 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. The output is formatted with 2-space indentation for readability. Only works within allowed directories. |
- -
path : string
+ -
max_depth : number
+ -
path : string
|
@@ -48,9 +49,9 @@
Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Returns a git-style diff showing the changes made. Only works within allowed directories. |
- -
dryRun : boolean
- -
edits : {newText : string, oldText : string} [ ]
- -
path : string
+ -
dryRun : boolean
+ -
edits : {newText : string, oldText : string} [ ]
+ -
path : string
|
@@ -62,7 +63,7 @@
Retrieve detailed metadata about a file or directory. Returns comprehensive information including size, creation time, last modified time, permissions, and type. This tool is perfect for understanding file characteristics without reading the actual content. Only works within allowed directories. |
- -
path : string
+ -
path : string
|
@@ -85,127 +86,139 @@
Get a detailed listing of all files and directories in a specified path. Results clearly distinguish between files and directories with FILE and DIR prefixes. This tool is essential for understanding directory structure and finding specific files within a directory. Only works within allowed directories. |
- -
path : string
+ -
path : string
|
| 7. |
+
+ list_directory_with_sizes
+ |
+ Get a detailed listing of all files and directories in a specified path, including sizes. Results clearly distinguish between files and directories with FILE and DIR prefixes. This tool is useful for understanding directory structure and finding specific files within a directory. Only works within allowed directories. |
+
+
+ |
+
+
+ | 8. |
move_file
|
Move or rename files and directories. Can move files between directories and rename them in a single operation. If the destination exists, the operation will fail. Works across different directories and can be used for simple renaming within the same directory. Both source and destination must be within allowed directories. |
- -
destination : string
- -
source : string
+ -
destination : string
+ -
source : string
|
- | 8. |
+ 9. |
read_file
|
Read the complete contents of a file from the file system. Handles various text encodings and provides detailed error messages if the file cannot be read. Use this tool when you need to examine the contents of a single file. Only works within allowed directories. |
- -
path : string
+ -
path : string
|
- | 9. |
+ 10. |
read_multiple_files
|
Read the contents of multiple files simultaneously. This is more efficient than reading files one by one when you need to analyze or compare multiple files. Each file's content is returned with its path as a reference. Failed reads for individual files won't stop the entire operation. Only works within allowed directories. |
- -
paths : string [ ]
+ -
paths : string [ ]
|
- | 10. |
+ 11. |
search_files
|
Recursively search for files and directories matching a pattern. Searches through all subdirectories from the starting path. The search is case-insensitive and matches partial names. Returns full paths to all matching items. Great for finding files when you don't know their exact location. Only searches within allowed directories. |
- -
excludePatterns : string [ ]
- -
path : string
- -
pattern : string
+ -
excludePatterns : string [ ]
+ -
path : string
+ -
pattern : string
|
- | 11. |
+ 12. |
search_files_content
|
Searches for text or regex patterns in the content of files matching matching a GLOB pattern.Returns detailed matches with file path, line number, column number and a preview of matched text.By default, it performs a literal text search; if the is_regex parameter is set to true, it performs a regular expression (regex) search instead.Ideal for finding specific code, comments, or text when you donβt know their exact location. |
- -
excludePatterns : string [ ]
- -
is_regex : boolean
- -
path : string
- -
pattern : string
- -
query : string
+ -
excludePatterns : string [ ]
+ -
is_regex : boolean
+ -
path : string
+ -
pattern : string
+ -
query : string
|
- | 12. |
+ 13. |
unzip_file
|
Extracts the contents of a ZIP archive to a specified target directory. It takes a source ZIP file path and a target extraction directory. The tool decompresses all files and directories stored in the ZIP, recreating their structure in the target location. Both the source ZIP file and the target directory should reside within allowed directories. |
- -
target_path : string
- -
zip_file : string
+ -
target_path : string
+ -
zip_file : string
|
- | 13. |
+ 14. |
write_file
|
Create a new file or completely overwrite an existing file with new content. Use with caution as it will overwrite existing files without warning. Handles text content with proper encoding. Only works within allowed directories. |
- -
content : string
- -
path : string
+ -
content : string
+ -
path : string
|
- | 14. |
+ 15. |
zip_directory
|
Creates a ZIP archive by compressing a directory , including files and subdirectories matching a specified glob pattern. It takes a path to the folder and a glob pattern to identify files to compress and a target path for the resulting ZIP file. Both the source directory and the target ZIP file should reside within allowed directories. |
- -
input_directory : string
- -
pattern : string
- -
target_zip_file : string
+ -
input_directory : string
+ -
pattern : string
+ -
target_zip_file : string
|
- | 15. |
+ 16. |
zip_files
|
Creates a ZIP archive by compressing files. It takes a list of files to compress and a target path for the resulting ZIP file. Both the source files and the target ZIP file should reside within allowed directories. |
- -
input_files : string [ ]
- -
target_zip_file : string
+ -
input_files : string [ ]
+ -
target_zip_file : string
|
@@ -215,5 +228,5 @@
-βΎ generated by [mcp-discovery](https://github.com/rust-mcp-stack/mcp-discovery)
+βΎ generated by [mcp-discovery](https://github.com/rust-mcp-stack/mcp-discovery)
\ No newline at end of file
diff --git a/docs/guide/install.md b/docs/guide/install.md
index 47c9557..e4833a0 100644
--- a/docs/guide/install.md
+++ b/docs/guide/install.md
@@ -7,13 +7,13 @@
```sh
-curl --proto '=https' --tlsv1.2 -LsSf https://github.com/rust-mcp-stack/rust-mcp-filesystem/releases/download/v0.1.10/rust-mcp-filesystem-installer.sh | sh
+curl --proto '=https' --tlsv1.2 -LsSf https://github.com/rust-mcp-stack/rust-mcp-filesystem/releases/download/v0.2.0/rust-mcp-filesystem-installer.sh | sh
```
#### **PowerShell script**
```sh
-powershell -ExecutionPolicy Bypass -c "irm https://github.com/rust-mcp-stack/rust-mcp-filesystem/releases/download/v0.1.10/rust-mcp-filesystem-installer.ps1 | iex"
+powershell -ExecutionPolicy Bypass -c "irm https://github.com/rust-mcp-stack/rust-mcp-filesystem/releases/download/v0.2.0/rust-mcp-filesystem-installer.ps1 | iex"
```
@@ -38,78 +38,78 @@ brew install rust-mcp-stack/tap/rust-mcp-filesystem
|
- rust-mcp-filesystem-aarch64-apple-darwin.tar.gz
+ rust-mcp-filesystem-aarch64-apple-darwin.tar.gz
|
Apple Silicon macOS |
- checksum
+ checksum
|
|
- rust-mcp-filesystem-x86_64-apple-darwin.tar.gz
+ rust-mcp-filesystem-x86_64-apple-darwin.tar.gz
|
Intel macOS |
- checksum
+ checksum
|
|
- rust-mcp-filesystem-x86_64-pc-windows-msvc.zip
+ rust-mcp-filesystem-x86_64-pc-windows-msvc.zip
|
x64 Windows (zip) |
- checksum
+ checksum
|
|
- rust-mcp-filesystem-x86_64-pc-windows-msvc.msi
+ rust-mcp-filesystem-x86_64-pc-windows-msvc.msi
|
x64 Windows (msi) |
- checksum
+ checksum
|
|
- rust-mcp-filesystem-aarch64-unknown-linux-gnu.tar.gz
+ rust-mcp-filesystem-aarch64-unknown-linux-gnu.tar.gz
|
ARM64 Linux |
- checksum
+ checksum
|
|
- rust-mcp-filesystem-x86_64-unknown-linux-gnu.tar.gz
+ rust-mcp-filesystem-x86_64-unknown-linux-gnu.tar.gz
|
x64 Linux |
- checksum
+ checksum
|
diff --git a/docs/quickstart.md b/docs/quickstart.md
index b05275d..b14ddf1 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -7,13 +7,13 @@
```sh
-curl --proto '=https' --tlsv1.2 -LsSf https://github.com/rust-mcp-stack/rust-mcp-filesystem/releases/download/v0.1.10/rust-mcp-filesystem-installer.sh | sh
+curl --proto '=https' --tlsv1.2 -LsSf https://github.com/rust-mcp-stack/rust-mcp-filesystem/releases/download/v0.2.0/rust-mcp-filesystem-installer.sh | sh
```
#### **PowerShell script**
```sh
-powershell -ExecutionPolicy Bypass -c "irm https://github.com/rust-mcp-stack/rust-mcp-filesystem/releases/download/v0.1.10/rust-mcp-filesystem-installer.ps1 | iex"
+powershell -ExecutionPolicy Bypass -c "irm https://github.com/rust-mcp-stack/rust-mcp-filesystem/releases/download/v0.2.0/rust-mcp-filesystem-installer.ps1 | iex"
```
@@ -38,78 +38,78 @@ brew install rust-mcp-stack/tap/rust-mcp-filesystem
|
- rust-mcp-filesystem-aarch64-apple-darwin.tar.gz
+ rust-mcp-filesystem-aarch64-apple-darwin.tar.gz
|
Apple Silicon macOS |
- checksum
+ checksum
|
|
- rust-mcp-filesystem-x86_64-apple-darwin.tar.gz
+ rust-mcp-filesystem-x86_64-apple-darwin.tar.gz
|
Intel macOS |
- checksum
+ checksum
|
|
- rust-mcp-filesystem-x86_64-pc-windows-msvc.zip
+ rust-mcp-filesystem-x86_64-pc-windows-msvc.zip
|
x64 Windows (zip) |
- checksum
+ checksum
|
|
- rust-mcp-filesystem-x86_64-pc-windows-msvc.msi
+ rust-mcp-filesystem-x86_64-pc-windows-msvc.msi
|
x64 Windows (msi) |
- checksum
+ checksum
|
|
- rust-mcp-filesystem-aarch64-unknown-linux-gnu.tar.gz
+ rust-mcp-filesystem-aarch64-unknown-linux-gnu.tar.gz
|
ARM64 Linux |
- checksum
+ checksum
|
|
- rust-mcp-filesystem-x86_64-unknown-linux-gnu.tar.gz
+ rust-mcp-filesystem-x86_64-unknown-linux-gnu.tar.gz
|
x64 Linux |
- checksum
+ checksum
|
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 0000000..7855e6d
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,3 @@
+[toolchain]
+channel = "1.88.0"
+components = ["rustfmt", "clippy"]
diff --git a/src/fs_service.rs b/src/fs_service.rs
index a9c6ad0..7c009f2 100644
--- a/src/fs_service.rs
+++ b/src/fs_service.rs
@@ -6,6 +6,7 @@ use grep::{
regex::RegexMatcherBuilder,
searcher::{sinks::UTF8, BinaryDetection, Searcher},
};
+use serde_json::{json, Value};
use std::{
env,
@@ -67,7 +68,7 @@ impl FileSystemService {
.map_while(|dir| {
let expand_result = expand_home(dir.into());
if !expand_result.is_dir() {
- panic!("{}", format!("Error: {} is not a directory", dir));
+ panic!("{}", format!("Error: {dir} is not a directory"));
}
Some(expand_result)
})
@@ -179,7 +180,7 @@ impl FileSystemService {
if target_path.exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
- format!("'{}' already exists!", target_zip_file),
+ format!("'{target_zip_file}' already exists!"),
)
.into());
}
@@ -269,7 +270,7 @@ impl FileSystemService {
if target_path.exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
- format!("'{}' already exists!", target_zip_file),
+ format!("'{target_zip_file}' already exists!"),
)
.into());
}
@@ -326,7 +327,7 @@ impl FileSystemService {
if target_dir_path.exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
- format!("'{}' directory already exists!", target_dir),
+ format!("'{target_dir}' directory already exists!"),
)
.into());
}
@@ -473,7 +474,7 @@ impl FileSystemService {
let glob_pattern = if pattern.contains('*') {
pattern.clone()
} else {
- format!("*{}*", pattern)
+ format!("*{pattern}*")
};
Pattern::new(&glob_pattern)
@@ -501,6 +502,85 @@ impl FileSystemService {
Ok(result)
}
+ /// Generates a JSON representation of a directory tree starting at the given path.
+ ///
+ /// This function recursively builds a JSON array object representing the directory structure,
+ /// where each entry includes a `name` (file or directory name), `type` ("file" or "directory"),
+ /// and for directories, a `children` array containing their contents. Files do not have a
+ /// `children` field.
+ ///
+ /// The function supports optional constraints to limit the tree size:
+ /// - `max_depth`: Limits the depth of directory traversal.
+ /// - `max_files`: Limits the total number of entries (files and directories).
+ ///
+ /// # IMPORTANT NOTE
+ ///
+ /// use max_depth or max_files could lead to partial or skewed representations of actual directory tree
+ pub fn directory_tree>(
+ &self,
+ root_path: P,
+ max_depth: Option,
+ max_files: Option,
+ current_count: &mut usize,
+ ) -> ServiceResult {
+ let valid_path = self.validate_path(root_path.as_ref())?;
+
+ let metadata = fs::metadata(&valid_path)?;
+ if !metadata.is_dir() {
+ return Err(ServiceError::FromString(
+ "Root path must be a directory".into(),
+ ));
+ }
+
+ let mut children = Vec::new();
+
+ if max_depth != Some(0) {
+ for entry in WalkDir::new(valid_path)
+ .min_depth(1)
+ .max_depth(1)
+ .follow_links(true)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ {
+ let child_path = entry.path();
+ let metadata = fs::metadata(child_path)?;
+
+ let entry_name = child_path
+ .file_name()
+ .ok_or(ServiceError::FromString("Invalid path".to_string()))?
+ .to_string_lossy()
+ .into_owned();
+
+ // Increment the count for this entry
+ *current_count += 1;
+
+ // Check if we've exceeded max_files (if set)
+ if let Some(max) = max_files {
+ if *current_count > max {
+ continue; // Skip this entry but continue processing others
+ }
+ }
+
+ let mut json_entry = json!({
+ "name": entry_name,
+ "type": if metadata.is_dir() { "directory" } else { "file" }
+ });
+
+ if metadata.is_dir() {
+ let next_depth = max_depth.map(|d| d - 1);
+ let child_children =
+ self.directory_tree(child_path, next_depth, max_files, current_count)?;
+ json_entry
+ .as_object_mut()
+ .unwrap()
+ .insert("children".to_string(), child_children);
+ }
+ children.push(json_entry);
+ }
+ }
+ Ok(Value::Array(children))
+ }
+
pub fn create_unified_diff(
&self,
original_content: &str,
@@ -519,8 +599,8 @@ impl FileSystemService {
let patch = diff
.unified_diff()
.header(
- format!("{}\toriginal", file_name).as_str(),
- format!("{}\tmodified", file_name).as_str(),
+ format!("{file_name}\toriginal").as_str(),
+ format!("{file_name}\tmodified").as_str(),
)
.context_radius(4)
.to_string();
diff --git a/src/fs_service/utils.rs b/src/fs_service/utils.rs
index 3bd11a7..189c69d 100644
--- a/src/fs_service/utils.rs
+++ b/src/fs_service/utils.rs
@@ -82,7 +82,7 @@ pub fn format_bytes(bytes: u64) -> String {
return format!("{:.2} {}", bytes as f64 / threshold as f64, unit);
}
}
- format!("{} bytes", bytes)
+ format!("{bytes} bytes")
}
pub async fn write_zip_entry(
diff --git a/src/handler.rs b/src/handler.rs
index f8637a7..a2841d7 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -77,7 +77,7 @@ impl ServerHandler for MyServerHandler {
) -> std::result::Result {
runtime
.set_client_details(initialize_request.params.clone())
- .map_err(|err| RpcError::internal_error().with_message(format!("{}", err)))?;
+ .map_err(|err| RpcError::internal_error().with_message(format!("{err}")))?;
let mut server_info = runtime.server_info().to_owned();
// Provide compatibility for clients using older MCP protocol versions.
@@ -150,6 +150,9 @@ impl ServerHandler for MyServerHandler {
FileSystemTools::SearchFilesContentTool(params) => {
SearchFilesContentTool::run_tool(params, &self.fs_service).await
}
+ FileSystemTools::ListDirectoryWithSizesTool(params) => {
+ ListDirectoryWithSizesTool::run_tool(params, &self.fs_service).await
+ }
}
}
}
diff --git a/src/server.rs b/src/server.rs
index 8ebaa88..af7151e 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -11,6 +11,7 @@ pub fn server_details() -> InitializeResult {
server_info: Implementation {
name: "rust-mcp-filesystem".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
+ title:Some("Filesystem MCP Server: fast and efficient tools for managing filesystem operations.".to_string())
},
capabilities: ServerCapabilities {
experimental: None,
diff --git a/src/tools.rs b/src/tools.rs
index 3fbab26..fea0583 100644
--- a/src/tools.rs
+++ b/src/tools.rs
@@ -4,6 +4,7 @@ mod edit_file;
mod get_file_info;
mod list_allowed_directories;
mod list_directory;
+mod list_directory_with_sizes;
mod move_file;
mod read_files;
mod read_multiple_files;
@@ -18,6 +19,7 @@ pub use edit_file::{EditFileTool, EditOperation};
pub use get_file_info::GetFileInfoTool;
pub use list_allowed_directories::ListAllowedDirectoriesTool;
pub use list_directory::ListDirectoryTool;
+pub use list_directory_with_sizes::ListDirectoryWithSizesTool;
pub use move_file::MoveFileTool;
pub use read_files::ReadFileTool;
pub use read_multiple_files::ReadMultipleFilesTool;
@@ -45,7 +47,8 @@ tool_box!(
ZipFilesTool,
UnzipFileTool,
ZipDirectoryTool,
- SearchFilesContentTool
+ SearchFilesContentTool,
+ ListDirectoryWithSizesTool
]
);
@@ -68,6 +71,7 @@ impl FileSystemTools {
| FileSystemTools::ListDirectoryTool(_)
| FileSystemTools::ReadMultipleFilesTool(_)
| FileSystemTools::SearchFilesContentTool(_)
+ | FileSystemTools::ListDirectoryWithSizesTool(_)
| FileSystemTools::SearchFilesTool(_) => false,
}
}
diff --git a/src/tools/create_directory.rs b/src/tools/create_directory.rs
index e84fe9b..48f01ff 100644
--- a/src/tools/create_directory.rs
+++ b/src/tools/create_directory.rs
@@ -1,12 +1,14 @@
use std::path::Path;
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use crate::fs_service::FileSystemService;
#[mcp_tool(
name = "create_directory",
+ title="Create Directory",
description = concat!("Create a new directory or ensure a directory exists. ",
"Can create multiple nested directories in one operation. ",
"If the directory already exists, this operation will succeed silently. ",
@@ -33,9 +35,8 @@ impl CreateDirectoryTool {
.await
.map_err(CallToolError::new)?;
- Ok(CallToolResult::text_content(
+ Ok(CallToolResult::text_content(vec![TextContent::from(
format!("Successfully created directory {}", ¶ms.path),
- None,
- ))
+ )]))
}
}
diff --git a/src/tools/directory_tree.rs b/src/tools/directory_tree.rs
index 21dce17..8d2491e 100644
--- a/src/tools/directory_tree.rs
+++ b/src/tools/directory_tree.rs
@@ -1,16 +1,19 @@
-use std::path::Path;
-
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use serde_json::json;
+use crate::error::ServiceError;
use crate::fs_service::FileSystemService;
#[mcp_tool(
name = "directory_tree",
+ title= "Directory Tree",
description = concat!("Get a recursive tree view of files and directories as a JSON structure. ",
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. ",
"Files have no children array, while directories always have a children array (which may be empty). ",
+ "If the 'max_depth' parameter is provided, the traversal will be limited to the specified depth. ",
+ "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. ",
"The output is formatted with 2-space indentation for readability. Only works within allowed directories."),
destructive_hint = false,
idempotent_hint = false,
@@ -21,28 +24,33 @@ use crate::fs_service::FileSystemService;
pub struct DirectoryTreeTool {
/// The root path of the directory tree to generate.
pub path: String,
+ /// Limits the depth of directory traversal
+ pub max_depth: Option,
}
impl DirectoryTreeTool {
pub async fn run_tool(
params: Self,
context: &FileSystemService,
) -> std::result::Result {
+ let mut entry_counter: usize = 0;
let entries = context
- .list_directory(Path::new(¶ms.path))
- .await
+ .directory_tree(
+ params.path,
+ params.max_depth.map(|v| v as usize),
+ None,
+ &mut entry_counter,
+ )
.map_err(CallToolError::new)?;
- let json_tree: Vec = entries
- .iter()
- .map(|entry| {
- json!({
- "name": entry.file_name().to_str().unwrap_or_default(),
- "type": if entry.path().is_dir(){"directory"}else{"file"}
- })
- })
- .collect();
- let json_str =
- serde_json::to_string_pretty(&json!(json_tree)).map_err(CallToolError::new)?;
- Ok(CallToolResult::text_content(json_str, None))
+ if entry_counter == 0 {
+ return Err(CallToolError::new(ServiceError::FromString(
+ "Could not find any entries".to_string(),
+ )));
+ }
+
+ let json_str = serde_json::to_string_pretty(&json!(entries)).map_err(CallToolError::new)?;
+ Ok(CallToolResult::text_content(vec![TextContent::from(
+ json_str,
+ )]))
}
}
diff --git a/src/tools/edit_file.rs b/src/tools/edit_file.rs
index 502a3fb..a29311c 100644
--- a/src/tools/edit_file.rs
+++ b/src/tools/edit_file.rs
@@ -1,6 +1,7 @@
use std::path::Path;
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use crate::fs_service::FileSystemService;
@@ -18,6 +19,7 @@ pub struct EditOperation {
#[mcp_tool(
name = "edit_file",
+ title="Edit File",
description = concat!("Make line-based edits to a text file. ",
"Each edit replaces exact line sequences with new content. ",
"Returns a git-style diff showing the changes made. ",
@@ -53,6 +55,6 @@ impl EditFileTool {
.await
.map_err(CallToolError::new)?;
- Ok(CallToolResult::text_content(diff, None))
+ Ok(CallToolResult::text_content(vec![TextContent::from(diff)]))
}
}
diff --git a/src/tools/get_file_info.rs b/src/tools/get_file_info.rs
index fcb48cf..55c3a03 100644
--- a/src/tools/get_file_info.rs
+++ b/src/tools/get_file_info.rs
@@ -1,12 +1,14 @@
use std::path::Path;
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use crate::fs_service::FileSystemService;
#[mcp_tool(
name = "get_file_info",
+ title="Get File Info",
description = concat!("Retrieve detailed metadata about a file or directory. ",
"Returns comprehensive information including size, creation time, ",
"last modified time, permissions, and type. ",
@@ -32,6 +34,8 @@ impl GetFileInfoTool {
.get_file_stats(Path::new(¶ms.path))
.await
.map_err(CallToolError::new)?;
- Ok(CallToolResult::text_content(stats.to_string(), None))
+ Ok(CallToolResult::text_content(vec![TextContent::from(
+ stats.to_string(),
+ )]))
}
}
diff --git a/src/tools/list_allowed_directories.rs b/src/tools/list_allowed_directories.rs
index 4e46d34..36e52c2 100644
--- a/src/tools/list_allowed_directories.rs
+++ b/src/tools/list_allowed_directories.rs
@@ -1,10 +1,12 @@
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use crate::fs_service::FileSystemService;
#[mcp_tool(
name = "list_allowed_directories",
+ title="List Allowed Directories",
description = concat!("Returns a list of directories that the server has permission ",
"to access Subdirectories within these allowed directories are also accessible. ",
"Use this to identify which directories and their nested paths are available ",
@@ -31,6 +33,8 @@ impl ListAllowedDirectoriesTool {
.collect::>()
.join("\n")
);
- Ok(CallToolResult::text_content(result, None))
+ Ok(CallToolResult::text_content(vec![TextContent::from(
+ result,
+ )]))
}
}
diff --git a/src/tools/list_directory.rs b/src/tools/list_directory.rs
index 5446ef8..cb0b95e 100644
--- a/src/tools/list_directory.rs
+++ b/src/tools/list_directory.rs
@@ -1,12 +1,14 @@
use std::path::Path;
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use crate::fs_service::FileSystemService;
#[mcp_tool(
name = "list_directory",
+ title="List Directory",
description = concat!("Get a detailed listing of all files and directories in a specified path. ",
"Results clearly distinguish between files and directories with [FILE] and [DIR] ",
"prefixes. This tool is essential for understanding directory structure and ",
@@ -47,6 +49,8 @@ impl ListDirectoryTool {
})
.collect();
- Ok(CallToolResult::text_content(formatted.join("\n"), None))
+ Ok(CallToolResult::text_content(vec![TextContent::from(
+ formatted.join("\n"),
+ )]))
}
}
diff --git a/src/tools/list_directory_with_sizes.rs b/src/tools/list_directory_with_sizes.rs
new file mode 100644
index 0000000..8929cf9
--- /dev/null
+++ b/src/tools/list_directory_with_sizes.rs
@@ -0,0 +1,95 @@
+use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
+use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
+use std::fmt::Write;
+use std::path::Path;
+
+use crate::fs_service::utils::format_bytes;
+use crate::fs_service::FileSystemService;
+
+#[mcp_tool(
+ name = "list_directory_with_sizes",
+ title="List Directory With File Sizes",
+ description = concat!("Get a detailed listing of all files and directories in a specified path, including sizes. " ,
+ "Results clearly distinguish between files and directories with [FILE] and [DIR] prefixes. " ,
+ "This tool is useful for understanding directory structure and " ,
+ "finding specific files within a directory. Only works within allowed directories."),
+ destructive_hint = false,
+ idempotent_hint = false,
+ open_world_hint = false,
+ read_only_hint = true
+)]
+#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug, JsonSchema)]
+pub struct ListDirectoryWithSizesTool {
+ /// The path of the directory to list.
+ pub path: String,
+}
+
+impl ListDirectoryWithSizesTool {
+ async fn format_directory_entries(
+ &self,
+ mut entries: Vec,
+ ) -> std::result::Result {
+ let mut file_count = 0;
+ let mut dir_count = 0;
+ let mut total_size: u64 = 0;
+
+ // Estimate initial capacity: assume ~50 bytes per entry + summary
+ let mut output = String::with_capacity(entries.len() * 50 + 120);
+
+ // Sort entries by file name
+ entries.sort_by_key(|a| a.file_name());
+
+ // build the output string
+ for entry in &entries {
+ let file_name = entry.file_name();
+ let file_name = file_name.to_string_lossy();
+
+ if entry.path().is_dir() {
+ writeln!(output, "[DIR] {file_name:<30}").map_err(CallToolError::new)?;
+ dir_count += 1;
+ } else if entry.path().is_file() {
+ let metadata = entry.metadata().await.map_err(CallToolError::new)?;
+
+ let file_size = metadata.len();
+ writeln!(
+ output,
+ "[FILE] {:<30} {:>10}",
+ file_name,
+ format_bytes(file_size)
+ )
+ .map_err(CallToolError::new)?;
+ file_count += 1;
+ total_size += file_size;
+ }
+ }
+
+ // Append summary
+ writeln!(
+ output,
+ "\nTotal: {file_count} files, {dir_count} directories"
+ )
+ .map_err(CallToolError::new)?;
+ writeln!(output, "Total size: {}", format_bytes(total_size)).map_err(CallToolError::new)?;
+
+ Ok(output)
+ }
+
+ pub async fn run_tool(
+ params: Self,
+ context: &FileSystemService,
+ ) -> std::result::Result {
+ let entries = context
+ .list_directory(Path::new(¶ms.path))
+ .await
+ .map_err(CallToolError::new)?;
+
+ let output = params
+ .format_directory_entries(entries)
+ .await
+ .map_err(CallToolError::new)?;
+ Ok(CallToolResult::text_content(vec![TextContent::from(
+ output,
+ )]))
+ }
+}
diff --git a/src/tools/move_file.rs b/src/tools/move_file.rs
index b3a2cd1..adafaa7 100644
--- a/src/tools/move_file.rs
+++ b/src/tools/move_file.rs
@@ -1,12 +1,14 @@
use std::path::Path;
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use crate::fs_service::FileSystemService;
#[mcp_tool(
name = "move_file",
+ title="Move File",
description = concat!("Move or rename files and directories. Can move files between directories ",
"and rename them in a single operation. If the destination exists, the ",
"operation will fail. Works across different directories and can be used ",
@@ -35,12 +37,11 @@ impl MoveFileTool {
.await
.map_err(CallToolError::new)?;
- Ok(CallToolResult::text_content(
+ Ok(CallToolResult::text_content(vec![TextContent::from(
format!(
"Successfully moved {} to {}",
¶ms.source, ¶ms.destination
),
- None,
- ))
+ )]))
}
}
diff --git a/src/tools/read_files.rs b/src/tools/read_files.rs
index fdd5b67..e4bc34a 100644
--- a/src/tools/read_files.rs
+++ b/src/tools/read_files.rs
@@ -1,12 +1,14 @@
use std::path::Path;
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use crate::fs_service::FileSystemService;
#[mcp_tool(
name = "read_file",
+ title="Read File",
description = concat!("Read the complete contents of a file from the file system. ",
"Handles various text encodings and provides detailed error messages if the ",
"file cannot be read. Use this tool when you need to examine the contents of ",
@@ -32,6 +34,8 @@ impl ReadFileTool {
.await
.map_err(CallToolError::new)?;
- Ok(CallToolResult::text_content(content, None))
+ Ok(CallToolResult::text_content(vec![TextContent::from(
+ content,
+ )]))
}
}
diff --git a/src/tools/read_multiple_files.rs b/src/tools/read_multiple_files.rs
index 666e97b..6df881f 100644
--- a/src/tools/read_multiple_files.rs
+++ b/src/tools/read_multiple_files.rs
@@ -2,12 +2,14 @@ use std::path::Path;
use futures::future::join_all;
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use crate::fs_service::FileSystemService;
#[mcp_tool(
name = "read_multiple_files",
+ title="Read Multiple Files",
description = concat!("Read the contents of multiple files simultaneously. ",
"This is more efficient than reading files one by one when you need to analyze ",
"or compare multiple files. Each file's content is returned with its ",
@@ -40,8 +42,8 @@ impl ReadMultipleFilesTool {
.map_err(CallToolError::new);
content.map_or_else(
- |err| format!("{}: Error - {}", path, err),
- |value| format!("{}:\n{}\n", path, value),
+ |err| format!("{path}: Error - {err}"),
+ |value| format!("{path}:\n{value}\n"),
)
}
})
@@ -49,6 +51,8 @@ impl ReadMultipleFilesTool {
let contents = join_all(content_futures).await;
- Ok(CallToolResult::text_content(contents.join("\n---\n"), None))
+ Ok(CallToolResult::text_content(vec![TextContent::from(
+ contents.join("\n---\n"),
+ )]))
}
}
diff --git a/src/tools/search_file.rs b/src/tools/search_file.rs
index 4df8f44..dfee718 100644
--- a/src/tools/search_file.rs
+++ b/src/tools/search_file.rs
@@ -1,11 +1,13 @@
use std::path::Path;
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use crate::fs_service::FileSystemService;
#[mcp_tool(
name = "search_files",
+ title="Search Files",
description = concat!("Recursively search for files and directories matching a pattern. ",
"Searches through all subdirectories from the starting path. The search ",
"is case-insensitive and matches partial names. Returns full paths to all ",
@@ -49,6 +51,8 @@ impl SearchFilesTool {
} else {
"No matches found".to_string()
};
- Ok(CallToolResult::text_content(result, None))
+ Ok(CallToolResult::text_content(vec![TextContent::from(
+ result,
+ )]))
}
}
diff --git a/src/tools/search_files_content.rs b/src/tools/search_files_content.rs
index a8911ef..e1e50e9 100644
--- a/src/tools/search_files_content.rs
+++ b/src/tools/search_files_content.rs
@@ -1,10 +1,12 @@
use crate::error::ServiceError;
use crate::fs_service::{FileSearchResult, FileSystemService};
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use std::fmt::Write;
#[mcp_tool(
name = "search_files_content",
+ title="Move Files Content",
description = concat!("Searches for text or regex patterns in the content of files matching matching a GLOB pattern.",
"Returns detailed matches with file path, line number, column number and a preview of matched text.",
"By default, it performs a literal text search; if the 'is_regex' parameter is set to true, it performs a regular expression (regex) search instead.",
@@ -76,10 +78,9 @@ impl SearchFilesContentTool {
ServiceError::FromString("No matches found in the files content.".into()),
)));
}
- Ok(CallToolResult::text_content(
+ Ok(CallToolResult::text_content(vec![TextContent::from(
params.format_result(results),
- None,
- ))
+ )]))
}
Err(err) => Ok(CallToolResult::with_error(CallToolError::new(err))),
}
diff --git a/src/tools/write_file.rs b/src/tools/write_file.rs
index f83c398..f323950 100644
--- a/src/tools/write_file.rs
+++ b/src/tools/write_file.rs
@@ -1,4 +1,7 @@
-use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::{
+ macros::{mcp_tool, JsonSchema},
+ schema::TextContent,
+};
use std::path::Path;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
@@ -6,6 +9,7 @@ use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use crate::fs_service::FileSystemService;
#[mcp_tool(
name = "write_file",
+ title="Write File",
description = concat!("Create a new file or completely overwrite an existing file with new content. ",
"Use with caution as it will overwrite existing files without warning. ",
"Handles text content with proper encoding. Only works within allowed directories."),
@@ -32,9 +36,8 @@ impl WriteFileTool {
.await
.map_err(CallToolError::new)?;
- Ok(CallToolResult::text_content(
+ Ok(CallToolResult::text_content(vec![TextContent::from(
format!("Successfully wrote to {}", ¶ms.path),
- None,
- ))
+ )]))
}
}
diff --git a/src/tools/zip_unzip.rs b/src/tools/zip_unzip.rs
index 5e7609f..80a3e7c 100644
--- a/src/tools/zip_unzip.rs
+++ b/src/tools/zip_unzip.rs
@@ -1,10 +1,12 @@
use rust_mcp_sdk::macros::{mcp_tool, JsonSchema};
+use rust_mcp_sdk::schema::TextContent;
use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult};
use crate::fs_service::FileSystemService;
#[mcp_tool(
name = "zip_files",
+ title="Zip Files",
description = concat!("Creates a ZIP archive by compressing files. ",
"It takes a list of files to compress and a target path for the resulting ZIP file. ",
"Both the source files and the target ZIP file should reside within allowed directories."),
@@ -31,12 +33,15 @@ impl ZipFilesTool {
.await
.map_err(CallToolError::new)?;
//TODO: return resource?
- Ok(CallToolResult::text_content(result_content, None))
+ Ok(CallToolResult::text_content(vec![TextContent::from(
+ result_content,
+ )]))
}
}
#[mcp_tool(
name = "unzip_file",
+ title = "Unzip Files",
description = "Extracts the contents of a ZIP archive to a specified target directory.
It takes a source ZIP file path and a target extraction directory.
The tool decompresses all files and directories stored in the ZIP, recreating their structure in the target location.
@@ -60,12 +65,15 @@ impl UnzipFileTool {
.await
.map_err(CallToolError::new)?;
//TODO: return resource?
- Ok(CallToolResult::text_content(result_content, None))
+ Ok(CallToolResult::text_content(vec![TextContent::from(
+ result_content,
+ )]))
}
}
#[mcp_tool(
name = "zip_directory",
+ title = "Zip Directory",
description = "Creates a ZIP archive by compressing a directory , including files and subdirectories matching a specified glob pattern.
It takes a path to the folder and a glob pattern to identify files to compress and a target path for the resulting ZIP file.
Both the source directory and the target ZIP file should reside within allowed directories."
@@ -91,6 +99,8 @@ impl ZipDirectoryTool {
.await
.map_err(CallToolError::new)?;
//TODO: return resource?
- Ok(CallToolResult::text_content(result_content, None))
+ Ok(CallToolResult::text_content(vec![TextContent::from(
+ result_content,
+ )]))
}
}
diff --git a/tests/test_tools.rs b/tests/test_tools.rs
index 9cccee9..8c46611 100644
--- a/tests/test_tools.rs
+++ b/tests/test_tools.rs
@@ -3,7 +3,7 @@ pub mod common;
use common::setup_service;
use rust_mcp_filesystem::tools::*;
-use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResultContentItem};
+use rust_mcp_sdk::schema::{schema_utils::CallToolError, ContentBlock};
use std::fs;
#[tokio::test]
@@ -22,7 +22,7 @@ async fn test_create_directory_new_directory() {
let content = call_result.content.first().unwrap();
match content {
- CallToolResultContentItem::TextContent(text_content) => {
+ ContentBlock::TextContent(text_content) => {
assert_eq!(
text_content.text,
format!(
@@ -54,7 +54,7 @@ async fn test_create_directory_existing_directory() {
let content = call_result.content.first().unwrap();
match content {
- CallToolResultContentItem::TextContent(text_content) => {
+ ContentBlock::TextContent(text_content) => {
assert_eq!(
text_content.text,
format!(
@@ -85,7 +85,7 @@ async fn test_create_directory_nested() {
let content = call_result.content.first().unwrap();
match content {
- CallToolResultContentItem::TextContent(text_content) => {
+ ContentBlock::TextContent(text_content) => {
assert_eq!(
text_content.text,
format!(