AkurAI Build
Menu

Public repository / a5e3b5db0cc1646bda2629db5d96f1d1356df08d

AkurAI-PassVault / crates/passvault-mcp/tests/notification_test.rs

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

//! Regression: JSON-RPC notifications (messages with no `id`) must produce NO response
//! body, so the HTTP layer can answer with `202 Accepted` + empty body. Returning a
//! JSON-RPC body to a notification breaks strict MCP clients (codex/rmcp), which fail
//! with "Transport channel closed, when send initialized notification".

use std::sync::{Arc, Mutex};

use akurai_storage::BTree;
use passvault_core::crypto::Crypto;
use passvault_mcp::tools::ToolRegistry;

fn registry(tag: &str) -> ToolRegistry {
    let db_path = format!("{}/passvault_notif_{tag}.db", env!("CARGO_TARGET_TMPDIR"));
    let _ = std::fs::remove_file(&db_path);
    let db = BTree::open(&db_path).expect("open temp db");
    let crypto = Crypto::from_hex_key(&"ab".repeat(32)).expect("crypto key");
    ToolRegistry::new(Arc::new(Mutex::new(db)), Arc::new(crypto))
}

#[test]
fn notification_initialized_has_no_response_body() {
    let out = registry("init")
        .handle_request(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#);
    assert!(out.is_empty(), "notification must yield empty body, got: {out}");
}

#[test]
fn message_without_id_has_no_response_body() {
    let out =
        registry("noid").handle_request(r#"{"jsonrpc":"2.0","method":"tools/list"}"#);
    assert!(out.is_empty(), "id-less message is a notification, got: {out}");
}

#[test]
fn request_with_id_still_returns_json() {
    let out = registry("req").handle_request(
        r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#,
    );
    assert!(out.contains("\"id\":1"), "request must get a response with id, got: {out}");
    assert!(out.contains("serverInfo"), "initialize must return serverInfo, got: {out}");
}