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