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