AkurAI Build
Menu

Public repository / a5e3b5db0cc1646bda2629db5d96f1d1356df08d

AkurAI-PassVault / crates/passvault-mcp/src/server.rs

Latest change a5e3b5db0cc1646bda2629db5d96f1d1356df08d - fix(mcp): return 202 + empty body for JSON-RPC notifications by Claude Code

use std::sync::{Arc, Mutex};
use akurai_http::{Response, Server};
use akurai_storage::BTree;
use passvault_core::crypto::Crypto;
use crate::tools::ToolRegistry;

pub struct McpConfig {
    pub host: String,
    pub port: u16,
    pub db_path: String,
    pub token: String,
    pub encryption_key: String,
}

pub struct McpServer {
    config: McpConfig,
}

impl McpServer {
    pub fn new(config: McpConfig) -> Self {
        McpServer { config }
    }

    pub fn serve(&self) -> Result<(), String> {
        let db = BTree::open(&self.config.db_path)
            .map_err(|e| format!("failed to open db: {e}"))?;
        let db = Arc::new(Mutex::new(db));
        let crypto = Crypto::from_hex_key(&self.config.encryption_key)
            .map_err(|e| format!("crypto init: {e}"))?;
        let crypto = Arc::new(crypto);
        let token = self.config.token.clone();
        let registry = Arc::new(ToolRegistry::new(db, crypto));

        let handler = move |req: &akurai_http::Request| -> Response {
            if req.path != "/mcp" || req.method != akurai_http::Method::Post {
                return Response::new(404).with_text("not found");
            }

            let auth = req.header("authorization").unwrap_or("");
            let expected = format!("Bearer {token}");
            if auth != expected {
                return Response::new(401).with_text("unauthorized");
            }

            let body = req.body_str();
            let result = registry.handle_request(&body);
            // Empty result = JSON-RPC notification: acknowledge with 202 + empty body
            // (MCP Streamable-HTTP spec). Strict clients (codex/rmcp) break if a
            // notification receives a JSON-RPC response body.
            if result.is_empty() {
                return Response::new(202);
            }
            Response::new(200)
                .with_header("Content-Type", "application/json")
                .with_body("application/json", result.into_bytes())
        };

        let addr = format!("{}:{}", self.config.host, self.config.port);
        Server::bind(&addr)
            .map_err(|e| format!("bind: {e}"))?
            .run(handler)
            .map_err(|e| format!("server: {e}"))
    }
}