Menu
Public repository / c7bf22ef78ad08f1f59b04d08e88d86e4ca91815
AkurAI-CRM / crates/crm-mcp/src/server.rs
Latest change c7bf22ef78ad08f1f59b04d08e88d86e4ca91815 - feat: add tenant-safe account graph by Ólafur Búi Ólafsson
use crate::tools::ToolRegistry;
use akurai_http::{Method, Request, Response};
use akurai_storage::BTree;
use std::sync::{Arc, Mutex};
pub struct McpConfig {
pub host: String,
pub port: u16,
pub db_path: String,
}
pub struct McpServer {
config: McpConfig,
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 {
config,
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 r#"{"jsonrpc":"2.0","error":{"code":-32001,"message":"invalid MCP credential"}}"#.into();
}
let expected_tenant = std::env::var("CRM_MCP_TENANT_ID").unwrap_or_default();
if expected_tenant.is_empty() || tenant != expected_tenant {
return r#"{"jsonrpc":"2.0","error":{"code":-32003,"message":"tenant is not authorized for credential"}}"#.into();
}
self.tools.handle_request_authenticated(body, tenant)
}
/// Start the MCP HTTP server
pub fn serve(&self) -> Result<(), String> {
let token =
std::env::var("CRM_MCP_TOKEN").map_err(|_| "CRM_MCP_TOKEN is required".to_string())?;
let tenant = std::env::var("CRM_MCP_TENANT_ID")
.map_err(|_| "CRM_MCP_TENANT_ID is required".to_string())?;
if token.is_empty() || tenant.trim().is_empty() {
return Err("CRM_MCP_TOKEN and CRM_MCP_TENANT_ID are required".into());
}
let addr = format!("{}:{}", self.config.host, self.config.port);
println!("🔌 MCP server listening on http://{addr}/mcp");
let tools = Arc::clone(&self.tools);
let handler = move |req: &Request| -> Response {
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("");
if !constant_time_eq(credential.as_bytes(), token.as_bytes()) {
return Response::new(401).with_text("MCP authentication required");
}
let body = req.body_str();
let result = tools.handle_request_authenticated(body.as_ref(), &tenant);
Response::ok()
.with_header("Content-Type", "application/json")
.with_body("application/json", result.into_bytes())
};
let server = akurai_http::Server::bind(&addr)
.map_err(|e| format!("failed to bind MCP server: {e}"))?;
server
.run(handler)
.map_err(|e| format!("server error: {e}"))
}
}
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
}