Skip to content

Commit f597498

Browse files
refactor: reduce complexity of generate_mcpg_config in compile/common.rs (#322)
1 parent 29f16ca commit f597498

1 file changed

Lines changed: 136 additions & 139 deletions

File tree

src/compile/common.rs

Lines changed: 136 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,141 @@ pub fn generate_agentic_depends_on(setup_steps: &[serde_yaml::Value]) -> String
11371137
}
11381138
}
11391139

1140+
/// Returns `Some(v.to_vec())` when `v` is non-empty, otherwise `None`.
1141+
fn nonempty_vec<T: Clone>(v: &[T]) -> Option<Vec<T>> {
1142+
if v.is_empty() { None } else { Some(v.to_vec()) }
1143+
}
1144+
1145+
/// Returns `Some(m.clone())` when `m` is non-empty, otherwise `None`.
1146+
fn nonempty_map<K, V>(m: &HashMap<K, V>) -> Option<HashMap<K, V>>
1147+
where
1148+
K: Clone + Eq + std::hash::Hash,
1149+
V: Clone,
1150+
{
1151+
if m.is_empty() { None } else { Some(m.clone()) }
1152+
}
1153+
1154+
/// Validate a container-based MCP entry and emit any warnings.
1155+
fn validate_stdio_mcp(name: &str, container: &str, opts: &crate::compile::types::McpOptions) {
1156+
for w in validate::validate_container_image(container, name) { eprintln!("{}", w); }
1157+
for mount in &opts.mounts {
1158+
for w in validate::validate_mount_source(mount, name) { eprintln!("{}", w); }
1159+
}
1160+
for w in validate::validate_docker_args(&opts.args, name) { eprintln!("{}", w); }
1161+
for w in validate::warn_potential_secrets(name, &opts.env, &opts.headers) { eprintln!("{}", w); }
1162+
}
1163+
1164+
/// Build a stdio `McpgServerConfig` from a container-based MCP options block.
1165+
fn build_stdio_mcpg_server(container: &str, opts: &crate::compile::types::McpOptions) -> McpgServerConfig {
1166+
McpgServerConfig {
1167+
server_type: "stdio".to_string(),
1168+
container: Some(container.to_string()),
1169+
entrypoint: opts.entrypoint.clone(),
1170+
entrypoint_args: nonempty_vec(&opts.entrypoint_args),
1171+
mounts: nonempty_vec(&opts.mounts),
1172+
args: nonempty_vec(&opts.args),
1173+
url: None,
1174+
headers: None,
1175+
env: nonempty_map(&opts.env),
1176+
tools: nonempty_vec(&opts.allowed),
1177+
}
1178+
}
1179+
1180+
/// Build an HTTP `McpgServerConfig` from a URL-based MCP options block.
1181+
fn build_http_mcpg_server(url: &str, opts: &crate::compile::types::McpOptions) -> McpgServerConfig {
1182+
McpgServerConfig {
1183+
server_type: "http".to_string(),
1184+
container: None,
1185+
entrypoint: None,
1186+
entrypoint_args: None,
1187+
mounts: None,
1188+
args: None,
1189+
url: Some(url.to_string()),
1190+
headers: nonempty_map(&opts.headers),
1191+
env: None,
1192+
tools: nonempty_vec(&opts.allowed),
1193+
}
1194+
}
1195+
1196+
/// Validate and insert a single user-defined MCP server into `servers`.
1197+
///
1198+
/// Returns `Ok(())` on success. Returns `Err` for invalid server names.
1199+
/// Silently skips reserved names, disabled entries, and unconfigured entries.
1200+
fn try_add_user_mcp(
1201+
name: &str,
1202+
config: &McpConfig,
1203+
servers: &mut HashMap<String, McpgServerConfig>,
1204+
) -> Result<()> {
1205+
// Prevent user-defined MCPs from overwriting the reserved safeoutputs backend
1206+
if name.eq_ignore_ascii_case("safeoutputs") {
1207+
log::warn!(
1208+
"MCP name 'safeoutputs' is reserved for the safe outputs HTTP backend — skipping"
1209+
);
1210+
return Ok(());
1211+
}
1212+
1213+
// Validate server name for URL safety — names are embedded in MCPG routed
1214+
// endpoints (/mcp/{name}) and must be safe URL path segments.
1215+
// Leading dots are rejected to prevent path normalization issues (e.g., ".." → parent).
1216+
if name.is_empty()
1217+
|| name.starts_with('.')
1218+
|| !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
1219+
{
1220+
anyhow::bail!(
1221+
"MCP server name '{}' is invalid — must be non-empty, not start with '.', and contain only ASCII alphanumerics, hyphens, underscores, and dots",
1222+
name
1223+
);
1224+
}
1225+
1226+
// Skip if already auto-configured by an extension (e.g., tools.azure-devops)
1227+
if servers.contains_key(name) {
1228+
return Ok(());
1229+
}
1230+
1231+
let opts = match config {
1232+
McpConfig::Enabled(false) => return Ok(()),
1233+
McpConfig::Enabled(true) => {
1234+
log::warn!("MCP '{}' has no container or url — skipping", name);
1235+
return Ok(());
1236+
}
1237+
McpConfig::WithOptions(opts) => {
1238+
if !opts.enabled.unwrap_or(true) {
1239+
return Ok(());
1240+
}
1241+
opts
1242+
}
1243+
};
1244+
1245+
if opts.container.is_some() && opts.url.is_some() {
1246+
log::warn!(
1247+
"MCP '{}': both 'container' and 'url' are set — using 'container' (stdio). \
1248+
Remove 'url' to silence this warning.",
1249+
name
1250+
);
1251+
}
1252+
1253+
if let Some(container) = &opts.container {
1254+
validate_stdio_mcp(name, container, opts);
1255+
servers.insert(name.to_string(), build_stdio_mcpg_server(container, opts));
1256+
} else if let Some(url) = &opts.url {
1257+
// HTTP-based MCP (remote server)
1258+
for w in validate::validate_mcp_url(url, name) { eprintln!("{}", w); }
1259+
for w in validate::warn_potential_secrets(name, &HashMap::new(), &opts.headers) { eprintln!("{}", w); }
1260+
if !opts.env.is_empty() {
1261+
eprintln!(
1262+
"Warning: MCP '{}': env vars are not supported for HTTP MCPs — they will be ignored. \
1263+
Use headers for authentication instead.",
1264+
name
1265+
);
1266+
}
1267+
servers.insert(name.to_string(), build_http_mcpg_server(url, opts));
1268+
} else {
1269+
log::warn!("MCP '{}' has no container or url — skipping", name);
1270+
}
1271+
1272+
Ok(())
1273+
}
1274+
11401275
/// Generate MCPG configuration from front matter.
11411276
///
11421277
/// Converts the front matter `mcp-servers` definitions into MCPG-compatible JSON.
@@ -1157,145 +1292,7 @@ pub fn generate_mcpg_config(
11571292
}
11581293

