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);
}