use serde_json::{json, Value};
use crate::audit::{audit_call, CallStatus};
use crate::backend;
use crate::errors::{McpError, McpErrorKind};
use crate::session::Session;
pub fn call(session: &Session, raw: Value) -> Result<Value, McpError> {
let key = parse_args(&raw)?;
if !session.is_in_scope(&key) {
audit_call(
session,
"tsafe_has_key",
Some(&key),
Vec::new(),
None,
None,
CallStatus::Success,
None,
);
return Ok(json!({"present": false}));
}
let vault = match backend::open_vault(session) {
Ok(v) => v,
Err(e) => {
audit_call(
session,
"tsafe_has_key",
Some(&key),
Vec::new(),
None,
None,
CallStatus::Failure,
Some(&e.message),
);
return Err(e);
}
};
let present = backend::vault_has_key(&vault, &key);
drop(vault);
audit_call(
session,
"tsafe_has_key",
Some(&key),
Vec::new(),
None,
None,
CallStatus::Success,
None,
);
Ok(json!({"present": present}))
}
fn parse_args(raw: &Value) -> Result<String, McpError> {
let obj = raw
.as_object()
.ok_or_else(|| McpError::new(McpErrorKind::InvalidParams, "expected an object"))?;
for k in obj.keys() {
if k != "key" {
return Err(McpError::new(
McpErrorKind::InvalidParams,
format!("unknown field '{k}'"),
));
}
}
let key = obj
.get("key")
.and_then(|v| v.as_str())
.map(str::to_string)
.ok_or_else(|| McpError::new(McpErrorKind::InvalidParams, "missing 'key'"))?;
if key.is_empty() {
return Err(McpError::new(
McpErrorKind::InvalidParams,
"'key' must be non-empty",
));
}
Ok(key)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::path::PathBuf;
fn session(allowed: &[&str]) -> Session {
Session {
profile: "demo".to_string(),
allowed_globs: allowed.iter().map(|s| s.to_string()).collect(),
denied_globs: vec![],
contract: None,
allow_reveal: false,
audit_source: "mcp:test:1".to_string(),
pid: 1,
require_agent: false,
vault_path: PathBuf::from("nonexistent"),
}
}
fn isolated<F: FnOnce()>(f: F) {
let tmp = tempfile::tempdir().unwrap();
let vault_dir = tmp.path().join("vaults");
std::fs::create_dir_all(&vault_dir).unwrap();
temp_env::with_var("TSAFE_VAULT_DIR", Some(vault_dir.as_os_str()), f);
}
#[test]
fn out_of_scope_key_returns_false_without_opening_vault() {
isolated(|| {
let s = session(&["in_scope_*"]);
let resp = call(&s, json!({"key": "out_of_scope"})).unwrap();
assert_eq!(resp["present"], false);
});
}
#[test]
fn parse_args_rejects_missing_key() {
assert!(parse_args(&json!({})).is_err());
assert!(parse_args(&json!({"key": ""})).is_err());
assert!(parse_args(&json!({"other": "x"})).is_err());
}
}