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.unwrap_or_default(),
15        }
16    }
17
18    /// Resolve an agent card from the given base URL.
19    ///
20    /// Fetches `{base_url}/.well-known/agent-card.json`.
21    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}