Skip to main content

rustfs_mcp/
config.rs

1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use anyhow::Result;
16use clap::Parser;
17use tracing::info;
18
19/// Configuration for `rustfs-mcp`.
20#[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    /// AWS Access Key ID
51    #[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    /// AWS Secret Access Key
59    #[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    /// AWS Region
67    #[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    /// Custom S3 endpoint URL
76    #[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    /// Log level
84    #[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    /// Force path-style addressing
93    #[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    /// Parse configuration from command-line arguments and environment variables.
102    pub fn new() -> Self {
103        Config::parse()
104    }
105
106    /// Validate required credentials are present.
107    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    /// Return validated AWS access key id.
124    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    /// Return validated AWS secret access key.
131    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    /// Log a redacted configuration summary.
138    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}