use std::collections::HashMap;
use std::io::Read;
use serde::Deserialize;
pub const MCP_ROUTES_ENTRY: &str = "assets/mcp-routes.json";
#[derive(Debug, Clone, Deserialize)]
pub struct PackMcpRoute {
pub server_id: String,
#[serde(default)]
pub name: String,
#[serde(default = "default_transport")]
pub transport: String,
#[serde(default)]
pub transport_url: Option<String>,
#[serde(default)]
pub auth_header_name: Option<String>,
#[serde(default)]
pub auth_team: Option<String>,
#[serde(default)]
pub component_ref: Option<String>,
#[serde(default)]
pub component_version: Option<String>,
#[serde(default)]
pub component_digest: Option<String>,
}
fn default_transport() -> String {
"http".to_string()
}
#[derive(Debug, Clone, Default)]
pub struct PackMcpRoutes {
by_server: HashMap<String, PackMcpRoute>,
}
impl PackMcpRoutes {
pub fn from_pack_bytes(pack_bytes: &[u8]) -> Option<Self> {
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(pack_bytes))
.inspect_err(|e| tracing::warn!(error = %e, "mcp-routes: pack is not readable"))
.ok()?;
let mut entry = archive.by_name(MCP_ROUTES_ENTRY).ok()?;
let mut bytes = Vec::new();
entry
.read_to_end(&mut bytes)
.inspect_err(|e| tracing::warn!(error = %e, "mcp-routes: entry unreadable"))
.ok()?;
Self::from_sidecar_bytes(&bytes)
}
pub fn from_sidecar_bytes(sidecar_bytes: &[u8]) -> Option<Self> {
let routes: Vec<PackMcpRoute> = serde_json::from_slice(sidecar_bytes)
.inspect_err(|e| tracing::warn!(error = %e, "mcp-routes: malformed; ignoring sidecar"))
.ok()?;
let by_server = routes
.into_iter()
.map(|route| (route.server_id.clone(), route))
.collect();
Some(Self { by_server })
}
pub fn get(&self, server_id: &str) -> Option<&PackMcpRoute> {
self.by_server.get(server_id)
}
pub fn iter(&self) -> impl Iterator<Item = &PackMcpRoute> {
self.by_server.values()
}
pub fn is_empty(&self) -> bool {
self.by_server.is_empty()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn auth_team_round_trips_from_the_sidecar() {
let routes = PackMcpRoutes::from_sidecar_bytes(
br#"[{"server_id":"srv-1","transport_url":"https://x/","auth_team":"sales"}]"#,
)
.expect("sidecar parses");
assert_eq!(
routes.get("srv-1").unwrap().auth_team.as_deref(),
Some("sales")
);
}
#[test]
fn a_sidecar_predating_auth_team_still_parses_and_means_tenant_default() {
let routes = PackMcpRoutes::from_sidecar_bytes(
br#"[{"server_id":"srv-1","transport_url":"https://x/"}]"#,
)
.expect("sidecar parses");
assert_eq!(routes.get("srv-1").unwrap().auth_team, None);
}
#[test]
fn an_unknown_sidecar_field_is_ignored_rather_than_failing_the_pack() {
let routes = PackMcpRoutes::from_sidecar_bytes(
br#"[{"server_id":"srv-1","transport_url":"https://x/","future_field":1}]"#,
)
.expect("sidecar parses");
assert!(routes.get("srv-1").is_some());
}
#[test]
fn iter_yields_every_declared_route() {
let routes = PackMcpRoutes::from_sidecar_bytes(
br#"[{"server_id":"a","transport_url":"https://a/"},
{"server_id":"b","transport":"local-wasm","component_ref":"w.component"}]"#,
)
.expect("sidecar parses");
let mut ids: Vec<&str> = routes.iter().map(|r| r.server_id.as_str()).collect();
ids.sort_unstable();
assert_eq!(ids, ["a", "b"]);
assert_eq!(routes.get("a").unwrap().transport, "http");
assert_eq!(routes.get("b").unwrap().transport, "local-wasm");
}
#[test]
fn a_malformed_sidecar_is_ignored_rather_than_failing_the_pack() {
assert!(PackMcpRoutes::from_sidecar_bytes(b"{not json").is_none());
}
}