use std::sync::Arc;
use harn_vm::mcp_host::{set_allowlist, AllowlistDecision, AllowlistGuard};
use crate::auth::{AllowlistOutcome, AuthPolicy};
pub fn install_mcp_host_allowlist(policy: &AuthPolicy) {
let Some(allowlist) = policy.mcp_allowlist.clone() else {
set_allowlist(None);
return;
};
let guard: AllowlistGuard =
Arc::new(
move |server: &str, tool: Option<&str>| match allowlist.check(server, tool) {
AllowlistOutcome::Allow => AllowlistDecision::Allow,
AllowlistOutcome::ServerDenied => AllowlistDecision::Deny {
reason: format!("MCP server '{server}' is not on the tenant allowlist"),
},
AllowlistOutcome::ToolDenied => AllowlistDecision::Deny {
reason: format!(
"MCP tool '{}' on server '{server}' is not on the tenant allowlist",
tool.unwrap_or("<unknown>")
),
},
},
);
set_allowlist(Some(guard));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::{AuthPolicy, McpAllowlist, McpAllowlistTools};
use harn_vm::mcp_host;
fn global_guard() -> &'static std::sync::Mutex<()> {
static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
LOCK.get_or_init(|| std::sync::Mutex::new(()))
}
fn policy_with(allowlist: Option<McpAllowlist>) -> AuthPolicy {
AuthPolicy {
methods: Vec::new(),
mcp_allowlist: allowlist,
}
}
#[test]
fn install_with_none_clears_guard() {
let _guard = global_guard().lock().unwrap_or_else(|e| e.into_inner());
mcp_host::reset_for_tests();
install_mcp_host_allowlist(&policy_with(None));
let mut deny_all = McpAllowlist::deny_all();
deny_all.allow("permitted", McpAllowlistTools::All);
install_mcp_host_allowlist(&policy_with(Some(deny_all)));
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let denied = runtime.block_on(mcp_host::call("blocked", "anything", serde_json::json!({})));
assert!(
denied
.unwrap_err()
.to_string()
.contains("denied by allowlist"),
"expected blocked server to be rejected at the dispatch boundary"
);
mcp_host::reset_for_tests();
}
#[test]
fn tool_filter_only_admits_listed_tools() {
let _guard = global_guard().lock().unwrap_or_else(|e| e.into_inner());
mcp_host::reset_for_tests();
let mut allowlist = McpAllowlist::deny_all();
let mut tools = std::collections::BTreeSet::new();
tools.insert("read_repo".to_string());
allowlist.allow("github", McpAllowlistTools::Only(tools));
install_mcp_host_allowlist(&policy_with(Some(allowlist)));
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let err = runtime
.block_on(mcp_host::call(
"github",
"delete_repo",
serde_json::json!({}),
))
.unwrap_err()
.to_string();
assert!(
err.contains("denied by allowlist"),
"expected disallowed tool to be rejected, got: {err}"
);
mcp_host::reset_for_tests();
}
}