use serde::{Deserialize, Serialize};
use crate::risk::RiskLevel;
use crate::tools::ToolMeta;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Transport {
Stdio,
Sse,
StreamableHttp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WritesAllowed {
None,
Gated,
Approved,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerManifest {
pub server_id: String,
pub display_name: String,
pub version: String,
pub domain: String,
pub transports: Vec<Transport>,
pub risk_level: RiskLevel,
pub writes_allowed: WritesAllowed,
pub sdk_version: String,
#[serde(default)]
pub tools: Vec<ToolMeta>,
#[serde(default)]
pub credentials: Vec<String>,
#[serde(default)]
pub governance_gates: Vec<String>,
#[serde(default)]
pub environments: Vec<String>,
}
impl ServerManifest {
pub fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
toml::from_str(s)
}
pub fn from_file(path: &std::path::Path) -> Result<Self, ManifestError> {
let content = std::fs::read_to_string(path)
.map_err(|e| ManifestError::Io(e.to_string()))?;
Self::from_toml(&content).map_err(|e| ManifestError::Parse(e.to_string()))
}
pub fn validate(&self) -> Vec<String> {
let mut errors = Vec::new();
if self.server_id.is_empty() {
errors.push("server_id is empty".into());
}
if self.display_name.is_empty() {
errors.push("display_name is empty".into());
}
if self.version.is_empty() {
errors.push("version is empty".into());
}
if self.transports.is_empty() {
errors.push("at least one transport is required".into());
}
let mut seen_tools = std::collections::HashSet::new();
for tool in &self.tools {
if tool.name.is_empty() {
errors.push("tool has empty name".into());
} else if !seen_tools.insert(&tool.name) {
errors.push(format!("duplicate tool name: {}", tool.name));
}
for cred in &tool.credential_bindings {
if !cred.starts_with("vault://") {
errors.push(format!("tool '{}': credential '{}' must use vault:// URI", tool.name, cred));
}
}
}
for cred in &self.credentials {
if !cred.starts_with("vault://") {
errors.push(format!("credential '{}' must use vault:// URI", cred));
}
}
errors
}
}
#[derive(Debug, thiserror::Error)]
pub enum ManifestError {
#[error("IO error: {0}")]
Io(String),
#[error("Parse error: {0}")]
Parse(String),
}