ares/mcp/auth.rs
1// ares/src/mcp/auth.rs
2// Extracts and validates API key from MCP connection configuration.
3// The API key is passed as an environment variable when the MCP server process is spawned.
4
5use crate::db::tenants::TenantDb;
6use crate::models::TenantContext;
7
8/// Error type for MCP authentication.
9#[derive(Debug, thiserror::Error)]
10pub enum McpAuthError {
11 #[error("No API key provided. Set ARES_API_KEY environment variable.")]
12 NoApiKey,
13
14 #[error("Invalid API key: {0}")]
15 InvalidKey(String),
16
17 #[error("Database error during auth: {0}")]
18 DbError(#[from] crate::types::AppError),
19}
20
21/// Extracts the ARES API key from the environment.
22///
23/// MCP servers are spawned as child processes. The API key is passed via
24/// the ARES_API_KEY environment variable, which is set in the MCP client
25/// config (e.g., claude_desktop_config.json → env block).
26///
27/// # Returns
28/// The raw API key string (starts with "ares_").
29pub fn extract_api_key_from_env() -> Result<String, McpAuthError> {
30 std::env::var("ARES_API_KEY").map_err(|_| McpAuthError::NoApiKey)
31}
32
33/// Validates an API key and returns the TenantContext.
34///
35/// This calls the same validation logic used by the HTTP API middleware.
36/// The TenantContext contains tenant_id, tier, and quota info.
37///
38/// # Arguments
39/// - `tenant_db`: Tenant database for key validation
40/// - `api_key`: Raw API key string (e.g., "ares_abc123...")
41///
42/// # Returns
43/// - `Ok(TenantContext)` if the key is valid and the tenant is active
44/// - `Err(McpAuthError)` if the key is invalid, expired, or the tenant is suspended
45pub async fn validate_mcp_api_key(
46 tenant_db: &TenantDb,
47 api_key: &str,
48) -> Result<TenantContext, McpAuthError> {
49 // Verify the key starts with the expected prefix
50 if !api_key.starts_with("ares_") {
51 return Err(McpAuthError::InvalidKey(
52 "API key must start with 'ares_' prefix".to_string(),
53 ));
54 }
55
56 // Use the shared validation logic from the tenant module.
57 let tenant = tenant_db
58 .verify_api_key(api_key)
59 .await
60 .map_err(|e| McpAuthError::InvalidKey(e.to_string()))?
61 .ok_or_else(|| McpAuthError::InvalidKey("API key not found or inactive".to_string()))?;
62
63 tracing::info!(
64 tenant_id = %tenant.tenant_id,
65 tier = %tenant.tier.as_str(),
66 "MCP connection authenticated"
67 );
68
69 Ok(tenant)
70}
71
72/// Struct that holds the authenticated context for an MCP session.
73/// Created once at connection time, reused for every tool call.
74#[derive(Debug, Clone)]
75pub struct McpSession {
76 /// The validated tenant context
77 pub tenant: TenantContext,
78 /// The raw API key (for forwarding to Eruka if needed)
79 pub api_key: String,
80 /// Eruka workspace ID for this tenant (derived from tenant_id)
81 pub eruka_workspace_id: String,
82}
83
84impl McpSession {
85 /// Creates a new MCP session from a validated tenant context.
86 pub fn new(tenant: TenantContext, api_key: String) -> Self {
87 // Convention: Eruka workspace ID = tenant_id
88 let eruka_workspace_id = tenant.tenant_id.clone();
89
90 Self {
91 tenant,
92 api_key,
93 eruka_workspace_id,
94 }
95 }
96
97 /// Returns the tenant ID for this session.
98 pub fn tenant_id(&self) -> &str {
99 &self.tenant.tenant_id
100 }
101
102 /// Returns the tenant tier (Free, Dev, Pro, Enterprise).
103 pub fn tier(&self) -> &str {
104 self.tenant.tier.as_str()
105 }
106}