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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This project is a pure Rust rewrite of the JavaScript-based `@modelcontextprotoc
- **πŸ”„ MCP Roots support**: enabling clients to dynamically modify the list of allowed directories (disabled by default).
- **πŸ“¦ ZIP Archive Support**: Tools to create ZIP archives from files or directories and extract ZIP files with ease.
- **πŸͺΆ Lightweight**: Standalone with no external dependencies (e.g., no Node.js, Python etc required), compiled to a single binary with a minimal resource footprint, ideal for both lightweight and extensive deployment scenarios.
- **πŸŽ›οΈ Tool Disabling**: Disable specific tools to limit functionality and reduce the number of available tools, helping to save tokens.

#### πŸ‘‰ Refer to [capabilities](https://rust-mcp-stack.github.io/rust-mcp-filesystem/#/capabilities) for a full list of tools and other capabilities.

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This project is a pure Rust rewrite of the JavaScript-based **@modelcontextproto
- **πŸ”„ MCP Roots support**: enabling clients to dynamically modify the list of allowed directories (disabled by default).
- **πŸ“¦ ZIP Archive Support**: Tools to create ZIP archives from files or directories and extract ZIP files with ease.
- **πŸͺΆ Lightweight**: Standalone with no external dependencies (e.g., no Node.js, Python etc required), compiled to a single binary with a minimal resource footprint, ideal for both lightweight and extensive deployment scenarios.
- **πŸŽ›οΈ Tool Disabling**: Disable specific tools to limit functionality and reduce the number of available tools, helping to save tokens.

#### Refer to   [capabilities](capabilities.md)   for a full list of tools and other capabilities.

Expand Down
24 changes: 23 additions & 1 deletion docs/_configs/claude-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,31 @@ Incorporate the following into your `claude_desktop_config.json`, based on your
}
```

### Disabling Specific Tools

You can disable specific tools using the `-d` or `--disable-tools` flag:

```json
{
"mcpServers": {
"filesystem": {
"command": "rust-mcp-filesystem",
"args": [
"-d", "write_file,edit_file,move_file",
"~/Documents"
]
}
}
}
```

This example disables `write_file`, `edit_file` and `move_file`. See the [CLI Options](../guide/cli-command-options.md) documentation for more details.

## Running via Docker

**Note:** In the example below, all allowed directories are mounted to `/projects`, and `/projects` is passed as the allowed directory argument to the server CLI. You can modify this as needed to fit your requirements.

`ALLOW_WRITE` and `ENABLE_ROOTS` environments could be used to enable write and MCP Roots support.
`ALLOW_WRITE`, `ENABLE_ROOTS`, and `DISABLE_TOOLS` environments could be used to enable write, MCP Roots support, and disable specific tools.

```json
{
Expand All @@ -52,6 +72,8 @@ Incorporate the following into your `claude_desktop_config.json`, based on your
"ALLOW_WRITE=false",
"-e",
"ENABLE_ROOTS=false",
"-e",
"DISABLE_TOOLS=write_file,edit_file",
"--mount",
"type=bind,src=/Users/username/Documents,dst=/projects/Documents",
"--mount",
Expand Down
34 changes: 34 additions & 0 deletions docs/guide/cli-command-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ Arguments:
Example: rust-mcp-filesystem /path/to/dir1 /path/to/dir2 /path/to/dir3

Options:
-d, --disable-tools <DISABLE_TOOLS>
Comma-separated list of tools to disable. By default, all tools are enabled.
Visit https://rust-mcp-stack.github.io/rust-mcp-filesystem/#/capabilities to view the full list of available tools.

[env: DISABLE_TOOLS=]

-w, --allow-write [<ALLOW_WRITE>]
Enables write mode for the app, allowing both reading and writing. Defaults to disabled.

Expand All @@ -33,3 +39,31 @@ Options:
-V, --version
Print version
```

## Disabling Tools

You can disable specific tools to limit the functionality of the server. This is useful for:

- Security hardening by disabling write operations
- Reducing the number of available tools to save tokens
- Customizing available functionality for specific use cases

### Examples

```sh
# Disable write-related tools
rust-mcp-filesystem -d write_file,edit_file,move_file /path/to/dir

# Disable multiple tools
rust-mcp-filesystem -d read_text_file,search_files /path/to/dir

# Using environment variable
DISABLE_TOOLS=write_file,edit_file rust-mcp-filesystem /path/to/dir

# Disable all write tools at once
rust-mcp-filesystem -d write_file,edit_file,move_file,create_directory,delete_file,zip_files,unzip_file /path/to/dir
```

### Available Tool Names

