use std::path::{Path, PathBuf};
use agent_client_protocol_schema::v1::{
EnvVariable, HttpHeader, McpServer, McpServerHttp, McpServerStdio,
};
use aion_integrations::HarnessError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum McpServerSpec {
Stdio {
name: String,
command: PathBuf,
args: Vec<String>,
env: Vec<(String, String)>,
},
Http {
name: String,
url: String,
headers: Vec<(String, String)>,
},
}
impl McpServerSpec {
#[must_use]
pub fn name(&self) -> &str {
match self {
Self::Stdio { name, .. } | Self::Http { name, .. } => name,
}
}
#[must_use]
pub const fn requires_http_capability(&self) -> bool {
matches!(self, Self::Http { .. })
}
#[must_use]
pub fn absolute_path(&self) -> Option<&Path> {
match self {
Self::Stdio { command, .. } => Some(command.as_path()),
Self::Http { .. } => None,
}
}
#[must_use]
pub fn to_wire(&self) -> McpServer {
match self {
Self::Stdio {
name,
command,
args,
env,
} => McpServer::Stdio(
McpServerStdio::new(name.clone(), command.clone())
.args(args.clone())
.env(
env.iter()
.map(|(key, value)| EnvVariable::new(key.clone(), value.clone()))
.collect(),
),
),
Self::Http { name, url, headers } => McpServer::Http(
McpServerHttp::new(name.clone(), url.clone()).headers(
headers
.iter()
.map(|(key, value)| HttpHeader::new(key.clone(), value.clone()))
.collect(),
),
),
}
}
}
pub(crate) fn require_transport_support(
servers: &[McpServerSpec],
http_supported: bool,
) -> Result<(), HarnessError> {
if http_supported {
return Ok(());
}
for server in servers {
if server.requires_http_capability() {
return Err(HarnessError::configuration(format!(
"the MCP server `{}` is configured over HTTP, and the configured agent did not \
advertise `mcpCapabilities.http` in its initialize response — it cannot reach an \
HTTP MCP server, and opening the session without it would silently deprive the \
agent of the tools this harness was configured to hand it; serve that server over \
stdio, or run this harness against an agent that supports the HTTP transport",
server.name()
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_stdio_spec_serialises_to_the_wire_form_an_agent_reads() -> Result<(), serde_json::Error> {
let spec = McpServerSpec::Stdio {
name: "meridian".to_owned(),
command: PathBuf::from("/opt/meridian/mcp"),
args: vec!["--stdio".to_owned()],
env: vec![("MERIDIAN_HOME".to_owned(), "/var/meridian".to_owned())],
};
let wire = serde_json::to_value(spec.to_wire())?;
assert_eq!(wire["name"], serde_json::json!("meridian"));
assert_eq!(wire["command"], serde_json::json!("/opt/meridian/mcp"));
assert_eq!(wire["args"], serde_json::json!(["--stdio"]));
assert_eq!(
wire["env"],
serde_json::json!([{ "name": "MERIDIAN_HOME", "value": "/var/meridian" }])
);
assert_eq!(wire.get("type"), None);
Ok(())
}
#[test]
fn an_http_spec_carries_its_type_url_and_headers() -> Result<(), serde_json::Error> {
let spec = McpServerSpec::Http {
name: "aion".to_owned(),
url: "http://127.0.0.1:8080/mcp".to_owned(),
headers: vec![("authorization".to_owned(), "Bearer t".to_owned())],
};
let wire = serde_json::to_value(spec.to_wire())?;
assert_eq!(wire["type"], serde_json::json!("http"));
assert_eq!(wire["name"], serde_json::json!("aion"));
assert_eq!(wire["url"], serde_json::json!("http://127.0.0.1:8080/mcp"));
assert_eq!(
wire["headers"],
serde_json::json!([{ "name": "authorization", "value": "Bearer t" }])
);
Ok(())
}
#[test]
fn an_http_server_is_refused_against_an_agent_that_did_not_advertise_http() -> Result<(), String>
{
let servers = vec![McpServerSpec::Http {
name: "aion".to_owned(),
url: "http://127.0.0.1:8080/mcp".to_owned(),
headers: Vec::new(),
}];
match require_transport_support(&servers, false) {
Ok(()) => Err("an unadvertised HTTP transport must be refused".to_owned()),
Err(error) => {
assert!(matches!(error, HarnessError::Configuration { .. }));
assert!(
error.to_string().contains("aion"),
"the refusal must name the server: {error}"
);
Ok(())
}
}
}
#[test]
fn a_stdio_server_needs_no_capability_because_every_agent_must_serve_it() {
let servers = vec![McpServerSpec::Stdio {
name: "meridian".to_owned(),
command: PathBuf::from("/opt/meridian/mcp"),
args: Vec::new(),
env: Vec::new(),
}];
assert!(require_transport_support(&servers, false).is_ok());
}
}