1use a2a::{A2AError, AgentCard};
5use reqwest::Client;
6
7pub struct AgentCardResolver {
9 client: Client,
10}
11
12impl AgentCardResolver {
13 pub fn new(client: Option<Client>) -> Self {
14 AgentCardResolver {
15 client: client
16 .unwrap_or_else(|| crate::default_reqwest_client(None).expect("default client")),
17 }
18 }
19
20 pub async fn resolve(&self, base_url: &str) -> Result<AgentCard, A2AError> {
24 let url = format!(
25 "{}/.well-known/agent-card.json",
26 base_url.trim_end_matches('/')
27 );
28
29 let resp = self
30 .client
31 .get(&url)
32 .send()
33 .await
34 .map_err(|e| A2AError::internal(format!("failed to fetch agent card: {e}")))?;
35
36 if !resp.status().is_success() {
37 return Err(A2AError::internal(format!(
38 "agent card fetch returned HTTP {}",
39 resp.status()
40 )));
41 }
42
43 resp.json::<AgentCard>()
44 .await
45 .map_err(|e| A2AError::internal(format!("failed to parse agent card: {e}")))
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use tokio::io::{AsyncReadExt, AsyncWriteExt};
53 use tokio::net::TcpListener;
54
55 async fn spawn_agent_card_server(body: &'static str) -> String {
56 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
57 let addr = listener.local_addr().unwrap();
58
59 tokio::spawn(async move {
60 let (mut socket, _) = listener.accept().await.unwrap();
61 let mut buffer = [0_u8; 4096];
62 let _ = socket.read(&mut buffer).await;
63
64 let response = format!(
65 "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
66 body.len(),
67 body,
68 );
69 socket.write_all(response.as_bytes()).await.unwrap();
70 });
71
72 format!("http://{addr}")
73 }
74
75 #[tokio::test]
76 async fn test_resolve_accepts_null_skills() {
77 let server = spawn_agent_card_server(
78 r#"{
79 "name": "Test Agent",
80 "description": "A test agent",
81 "version": "1.0.0",
82 "supportedInterfaces": [
83 {
84 "url": "http://127.0.0.1:3000/jsonrpc",
85 "protocolBinding": "JSONRPC",
86 "protocolVersion": "1.0"
87 }
88 ],
89 "capabilities": { "streaming": true },
90 "defaultInputModes": ["text/plain"],
91 "defaultOutputModes": ["text/plain"],
92 "skills": null
93 }"#,
94 )
95 .await;
96
97 let resolver = AgentCardResolver::new(None);
98 let card = resolver.resolve(&server).await.unwrap();
99
100 assert!(card.skills.is_empty());
101 assert_eq!(card.supported_interfaces[0].protocol_binding, "JSONRPC");
102 }
103}