Skip to content

Commit 1c8a53d

Browse files
authored
feat: add --disable-tools CLI option to disable specific tools (#79)
* feat: fail on multiple matches by default and add replaceAll option for bulk replacement * chore. ignore doctest * chore: update dependencies * feat: exposing only subset of tools * chore: use hashset for disabled tools * chore: enhancement and add tests * chore: update documentation
1 parent 8f1e2a3 commit 1c8a53d

9 files changed

Lines changed: 247 additions & 7 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ This project is a pure Rust rewrite of the JavaScript-based `@modelcontextprotoc
1919
- **🔄 MCP Roots support**: enabling clients to dynamically modify the list of allowed directories (disabled by default).
2020
- **📦 ZIP Archive Support**: Tools to create ZIP archives from files or directories and extract ZIP files with ease.
2121
- **🪶 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.
22+
- **🎛️ Tool Disabling**: Disable specific tools to limit functionality and reduce the number of available tools, helping to save tokens.
2223

2324
#### 👉 Refer to [capabilities](https://rust-mcp-stack.github.io/rust-mcp-filesystem/#/capabilities) for a full list of tools and other capabilities.
2425

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This project is a pure Rust rewrite of the JavaScript-based **@modelcontextproto
1111
- **🔄 MCP Roots support**: enabling clients to dynamically modify the list of allowed directories (disabled by default).
1212
- **📦 ZIP Archive Support**: Tools to create ZIP archives from files or directories and extract ZIP files with ease.
1313
- **🪶 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.
14+
- **🎛️ Tool Disabling**: Disable specific tools to limit functionality and reduce the number of available tools, helping to save tokens.
1415

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

docs/_configs/claude-desktop.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,31 @@ Incorporate the following into your `claude_desktop_config.json`, based on your
3333
}
3434
```
3535

36+
### Disabling Specific Tools
37+
38+
You can disable specific tools using the `-d` or `--disable-tools` flag:
39+
40+
```json
41+
{
42+
"mcpServers": {
43+
"filesystem": {
44+
"command": "rust-mcp-filesystem",
45+
"args": [
46+
"-d", "write_file,edit_file,move_file",
47+
"~/Documents"
48+
]
49+
}
50+
}
51+
}
52+
```
53+
54+
This example disables `write_file`, `edit_file` and `move_file`. See the [CLI Options](../guide/cli-command-options.md) documentation for more details.
55+
3656
## Running via Docker
3757

3858
**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.
3959

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

4262
```json
4363
{
@@ -52,6 +72,8 @@ Incorporate the following into your `claude_desktop_config.json`, based on your
5272
"ALLOW_WRITE=false",
5373
"-e",
5474
"ENABLE_ROOTS=false",
75+
"-e",
76+
"DISABLE_TOOLS=write_file,edit_file",
5577
"--mount",
5678
"type=bind,src=/Users/username/Documents,dst=/projects/Documents",
5779
"--mount",

docs/guide/cli-command-options.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ Arguments:
1111
Example: rust-mcp-filesystem /path/to/dir1 /path/to/dir2 /path/to/dir3
1212

1313
Options:
14+
-d, --disable-tools <DISABLE_TOOLS>
15+
Comma-separated list of tools to disable. By default, all tools are enabled.
16+
Visit https://rust-mcp-stack.github.io/rust-mcp-filesystem/#/capabilities to view the full list of available tools.
17+
18+
[env: DISABLE_TOOLS=]
19+
1420
-w, --allow-write [<ALLOW_WRITE>]
1521
Enables write mode for the app, allowing both reading and writing. Defaults to disabled.
1622

@@ -33,3 +39,31 @@ Options:
3339
-V, --version
3440
Print version
3541
```
42+
43+
## Disabling Tools
44+
45+
You can disable specific tools to limit the functionality of the server. This is useful for:
46+
47+
- Security hardening by disabling write operations
48+
- Reducing the number of available tools to save tokens
49+
- Customizing available functionality for specific use cases
50+
51+
### Examples
52+
53+
```sh
54+
# Disable write-related tools
55+
rust-mcp-filesystem -d write_file,edit_file,move_file /path/to/dir
56+
57+
# Disable multiple tools
58+
rust-mcp-filesystem -d read_text_file,search_files /path/to/dir
59+
60+
# Using environment variable
61+
DISABLE_TOOLS=write_file,edit_file rust-mcp-filesystem /path/to/dir
62+
63+
# Disable all write tools at once
64+
rust-mcp-filesystem -d write_file,edit_file,move_file,create_directory,delete_file,zip_files,unzip_file /path/to/dir
65+
```
66+
67+
### Available Tool Names
68+
69+
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).

src/cli.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
use crate::tools::FileSystemTools;
12
use clap::{Parser, arg, command};
3+
use std::collections::HashSet;
24

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

21+
#[arg(
22+
short = 'd',
23+
long = "disable-tools",
24+
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.",
25+
env = "DISABLE_TOOLS"
26+
)]
27+
pub disable_tools: Option<String>,
28+
1929
#[arg(
2030
short = 't',
2131
long,
@@ -32,16 +42,46 @@ pub struct CommandArguments {
3242
required = false
3343
)]
3444
pub allowed_directories: Vec<String>,
45+
46+
// internal-only field, not exposed as CLI arg
47+
#[arg(skip)]
48+
pub disabled_tool_names: Option<Vec<String>>,
3549
}
3650