11591294
for (name, config) in &front_matter.mcp_servers {
1160-
// Prevent user-defined MCPs from overwriting the reserved safeoutputs backend
1161-
if name.eq_ignore_ascii_case("safeoutputs") {
1162-
log::warn!(
1163-
"MCP name 'safeoutputs' is reserved for the safe outputs HTTP backend — skipping"
1164-
);
1165-
continue;
1166-
}
1167-
1168-
// Validate server name for URL safety — names are embedded in MCPG routed
1169-
// endpoints (/mcp/{name}) and must be safe URL path segments.
1170-
// Leading dots are rejected to prevent path normalization issues (e.g., ".." → parent).
1171-
if name.is_empty()
1172-
|| name.starts_with('.')
1173-
|| !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
1174-
{
1175-
anyhow::bail!(
1176-
"MCP server name '{}' is invalid — must be non-empty, not start with '.', and contain only ASCII alphanumerics, hyphens, underscores, and dots",
1177-
name
1178-
);
1179-
}
1180-
1181-
// Skip if already auto-configured by an extension (e.g., tools.azure-devops)
1182-
if mcp_servers.contains_key(name) {
1183-
continue;
1184-
}
1185-
1186-
let (is_enabled, options) = match config {
1187-
McpConfig::Enabled(enabled) => (*enabled, None),
1188-
McpConfig::WithOptions(opts) => (opts.enabled.unwrap_or(true), Some(opts)),
1189-
};
1190-
1191-
if !is_enabled {
1192-
continue;
1193-
}
1194-
1195-
if let Some(opts) = options {
1196-
if opts.container.is_some() && opts.url.is_some() {
1197-
log::warn!(
1198-
"MCP '{}': both 'container' and 'url' are set — using 'container' (stdio). \
1199-
Remove 'url' to silence this warning.",
1200-
name
1201-
);
1202-
}
1203-
1204-
if let Some(container) = &opts.container {
1205-
// Container-based stdio MCP (MCPG-native, per spec §3.2.1)
1206-
for w in validate::validate_container_image(container, name) { eprintln!("{}", w); }
1207-
// Validate mount paths for sensitive host directories
1208-
for mount in &opts.mounts {
1209-
for w in validate::validate_mount_source(mount, name) { eprintln!("{}", w); }
1210-
}
1211-
// Validate Docker runtime args for privilege escalation
1212-
for w in validate::validate_docker_args(&opts.args, name) { eprintln!("{}", w); }
1213-
// Warn about potential inline secrets (check headers too in case user set both)
1214-
for w in validate::warn_potential_secrets(name, &opts.env, &opts.headers) { eprintln!("{}", w); }
1215-
let entrypoint_args = if opts.entrypoint_args.is_empty() {
1216-
None
1217-
} else {
1218-
Some(opts.entrypoint_args.clone())
1219-
};
1220-
let args = if opts.args.is_empty() {
1221-
None
1222-
} else {
1223-
Some(opts.args.clone())
1224-
};
1225-
let mounts = if opts.mounts.is_empty() {
1226-
None
1227-
} else {
1228-
Some(opts.mounts.clone())
1229-
};
1230-
let env = if opts.env.is_empty() {
1231-
None
1232-
} else {
1233-
Some(opts.env.clone())
1234-
};
1235-
let tools = if opts.allowed.is_empty() {
1236-
None
1237-
} else {
1238-
Some(opts.allowed.clone())
1239-
};
1240-
mcp_servers.insert(
1241-
name.clone(),
1242-
McpgServerConfig {
1243-
server_type: "stdio".to_string(),
1244-
container: Some(container.clone()),
1245-
entrypoint: opts.entrypoint.clone(),
1246-
entrypoint_args,
1247-
mounts,
1248-
args,
1249-
url: None,
1250-
headers: None,
1251-
env,
1252-
tools,
1253-
},
1254-
);
1255-
} else if let Some(url) = &opts.url {
1256-
// HTTP-based MCP (remote server)
1257-
for w in validate::validate_mcp_url(url, name) { eprintln!("{}", w); }
1258-
// Warn about potential inline secrets in headers
1259-
for w in validate::warn_potential_secrets(name, &HashMap::new(), &opts.headers) { eprintln!("{}", w); }
1260-
if !opts.env.is_empty() {
1261-
eprintln!(
1262-
"Warning: MCP '{}': env vars are not supported for HTTP MCPs — they will be ignored. \
1263-
Use headers for authentication instead.",
1264-
name
1265-
);
1266-
}
1267-
let headers = if opts.headers.is_empty() {
1268-
None
1269-
} else {
1270-
Some(opts.headers.clone())
1271-
};
1272-
let tools = if opts.allowed.is_empty() {
1273-
None
1274-
} else {
1275-
Some(opts.allowed.clone())
1276-
};
1277-
mcp_servers.insert(
1278-
name.clone(),
1279-
McpgServerConfig {
1280-
server_type: "http".to_string(),
1281-
container: None,
1282-
entrypoint: None,
1283-
entrypoint_args: None,
1284-
mounts: None,
1285-
args: None,
1286-
url: Some(url.clone()),
1287-
headers,
1288-
env: None,
1289-
tools,
1290-
},
1291-
);
1292-
} else {
1293-
log::warn!("MCP '{}' has no container or url — skipping", name);
1294-
continue;
1295-
}
1296-
} else {
1297-
log::warn!("MCP '{}' has no container or url — skipping", name);
1298-
}
1295+
try_add_user_mcp(name, config, &mut mcp_servers)?;
12991296
}
13001297

13011298
Ok(McpgConfig {

0 commit comments

Comments
 (0)