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)?;
audit_call(
session,
"tsafe_reveal",
Some(&key),
Vec::new(),
None,
None,
CallStatus::Success,
Some("reveal requested"),
);
if !session.is_in_scope(&key) {
let err = McpError::new(
McpErrorKind::KeyOutOfScope,
format!("key '{key}' is outside the configured scope for this server"),
)
.with_data(json!({"key": key}));
audit_call(
session,
"tsafe_reveal",
Some(&key),
Vec::new(),
None,
None,
CallStatus::Failure,
Some(&err.message),
);
return Err(err);
}
let vault = match backend::open_vault(session) {
Ok(v) => v,
Err(e) => {
audit_call(
session,
"tsafe_reveal",
Some(&key),
Vec::new(),
None,
None,
CallStatus::Failure,
Some(&e.message),
);
return Err(e);
}
};
let value = match backend::lookup_key(&vault, &key) {
Ok(v) => v,
Err(e) => {
audit_call(
session,
"tsafe_reveal",
Some(&key),
Vec::new(),
None,
None,
CallStatus::Failure,
Some(&e.message),
);
return Err(e);
}
};
let plaintext = value.as_str().to_string();
drop(vault);
Ok(json!({"value": plaintext}))
}
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() -> Session {
Session {
profile: "demo".to_string(),
allowed_globs: vec!["demo/*".to_string()],
denied_globs: vec![],
contract: None,
allow_reveal: true,
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 parse_args_rejects_unknown_or_missing() {
assert!(parse_args(&json!({})).is_err());
assert!(parse_args(&json!({"key": ""})).is_err());
assert!(parse_args(&json!({"other": "x"})).is_err());
assert_eq!(parse_args(&json!({"key": "DEMO_API"})).unwrap(), "DEMO_API");
}
#[test]
fn out_of_scope_key_returns_key_out_of_scope_and_audits() {
isolated(|| {
let err = call(&session(), json!({"key": "other/forbidden"})).unwrap_err();
assert_eq!(err.kind, McpErrorKind::KeyOutOfScope);
let log_path = tsafe_core::profile::audit_log_path("demo");
let log = tsafe_core::audit::AuditLog::new(&log_path);
let entries = log.read(None).unwrap();
assert!(entries.iter().any(
|e| e.operation == "mcp.reveal" && e.key.as_deref() == Some("other/forbidden")
));
});
}
}