-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathhandler.rs
More file actions
237 lines (219 loc) · 8.83 KB
/
handler.rs
File metadata and controls
237 lines (219 loc) · 8.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
use crate::cli::CommandArguments;
use crate::error::ServiceError;
use crate::invoke_tools;
use crate::{error::ServiceResult, fs_service::FileSystemService, tools::*};
use async_trait::async_trait;
use rust_mcp_sdk::McpServer;
use rust_mcp_sdk::mcp_server::ServerHandler;
use rust_mcp_sdk::schema::RootsListChangedNotification;
use rust_mcp_sdk::schema::{
CallToolRequest, CallToolResult, InitializeRequest, InitializeResult, ListToolsRequest,
ListToolsResult, RpcError, schema_utils::CallToolError,
};
use std::cmp::Ordering;
use std::sync::Arc;
pub struct FileSystemHandler {
readonly: bool,
mcp_roots_support: bool,
fs_service: Arc<FileSystemService>,
}
impl FileSystemHandler {
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,
})
}
pub fn assert_write_access(&self) -> std::result::Result<(), CallToolError> {
if self.readonly {
Err(CallToolError::new(ServiceError::NoWriteAccess))
} else {
Ok(())
}
}
pub async fn startup_message(&self) -> String {
let common_message = format!(
"Secure MCP Filesystem Server running in \"{}\" mode {} \"MCP Roots\" support.",
if !self.readonly {
"read/write"
} else {
"readonly"
},
if self.mcp_roots_support {
"with"
} else {
"without"
},
);
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()
} else {
format!(
"Allowed directories:\n{}",
allowed_directories
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<String>>()
.join(",\n")
)
};
format!("{common_message}\n{sub_message}")
}
pub(crate) async fn update_allowed_directories(&self, runtime: Arc<dyn McpServer>) {
// 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() {
// 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 {
// 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();
// 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,
Err(_err) => {
vec![]
}
};
let valid_roots = if roots.is_empty() {
vec![]
} else {
let roots: Vec<_> = roots.iter().map(|v| v.uri.as_str()).collect();
match fs_service.valid_roots(roots) {
Ok((roots, skipped)) => {
if let Some(message) = skipped {
let _ = runtime.stderr_message(message.to_string()).await;
}
roots
}
Err(_err) => vec![],
}
};
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 {
"Client provided empty roots. Allowed directories passed from command-line will be used."
};
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",
);
let _ = runtime.stderr_message(message.to_string()).await;
}
}
}
}
#[async_trait]
impl ServerHandler for FileSystemHandler {
async fn on_initialized(&self, runtime: Arc<dyn McpServer>) {
let _ = runtime.stderr_message(self.startup_message().await).await;
self.update_allowed_directories(runtime).await;
}
async fn handle_roots_list_changed_notification(
&self,
_notification: RootsListChangedNotification,
runtime: Arc<dyn McpServer>,
) -> std::result::Result<(), RpcError> {
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(())
}
async fn handle_list_tools_request(
&self,
_: ListToolsRequest,
_: Arc<dyn McpServer>,
) -> std::result::Result<ListToolsResult, RpcError> {
Ok(ListToolsResult {
tools: FileSystemTools::tools(),
meta: None,
next_cursor: None,
})
}
async fn handle_initialize_request(
&self,
initialize_request: InitializeRequest,
runtime: Arc<dyn McpServer>,
) -> std::result::Result<InitializeResult, RpcError> {
runtime
.set_client_details(initialize_request.params.clone())
.await
.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.
if server_info
.protocol_version
.cmp(&initialize_request.params.protocol_version)
== Ordering::Greater
{
server_info.protocol_version = initialize_request.params.protocol_version;
}
Ok(server_info)
}
async fn handle_call_tool_request(
&self,
request: CallToolRequest,
_: Arc<dyn McpServer>,
) -> std::result::Result<CallToolResult, CallToolError> {
let tool_params: FileSystemTools =
FileSystemTools::try_from(request.params).map_err(CallToolError::new)?;
// Verify write access for tools that modify the file system
if tool_params.require_write_access() {
self.assert_write_access()?;
}
invoke_tools!(
tool_params,
&self.fs_service,
ReadMediaFile,
ReadMultipleMediaFiles,
ReadTextFile,
ReadMultipleTextFiles,
WriteFile,
EditFile,
CreateDirectory,
ListDirectory,
DirectoryTree,
MoveFile,
SearchFiles,
GetFileInfo,
ListAllowedDirectories,
ZipFiles,
UnzipFile,
ZipDirectory,
SearchFilesContent,
ListDirectoryWithSizes,
HeadFile,
TailFile,
ReadFileLines,
FindEmptyDirectories,
CalculateDirectorySize,
FindDuplicateFiles
)
}
}