3751
impl CommandArguments {
38-
pub fn validate(&self) -> Result<(), String> {
52+
pub fn validate(&mut self) -> Result<(), String> {
3953
if !self.enable_roots && self.allowed_directories.is_empty() {
4054
return Err(format!(
4155
" <ALLOWED_DIRECTORIES> is required when `--enable-roots` is not provided.\n Run `{} --help` to view the usage instructions.",
4256
env!("CARGO_PKG_NAME")
4357
));
4458
}
59+
60+
// verify disable_tools are valid
61+
if let Some(tools) = self.disable_tools.as_ref() {
62+
let disabled_tools: Vec<_> = tools
63+
.split(',')
64+
.map(|t| t.trim().to_lowercase())
65+
.filter(|t| !t.is_empty())
66+
.collect();
67+
68+
let valid_tools: HashSet<_> = FileSystemTools::tools()
69+
.iter()
70+
.map(|t| t.name.to_lowercase())
71+
.collect();
72+
73+
for tool in &disabled_tools {
74+
if !valid_tools.contains(tool) {
75+
return Err(format!(
76+
"Invalid entry detected in the disable-tools list : '{}'",
77+
tool
78+
));
79+
}
80+
}
81+
82+
// Update the struct field with the cleaned list as a **comma-separated string**
83+
self.disabled_tool_names = Some(disabled_tools);
84+
}
4585
Ok(())
4686
}
4787
}

src/handler.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,28 @@ use rust_mcp_sdk::schema::{
1212
CallToolResult, InitializeResult, ListToolsResult, RpcError, schema_utils::CallToolError,
1313
};
1414
use std::cmp::Ordering;
15+
use std::collections::HashSet;
1516
use std::sync::Arc;
1617

1718
pub struct FileSystemHandler {
1819
readonly: bool,
1920
mcp_roots_support: bool,
2021
fs_service: Arc<FileSystemService>,
22+
disabled_tools: HashSet<String>,
2123
}
2224

2325
impl FileSystemHandler {
24-
pub fn new(args: &CommandArguments) -> ServiceResult<Self> {
26+
pub fn new(args: CommandArguments) -> ServiceResult<Self> {
2527
let fs_service = FileSystemService::try_new(&args.allowed_directories)?;
2628
Ok(Self {
2729
fs_service: Arc::new(fs_service),
2830
readonly: !args.allow_write,
2931
mcp_roots_support: args.enable_roots,
32+
disabled_tools: args
33+
.disabled_tool_names
34+
.unwrap_or_default()
35+
.into_iter()
36+
.collect(),
3037
})
3138
}
3239

@@ -53,6 +60,25 @@ impl FileSystemHandler {
5360
},
5461
);
5562

63+
let disabled_tool_message = if !self.disabled_tools.is_empty() {
64+
let count = self.disabled_tools.len();
65+
let plural = if count > 1 { "s" } else { "" };
66+
let verb = if count > 1 { "are" } else { "is" };
67+
format!(
68+
"{} tool{} {} disabled: {}",
69+
count,
70+
plural,
71+
verb,
72+
self.disabled_tools
73+
.iter()
74+
.cloned()
75+
.collect::<Vec<String>>()
76+
.join(", ")
77+
)
78+
} else {
79+
"No tools are disabled 👍".to_string()
80+
};
81+
5682
let allowed_directories = self.fs_service.allowed_directories().await;
5783
let sub_message: String = if allowed_directories.is_empty() && self.mcp_roots_support {
5884
"No allowed directories is set - waiting for client to provide roots via MCP protocol...".to_string()
@@ -67,7 +93,7 @@ impl FileSystemHandler {
6793
)
6894
};
6995

70-
format!("{common_message}\n{sub_message}")
96+
format!("{common_message}\n{disabled_tool_message}\n{sub_message}")
7197
}
7298

7399
pub(crate) async fn update_allowed_directories(&self, runtime: Arc<dyn McpServer>) {
@@ -165,7 +191,10 @@ impl ServerHandler for FileSystemHandler {
165191
_: Arc<dyn McpServer>,
166192
) -> std::result::Result<ListToolsResult, RpcError> {
167193
Ok(ListToolsResult {
168-
tools: FileSystemTools::tools(),
194+
tools: FileSystemTools::tools()
195+
.into_iter()
196+
.filter(|t| !self.disabled_tools.contains(&t.name))
197+
.collect(),
169198
meta: None,
170199
next_cursor: None,
171200
})
@@ -194,6 +223,14 @@ impl ServerHandler for FileSystemHandler {
194223
params: CallToolRequestParams,
195224
_: Arc<dyn McpServer>,
196225
) -> std::result::Result<CallToolResult, CallToolError> {
226+
// check if tool is disabled
227+
if self.disabled_tools.contains(&params.name) {
228+
return Err(CallToolError::from_message(format!(
229+
"Error: The tool '{}' is disabled. Check the 'disable-tools' list in your configuration and ensure it's enabled before trying again.",
230+
&params.name
231+
)));
232+
}
233+
197234
let tool_params: FileSystemTools =
198235
FileSystemTools::try_from(params).map_err(CallToolError::new)?;
199236

src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use rust_mcp_filesystem::{cli, server};
33

44
#[tokio::main]
55
async fn main() {
6-
let arguments = cli::CommandArguments::parse();
6+
let mut arguments = cli::CommandArguments::parse();
7+
78
if let Err(err) = arguments.validate() {
89
eprintln!("Error: {err}");
910
return;

src/server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub fn server_details() -> InitializeResult {
4141
pub async fn start_server(args: CommandArguments) -> ServiceResult<()> {
4242
let transport = StdioTransport::new(TransportOptions::default())?;
4343

44-
let handler = FileSystemHandler::new(&args)?;
44+
let handler = FileSystemHandler::new(args)?;
4545
let server = server_runtime::create_server(McpServerOptions {
4646
server_details: server_details(),
4747
handler: handler.to_mcp_server_handler(),

tests/test_cli.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,107 @@ fn test_invalid_flag() {
8080
assert_eq!(e.kind(), clap::error::ErrorKind::UnknownArgument);
8181
}
8282
}
83+
84+
#[test]
85+
fn test_disable_tools_single_tool() {
86+
let args = ["mcp-server", "-d", "read_text_file", "/path/to/dir"];
87+
let mut result = parse_args(&args).unwrap();
88+
let validated = result.validate();
89+
assert!(validated.is_ok());
90+
assert_eq!(
91+
result.disabled_tool_names,
92+
Some(vec!["read_text_file".to_string()])
93+
);
94+
}
95+
96+
#[test]
97+
fn test_disable_tools_multiple_tools() {
98+
let args = [
99+
"mcp-server",
100+
"-d",
101+
"read_text_file,write_file,edit_file",
102+
"/path/to/dir",
103+
];
104+
let mut result = parse_args(&args).unwrap();
105+
let validated = result.validate();
106+
assert!(validated.is_ok());
107+
let mut expected = result.disabled_tool_names.unwrap();
108+
expected.sort();
109+
assert_eq!(expected, vec!["edit_file", "read_text_file", "write_file"]);
110+
}
111+
112+
#[test]
113+
fn test_disable_tools_case_insensitive() {
114+
let args = ["mcp-server", "-d", "Read_Text_File", "/path/to/dir"];
115+
let mut result = parse_args(&args).unwrap();
116+
let validated = result.validate();
117+
assert!(validated.is_ok());
118+
assert_eq!(
119+
result.disabled_tool_names,
120+
Some(vec!["read_text_file".to_string()])
121+
);
122+
}
123+
124+
#[test]
125+
fn test_disable_tools_with_spaces() {
126+
let args = [
127+
"mcp-server",
128+
"-d",
129+
"read_text_file, write_file ",
130+
"/path/to/dir",
131+
];
132+
let mut result = parse_args(&args).unwrap();
133+
let validated = result.validate();
134+
assert!(validated.is_ok());
135+
let mut expected = result.disabled_tool_names.unwrap();
136+
expected.sort();
137+
assert_eq!(expected, vec!["read_text_file", "write_file"]);
138+
}
139+
140+
#[test]
141+
fn test_disable_tools_invalid_tool() {
142+
let args = ["mcp-server", "-d", "invalidtool", "/path/to/dir"];
143+
let mut result = parse_args(&args).unwrap();
144+
let validated = result.validate();
145+
assert!(validated.is_err());
146+
assert!(
147+
validated
148+
.unwrap_err()
149+
.contains("Invalid entry detected in the disable-tools list : 'invalidtool'")
150+
);
151+
}
152+
153+
#[test]
154+
fn test_disable_tools_empty_value() {
155+
let args = ["mcp-server", "-d", "", "/path/to/dir"];
156+
let mut result = parse_args(&args).unwrap();
157+
let validated = result.validate();
158+
assert!(validated.is_ok());
159+
assert_eq!(result.disabled_tool_names, Some(vec![]));
160+
}
161+
162+
#[test]
163+
fn test_disable_tools_whitespace_only() {
164+
let args = ["mcp-server", "-d", " ", "/path/to/dir"];
165+
let mut result = parse_args(&args).unwrap();
166+
let validated = result.validate();
167+
assert!(validated.is_ok());
168+
assert_eq!(result.disabled_tool_names, Some(vec![]));
169+
}
170+
171+
#[test]
172+
fn test_disable_tools_long_flag() {
173+
let args = [
174+
"mcp-server",
175+
"--disable-tools",
176+
"read_text_file",
177+
"/path/to/dir",
178+
];
179+
let mut result = parse_args(&args).unwrap();
180+
let validated = result.validate();
181+
assert!(validated.is_ok());
182+
assert_eq!(
183+
result.disabled_tool_names,
184+
Some(vec!["read_text_file".to_string()])
185+
);
186+
}

0 commit comments

Comments
 (0)