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