use std::{io::Read, path::Path};
use anyhow::Context;
use hmac::{Hmac, KeyInit, Mac};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use super::super::services::{DEFAULT_MCP_TIMEOUT_SECONDS, McpServerConfig};
use crate::persistence::{CrossProcessFileLock, atomic_write_with_permissions, open_regular_file};
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub(crate) struct McpDefinitionFingerprint {
#[schemars(range(min = 1, max = 1))]
pub version: u32,
#[schemars(regex(pattern = "^[0-9a-f]{64}$"))]
pub digest: String,
}
impl std::fmt::Debug for McpDefinitionFingerprint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("McpDefinitionFingerprint(<redacted>)")
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub(crate) enum McpApproval {
Definition(McpDefinitionFingerprint),
Legacy(bool),
#[schemars(skip)]
Invalid(serde_json::Value),
}
impl std::fmt::Debug for McpApproval {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("McpApproval(<redacted>)")
}
}
impl McpApproval {
pub(super) fn status(&self, expected: &McpDefinitionFingerprint) -> &'static str {
match self {
Self::Legacy(false) => "revoked",
Self::Legacy(true) => "legacy approval requires renewal",
Self::Definition(saved)
if saved.version == 1
&& saved.digest.len() == 64
&& saved
.digest
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) =>
{
if saved == expected {
"approved"
} else {
"definition changed"
}
}
_ => "invalid approval state",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct McpApprovalIdentity {
pub source: String,
pub fingerprint: McpDefinitionFingerprint,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct McpApprovalSnapshot {
pub source: String,
pub fingerprint: McpDefinitionFingerprint,
pub status: &'static str,
pub definition: McpServerConfig,
}
impl McpApprovalSnapshot {
pub(crate) fn identity(&self) -> McpApprovalIdentity {
McpApprovalIdentity {
source: self.source.clone(),
fingerprint: self.fingerprint.clone(),
}
}
}
pub(super) fn fingerprint(
key: &[u8; 32],
source: &str,
name: &str,
definition: &McpServerConfig,
) -> anyhow::Result<McpDefinitionFingerprint> {
let mut definition = definition.clone();
definition.set_enabled(false);
match &mut definition {
McpServerConfig::Stdio(config) => {
config.timeout = Some(config.timeout.unwrap_or(DEFAULT_MCP_TIMEOUT_SECONDS));
}
McpServerConfig::Http(config) => {
config.timeout = Some(config.timeout.unwrap_or(DEFAULT_MCP_TIMEOUT_SECONDS));
}
}
let bytes = serde_json::to_vec(&("magi-code:mcp-approval:v1", source, name, definition))?;
let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC accepts a 32-byte key");
mac.update(&bytes);
let digest = mac.finalize().into_bytes();
Ok(McpDefinitionFingerprint {
version: 1,
digest: digest.iter().map(|byte| format!("{byte:02x}")).collect(),
})
}
pub(super) fn approval_key(root: &Path) -> anyhow::Result<[u8; 32]> {
let path = root.join("mcp-approval.key");
if let Some(key) = read_key(&path)? {
return Ok(key);
}
let _lock = CrossProcessFileLock::acquire(&path)?;
if let Some(key) = read_key(&path)? {
return Ok(key);
}
let mut key = [0; 32];
getrandom::fill(&mut key).map_err(|_| anyhow::anyhow!("cannot generate MCP approval key"))?;
atomic_write_with_permissions(&path, &key, Some(0o600))?;
Ok(key)
}
fn read_key(path: &Path) -> anyhow::Result<Option<[u8; 32]>> {
let file = match open_regular_file(path, false) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error).context("cannot read MCP approval key"),
};
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let metadata = file.metadata()?;
let owner = unsafe { libc::geteuid() };
anyhow::ensure!(
metadata.mode() & 0o077 == 0 && metadata.uid() == owner,
"MCP approval key must be private and owned by the current user"
);
}
let mut bytes = Vec::new();
file.take(33).read_to_end(&mut bytes)?;
let key = bytes
.try_into()
.map_err(|_| anyhow::anyhow!("invalid MCP approval key"))?;
Ok(Some(key))
}