use agent_client_protocol::schema::v1 as acp;
use crate::harness::{McpServer, McpTransport};
pub const TOOLS_SERVER: &str = "bitrouter_tools";
pub const SKILLS_SERVER: &str = "bitrouter_skills";
pub fn gateway_servers(
base_url: &str,
auth: &str,
aggregate_route: Option<&str>,
) -> Vec<McpServer> {
let mut servers = Vec::new();
if let Some(route) = aggregate_route {
servers.push(McpServer {
name: TOOLS_SERVER.to_string(),
transport: McpTransport::Http {
url: join_route(base_url, route),
headers: vec![("Authorization".to_string(), format!("Bearer {auth}"))],
},
});
}
servers.push(McpServer {
name: SKILLS_SERVER.to_string(),
transport: McpTransport::Stdio {
command: std::env::current_exe()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "bitrouter".to_string()),
args: ["mcp", "serve", "--backend", "skills"]
.map(str::to_string)
.to_vec(),
},
});
servers
}
pub fn to_acp(server: &McpServer) -> acp::McpServer {
match &server.transport {
McpTransport::Stdio { command, args } => acp::McpServer::Stdio(
acp::McpServerStdio::new(server.name.clone(), command.clone()).args(args.clone()),
),
McpTransport::Http { url, headers } => acp::McpServer::Http(
acp::McpServerHttp::new(server.name.clone(), url.clone()).headers(
headers
.iter()
.map(|(name, value)| acp::HttpHeader::new(name.clone(), value.clone()))
.collect(),
),
),
}
}
fn join_route(base_url: &str, route: &str) -> String {
let base = base_url.trim_end_matches('/');
if route.starts_with('/') {
format!("{base}{route}")
} else {
format!("{base}/{route}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tools_rides_the_aggregate_route_with_bearer_auth() {
let servers = gateway_servers("http://127.0.0.1:4356/", "tok", Some("/mcp"));
assert_eq!(servers.len(), 2);
let wire = serde_json::to_value(to_acp(&servers[0])).expect("serialize");
assert_eq!(wire["type"], "http");
assert_eq!(wire["name"], "bitrouter_tools");
assert_eq!(wire["url"], "http://127.0.0.1:4356/mcp");
assert_eq!(wire["headers"][0]["name"], "Authorization");
assert_eq!(wire["headers"][0]["value"], "Bearer tok");
}
#[test]
fn skills_is_stdio_and_survives_a_disabled_aggregate() {
let servers = gateway_servers("http://127.0.0.1:4356", "tok", None);
assert_eq!(servers.len(), 1, "aggregate off drops bitrouter_tools");
let wire = serde_json::to_value(to_acp(&servers[0])).expect("serialize");
assert_eq!(wire["name"], "bitrouter_skills");
assert!(wire.get("type").is_none(), "stdio is the untagged variant");
assert_eq!(
wire["args"],
serde_json::json!(["mcp", "serve", "--backend", "skills"])
);
}
#[test]
fn join_route_normalizes_slashes() {
assert_eq!(join_route("http://x:1/", "/mcp"), "http://x:1/mcp");
assert_eq!(join_route("http://x:1", "mcp"), "http://x:1/mcp");
}
}