use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::{Mutex as SyncMutex, RwLock as SyncRwLock};
use tokio_util::sync::CancellationToken;
use dashmap::DashMap;
use tokio::sync::RwLock;
use tokio::sync::{mpsc, watch};
type StatusTx = mpsc::UnboundedSender<String>;
type ServerTrust = Arc<
tokio::sync::RwLock<HashMap<String, (McpTrustLevel, Option<Vec<String>>, Vec<String>, bool)>>,
>;
use rmcp::transport::auth::CredentialStore;
use crate::client::{McpClient, ToolRefreshEvent};
use crate::elicitation::ElicitationEvent;
use crate::embedding_guard::EmbeddingAnomalyGuard;
use crate::policy::PolicyEnforcer;
use crate::prober::DefaultMcpProber;
use crate::tool::{McpTool, ToolSecurityMeta};
use crate::trust_score::TrustScoreStore;
fn default_elicitation_timeout() -> u64 {
120
}
pub(crate) use zeph_config::McpTrustLevel;
const MAX_INJECTION_PENALTIES_PER_REGISTRATION: usize = 3;
#[non_exhaustive]
#[derive(Clone, serde::Deserialize)]
pub enum McpTransport {
Stdio {
command: String,
args: Vec<String>,
env: HashMap<String, String>,
},
Http {
url: String,
#[serde(default)]
headers: HashMap<String, String>,
},
OAuth {
url: String,
scopes: Vec<String>,
callback_port: u16,
client_name: String,
},
}
impl std::fmt::Debug for McpTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Stdio { command, args, env } => {
let redacted: HashMap<&str, &str> =
env.keys().map(|k| (k.as_str(), "[REDACTED]")).collect();
f.debug_struct("Stdio")
.field("command", command)
.field("args", args)
.field("env", &redacted)
.finish()
}
Self::Http { url, headers } => {
let redacted: HashMap<&str, &str> =
headers.keys().map(|k| (k.as_str(), "[REDACTED]")).collect();
f.debug_struct("Http")
.field("url", url)
.field("headers", &redacted)
.finish()
}
Self::OAuth {
url,
scopes,
callback_port,
client_name,
} => f
.debug_struct("OAuth")
.field("url", url)
.field("scopes", scopes)
.field("callback_port", callback_port)
.field("client_name", client_name)
.finish(),
}
}
}
impl serde::Serialize for McpTransport {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStructVariant;
match self {
Self::Stdio { command, args, env } => {
let redacted: HashMap<&str, &str> =
env.keys().map(|k| (k.as_str(), "[REDACTED]")).collect();
let mut v = serializer.serialize_struct_variant("McpTransport", 0, "Stdio", 3)?;
v.serialize_field("command", command)?;
v.serialize_field("args", args)?;
v.serialize_field("env", &redacted)?;
v.end()
}
Self::Http { url, headers } => {
let redacted: HashMap<&str, &str> =
headers.keys().map(|k| (k.as_str(), "[REDACTED]")).collect();
let mut v = serializer.serialize_struct_variant("McpTransport", 1, "Http", 2)?;
v.serialize_field("url", url)?;
v.serialize_field("headers", &redacted)?;
v.end()
}
Self::OAuth {
url,
scopes,
callback_port,
client_name,
} => {
let mut v = serializer.serialize_struct_variant("McpTransport", 2, "OAuth", 4)?;
v.serialize_field("url", url)?;
v.serialize_field("scopes", scopes)?;
v.serialize_field("callback_port", callback_port)?;
v.serialize_field("client_name", client_name)?;
v.end()
}
}
}
}
#[allow(deprecated)]
#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ServerEntry {
pub id: String,
pub transport: McpTransport,
pub timeout: Duration,
#[serde(default)]
pub trust_level: McpTrustLevel,
#[serde(default)]
pub tool_allowlist: Option<Vec<String>>,
#[serde(default)]
pub allow_untrusted_without_allowlist: bool,
#[serde(default)]
pub expected_tools: Vec<String>,
#[serde(default)]
pub roots: Vec<rmcp::model::Root>,
#[serde(default)]
pub tool_metadata: HashMap<String, ToolSecurityMeta>,
#[serde(default)]
pub elicitation_enabled: bool,
#[serde(default = "default_elicitation_timeout")]
pub elicitation_timeout_secs: u64,
#[serde(default)]
pub env_isolation: bool,
#[serde(default)]
pub media_passthrough: bool,
}
#[derive(Debug, Clone, Copy)]
struct IngestLimits {
description_bytes: usize,
instructions_bytes: usize,
}
struct ConnectOutput {
client_entry: Option<(String, McpClient)>,
tools_entry: Option<(String, Vec<McpTool>)>,
tools: Vec<McpTool>,
outcome: ServerConnectOutcome,
instructions: Option<(String, String)>,
fingerprints: Option<(String, HashMap<String, crate::attestation::ToolFingerprint>)>,
}
#[derive(Debug, Clone)]
pub struct ServerConnectOutcome {
pub id: String,
pub connected: bool,
pub tool_count: usize,
pub error: String,
pub input_schemas_dropped: usize,
pub output_schemas_dropped: usize,
}
pub struct McpManager {
configs: Vec<ServerEntry>,
allowed_commands: Vec<String>,
clients: Arc<RwLock<HashMap<String, McpClient>>>,
connected_server_ids: SyncRwLock<HashSet<String>>,
enforcer: Arc<PolicyEnforcer>,
suppress_stderr: bool,
server_tools: Arc<RwLock<HashMap<String, Vec<McpTool>>>>,
refresh_tx: SyncMutex<Option<mpsc::Sender<ToolRefreshEvent>>>,
refresh_rx: SyncMutex<Option<mpsc::Receiver<ToolRefreshEvent>>>,
tools_watch_tx: watch::Sender<Vec<McpTool>>,
last_refresh: Arc<DashMap<String, Instant>>,
oauth_credentials: HashMap<String, Arc<dyn CredentialStore>>,
status_tx: Option<StatusTx>,
server_trust: ServerTrust,
server_fingerprints:
Arc<RwLock<HashMap<String, HashMap<String, crate::attestation::ToolFingerprint>>>>,
prober: Option<DefaultMcpProber>,
trust_store: Option<Arc<TrustScoreStore>>,
embedding_guard: Option<EmbeddingAnomalyGuard>,
server_tool_metadata: Arc<HashMap<String, HashMap<String, ToolSecurityMeta>>>,
max_description_bytes: usize,
max_instructions_bytes: usize,
server_instructions: Arc<RwLock<HashMap<String, String>>>,
elicitation_tx: SyncMutex<Option<mpsc::Sender<ElicitationEvent>>>,
elicitation_rx: SyncMutex<Option<mpsc::Receiver<ElicitationEvent>>>,
server_elicitation: HashMap<String, bool>,
server_elicitation_timeout: HashMap<String, u64>,
add_remove_lock: tokio::sync::Mutex<()>,
shutdown_token: CancellationToken,
max_connect_attempts: u8,
startup_retry_backoff_ms: u64,
tool_timeout_secs: Option<u64>,
lock_tool_list: bool,
tool_list_locked: Arc<DashMap<String, ()>>,
}
impl std::fmt::Debug for McpManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("McpManager")
.field("server_count", &self.configs.len())
.finish_non_exhaustive()
}
}
struct IngestConfig<'a> {
server_id: &'a str,
trust_level: McpTrustLevel,
allowlist: Option<&'a [String]>,
allow_untrusted_without_allowlist: bool,
expected_tools: &'a [String],
status_tx: Option<&'a StatusTx>,
max_description_bytes: usize,
tool_metadata: &'a HashMap<String, ToolSecurityMeta>,
previous_fingerprints: Option<&'a HashMap<String, crate::attestation::ToolFingerprint>>,
}
mod builder;
mod call;
mod connect;
mod ingest;
mod retry;
mod server;
#[cfg(test)]
mod tests;