Skip to main content

a2a_client/
agent_card.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3use a2a::{A2AError, AgentCard};
4use reqwest::Client;
5
6/// Resolves agent cards from `.well-known/agent-card.json` endpoints.
7pub 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    /// Resolve an agent card from the given base URL.
20    ///
21    /// Fetches `{base_url}/.well-known/agent-card.json`.
22    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}