Skip to main content

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}