Skip to main content

a2a_server/
agent_card.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3use std::sync::Arc;
4
5use a2a::AgentCard;
6use axum::{
7    Json,
8    extract::State,
9    http::{HeaderMap, StatusCode, header},
10    response::IntoResponse,
11};
12
13/// Well-known path for the public agent card.
14pub const WELL_KNOWN_AGENT_CARD_PATH: &str = "/.well-known/agent-card.json";
15
16/// Trait for producing agent cards dynamically.
17pub trait AgentCardProducer: Send + Sync + 'static {
18    fn card(&self) -> AgentCard;
19}
20
21/// A static agent card producer.
22pub struct StaticAgentCard {
23    card: AgentCard,
24}
25
26impl StaticAgentCard {
27    pub fn new(card: AgentCard) -> Self {
28        StaticAgentCard { card }
29    }
30}
31
32impl AgentCardProducer for StaticAgentCard {
33    fn card(&self) -> AgentCard {
34        self.card.clone()
35    }
36}
37
38/// Create an axum router serving the agent card at `/.well-known/agent-card.json`
39/// with CORS headers for public discovery.
40pub fn agent_card_router<P: AgentCardProducer>(producer: Arc<P>) -> axum::Router {
41    axum::Router::new()
42        .route(
43            WELL_KNOWN_AGENT_CARD_PATH,
44            axum::routing::get(handle_agent_card::<P>),
45        )
46        .with_state(producer)
47}
48
49async fn handle_agent_card<P: AgentCardProducer>(
50    State(producer): State<Arc<P>>,
51    headers: HeaderMap,
52) -> impl IntoResponse {
53    let card = producer.card();
54    let mut resp_headers = HeaderMap::new();
55
56    // CORS headers for public discovery
57    let origin = headers
58        .get(header::ORIGIN)
59        .and_then(|v| v.to_str().ok())
60        .unwrap_or("*");
61
62    if origin != "*" {
63        resp_headers.insert(
64            header::ACCESS_CONTROL_ALLOW_ORIGIN,
65            origin.parse().unwrap_or_else(|_| "*".parse().unwrap()),
66        );
67        resp_headers.insert(
68            header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
69            "true".parse().unwrap(),
70        );
71        resp_headers.insert(header::VARY, "Origin".parse().unwrap());
72    } else {
73        resp_headers.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap());
74    }
75
76    (StatusCode::OK, resp_headers, Json(card))
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use a2a::AgentCapabilities;
83    use axum::body::Body;
84    use axum::http::Request;
85    use tower::ServiceExt;
86
87    fn test_card() -> AgentCard {
88        AgentCard {
89            name: "TestAgent".into(),
90            description: "A test agent".into(),
91            version: "1.0".into(),
92            supported_interfaces: vec![],
93            capabilities: AgentCapabilities::default(),
94            default_input_modes: vec!["text".into()],
95            default_output_modes: vec!["text".into()],
96            skills: vec![],
97            provider: None,
98            documentation_url: None,
99            icon_url: None,
100            security_schemes: None,
101            security_requirements: None,
102            signatures: None,
103        }
104    }
105
106    #[test]
107    fn test_static_agent_card() {
108        let card = test_card();
109        let sac = StaticAgentCard::new(card.clone());
110        assert_eq!(sac.card().name, "TestAgent");
111    }
112
113    #[test]
114    fn test_well_known_path() {
115        assert_eq!(WELL_KNOWN_AGENT_CARD_PATH, "/.well-known/agent-card.json");
116    }
117
118    #[tokio::test]
119    async fn test_agent_card_router_no_origin() {
120        let card = test_card();
121        let producer = Arc::new(StaticAgentCard::new(card));
122        let app = agent_card_router(producer);
123
124        let req = Request::builder()
125            .uri("/.well-known/agent-card.json")
126            .body(Body::empty())
127            .unwrap();
128
129        let resp = app.oneshot(req).await.unwrap();
130        assert_eq!(resp.status(), StatusCode::OK);
131        assert_eq!(
132            resp.headers()
133                .get("access-control-allow-origin")
134                .unwrap()
135                .to_str()
136                .unwrap(),
137            "*"
138        );
139    }
140
141    #[tokio::test]
142    async fn test_agent_card_router_with_origin() {
143        let card = test_card();
144        let producer = Arc::new(StaticAgentCard::new(card));
145        let app = agent_card_router(producer);
146
147        let req = Request::builder()
148            .uri("/.well-known/agent-card.json")
149            .header("origin", "https://example.com")
150            .body(Body::empty())
151            .unwrap();
152
153        let resp = app.oneshot(req).await.unwrap();
154        assert_eq!(resp.status(), StatusCode::OK);
155        assert_eq!(
156            resp.headers()
157                .get("access-control-allow-origin")
158                .unwrap()
159                .to_str()
160                .unwrap(),
161            "https://example.com"
162        );
163        assert_eq!(
164            resp.headers()
165                .get("access-control-allow-credentials")
166                .unwrap()
167                .to_str()
168                .unwrap(),
169            "true"
170        );
171    }
172}