tsafe-mcp 2.0.0

Bound-contract MCP server for tsafe — run policy-scoped commands without exposing secret values.
Documentation
//! Bound MCP protocol posture: initialize, initialized notification, and ping.

use serde_json::Value;

use super::common::{
    make_vault, normalized_temp_path_for_assertion, spawn_serve_with_env, write_contract,
};

const SECRET_VALUE: &str = "protocol-surface-secret-value";

fn bound_args<'a>(profile: &'a str, contract: &'a str, workdir: &'a str) -> [&'a str; 6] {
    [
        "--profile",
        profile,
        "--contract",
        contract,
        "--workdir",
        workdir,
    ]
}

fn spawn_bound_harness() -> (tempfile::TempDir, super::common::McpHarness, String) {
    let (tmp, vault_dir) = make_vault("demo", "pw", &[("demo/api_token", SECRET_VALUE)]);
    write_contract(
        tmp.path(),
        "diagnostic",
        r#"    allowed_secrets: []
    allowed_targets: [cargo]
    trust_level: hardened
"#,
    );

    let workdir = tmp.path().to_str().unwrap().to_string();
    let harness = spawn_serve_with_env(
        &vault_dir,
        &bound_args("demo", "diagnostic", workdir.as_str()),
        &[],
    );
    (tmp, harness, workdir)
}

fn tool_names(list: &Value) -> Vec<&str> {
    list["result"]["tools"]
        .as_array()
        .expect("tools array")
        .iter()
        .map(|tool| tool["name"].as_str().expect("tool name"))
        .collect()
}

fn list_tools(harness: &mut super::common::McpHarness, id: u64) -> Value {
    harness.send(&format!(
        r#"{{"jsonrpc":"2.0","id":{id},"method":"tools/list"}}"#
    ));
    harness.recv()
}

fn status(harness: &mut super::common::McpHarness, id: u64) -> Value {
    harness.send(&format!(
        r#"{{"jsonrpc":"2.0","id":{id},"method":"tools/call","params":{{"name":"tsafe_mcp_status","arguments":{{}}}}}}"#
    ));
    harness.recv()
}

fn assert_no_secret_material(value: &Value) {
    let body = serde_json::to_string(value).expect("serialize response");
    assert!(
        !body.contains(SECRET_VALUE),
        "response leaked seeded secret value: {body}"
    );
}

#[test]
fn bound_initialize_returns_pinned_protocol_and_safe_capabilities() {
    let (tmp, mut harness, _workdir) = spawn_bound_harness();

    let init = harness.initialize();
    assert_eq!(init["id"], 1);
    assert_eq!(init["result"]["protocolVersion"], "2025-06-18");
    assert_eq!(init["result"]["serverInfo"]["name"], "tsafe");
    let instructions = init["result"]["instructions"]
        .as_str()
        .expect("initialize instructions");
    for expected in ["show_exec_plan", "run_contract_command", "tsafe_mcp_status"] {
        assert!(
            instructions.contains(expected),
            "bound initialize instructions should mention {expected}: {instructions:?}"
        );
    }
    for forbidden in ["tsafe_run", "tsafe_reveal"] {
        assert!(
            !instructions.contains(forbidden),
            "bound initialize instructions must not advertise {forbidden}: {instructions:?}"
        );
    }

    let capabilities = init["result"]["capabilities"]
        .as_object()
        .expect("capabilities object");
    assert!(
        capabilities.contains_key("tools"),
        "bound server must advertise tool capability"
    );
    for forbidden in ["resources", "prompts", "sampling", "roots", "logging"] {
        assert!(
            !capabilities.contains_key(forbidden),
            "bound initialize must not advertise {forbidden}: {capabilities:?}"
        );
    }
    assert_no_secret_material(&init);

    let _ = harness.shutdown_and_collect();
    drop(tmp);
}

#[test]
fn initialized_notification_does_not_change_catalog_or_authority_scope() {
    let (tmp, mut harness, workdir) = spawn_bound_harness();

    let init = harness.initialize();
    assert_eq!(init["result"]["protocolVersion"], "2025-06-18");

    let before_list = list_tools(&mut harness, 2);
    let before_names = tool_names(&before_list);
    assert_eq!(
        before_names,
        vec!["show_exec_plan", "run_contract_command", "tsafe_mcp_status"]
    );

    let before_status = status(&mut harness, 3);
    let before_payload = &before_status["result"]["structuredContent"];
    assert_eq!(before_payload["profile"], "demo");
    assert_eq!(before_payload["contract"], "diagnostic");
    assert_eq!(
        normalized_temp_path_for_assertion(
            before_payload["workdir"].as_str().expect("before workdir")
        ),
        normalized_temp_path_for_assertion(&workdir)
    );

    harness.send(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#);

    let after_list = list_tools(&mut harness, 4);
    assert_eq!(tool_names(&after_list), before_names);

    let after_status = status(&mut harness, 5);
    let after_payload = &after_status["result"]["structuredContent"];
    assert_eq!(after_payload["profile"], before_payload["profile"]);
    assert_eq!(after_payload["contract"], before_payload["contract"]);
    assert_eq!(
        normalized_temp_path_for_assertion(
            after_payload["workdir"].as_str().expect("after workdir")
        ),
        normalized_temp_path_for_assertion(
            before_payload["workdir"].as_str().expect("before workdir")
        )
    );
    assert_eq!(
        after_payload["capabilities"], before_payload["capabilities"],
        "initialized notification must not widen compiled capabilities"
    );
    assert_no_secret_material(&after_list);
    assert_no_secret_material(&after_status);

    let _ = harness.shutdown_and_collect();
    drop(tmp);
}

#[test]
fn ping_returns_safe_liveness_without_vault_or_secret_material() {
    let (tmp, mut harness, _workdir) = spawn_bound_harness();
    harness.handshake();

    harness.send(r#"{"jsonrpc":"2.0","id":2,"method":"ping"}"#);
    let ping = harness.recv();
    assert_eq!(ping["id"], 2);
    assert!(
        ping.get("error").is_none(),
        "ping must return liveness, got: {ping:?}"
    );
    assert!(
        ping["result"].is_object(),
        "ping liveness response should be a JSON object: {ping:?}"
    );

    let body = serde_json::to_string(&ping).expect("serialize ping response");
    for forbidden in [
        SECRET_VALUE,
        "api_token",
        "vault",
        "secret",
        "demo/api_token",
    ] {
        assert!(
            !body.contains(forbidden),
            "ping response leaked forbidden material {forbidden:?}: {body}"
        );
    }

    let _ = harness.shutdown_and_collect();
    drop(tmp);
}