1use std::collections::BTreeMap;
11use std::path::PathBuf;
12
13use serde::Deserialize;
14
15use crate::error::{Result, TuffError};
16use crate::manifest::{
17 CapabilityManifest, CapabilityType, EnvRef, McpServerConfig, McpServerMetadata, McpTransport,
18};
19
20const CATALOG_TOML: &str = include_str!("../assets/mcp-catalog.toml");
21
22pub const SOURCE_TYPE: &str = "catalog";
24pub const SOURCE_URL: &str = "builtin";
27
28#[derive(Debug, Deserialize)]
29struct Catalog {
30 #[serde(default)]
31 servers: Vec<CatalogServer>,
32}
33
34#[derive(Debug, Deserialize)]
35struct CatalogServer {
36 id: String,
37 version: String,
41 description: String,
42 #[serde(default)]
43 transport: McpTransport,
44 #[serde(default)]
45 command: Option<String>,
46 #[serde(default)]
47 args: Vec<String>,
48 #[serde(default)]
49 url: Option<String>,
50 #[serde(default)]
53 env: Vec<String>,
54 #[serde(default)]
55 tools_summary: Option<String>,
56}
57
58fn catalog() -> Catalog {
59 toml::from_str(CATALOG_TOML).expect("embedded MCP catalog must parse; covered by unit test")
60}
61
62pub fn ids() -> Vec<String> {
64 catalog().servers.into_iter().map(|s| s.id).collect()
65}
66
67pub fn lookup(id: &str) -> Result<Option<CapabilityManifest>> {
71 let catalog = catalog();
72 let Some(server) = catalog.servers.into_iter().find(|s| s.id == id) else {
73 return Ok(None);
74 };
75
76 let env: BTreeMap<String, EnvRef> = server
77 .env
78 .into_iter()
79 .map(|name| (name.clone(), EnvRef { from_env: name }))
80 .collect();
81 let config = McpServerConfig {
82 transport: server.transport,
83 command: server.command,
84 args: server.args,
85 url: server.url,
86 env,
87 metadata: server.tools_summary.map(|tools_summary| McpServerMetadata {
88 tools_summary: Some(tools_summary),
89 }),
90 };
91 crate::manifest::validate_mcp_server(&config)
92 .map_err(|error| TuffError::new(format!("catalog entry '{id}' is invalid: {error}")))?;
93
94 Ok(Some(CapabilityManifest {
95 id: server.id,
96 version: server.version,
97 capability_type: CapabilityType::McpServer,
98 description: server.description,
99 files: Vec::new(),
100 parameters: None,
101 implementation: None,
102 hook: None,
103 workflow: None,
104 server: Some(config),
105 targets: Vec::new(),
106 root: PathBuf::new(),
107 }))
108}
109
110pub fn required_env(server: &McpServerConfig) -> Vec<String> {
113 server
114 .env
115 .values()
116 .map(|reference| reference.from_env.clone())
117 .collect::<std::collections::BTreeSet<_>>()
118 .into_iter()
119 .collect()
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn embedded_catalog_parses_and_every_entry_validates() {
128 let catalog = catalog();
129 assert!(!catalog.servers.is_empty());
130 for id in ids() {
131 let manifest = lookup(&id).unwrap().expect("listed id resolves");
132 assert_eq!(manifest.id, id);
133 assert_eq!(manifest.capability_type, CapabilityType::McpServer);
134 assert!(!manifest.version.is_empty());
135 assert!(manifest.server.is_some());
136 }
137 }
138
139 #[test]
140 fn each_entry_versions_independently() {
141 let ids = ids();
144 assert!(ids.len() >= 2);
145 for id in &ids {
146 let manifest = lookup(id).unwrap().unwrap();
147 assert_eq!(manifest.version, "1.0.0");
148 }
149 }
150
151 #[test]
152 fn github_entry_uses_the_current_docker_based_launch() {
153 let manifest = lookup("github").unwrap().unwrap();
158 let server = manifest.server.unwrap();
159 assert_eq!(server.transport, McpTransport::Stdio);
160 assert_eq!(server.command.as_deref(), Some("docker"));
161 assert!(
162 server
163 .args
164 .iter()
165 .any(|arg| arg == "ghcr.io/github/github-mcp-server")
166 );
167 assert_eq!(
168 server.env["GITHUB_PERSONAL_ACCESS_TOKEN"].from_env,
169 "GITHUB_PERSONAL_ACCESS_TOKEN"
170 );
171 assert_eq!(
172 required_env(&server),
173 vec!["GITHUB_PERSONAL_ACCESS_TOKEN".to_string()]
174 );
175 }
176
177 #[test]
178 fn excluded_servers_are_not_in_the_catalog() {
179 let ids = ids();
184 for excluded in [
185 "postgres", "sqlite", "slack", "gitlab", "linear", "context7",
186 ] {
187 assert!(
188 !ids.contains(&excluded.to_string()),
189 "{excluded} should not be in the catalog"
190 );
191 }
192 }
193
194 #[test]
195 fn unknown_id_is_none_not_an_error() {
196 assert!(lookup("does-not-exist").unwrap().is_none());
197 }
198}