a2a_rs/adapter/transport/http/
server.rs1use 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
25pub 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 processor: Arc<P>,
35 agent_info: Arc<A>,
37 address: String,
39 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 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 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 #[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 #[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 let connect_router = processor.register(connectrpc::Router::new());
115
116 let mut app = Router::new()
117 .route("/.well-known/agent-card.json", get(handle_agent_card))
119 .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 if let Some(auth) = &self.authenticator {
130 let auth_clone = auth.clone();
132
133 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
165fn 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#[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 (
204 StatusCode::INTERNAL_SERVER_ERROR,
205 Json(serde_json::json!({
206 "error": e.to_string()
207 })),
208 )
209 .into_response()
210 }
211 }
212}
213
214async 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
231async 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}