1use anyhow::Result;
16use clap::Parser;
17use tracing::info;
18
19#[derive(Parser, Debug, Clone)]
21#[command(
22 name = "rustfs-mcp-server",
23 about = "RustFS MCP (Model Context Protocol) Server for S3 operations",
24 version,
25 long_about = r#"
26RustFS MCP Server - Model Context Protocol server for S3 operations
27
28This server provides S3 operations through the Model Context Protocol (MCP),
29allowing AI assistants to interact with S3-compatible storage systems.
30
31ENVIRONMENT VARIABLES:
32 All command-line options can also be set via environment variables.
33 Command-line arguments take precedence over environment variables.
34
35EXAMPLES:
36 # Using command-line arguments
37 rustfs-mcp-server --access-key-id your_key --secret-access-key your_secret
38
39 # Using environment variables
40 export AWS_ACCESS_KEY_ID=your_key
41 export AWS_SECRET_ACCESS_KEY=your_secret
42 rustfs-mcp-server
43
44 # Mixed usage (command-line overrides environment)
45 export AWS_REGION=us-east-1
46 rustfs-mcp-server --access-key-id mykey --secret-access-key mysecret --endpoint-url http://localhost:9000
47"#
48)]
49pub struct Config {
50 #[arg(
52 long = "access-key-id",
53 env = "AWS_ACCESS_KEY_ID",
54 help = "AWS Access Key ID for S3 authentication"
55 )]
56 pub access_key_id: Option<String>,
57
58 #[arg(
60 long = "secret-access-key",
61 env = "AWS_SECRET_ACCESS_KEY",
62 help = "AWS Secret Access Key for S3 authentication"
63 )]
64 pub secret_access_key: Option<String>,
65
66 #[arg(
68 long = "region",
69 env = "AWS_REGION",
70 default_value = "us-east-1",
71 help = "AWS region to use for S3 operations"
72 )]
73 pub region: String,
74
75 #[arg(
77 long = "endpoint-url",
78 env = "AWS_ENDPOINT_URL",
79 help = "Custom S3 endpoint URL (for MinIO, LocalStack, etc.)"
80 )]
81 pub endpoint_url: Option<String>,
82
83 #[arg(
85 long = "log-level",
86 env = "RUST_LOG",
87 default_value = "rustfs_mcp_server=info",
88 help = "Log level configuration"
89 )]
90 pub log_level: String,
91
92 #[arg(
94 long = "force-path-style",
95 help = "Force path-style S3 addressing (automatically enabled for custom endpoints)"
96 )]
97 pub force_path_style: bool,
98}
99
100impl Config {
101 pub fn new() -> Self {
103 Config::parse()
104 }
105
106 pub fn validate(&self) -> Result<()> {
108 if self.access_key_id.is_none() {
109 anyhow::bail!(
110 "AWS Access Key ID is required. Set via --access-key-id or AWS_ACCESS_KEY_ID environment variable"
111 );
112 }
113
114 if self.secret_access_key.is_none() {
115 anyhow::bail!(
116 "AWS Secret Access Key is required. Set via --secret-access-key or AWS_SECRET_ACCESS_KEY environment variable"
117 );
118 }
119
120 Ok(())
121 }
122
123 pub fn access_key_id(&self) -> &str {
125 self.access_key_id
126 .as_ref()
127 .expect("Access key ID should be validated")
128 }
129
130 pub fn secret_access_key(&self) -> &str {
132 self.secret_access_key
133 .as_ref()
134 .expect("Secret access key should be validated")
135 }
136
137 pub fn log_configuration(&self) {
139 let access_key_display = self
140 .access_key_id
141 .as_ref()
142 .map(|key| {
143 if key.len() > 8 {
144 format!("{}...{}", &key[..4], &key[key.len() - 4..])
145 } else {
146 "*".repeat(key.len())
147 }
148 })
149 .unwrap_or_else(|| "Not set".to_string());
150
151 let endpoint_display = self
152 .endpoint_url
153 .as_ref()
154 .map(|url| format!("Custom endpoint: {url}"))
155 .unwrap_or_else(|| "Default AWS endpoints".to_string());
156
157 info!("Configuration:");
158 info!(" AWS Region: {}", self.region);
159 info!(" AWS Access Key ID: {}", access_key_display);
160 info!(" AWS Secret Access Key: [HIDDEN]");
161 info!(" S3 Endpoint: {}", endpoint_display);
162 info!(" Force Path Style: {}", self.force_path_style);
163 info!(" Log Level: {}", self.log_level);
164 }
165}
166
167impl Default for Config {
168 fn default() -> Self {
169 Config {
170 access_key_id: None,
171 secret_access_key: None,
172 region: "us-east-1".to_string(),
173 endpoint_url: None,
174 log_level: "rustfs_mcp_server=info".to_string(),
175 force_path_style: false,
176 }
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_config_validation_success() {
186 let config = Config {
187 access_key_id: Some("test_key".to_string()),
188 secret_access_key: Some("test_secret".to_string()),
189 ..Config::default()
190 };
191
192 assert!(config.validate().is_ok());
193 assert_eq!(config.access_key_id(), "test_key");
194 assert_eq!(config.secret_access_key(), "test_secret");
195 }
196
197 #[test]
198 fn test_config_validation_missing_access_key() {
199 let config = Config {
200 access_key_id: None,
201 secret_access_key: Some("test_secret".to_string()),
202 ..Config::default()
203 };
204
205 let result = config.validate();
206 assert!(result.is_err());
207 assert!(result.unwrap_err().to_string().contains("Access Key ID"));
208 }
209
210 #[test]
211 fn test_config_validation_missing_secret_key() {
212 let config = Config {
213 access_key_id: Some("test_key".to_string()),
214 secret_access_key: None,
215 ..Config::default()
216 };
217
218 let result = config.validate();
219 assert!(result.is_err());
220 assert!(
221 result
222 .unwrap_err()
223 .to_string()
224 .contains("Secret Access Key")
225 );
226 }
227
228 #[test]
229 fn test_config_default() {
230 let config = Config::default();
231 assert_eq!(config.region, "us-east-1");
232 assert_eq!(config.log_level, "rustfs_mcp_server=info");
233 assert!(!config.force_path_style);
234 assert!(config.access_key_id.is_none());
235 assert!(config.secret_access_key.is_none());
236 assert!(config.endpoint_url.is_none());
237 }
238}