AkurAI Build
Menu

Public repository / 54570403655120661aa5aeb6120646216896cc76

AkurAI-CRM / crates/crm-mcp/src/server.rs

Latest change ac0d3d868ec83e4002ee32531d1532c1f990c589 - style: cargo fmt by Ólafur Búi Ólafsson

use crate::tools::ToolRegistry;
use akurai_http::{Method, Request, Response};
use akurai_storage::BTree;
use std::sync::{Arc, Mutex};

const INVALID_CREDENTIAL: &str =
    r#"{"jsonrpc":"2.0","error":{"code":-32001,"message":"invalid MCP credential"}}"#;
const UNAUTHORIZED_TENANT: &str = r#"{"jsonrpc":"2.0","error":{"code":-32003,"message":"tenant is not authorized for credential"}}"#;

pub struct McpConfig {
    pub db_path: String,
}

pub struct McpServer {
    tools: Arc<ToolRegistry>,
}

impl McpServer {
    pub fn new(config: McpConfig) -> Result<Self, String> {
        let btree = BTree::open(&config.db_path)
            .map_err(|e| format!("failed to open MCP database: {e}"))?;
        let tools = ToolRegistry::new(Arc::new(Mutex::new(btree)));
        Ok(Self {
            tools: Arc::new(tools),
        })
    }

    /// Local callers must use `handle_request_with_credential`; unauthenticated
    /// dispatch is intentionally refused.
    pub fn handle_request(&self, _body: &str) -> String {
        r#"{"jsonrpc":"2.0","error":{"code":-32001,"message":"authenticated MCP credential required"}}"#.into()
    }

    pub fn handle_request_with_credential(
        &self,
        body: &str,
        credential: &str,
        tenant: &str,
    ) -> String {
        let expected = std::env::var("CRM_MCP_TOKEN").unwrap_or_default();
        if expected.is_empty() || !constant_time_eq(credential.as_bytes(), expected.as_bytes()) {
            return INVALID_CREDENTIAL.into();
        }
        let expected_tenant = std::env::var("CRM_MCP_TENANT_ID").unwrap_or_default();
        if expected_tenant.is_empty() || tenant != expected_tenant {
            return UNAUTHORIZED_TENANT.into();
        }
        self.tools.handle_request_authenticated(body, tenant)
    }

    /// Build the MCP endpoint handler for mounting into an existing HTTP server.
    pub fn http_handler(self: Arc<Self>) -> Box<dyn Fn(&Request) -> Response + Send + Sync> {
        Box::new(move |req| {
            if req.path != "/mcp" {
                return Response::new(404).with_text("Not found");
            }
            if req.method != Method::Post {
                return Response::new(405).with_text("Method not allowed");
            }
            let credential = req
                .header("Authorization")
                .and_then(|value| value.strip_prefix("Bearer "))
                .map(str::trim)
                .unwrap_or("");
            let tenant = req
                .header("X-Tenant-Id")
                .or_else(|| req.header("X-Tenant-ID"))
                .map(str::trim)
                .unwrap_or("");
            let result =
                self.handle_request_with_credential(req.body_str().as_ref(), credential, tenant);
            let status = match result.as_str() {
                INVALID_CREDENTIAL => 401,
                UNAUTHORIZED_TENANT => 403,
                _ => 200,
            };
            Response::new(status)
                .with_header("Content-Type", "application/json")
                .with_body("application/json", result.into_bytes())
        })
    }
}
fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
    let mut difference = left.len() ^ right.len();
    for index in 0..left.len().max(right.len()) {
        difference |= usize::from(
            left.get(index).copied().unwrap_or(0) ^ right.get(index).copied().unwrap_or(0),
        );
    }
    difference == 0
}