use a2a_agents_common::llm::LlmSettings;
use crate::core::config::AgentConfig;
use crate::core::config::HandlerType;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Requirement {
HttpBind {
host: String,
port: u16,
},
McpHttpBind {
host: String,
port: u16,
},
McpCommand {
server: String,
command: String,
},
LlmProvider(LlmSource),
UnknownHandler {
name: String,
},
ContainerImage {
image: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LlmSource {
Config(LlmSettings),
Environment,
}
pub fn requirements(config: &AgentConfig) -> Vec<Requirement> {
let mut requirements = Vec::new();
if config.server.http_port != 0 {
requirements.push(Requirement::HttpBind {
host: config.server.host.clone(),
port: config.server.http_port,
});
}
let mcp_server = &config.features.mcp_server;
if mcp_server.enabled && mcp_server.http.enabled {
requirements.push(Requirement::McpHttpBind {
host: mcp_server.http.host.clone(),
port: mcp_server.http.port,
});
}
if let Some(image) = config.image() {
requirements.push(Requirement::ContainerImage {
image: image.to_string(),
});
return requirements;
}
if config.features.mcp_client.enabled {
for server in &config.features.mcp_client.servers {
requirements.push(Requirement::McpCommand {
server: server.name.clone(),
command: server.command.clone(),
});
}
}
match config.handler_type() {
HandlerType::Llm => {
requirements.push(Requirement::LlmProvider(match config.llm.as_ref() {
Some(llm) => LlmSource::Config(llm.into()),
None => LlmSource::Environment,
}))
}
HandlerType::Custom(name) => requirements.push(Requirement::UnknownHandler { name }),
#[cfg(not(feature = "reimbursement-agent"))]
HandlerType::Reimbursement => requirements.push(Requirement::UnknownHandler {
name: "reimbursement".to_string(),
}),
#[cfg(feature = "reimbursement-agent")]
HandlerType::Reimbursement => {}
HandlerType::Echo => {}
}
requirements
}
#[cfg(test)]
mod tests {
use super::*;
fn config(toml: &str) -> AgentConfig {
AgentConfig::from_toml(toml).expect("fixture config parses")
}
#[test]
fn an_echo_agent_only_needs_its_port() {
let config = config(
r#"
[agent]
name = "Echo"
[server]
host = "127.0.0.1"
http_port = 8080
"#,
);
assert_eq!(
requirements(&config),
[Requirement::HttpBind {
host: "127.0.0.1".into(),
port: 8080
}]
);
}
#[test]
fn port_zero_needs_no_bind() {
let config = config(
r#"
[agent]
name = "Stdio Only"
[server]
http_port = 0
[features.mcp_server]
enabled = true
"#,
);
assert!(requirements(&config).is_empty());
}
#[test]
fn an_mcp_http_agent_needs_both_binds() {
let config = config(
r#"
[agent]
name = "Both"
[server]
host = "127.0.0.1"
http_port = 8080
[features.mcp_server]
enabled = true
[features.mcp_server.http]
enabled = true
host = "127.0.0.1"
port = 8000
"#,
);
assert_eq!(
requirements(&config),
[
Requirement::HttpBind {
host: "127.0.0.1".into(),
port: 8080
},
Requirement::McpHttpBind {
host: "127.0.0.1".into(),
port: 8000
},
]
);
}
#[test]
fn mcp_client_commands_are_requirements_only_when_enabled() {
let toml = r#"
[agent]
name = "Tools"
[server]
http_port = 8080
[features.mcp_client]
enabled = {enabled}
[[features.mcp_client.servers]]
name = "filesystem"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "."]
"#;
let enabled = config(&toml.replace("{enabled}", "true"));
assert!(requirements(&enabled).contains(&Requirement::McpCommand {
server: "filesystem".into(),
command: "npx".into(),
}));
let disabled = config(&toml.replace("{enabled}", "false"));
assert!(
!disabled.features.mcp_client.servers.is_empty(),
"the fixture must still declare a server"
);
assert!(
requirements(&disabled)
.iter()
.all(|r| !matches!(r, Requirement::McpCommand { .. }))
);
}
#[test]
fn an_llm_agent_without_a_config_block_needs_the_environment() {
let config = config(
r#"
[agent]
name = "Chat"
[server]
http_port = 8080
[handler]
type = "llm"
"#,
);
assert!(requirements(&config).contains(&Requirement::LlmProvider(LlmSource::Environment)));
}
#[test]
fn an_llm_block_is_carried_into_the_requirement() {
let config = config(
r#"
[agent]
name = "Chat"
[server]
http_port = 8080
[handler]
type = "llm"
[llm]
provider = "openai"
api_key = "sk-configured"
model = "gpt-5"
"#,
);
let expected = LlmSettings {
provider: "openai".into(),
api_key: Some("sk-configured".into()),
model: Some("gpt-5".into()),
..Default::default()
};
assert!(
requirements(&config).contains(&Requirement::LlmProvider(LlmSource::Config(expected)))
);
}
#[test]
fn an_unknown_handler_is_reported() {
let config = config(
r#"
[agent]
name = "Custom"
[server]
http_port = 8080
[handler]
type = "weather"
"#,
);
assert!(
requirements(&config).contains(&Requirement::UnknownHandler {
name: "weather".into()
})
);
}
#[test]
fn an_image_answers_for_the_handler_it_carries() {
let config = config(
r#"
[agent]
name = "Custom"
[server]
host = "127.0.0.1"
http_port = 8080
[handler]
type = "weather"
[runtime]
image = "ghcr.io/acme/weather:2.0"
"#,
);
assert_eq!(
requirements(&config),
[
Requirement::HttpBind {
host: "127.0.0.1".into(),
port: 8080
},
Requirement::ContainerImage {
image: "ghcr.io/acme/weather:2.0".into()
}
]
);
}
#[test]
fn an_image_agents_needs_are_not_probed_here() {
let config = config(
r#"
[agent]
name = "Custom"
[server]
http_port = 8080
[handler]
type = "llm"
[runtime]
image = "ghcr.io/acme/weather:2.0"
[features.mcp_client]
enabled = true
[[features.mcp_client.servers]]
name = "filesystem"
command = "definitely-not-installed"
"#,
);
let requirements = requirements(&config);
assert!(
!requirements.iter().any(|r| matches!(
r,
Requirement::McpCommand { .. } | Requirement::LlmProvider(_)
)),
"got: {requirements:?}"
);
}
}