mod common;
use common::{init_vault, murk};
use rmcp::{
ServiceExt,
model::{CallToolRequestParams, ContentBlock},
object,
transport::{ConfigureCommandExt, TokioChildProcess},
};
use tokio::process::Command;
#[tokio::test]
async fn mcp_client_lists_and_calls_tools() {
let dir = assert_fs::TempDir::new().unwrap();
let (key, _) = init_vault(&dir);
murk(&dir, &key)
.args(["add", "API_KEY", "--tag", "agents", "--vault", "test.murk"])
.write_stdin("interop-secret\n")
.assert()
.success();
murk(&dir, &key)
.args([
"add",
"OTHER_KEY",
"--tag",
"agents",
"--vault",
"test.murk",
])
.write_stdin("other-secret\n")
.assert()
.success();
let grant = dir.path().join("grant.key");
murk(&dir, &key)
.args([
"agent",
"init",
"--name",
"probe",
"--only",
"API_KEY",
"--allow-tag",
"agents",
"--out",
grant.to_str().unwrap(),
"--vault",
"test.murk",
])
.assert()
.success();
let transport =
TokioChildProcess::new(Command::new(env!("CARGO_BIN_EXE_murk")).configure(|cmd| {
cmd.arg("mcp")
.arg("--vault")
.arg("test.murk")
.current_dir(dir.path())
.env("HOME", dir.path())
.env("XDG_RUNTIME_DIR", dir.path())
.env_remove("MURK_KEY")
.env("MURK_KEY_FILE", grant.to_str().unwrap())
.env("MURK_AGENT", "1");
}))
.unwrap();
let client = ().serve(transport).await.unwrap();
let tools = client.list_all_tools().await.unwrap();
let names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
assert!(names.contains(&"murk_plan"), "tools: {names:?}");
assert!(names.contains(&"murk_get"), "tools: {names:?}");
let in_scope = client
.call_tool(
CallToolRequestParams::new("murk_get").with_arguments(object!({ "key": "API_KEY" })),
)
.await
.unwrap();
assert_ne!(in_scope.is_error, Some(true));
let in_scope_text: String = in_scope
.content
.iter()
.filter_map(ContentBlock::as_text)
.map(|t| t.text.as_str())
.collect();
assert_eq!(in_scope_text, "interop-secret");
let out_of_scope = client
.call_tool(
CallToolRequestParams::new("murk_get").with_arguments(object!({ "key": "OTHER_KEY" })),
)
.await
.unwrap();
assert_eq!(out_of_scope.is_error, Some(true));
let out_of_scope_text: String = out_of_scope
.content
.iter()
.filter_map(ContentBlock::as_text)
.map(|t| t.text.as_str())
.collect();
assert!(
!out_of_scope_text.contains("other-secret"),
"leaked out-of-scope value: {out_of_scope_text}"
);
client.cancel().await.unwrap();
}