Skip to main content

a2a_rs/adapter/transport/http/
server.rs

1//! HTTP server adapter for the A2A protocol
2
3// This module is already conditionally compiled with #[cfg(feature = "http-server")] in mod.rs
4
5use std::sync::Arc;
6
7use axum::{Json, Router, extract::State, http::StatusCode, response::IntoResponse, routing::get};
8
9#[cfg(feature = "tracing")]
10use tracing::{debug, error, info, instrument};
11
12use crate::{
13    adapter::{
14        auth::{NoopAuthenticator, with_auth},
15        error::HttpServerError,
16    },
17    domain::{
18        A2AError,
19        generated::{A2aService, A2aServiceExt},
20    },
21    port::Authenticator,
22    services::server::AgentInfoProvider,
23};
24
25/// HTTP server for the A2A protocol
26pub struct HttpServer<P, A, Auth = NoopAuthenticator>
27where
28    P: A2aService + Send + Sync + 'static,
29    A: AgentInfoProvider + Send + Sync + 'static,
30    Auth: Authenticator + Send + Sync + 'static,
31{
32    /// The `A2aService` implementation this server dispatches requests to
33    /// (e.g. [`ConnectRpcAdapter`](crate::adapter::ConnectRpcAdapter)).
34    processor: Arc<P>,
35    /// Agent info provider
36    agent_info: Arc<A>,
37    /// Server address
38    address: String,
39    /// Authenticator
40    authenticator: Option<Arc<Auth>>,
41}
42
43impl<P, A> HttpServer<P, A>
44where
45    P: A2aService + Send + Sync + 'static,
46    A: AgentInfoProvider + Send + Sync + 'static,
47{
48    /// Create a new HTTP server with the given processor and agent info provider
49    pub fn new(processor: P, agent_info: A, address: String) -> Self {
50        Self {
51            processor: Arc::new(processor),
52            agent_info: Arc::new(agent_info),
53            address,
54            authenticator: None,
55        }
56    }
57}
58
59impl<P, A, Auth> HttpServer<P, A, Auth>
60where
61    P: A2aService + Send + Sync + 'static,
62    A: AgentInfoProvider + Send + Sync + 'static,
63    Auth: Authenticator + Clone + Send + Sync + 'static,
64{
65    /// Create a new HTTP server with authentication
66    pub fn with_auth(processor: P, agent_info: A, address: String, authenticator: Auth) -> Self {
67        Self {
68            processor: Arc::new(processor),
69            agent_info: Arc::new(agent_info),
70            address,
71            authenticator: Some(Arc::new(authenticator)),
72        }
73    }
74
75    /// Start the HTTP server on the configured address.
76    #[cfg_attr(feature = "tracing", instrument(skip(self), fields(
77        server.address = %self.address,
78        server.has_auth = self.authenticator.is_some()
79    )))]
80    pub async fn start(&self) -> Result<(), A2AError> {
81        let listener = tokio::net::TcpListener::bind(&self.address)
82            .await
83            .map_err(HttpServerError::Io)?;
84        self.serve_on(listener).await
85    }
86
87    /// Serve on a listener the caller has already bound.
88    ///
89    /// [`start`](Self::start) binds the address itself, which leaves a caller
90    /// that asked for port 0 no way to learn which port it got, and any caller
91    /// no way to know the socket is accepting yet. Binding first answers both:
92    /// `listener.local_addr()` reports the real address, and the kernel queues
93    /// connections from the moment of the bind, so a client may connect before
94    /// this future is ever polled. That is what a test needs to skip the
95    /// "sleep and hope" step, and what lets a supervisor hand out port 0 and
96    /// report back the port the agent actually listens on.
97    #[cfg_attr(feature = "tracing", instrument(skip(self, listener), fields(
98        server.has_auth = self.authenticator.is_some()
99    )))]
100    pub async fn serve_on(&self, listener: tokio::net::TcpListener) -> Result<(), A2AError> {
101        #[cfg(feature = "tracing")]
102        info!(
103            "HTTP server listening on {}",
104            listener
105                .local_addr()
106                .map(|addr| addr.to_string())
107                .unwrap_or_else(|_| self.address.clone())
108        );
109
110        let processor = self.processor.clone();
111        let agent_info = self.agent_info.clone();
112
113        // Register the ConnectRPC service
114        let connect_router = processor.register(connectrpc::Router::new());
115
116        let mut app = Router::new()
117            // v1.0.0 well-known URI endpoint (RFC 8615)
118            .route("/.well-known/agent-card.json", get(handle_agent_card))
119            // Backward compatibility routes
120            .route("/agent-card", get(handle_agent_card))
121            .route("/skills", get(handle_skills))
122            .route("/skills/{id}", get(handle_skill_by_id))
123            .fallback_service(connect_router.into_axum_service())
124            .with_state(ServerState {
125                agent_info: agent_info.clone(),
126            });
127
128        // Apply authentication if provided
129        if let Some(auth) = &self.authenticator {
130            // Clone the authenticator for the middleware
131            let auth_clone = auth.clone();
132
133            // Create an auth router with the authenticator
134            app = with_auth(app, (*auth_clone).clone());
135        }
136
137        axum::serve(listener, app).await.map_err(|e| {
138            #[cfg(feature = "tracing")]
139            error!("Server error: {}", e);
140            HttpServerError::Server(format!("Server error: {}", e))
141        })?;
142
143        Ok(())
144    }
145}
146
147struct ServerState<A>
148where
149    A: AgentInfoProvider + Send + Sync + 'static,
150{
151    agent_info: Arc<A>,
152}
153
154impl<A> Clone for ServerState<A>
155where
156    A: AgentInfoProvider + Send + Sync + 'static,
157{
158    fn clone(&self) -> Self {
159        Self {
160            agent_info: self.agent_info.clone(),
161        }
162    }
163}
164
165/// Force the card's primary interface to advertise the binding this server
166/// actually mounts.
167///
168/// [`HttpServer::start`] serves exactly one protocol — ConnectRPC, registered as
169/// the fallback service — but an agent card built via `SimpleAgentInfo::new`
170/// defaults its primary interface to `JSONRPC` (the spec default). Left alone,
171/// every `HttpServer` publishes a card that lies about its own transport, and
172/// card-driven clients negotiate to a JSON-RPC endpoint that was never mounted.
173/// Rather than make each caller remember `with_preferred_transport`, the server
174/// states the truth about itself.
175///
176/// Secondary interfaces are untouched, so a deployment fronted by a proxy that
177/// *does* offer other bindings still advertises them via
178/// `SimpleAgentInfo::add_interface`. A card with no interfaces at all carries no
179/// dialable URL either, so there is nothing truthful to stamp — it is left as-is.
180fn stamp_served_binding(card: &mut crate::domain::AgentCard) {
181    if let Some(primary) = card.supported_interfaces.first_mut() {
182        primary.protocol_binding = crate::domain::PROTOCOL_BINDING_CONNECTRPC.to_string();
183    }
184}
185
186/// Handle a request for the agent card
187#[cfg_attr(feature = "tracing", instrument(skip(state)))]
188async fn handle_agent_card<A>(State(state): State<ServerState<A>>) -> impl IntoResponse
189where
190    A: AgentInfoProvider + Send + Sync + 'static,
191{
192    #[cfg(feature = "tracing")]
193    debug!("Fetching agent card");
194    match state.agent_info.get_agent_card().await {
195        Ok(mut card) => {
196            #[cfg(feature = "tracing")]
197            debug!("Agent card retrieved successfully");
198            stamp_served_binding(&mut card);
199            (StatusCode::OK, Json(card)).into_response()
200        }
201        Err(e) => {
202            // Map A2AError to HTTP response
203            (
204                StatusCode::INTERNAL_SERVER_ERROR,
205                Json(serde_json::json!({
206                    "error": e.to_string()
207                })),
208            )
209                .into_response()
210        }
211    }
212}
213
214/// Handle a request for all agent skills
215async fn handle_skills<A>(State(state): State<ServerState<A>>) -> impl IntoResponse
216where
217    A: AgentInfoProvider + Send + Sync + 'static,
218{
219    match state.agent_info.get_skills().await {
220        Ok(skills) => (StatusCode::OK, Json(skills)).into_response(),
221        Err(e) => (
222            StatusCode::INTERNAL_SERVER_ERROR,
223            Json(serde_json::json!({
224                "error": e.to_string()
225            })),
226        )
227            .into_response(),
228    }
229}
230
231/// Handle a request for a specific agent skill by ID
232async fn handle_skill_by_id<A>(
233    State(state): State<ServerState<A>>,
234    axum::extract::Path(id): axum::extract::Path<String>,
235) -> impl IntoResponse
236where
237    A: AgentInfoProvider + Send + Sync + 'static,
238{
239    match state.agent_info.get_skill_by_id(&id).await {
240        Ok(Some(skill)) => (StatusCode::OK, Json(skill)).into_response(),
241        Ok(None) => (
242            StatusCode::NOT_FOUND,
243            Json(serde_json::json!({
244                "error": format!("Skill with ID '{}' not found", id)
245            })),
246        )
247            .into_response(),
248        Err(e) => (
249            StatusCode::INTERNAL_SERVER_ERROR,
250            Json(serde_json::json!({
251                "error": e.to_string()
252            })),
253        )
254            .into_response(),
255    }
256}