Download crates/forge_infra/src/auth/mcp_credentials.rs from SaylorTwift/forgecode: direct link, hf CLI and curl.
- Browser
- Download file 6.56 kB
-
https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_infra/src/auth/mcp_credentials.rs
- Command line
-
hf download hf://SaylorTwift/forgecode/crates/forge_infra/src/auth/mcp_credentials.rs
-
curl -L -o mcp_credentials.rs https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_infra/src/auth/mcp_credentials.rs
6.56 kB
| //! MCP OAuth Credential Storage | |
| //! | |
| //! Stores OAuth tokens separately from LLM provider credentials. | |
| //! Credentials are bound to specific MCP server URLs. | |
| use std::collections::HashMap; | |
| use anyhow::Result; | |
| use forge_domain::Environment; | |
| use serde::{Deserialize, Serialize}; | |
| use tokio::fs; | |
| /// MCP OAuth tokens for a single server. | |
| pub struct McpOAuthTokens { | |
| pub access_token: String, | |
| pub refresh_token: Option<String>, | |
| pub expires_at: Option<u64>, | |
| pub scope: Option<String>, | |
| } | |
| /// Client registration info (for dynamic registration) | |
| pub struct McpClientRegistration { | |
| pub client_id: String, | |
| pub client_secret: Option<String>, | |
| pub client_id_issued_at: Option<u64>, | |
| pub client_secret_expires_at: Option<u64>, | |
| } | |
| /// Complete credential entry for an MCP server | |
| pub struct McpCredentialEntry { | |
| /// Server URL (credential binding) | |
| pub server_url: String, | |
| /// OAuth tokens | |
| pub tokens: McpOAuthTokens, | |
| /// Client registration (if dynamically registered) | |
| pub client_registration: Option<McpClientRegistration>, | |
| } | |
| /// Credential store for all MCP servers | |
| pub struct McpCredentialStore { | |
| pub credentials: HashMap<String, McpCredentialEntry>, | |
| } | |
| impl McpCredentialStore { | |
| /// Load the credential store from disk | |
| /// | |
| /// # Arguments | |
| /// * `env` - The environment containing the base path for storage | |
| /// | |
| /// # Returns | |
| /// * `Ok(McpCredentialStore)` - The loaded store, or empty if file doesn't | |
| /// exist | |
| /// * `Err(...)` - If there was an error reading or parsing the file | |
| pub async fn load(env: &Environment) -> Result<Self> { | |
| let path = Self::credential_path(env); | |
| if !path.exists() { | |
| return Ok(Self::default()); | |
| } | |
| let content = fs::read_to_string(&path).await?; | |
| Ok(serde_json::from_str(&content)?) | |
| } | |
| /// Save the credential store to disk | |
| /// | |
| /// Sets file permissions to 0o600 (user read/write only) on Unix systems. | |
| /// | |
| /// # Arguments | |
| /// * `env` - The environment containing the base path for storage | |
| pub async fn save(&self, env: &Environment) -> Result<()> { | |
| let path = Self::credential_path(env); | |
| if let Some(parent) = path.parent() { | |
| fs::create_dir_all(parent).await?; | |
| } | |
| let content = serde_json::to_string_pretty(self)?; | |
| fs::write(&path, content).await?; | |
| // Set permissions to 0o600 (user read/write only) | |
| { | |
| use std::os::unix::fs::PermissionsExt; | |
| let metadata = fs::metadata(&path).await?; | |
| let mut perms = metadata.permissions(); | |
| perms.set_mode(0o600); | |
| fs::set_permissions(&path, perms).await?; | |
| } | |
| Ok(()) | |
| } | |
| /// Get the path to the credential file | |
| /// | |
| /// The file is stored at `<base_path>/.mcp-credentials.json` | |
| pub fn credential_path(env: &Environment) -> std::path::PathBuf { | |
| env.base_path.join(".mcp-credentials.json") | |
| } | |
| /// Get a credential entry by server URL | |
| pub fn get(&self, server_url: &str) -> Option<&McpCredentialEntry> { | |
| self.credentials.get(server_url) | |
| } | |
| /// Set a credential entry | |
| pub fn set(&mut self, entry: McpCredentialEntry) { | |
| self.credentials.insert(entry.server_url.clone(), entry); | |
| } | |
| /// Remove a credential entry by server URL | |
| pub fn remove(&mut self, server_url: &str) { | |
| self.credentials.remove(server_url); | |
| } | |
| /// Check if credentials exist for a server URL | |
| pub fn has_credentials(&self, server_url: &str) -> bool { | |
| self.credentials.contains_key(server_url) | |
| } | |
| } | |
| mod tests { | |
| use std::path::PathBuf; | |
| use tempfile::TempDir; | |
| use super::*; | |
| fn test_env(base_path: PathBuf) -> Environment { | |
| Environment { | |
| os: "test".to_string(), | |
| cwd: PathBuf::from("/tmp"), | |
| home: Some(PathBuf::from("/home/test")), | |
| shell: "bash".to_string(), | |
| base_path, | |
| } | |
| } | |
| async fn test_credential_store_save_load() { | |
| let tmp = TempDir::new().unwrap(); | |
| let env = test_env(tmp.path().to_path_buf()); | |
| let mut store = McpCredentialStore::default(); | |
| let entry = McpCredentialEntry { | |
| server_url: "https://api.example.com/mcp".to_string(), | |
| tokens: McpOAuthTokens { | |
| access_token: "test-access-token".to_string(), | |
| refresh_token: Some("test-refresh-token".to_string()), | |
| expires_at: Some(1234567890), | |
| scope: Some("read write".to_string()), | |
| }, | |
| client_registration: None, | |
| }; | |
| store.set(entry); | |
| store.save(&env).await.unwrap(); | |
| let loaded = McpCredentialStore::load(&env).await.unwrap(); | |
| assert!(loaded.has_credentials("https://api.example.com/mcp")); | |
| let entry = loaded.get("https://api.example.com/mcp").unwrap(); | |
| assert_eq!(entry.tokens.access_token, "test-access-token"); | |
| assert_eq!( | |
| entry.tokens.refresh_token, | |
| Some("test-refresh-token".to_string()) | |
| ); | |
| } | |
| async fn test_credential_store_remove() { | |
| let tmp = TempDir::new().unwrap(); | |
| let env = test_env(tmp.path().to_path_buf()); | |
| let mut store = McpCredentialStore::default(); | |
| store.set(McpCredentialEntry { | |
| server_url: "https://api.example.com/mcp".to_string(), | |
| tokens: McpOAuthTokens { | |
| access_token: "test".to_string(), | |
| refresh_token: None, | |
| expires_at: None, | |
| scope: None, | |
| }, | |
| client_registration: None, | |
| }); | |
| store.remove("https://api.example.com/mcp"); | |
| assert!(!store.has_credentials("https://api.example.com/mcp")); | |
| // Ensure the env reference is retained to keep the TempDir alive. | |
| let _ = McpCredentialStore::credential_path(&env); | |
| } | |
| async fn test_credential_store_empty() { | |
| let tmp = TempDir::new().unwrap(); | |
| let env = test_env(tmp.path().to_path_buf()); | |
| let store = McpCredentialStore::load(&env).await.unwrap(); | |
| assert!(store.credentials.is_empty()); | |
| } | |
| } | |