remote_mcp_kernel/
config.rs

1//! Configuration management for MCP OAuth server
2//!
3//! This module provides centralized configuration management following
4//! the principle of single responsibility and making configuration
5//! explicit and testable.
6
7use std::env;
8
9/// Application configuration
10#[derive(Debug, Clone)]
11pub struct Config {
12    pub server: ServerConfig,
13    pub oauth: OAuthConfig,
14    pub github: GitHubConfig,
15    pub cognito: CognitoConfig,
16    pub logging: LoggingConfig,
17}
18
19/// Server configuration
20#[derive(Debug, Clone)]
21pub struct ServerConfig {
22    pub bind_address: String,
23    pub host: String,
24    pub port: u16,
25    pub name: String,
26    pub version: String,
27    pub description: String,
28}
29
30/// OAuth provider configuration
31#[derive(Debug, Clone)]
32pub struct OAuthConfig {
33    pub authorization_endpoint: String,
34    pub token_endpoint: String,
35    pub client_registration_endpoint: Option<String>,
36    pub scopes_supported: Vec<String>,
37    pub allow_implicit_flow: bool,
38    pub allow_public_client_registration: bool,
39    pub cors_max_age: Option<u64>,
40}
41
42/// GitHub OAuth configuration
43#[derive(Debug, Clone)]
44pub struct GitHubConfig {
45    pub client_id: String,
46    pub client_secret: String,
47    pub redirect_uri: String,
48    pub scope: String,
49}
50
51/// AWS Cognito OAuth configuration
52#[derive(Debug, Clone)]
53pub struct CognitoConfig {
54    pub client_id: String,
55    pub client_secret: Option<String>,
56    pub redirect_uri: String,
57    pub scope: String,
58    pub cognito_domain: String,
59    pub region: String,
60    pub user_pool_id: String,
61}
62
63/// Logging configuration
64#[derive(Debug, Clone)]
65pub struct LoggingConfig {
66    pub level: String,
67    pub format: String,
68}
69
70impl Config {
71    /// Create configuration with default values (for testing/development)
72    pub fn default() -> Self {
73        Config {
74            server: ServerConfig {
75                bind_address: "0.0.0.0:8080".to_string(),
76                host: "localhost".to_string(),
77                port: 8080,
78                name: "MCP OAuth Server".to_string(),
79                version: env!("CARGO_PKG_VERSION").to_string(),
80                description: "MCP server with OAuth authentication capabilities".to_string(),
81            },
82            oauth: OAuthConfig {
83                authorization_endpoint: "/oauth/authorize".to_string(),
84                token_endpoint: "/oauth/token".to_string(),
85                client_registration_endpoint: Some("/oauth/register".to_string()),
86                scopes_supported: vec!["read".to_string(), "write".to_string()],
87                allow_implicit_flow: false,
88                allow_public_client_registration: true,
89                cors_max_age: Some(86400),
90            },
91            github: GitHubConfig {
92                client_id: "".to_string(),
93                client_secret: "".to_string(),
94                redirect_uri: "http://localhost:8080/oauth/callback".to_string(),
95                scope: "read:user".to_string(),
96            },
97            cognito: CognitoConfig {
98                client_id: "".to_string(),
99                client_secret: None,
100                redirect_uri: "http://localhost:8080/oauth/callback".to_string(),
101                scope: "openid email profile phone".to_string(),
102                cognito_domain: "mydomain.auth.us-east-1.amazoncognito.com".to_string(),
103                region: "us-east-1".to_string(),
104                user_pool_id: "us-east-1_XXXXXXXXX".to_string(),
105            },
106            logging: LoggingConfig {
107                level: "info".to_string(),
108                format: "json".to_string(),
109            },
110        }
111    }
112
113    /// Load configuration from environment variables with sensible defaults
114    pub fn from_env() -> Result<Self, ConfigError> {
115        let host = env::var("MCP_HOST").unwrap_or_else(|_| "localhost".to_string());
116        let port = env::var("MCP_PORT")
117            .unwrap_or_else(|_| "8080".to_string())
118            .parse::<u16>()
119            .map_err(|_| ConfigError::InvalidPort)?;
120
121        let bind_address =
122            env::var("MCP_BIND_ADDRESS").unwrap_or_else(|_| format!("0.0.0.0:{}", port));
123
124        let github_client_id =
125            env::var("GITHUB_CLIENT_ID").map_err(|_| ConfigError::MissingGitHubClientId)?;
126        let github_client_secret =
127            env::var("GITHUB_CLIENT_SECRET").map_err(|_| ConfigError::MissingGitHubClientSecret)?;
128
129        let cognito_client_id =
130            env::var("COGNITO_CLIENT_ID").map_err(|_| ConfigError::MissingCognitoClientId)?;
131        let cognito_client_secret = env::var("COGNITO_CLIENT_SECRET").ok();
132        let cognito_domain =
133            env::var("COGNITO_DOMAIN").map_err(|_| ConfigError::MissingCognitoDomain)?;
134        let cognito_region =
135            env::var("COGNITO_REGION").map_err(|_| ConfigError::MissingCognitoRegion)?;
136        let cognito_user_pool_id =
137            env::var("COGNITO_USER_POOL_ID").map_err(|_| ConfigError::MissingCognitoUserPoolId)?;
138
139        Ok(Config {
140            server: ServerConfig {
141                bind_address,
142                host: host.clone(),
143                port,
144                name: "MCP OAuth Server".to_string(),
145                version: env!("CARGO_PKG_VERSION").to_string(),
146                description: "MCP server with OAuth authentication capabilities".to_string(),
147            },
148            oauth: OAuthConfig {
149                authorization_endpoint: "/oauth/authorize".to_string(),
150                token_endpoint: "/oauth/token".to_string(),
151                client_registration_endpoint: Some("/oauth/register".to_string()),
152                scopes_supported: vec!["read".to_string(), "write".to_string()],
153                allow_implicit_flow: false,
154                allow_public_client_registration: true,
155                cors_max_age: Some(86400),
156            },
157            github: GitHubConfig {
158                client_id: github_client_id,
159                client_secret: github_client_secret,
160                redirect_uri: format!("http://{}:{}/oauth/callback", host, port),
161                scope: env::var("GITHUB_SCOPE").unwrap_or_else(|_| "read:user".to_string()),
162            },
163            cognito: CognitoConfig {
164                client_id: cognito_client_id,
165                client_secret: cognito_client_secret,
166                redirect_uri: format!("http://{}:{}/oauth/callback", host, port),
167                scope: env::var("COGNITO_SCOPE")
168                    .unwrap_or_else(|_| "openid email profile phone".to_string()),
169                cognito_domain,
170                region: cognito_region,
171                user_pool_id: cognito_user_pool_id,
172            },
173            logging: LoggingConfig {
174                level: env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
175                format: env::var("LOG_FORMAT").unwrap_or_else(|_| "json".to_string()),
176            },
177        })
178    }
179
180    /// Get GitHub OAuth configuration
181    pub fn github_oauth_config(&self) -> oauth_provider_rs::GitHubOAuthConfig {
182        oauth_provider_rs::GitHubOAuthConfig {
183            client_id: self.github.client_id.clone(),
184            client_secret: self.github.client_secret.clone(),
185            redirect_uri: self.github.redirect_uri.clone(),
186            scope: self.github.scope.clone(),
187            provider_name: "github".to_string(),
188        }
189    }
190
191    /// Get OAuth provider configuration for GitHub
192    pub fn github_oauth_provider_config(
193        &self,
194    ) -> oauth_provider_rs::provider_trait::OAuthProviderConfig {
195        oauth_provider_rs::provider_trait::OAuthProviderConfig {
196            client_id: self.github.client_id.clone(),
197            client_secret: self.github.client_secret.clone(),
198            redirect_uri: self.github.redirect_uri.clone(),
199            scope: self.github.scope.clone(),
200            provider_name: "github".to_string(),
201        }
202    }
203
204    /// Get Cognito OAuth configuration
205    pub fn cognito_oauth_config(&self) -> oauth_provider_rs::CognitoOAuthConfig {
206        oauth_provider_rs::CognitoOAuthConfig {
207            client_id: self.cognito.client_id.clone(),
208            client_secret: self.cognito.client_secret.clone().unwrap_or_default(),
209            redirect_uri: self.cognito.redirect_uri.clone(),
210            scope: self.cognito.scope.clone(),
211            provider_name: "cognito".to_string(),
212        }
213    }
214
215    /// Get OAuth provider configuration for Cognito
216    pub fn cognito_oauth_provider_config(
217        &self,
218    ) -> oauth_provider_rs::provider_trait::OAuthProviderConfig {
219        oauth_provider_rs::provider_trait::OAuthProviderConfig {
220            client_id: self.cognito.client_id.clone(),
221            client_secret: self.cognito.client_secret.clone().unwrap_or_default(),
222            redirect_uri: self.cognito.redirect_uri.clone(),
223            scope: self.cognito.scope.clone(),
224            provider_name: "cognito".to_string(),
225        }
226    }
227
228    /// Get OAuth provider configuration
229    pub fn oauth_provider_config(&self) -> oauth_provider_rs::http_integration::OAuthProviderConfig {
230        oauth_provider_rs::http_integration::OAuthProviderConfig {
231            authorization_endpoint: self.oauth.authorization_endpoint.clone(),
232            token_endpoint: self.oauth.token_endpoint.clone(),
233            client_registration_endpoint: self.oauth.client_registration_endpoint.clone(),
234            scopes_supported: Some(self.oauth.scopes_supported.clone()),
235            allow_implicit_flow: self.oauth.allow_implicit_flow,
236            api_routes: vec!["/api".to_string()],
237            allow_public_client_registration: self.oauth.allow_public_client_registration,
238            cors_allowed_origins: vec![],
239            cors_allowed_methods: vec![],
240            cors_allowed_headers: vec![],
241            cors_max_age: self.oauth.cors_max_age,
242            error_callback: None,
243        }
244    }
245
246    /// Get server bind address as SocketAddr
247    pub fn bind_socket_addr(&self) -> Result<std::net::SocketAddr, ConfigError> {
248        self.server
249            .bind_address
250            .parse()
251            .map_err(|_| ConfigError::InvalidBindAddress)
252    }
253}
254
255/// Configuration errors
256#[derive(Debug, thiserror::Error)]
257pub enum ConfigError {
258    #[error("Missing GITHUB_CLIENT_ID environment variable")]
259    MissingGitHubClientId,
260    #[error("Missing GITHUB_CLIENT_SECRET environment variable")]
261    MissingGitHubClientSecret,
262    #[error("Missing COGNITO_CLIENT_ID environment variable")]
263    MissingCognitoClientId,
264    #[error("Missing COGNITO_DOMAIN environment variable")]
265    MissingCognitoDomain,
266    #[error("Missing COGNITO_REGION environment variable")]
267    MissingCognitoRegion,
268    #[error("Missing COGNITO_USER_POOL_ID environment variable")]
269    MissingCognitoUserPoolId,
270    #[error("Invalid port number")]
271    InvalidPort,
272    #[error("Invalid bind address")]
273    InvalidBindAddress,
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn test_config_defaults() {
282        // Test default configuration without environment variables
283        let config = Config::default();
284
285        assert_eq!(config.server.name, "MCP OAuth Server");
286        assert_eq!(config.oauth.scopes_supported, vec!["read", "write"]);
287        assert!(!config.oauth.allow_implicit_flow);
288        assert_eq!(config.github.scope, "read:user");
289    }
290
291    #[test]
292    fn test_missing_github_config() {
293        unsafe {
294            env::remove_var("GITHUB_CLIENT_ID");
295            env::remove_var("GITHUB_CLIENT_SECRET");
296        }
297
298        let result = Config::from_env();
299        assert!(result.is_err());
300    }
301}