Skip to main content

a2a_client/
agent_card.rs

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