use std::collections::BTreeMap;
use std::path::PathBuf;
use serde::Deserialize;
use crate::error::{Result, TuffError};
use crate::manifest::{
CapabilityManifest, CapabilityType, EnvRef, McpServerConfig, McpServerMetadata, McpTransport,
};
const CATALOG_TOML: &str = include_str!("../assets/mcp-catalog.toml");
#[derive(Debug, Deserialize)]
struct Catalog {
#[serde(default)]
servers: Vec<CatalogServer>,
}
#[derive(Debug, Deserialize)]
struct CatalogServer {
id: String,
version: String,
description: String,
#[serde(default)]
transport: McpTransport,
#[serde(default)]
command: Option<String>,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
url: Option<String>,
#[serde(default)]
env: Vec<String>,
#[serde(default)]
tools_summary: Option<String>,
}
fn catalog() -> Catalog {
toml::from_str(CATALOG_TOML).expect("embedded MCP catalog must parse; covered by unit test")
}
pub fn ids() -> Vec<String> {
catalog().servers.into_iter().map(|s| s.id).collect()
}
pub fn lookup(id: &str) -> Result<Option<CapabilityManifest>> {
let catalog = catalog();
let Some(server) = catalog.servers.into_iter().find(|s| s.id == id) else {
return Ok(None);
};
let env: BTreeMap<String, EnvRef> = server
.env
.into_iter()
.map(|name| (name.clone(), EnvRef { from_env: name }))
.collect();
let config = McpServerConfig {
transport: server.transport,
command: server.command,
args: server.args,
url: server.url,
env,
headers: BTreeMap::new(),
metadata: server.tools_summary.map(|tools_summary| McpServerMetadata {
tools_summary: Some(tools_summary),
}),
};
crate::manifest::validate_mcp_server(&config)
.map_err(|error| TuffError::new(format!("catalog entry '{id}' is invalid: {error}")))?;
Ok(Some(CapabilityManifest {
id: server.id,
version: server.version,
capability_type: CapabilityType::McpServer,
description: server.description,
files: Vec::new(),
parameters: None,
implementation: None,
hook: None,
workflow: None,
server: Some(config),
targets: Vec::new(),
root: PathBuf::new(),
}))
}
pub fn required_env(server: &McpServerConfig) -> Vec<String> {
server
.env
.values()
.map(|reference| reference.from_env.clone())
.chain(
server
.headers
.values()
.map(|reference| reference.from_env.clone()),
)
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_catalog_parses_and_every_entry_validates() {
let catalog = catalog();
assert!(!catalog.servers.is_empty());
for id in ids() {
let manifest = lookup(&id).unwrap().expect("listed id resolves");
assert_eq!(manifest.id, id);
assert_eq!(manifest.capability_type, CapabilityType::McpServer);
assert!(!manifest.version.is_empty());
assert!(manifest.server.is_some());
}
}
#[test]
fn each_entry_versions_independently() {
let ids = ids();
assert!(ids.len() >= 2);
for id in &ids {
let manifest = lookup(id).unwrap().unwrap();
assert_eq!(manifest.version, "1.0.0");
}
}
#[test]
fn github_entry_uses_the_current_docker_based_launch() {
let manifest = lookup("github").unwrap().unwrap();
let server = manifest.server.unwrap();
assert_eq!(server.transport, McpTransport::Stdio);
assert_eq!(server.command.as_deref(), Some("docker"));
assert!(
server
.args
.iter()
.any(|arg| arg == "ghcr.io/github/github-mcp-server")
);
assert_eq!(
server.env["GITHUB_PERSONAL_ACCESS_TOKEN"].from_env,
"GITHUB_PERSONAL_ACCESS_TOKEN"
);
assert_eq!(
required_env(&server),
vec!["GITHUB_PERSONAL_ACCESS_TOKEN".to_string()]
);
}
#[test]
fn excluded_servers_are_not_in_the_catalog() {
let ids = ids();
for excluded in [
"postgres", "sqlite", "slack", "gitlab", "linear", "context7",
] {
assert!(
!ids.contains(&excluded.to_string()),
"{excluded} should not be in the catalog"
);
}
}
#[test]
fn unknown_id_is_none_not_an_error() {
assert!(lookup("does-not-exist").unwrap().is_none());
}
}