For a complete list of available tools and their names, see the [Capabilities](https://rust-mcp-stack.github.io/rust-mcp-filesystem/#/capabilities) page. Tool names are case-insensitive (e.g., `read_text_file` and `Read_Text_File` are equivalent).
42 changes: 41 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::tools::FileSystemTools;
use clap::{Parser, arg, command};
use std::collections::HashSet;

#[derive(Parser, Debug)]
#[command(name = env!("CARGO_PKG_NAME"))]
Expand All @@ -16,6 +18,14 @@ pub struct CommandArguments {
)]
pub allow_write: bool,

#[arg(
short = 'd',
long = "disable-tools",
help = "Comma-separated list of tools to disable. By default, all tools are enabled.\nVisit https://rust-mcp-stack.github.io/rust-mcp-filesystem/#/capabilities to view the full list of available tools.",
env = "DISABLE_TOOLS"
)]
pub disable_tools: Option<String>,

#[arg(
short = 't',
long,
Expand All @@ -32,16 +42,46 @@ pub struct CommandArguments {
required = false
)]
pub allowed_directories: Vec<String>,

// internal-only field, not exposed as CLI arg
#[arg(skip)]
pub disabled_tool_names: Option<Vec<String>>,
}

impl CommandArguments {
pub fn validate(&self) -> Result<(), String> {
pub fn validate(&mut self) -> Result<(), String> {
if !self.enable_roots && self.allowed_directories.is_empty() {
return Err(format!(
" <ALLOWED_DIRECTORIES> is required when `--enable-roots` is not provided.\n Run `{} --help` to view the usage instructions.",
env!("CARGO_PKG_NAME")
));
}

// verify disable_tools are valid
if let Some(tools) = self.disable_tools.as_ref() {
let disabled_tools: Vec<_> = tools
.split(',')
.map(|t| t.trim().to_lowercase())
.filter(|t| !t.is_empty())
.collect();

let valid_tools: HashSet<_> = FileSystemTools::tools()
.iter()
.map(|t| t.name.to_lowercase())
.collect();

for tool in &disabled_tools {
if !valid_tools.contains(tool) {
return Err(format!(
"Invalid entry detected in the disable-tools list : '{}'",
tool
));
}
}

// Update the struct field with the cleaned list as a **comma-separated string**
self.disabled_tool_names = Some(disabled_tools);
}
Ok(())
}
}
43 changes: 40 additions & 3 deletions src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,28 @@ use rust_mcp_sdk::schema::{
CallToolResult, InitializeResult, ListToolsResult, RpcError, schema_utils::CallToolError,
};
use std::cmp::Ordering;
use std::collections::HashSet;
use std::sync::Arc;

pub struct FileSystemHandler {
readonly: bool,
mcp_roots_support: bool,
fs_service: Arc<FileSystemService>,
disabled_tools: HashSet<String>,
}

impl FileSystemHandler {
pub fn new(args: &CommandArguments) -> ServiceResult<Self> {
pub fn new(args: CommandArguments) -> ServiceResult<Self> {
let fs_service = FileSystemService::try_new(&args.allowed_directories)?;
Ok(Self {
fs_service: Arc::new(fs_service),
readonly: !args.allow_write,
mcp_roots_support: args.enable_roots,
disabled_tools: args
.disabled_tool_names
.unwrap_or_default()
.into_iter()
.collect(),
})
}

Expand All @@ -53,6 +60,25 @@ impl FileSystemHandler {
},
);

let disabled_tool_message = if !self.disabled_tools.is_empty() {
let count = self.disabled_tools.len();
let plural = if count > 1 { "s" } else { "" };
let verb = if count > 1 { "are" } else { "is" };
format!(
"{} tool{} {} disabled: {}",
count,
plural,
verb,
self.disabled_tools
.iter()
.cloned()
.collect::<Vec<String>>()
.join(", ")
)
} else {
"No tools are disabled πŸ‘".to_string()
};

let allowed_directories = self.fs_service.allowed_directories().await;
let sub_message: String = if allowed_directories.is_empty() && self.mcp_roots_support {
"No allowed directories is set - waiting for client to provide roots via MCP protocol...".to_string()
Expand All @@ -67,7 +93,7 @@ impl FileSystemHandler {
)
};

format!("{common_message}\n{sub_message}")
format!("{common_message}\n{disabled_tool_message}\n{sub_message}")
}

pub(crate) async fn update_allowed_directories(&self, runtime: Arc<dyn McpServer>) {
Expand Down Expand Up @@ -165,7 +191,10 @@ impl ServerHandler for FileSystemHandler {
_: Arc<dyn McpServer>,
) -> std::result::Result<ListToolsResult, RpcError> {
Ok(ListToolsResult {
tools: FileSystemTools::tools(),
tools: FileSystemTools::tools()
.into_iter()
.filter(|t| !self.disabled_tools.contains(&t.name))
.collect(),
meta: None,
next_cursor: None,
})
Expand Down Expand Up @@ -194,6 +223,14 @@ impl ServerHandler for FileSystemHandler {
params: CallToolRequestParams,
_: Arc<dyn McpServer>,
) -> std::result::Result<CallToolResult, CallToolError> {
// check if tool is disabled
if self.disabled_tools.contains(&params.name) {
return Err(CallToolError::from_message(format!(
"Error: The tool '{}' is disabled. Check the 'disable-tools' list in your configuration and ensure it's enabled before trying again.",
&params.name
)));
}

let tool_params: FileSystemTools =
FileSystemTools::try_from(params).map_err(CallToolError::new)?;

Expand Down
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use rust_mcp_filesystem::{cli, server};

#[tokio::main]
async fn main() {
let arguments = cli::CommandArguments::parse();
let mut arguments = cli::CommandArguments::parse();

if let Err(err) = arguments.validate() {
eprintln!("Error: {err}");
return;
Expand Down
2 changes: 1 addition & 1 deletion src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub fn server_details() -> InitializeResult {
pub async fn start_server(args: CommandArguments) -> ServiceResult<()> {
let transport = StdioTransport::new(TransportOptions::default())?;

let handler = FileSystemHandler::new(&args)?;
let handler = FileSystemHandler::new(args)?;
let server = server_runtime::create_server(McpServerOptions {
server_details: server_details(),
handler: handler.to_mcp_server_handler(),
Expand Down
104 changes: 104 additions & 0 deletions tests/test_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,107 @@ fn test_invalid_flag() {
assert_eq!(e.kind(), clap::error::ErrorKind::UnknownArgument);
}
}

#[test]
fn test_disable_tools_single_tool() {
let args = ["mcp-server", "-d", "read_text_file", "/path/to/dir"];
let mut result = parse_args(&args).unwrap();
let validated = result.validate();
assert!(validated.is_ok());
assert_eq!(
result.disabled_tool_names,
Some(vec!["read_text_file".to_string()])
);
}

#[test]
fn test_disable_tools_multiple_tools() {
let args = [
"mcp-server",
"-d",
"read_text_file,write_file,edit_file",
"/path/to/dir",
];
let mut result = parse_args(&args).unwrap();
let validated = result.validate();
assert!(validated.is_ok());
let mut expected = result.disabled_tool_names.unwrap();
expected.sort();
assert_eq!(expected, vec!["edit_file", "read_text_file", "write_file"]);
}

#[test]
fn test_disable_tools_case_insensitive() {
let args = ["mcp-server", "-d", "Read_Text_File", "/path/to/dir"];
let mut result = parse_args(&args).unwrap();
let validated = result.validate();
assert!(validated.is_ok());
assert_eq!(
result.disabled_tool_names,
Some(vec!["read_text_file".to_string()])
);
}

#[test]
fn test_disable_tools_with_spaces() {
let args = [
"mcp-server",
"-d",
"read_text_file, write_file ",
"/path/to/dir",
];
let mut result = parse_args(&args).unwrap();
let validated = result.validate();
assert!(validated.is_ok());
let mut expected = result.disabled_tool_names.unwrap();
expected.sort();
assert_eq!(expected, vec!["read_text_file", "write_file"]);
}

#[test]
fn test_disable_tools_invalid_tool() {
let args = ["mcp-server", "-d", "invalidtool", "/path/to/dir"];
let mut result = parse_args(&args).unwrap();
let validated = result.validate();
assert!(validated.is_err());
assert!(
validated
.unwrap_err()
.contains("Invalid entry detected in the disable-tools list : 'invalidtool'")
);
}

#[test]
fn test_disable_tools_empty_value() {
let args = ["mcp-server", "-d", "", "/path/to/dir"];
let mut result = parse_args(&args).unwrap();
let validated = result.validate();
assert!(validated.is_ok());
assert_eq!(result.disabled_tool_names, Some(vec![]));
}

#[test]
fn test_disable_tools_whitespace_only() {
let args = ["mcp-server", "-d", " ", "/path/to/dir"];
let mut result = parse_args(&args).unwrap();
let validated = result.validate();
assert!(validated.is_ok());
assert_eq!(result.disabled_tool_names, Some(vec![]));
}

#[test]
fn test_disable_tools_long_flag() {
let args = [
"mcp-server",
"--disable-tools",
"read_text_file",
"/path/to/dir",
];
let mut result = parse_args(&args).unwrap();
let validated = result.validate();
assert!(validated.is_ok());
assert_eq!(
result.disabled_tool_names,
Some(vec!["read_text_file".to_string()])
);
}
Loading