Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
435 changes: 144 additions & 291 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.88.0"
channel = "1.89.0"
components = ["rustfmt", "clippy"]
28 changes: 13 additions & 15 deletions src/fs_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -792,10 +792,10 @@ impl FileSystemService {
*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
}
if let Some(max) = max_files
&& *current_count > max
{
continue; // Skip this entry but continue processing others
}

let mut json_entry = json!({
Expand Down Expand Up @@ -1463,10 +1463,8 @@ impl FileSystemService {
.filter_map(|e| e.ok())
.all(|e| !e.file_type().is_file() || is_system_metadata_file(e.file_name())); // Directory is empty if no files are found in it or subdirs, ".DS_Store" will be ignores on Mac

if is_empty {
if let Some(path_str) = entry.path().to_str() {
empty_dirs.push(path_str.to_string());
}
if is_empty && let Some(path_str) = entry.path().to_str() {
empty_dirs.push(path_str.to_string());
}
}

Expand Down Expand Up @@ -1505,13 +1503,13 @@ impl FileSystemService {
.filter(|e| e.file_type().is_file()); // Only files

for entry in entries {
if let Ok(metadata) = entry.metadata() {
if let Some(path_str) = entry.path().to_str() {
size_map
.entry(metadata.len())
.or_default()
.push(path_str.to_string());
}
if let Ok(metadata) = entry.metadata()
&& let Some(path_str) = entry.path().to_str()
{
size_map
.entry(metadata.len())
.or_default()
.push(path_str.to_string());
}
}

Expand Down
10 changes: 5 additions & 5 deletions src/fs_service/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ pub fn normalize_path(path: &Path) -> PathBuf {
}

pub fn expand_home(path: PathBuf) -> PathBuf {
if let Some(home_dir) = home_dir() {
if path.starts_with("~") {
let stripped_path = path.strip_prefix("~").unwrap_or(&path);
return home_dir.join(stripped_path);
}
if let Some(home_dir) = home_dir()
&& path.starts_with("~")
{
let stripped_path = path.strip_prefix("~").unwrap_or(&path);
return home_dir.join(stripped_path);
}
path
}
Expand Down
34 changes: 23 additions & 11 deletions src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,23 +70,29 @@ impl FileSystemHandler {
}

pub(crate) async fn update_allowed_directories(&self, runtime: Arc<dyn McpServer>) {
// if client does not support roots
// return if roots_support is not enabled
if !self.mcp_roots_support {
return;
}

let allowed_directories = self.fs_service.allowed_directories().await;
// if client does NOT support roots
if !runtime.client_supports_root_list().unwrap_or(false) {
// use allowed directories from command line
if !allowed_directories.is_empty() {
let _ = runtime.stderr_message(format!("Client does not support MCP Roots, using allowed directories set from server args:\n{}", allowed_directories
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<String>>()
.join(",\n"))).await;
// display message only if mcp_roots_support is enabled, otherwise this message will be redundant
if self.mcp_roots_support {
let _ = runtime.stderr_message("Client does not support MCP Roots. Allowed directories passed from command-line will be used.".to_string()).await;
}
} else {
// let message = "Server cannot operate: No allowed directories available. Server was started without command-line directories and client either does not support MCP roots protocol or provided empty roots. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories.";
// root lists not supported AND allowed directories are empty
let message = "Server cannot operate: No allowed directories available. Server was started without command-line directories and client does not support MCP roots protocol. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories.";
let _ = runtime.stderr_message(message.to_string()).await;
std::process::exit(1); // exit the server
}
} else {
// client supports roots
let fs_service = self.fs_service.clone();
let mcp_roots_support = self.mcp_roots_support;
// retrieve roots from the client and update the allowed directories accordingly
let roots = match runtime.clone().list_roots(None).await {
Ok(roots_result) => roots_result.roots,
Expand All @@ -111,7 +117,7 @@ impl FileSystemHandler {
}
};

if valid_roots.is_empty() && !mcp_roots_support {
if valid_roots.is_empty() {
let message = if allowed_directories.is_empty() {
"Server cannot operate: No allowed directories available. Server was started without command-line directories and client provided empty roots. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories."
} else {
Expand All @@ -120,7 +126,6 @@ impl FileSystemHandler {
let _ = runtime.stderr_message(message.to_string()).await;
} else {
let num_valid_roots = valid_roots.len();

fs_service.update_allowed_paths(valid_roots).await;
let message = format!(
"Updated allowed directories from MCP roots: {num_valid_roots} valid directories",
Expand All @@ -142,7 +147,14 @@ impl ServerHandler for FileSystemHandler {
_notification: RootsListChangedNotification,
runtime: Arc<dyn McpServer>,
) -> std::result::Result<(), RpcError> {
self.update_allowed_directories(runtime).await;
if self.mcp_roots_support {
self.update_allowed_directories(runtime).await;
} else {
let message =
"Skipping ROOTS client updates, server launched without the --enable-roots flag."
.to_string();
let _ = runtime.stderr_message(message).await;
};
Ok(())
}

Expand Down
2 changes: 1 addition & 1 deletion src/tools/read_media_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::fs_service::FileSystemService;

#[mcp_tool(
name = "read_media_file",
title="Read an Image or Audio file",
title="Read a media (Image/Audio) file",
description = concat!("Reads an image or audio file and returns its Base64-encoded content along with the corresponding MIME type. ",
"The max_bytes argument could be used to enforce an upper limit on the size of a file to read ",
"if the media file exceeds this limit, the operation will return an error instead of reading the media file. ",
Expand Down
2 changes: 1 addition & 1 deletion src/tools/search_files_content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use rust_mcp_sdk::schema::{CallToolResult, schema_utils::CallToolError};
use std::fmt::Write;
#[mcp_tool(
name = "search_files_content",
title="Move files content",
title="Search 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.",
Expand Down
16 changes: 8 additions & 8 deletions tests/test_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,16 @@ async fn ensure_tools_duplication() {
duplicate_names.push(t.name.to_string());
}

if let Some(title) = t.title {
if !titles.insert(title.to_string()) {
duplicate_titles.push(title.to_string());
}
if let Some(title) = t.title
&& !titles.insert(title.to_string())
{
duplicate_titles.push(title.to_string());
}

if let Some(description) = t.description {
if !descriptions.insert(description.to_string()) {
duplicate_descriptions.push(description.to_string());
}
if let Some(description) = t.description
&& !descriptions.insert(description.to_string())
{
duplicate_descriptions.push(description.to_string());
}
}

Expand Down