Skip to main content

a2a_agents/core/
server.rs

1//! Per-agent server that manages one agent's serving lifecycle.
2//!
3//! [`AgentServer`] handles starting HTTP/WebSocket (or MCP) servers, wiring
4//! components, and serving a single agent based on its configuration. It is the
5//! leaf a higher-level [`AgentRuntime`](crate::runtime::AgentRuntime) supervises
6//! — not to be confused with that fleet-level port.
7
8use crate::core::config::{AgentConfig, AuthConfig, StorageConfig};
9use a2a_rs::adapter::{BearerTokenAuthenticator, ConnectRpcAdapter, HttpServer, SimpleAgentInfo};
10use a2a_rs::port::{
11    AsyncMessageHandler, AsyncNotificationManager, AsyncStreamingHandler, AsyncTaskLifecycle,
12    AsyncTaskQuery,
13};
14use std::sync::Arc;
15use tracing::{info, warn};
16
17#[cfg(feature = "auth")]
18use a2a_rs::adapter::{JwtAuthenticator, OAuth2Authenticator};
19#[cfg(feature = "auth")]
20use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
21#[cfg(feature = "auth")]
22use std::collections::HashMap;
23
24/// Per-agent server that manages one agent's serving lifecycle.
25pub struct AgentServer<H, S> {
26    config: AgentConfig,
27    handler: Arc<H>,
28    storage: Arc<S>,
29    /// Optional shared streaming backend. When set, it is injected into the
30    /// transport adapter so SSE subscribers see the same broadcasts the handler
31    /// emits (e.g. via the `TaskStatusBroadcast` mixin). Without it, the adapter
32    /// defaults to a no-op streaming handler and updates never reach clients.
33    streaming: Option<Arc<dyn AsyncStreamingHandler>>,
34}
35
36/// Build a [`SimpleAgentInfo`] (an `AgentInfoProvider`) from agent configuration,
37/// independent of a running server. Shared by the runtime's HTTP/WS startup and
38/// by registry self-registration, which needs the agent card without binding a
39/// socket.
40pub(crate) fn agent_info_from_config(config: &AgentConfig, base_url: String) -> SimpleAgentInfo {
41    // `AgentServer::start_http` mounts a `ConnectRpcAdapter`, so the card must
42    // advertise CONNECTRPC or card-driven clients negotiate to a JSON-RPC
43    // endpoint that was never mounted. `HttpServer` also stamps this on the card
44    // it serves, but this provider is read off the HTTP path too — registry
45    // self-registration and MCP mode both take the card from here — so the
46    // truthful binding has to be set at the source.
47    let mut agent_info = SimpleAgentInfo::new(config.agent.name.clone(), base_url)
48        .with_preferred_transport(a2a_rs::domain::PROTOCOL_BINDING_CONNECTRPC.to_string());
49
50    if let Some(ref description) = config.agent.description {
51        agent_info = agent_info.with_description(description.clone());
52    }
53
54    if let Some(ref provider) = config.agent.provider {
55        agent_info = agent_info.with_provider(provider.name.clone(), provider.url.clone());
56    }
57
58    if let Some(ref doc_url) = config.agent.documentation_url {
59        agent_info = agent_info.with_documentation_url(doc_url.clone());
60    }
61
62    if let Some(ref version) = config.agent.version {
63        agent_info = agent_info.with_version(version.clone());
64    }
65
66    // Map AuthConfig into AgentCard security schemes
67    let mut schemes = std::collections::HashMap::new();
68    match &config.server.auth {
69        crate::core::config::AuthConfig::None => {}
70        crate::core::config::AuthConfig::Bearer { format, .. } => {
71            schemes.insert(
72                "bearer".to_string(),
73                a2a_rs::domain::SecurityScheme::http(
74                    "bearer".to_string(),
75                    format.clone(),
76                    Some("Bearer token authentication".to_string()),
77                ),
78            );
79        }
80        crate::core::config::AuthConfig::ApiKey { location, name, .. } => {
81            schemes.insert(
82                "api_key".to_string(),
83                a2a_rs::domain::SecurityScheme::api_key(
84                    name.clone(),
85                    location.clone(),
86                    Some("API Key authentication".to_string()),
87                ),
88            );
89        }
90        crate::core::config::AuthConfig::Jwt {
91            issuer, audience, ..
92        } => {
93            schemes.insert(
94                "jwt".to_string(),
95                a2a_rs::domain::SecurityScheme::http(
96                    "bearer".to_string(),
97                    Some("JWT".to_string()),
98                    Some(format!(
99                        "JWT authentication (issuer: {:?}, audience: {:?})",
100                        issuer, audience
101                    )),
102                ),
103            );
104        }
105        crate::core::config::AuthConfig::OAuth2 {
106            flow,
107            token_url,
108            authorization_url,
109            scopes,
110            ..
111        } => {
112            let scopes_map: std::collections::HashMap<String, String> =
113                scopes.iter().map(|s| (s.clone(), s.clone())).collect();
114            let flows = if flow == "client_credentials" {
115                a2a_rs::domain::OAuthFlows::client_credentials(
116                    a2a_rs::domain::ClientCredentialsOAuthFlow {
117                        token_url: token_url.clone(),
118                        refresh_url: String::new(),
119                        scopes: scopes_map,
120                        ..Default::default()
121                    },
122                )
123            } else {
124                a2a_rs::domain::OAuthFlows::authorization_code(
125                    a2a_rs::domain::AuthorizationCodeOAuthFlow {
126                        authorization_url: authorization_url.clone(),
127                        token_url: token_url.clone(),
128                        refresh_url: String::new(),
129                        scopes: scopes_map,
130                        ..Default::default()
131                    },
132                )
133            };
134            schemes.insert(
135                "oauth2".to_string(),
136                a2a_rs::domain::SecurityScheme::oauth2(
137                    flows,
138                    Some("OAuth2 authentication".to_string()),
139                    None,
140                ),
141            );
142        }
143    }
144    if !schemes.is_empty() {
145        agent_info = agent_info.with_security_schemes(schemes);
146    }
147
148    // Add features
149    if config.features.streaming {
150        agent_info = agent_info.with_streaming();
151    }
152
153    if config.features.push_notifications {
154        agent_info = agent_info.with_push_notifications();
155    }
156
157    if config.features.state_history {
158        agent_info = agent_info.with_state_transition_history();
159    }
160
161    if config.features.authenticated_card {
162        agent_info = agent_info.with_authenticated_extended_card();
163    }
164
165    // Add extensions
166    if let Some(ref ap2_config) = config.features.extensions.ap2 {
167        let roles_json: Vec<serde_json::Value> = ap2_config
168            .roles
169            .iter()
170            .map(|r| serde_json::Value::String(r.clone()))
171            .collect();
172
173        let mut params = std::collections::HashMap::new();
174        params.insert("roles".to_string(), serde_json::Value::Array(roles_json));
175        let params_val = serde_json::Value::Object(params.into_iter().collect());
176        let params_struct: buffa_types::google::protobuf::Struct =
177            serde_json::from_value(params_val).unwrap_or_default();
178
179        let ext = a2a_rs::domain::AgentExtension {
180            uri: "https://github.com/google-agentic-commerce/ap2/tree/v0.1".to_string(),
181            description: "Agent Payments Protocol (AP2) v0.1".to_string(),
182            required: ap2_config.required,
183            params: buffa::MessageField::some(params_struct),
184            ..Default::default()
185        };
186
187        agent_info = agent_info.add_extension(ext);
188        info!("💳 AP2 extension enabled (roles: {:?})", ap2_config.roles);
189    }
190
191    // Add skills
192    for skill in &config.skills {
193        agent_info = agent_info.add_comprehensive_skill(
194            skill.id.clone(),
195            skill.name.clone(),
196            skill.description.clone(),
197            if skill.keywords.is_empty() {
198                None
199            } else {
200                Some(skill.keywords.clone())
201            },
202            if skill.examples.is_empty() {
203                None
204            } else {
205                Some(skill.examples.clone())
206            },
207            Some(skill.input_formats.clone()),
208            Some(skill.output_formats.clone()),
209        );
210    }
211
212    agent_info
213}
214
215impl<H, S> AgentServer<H, S>
216where
217    H: AsyncMessageHandler + Clone + Send + Sync + 'static,
218    S: AsyncTaskLifecycle
219        + AsyncTaskQuery
220        + AsyncNotificationManager
221        + Clone
222        + Send
223        + Sync
224        + 'static,
225{
226    /// Create a new agent server.
227    pub fn new(config: AgentConfig, handler: Arc<H>, storage: Arc<S>) -> Self {
228        Self {
229            config,
230            handler,
231            storage,
232            streaming: None,
233        }
234    }
235
236    /// Attach a shared streaming backend.
237    ///
238    /// Pass the *same* [`AsyncStreamingHandler`] instance the message handler
239    /// broadcasts to (clones of an `InMemoryStreamingHandler` share their
240    /// subscriber registry). The runtime injects it into the transport adapter
241    /// so `tasks/subscribe` SSE streams observe those broadcasts.
242    pub fn with_streaming(mut self, streaming: Arc<dyn AsyncStreamingHandler>) -> Self {
243        self.streaming = Some(streaming);
244        self
245    }
246
247    /// Build agent info from this agent's configuration.
248    fn build_agent_info(&self, base_url: String) -> SimpleAgentInfo {
249        agent_info_from_config(&self.config, base_url)
250    }
251
252    /// Start HTTP server
253    pub async fn start_http(&self) -> Result<(), ServerError> {
254        if self.config.server.http_port == 0 {
255            return Err(ServerError::ServerNotConfigured(
256                "HTTP port is 0".to_string(),
257            ));
258        }
259
260        let base_url = format!(
261            "http://{}:{}",
262            self.config.server.host, self.config.server.http_port
263        );
264        let agent_info = self.build_agent_info(base_url);
265
266        let mut processor = ConnectRpcAdapter::new(
267            (*self.handler).clone(),
268            (*self.storage).clone(),
269            (*self.storage).clone(),
270            agent_info.clone(),
271        );
272
273        // Share the handler's streaming backend with the transport so SSE
274        // subscribers observe the same broadcasts. Without this the adapter
275        // keeps its default no-op streaming handler and updates never surface.
276        if let Some(streaming) = &self.streaming {
277            processor = processor.with_streaming_handler(streaming.clone());
278            info!("📡 Streaming backend wired into transport (SSE subscribers live)");
279        }
280
281        let bind_address = format!(
282            "{}:{}",
283            self.config.server.host, self.config.server.http_port
284        );
285
286        info!("🌐 Starting HTTP server on {}", bind_address);
287        self.print_agent_info("HTTP", &self.config.server.http_port.to_string());
288
289        match &self.config.server.auth {
290            AuthConfig::None => {
291                let server = HttpServer::new(processor, agent_info, bind_address);
292                server
293                    .start()
294                    .await
295                    .map_err(|e| ServerError::ServerError(e.to_string()))
296            }
297            AuthConfig::Bearer { tokens, format } => {
298                info!(
299                    "🔐 Authentication: Bearer token ({} token(s){})",
300                    tokens.len(),
301                    format
302                        .as_ref()
303                        .map(|f| format!(", format: {}", f))
304                        .unwrap_or_default()
305                );
306                let authenticator = BearerTokenAuthenticator::new(tokens.clone());
307                let server =
308                    HttpServer::with_auth(processor, agent_info, bind_address, authenticator);
309                server
310                    .start()
311                    .await
312                    .map_err(|e| ServerError::ServerError(e.to_string()))
313            }
314            AuthConfig::ApiKey {
315                keys,
316                location,
317                name,
318            } => {
319                warn!(
320                    "🔐 API key authentication configured ({} {}, {} key(s)) but not yet supported, using no auth",
321                    location,
322                    name,
323                    keys.len()
324                );
325                let server = HttpServer::new(processor, agent_info, bind_address);
326                server
327                    .start()
328                    .await
329                    .map_err(|e| ServerError::ServerError(e.to_string()))
330            }
331            #[cfg(feature = "auth")]
332            AuthConfig::Jwt {
333                secret,
334                rsa_pem_path,
335                algorithm,
336                issuer,
337                audience,
338            } => {
339                info!("🔐 Authentication: JWT (algorithm: {})", algorithm);
340
341                let mut authenticator = if let Some(secret) = secret {
342                    JwtAuthenticator::new_with_secret(secret.as_bytes())
343                } else if let Some(pem_path) = rsa_pem_path {
344                    let pem_data = std::fs::read(pem_path).map_err(|e| {
345                        ServerError::ServerError(format!("Failed to read RSA PEM file: {}", e))
346                    })?;
347                    JwtAuthenticator::new_with_rsa_pem(&pem_data).map_err(|e| {
348                        ServerError::ServerError(format!(
349                            "Failed to create JWT authenticator: {}",
350                            e
351                        ))
352                    })?
353                } else {
354                    return Err(ServerError::ServerError(
355                        "JWT authentication requires either 'secret' or 'rsa_pem_path'".to_string(),
356                    ));
357                };
358
359                if let Some(iss) = issuer {
360                    authenticator = authenticator.with_issuer(iss.clone());
361                    info!("   Issuer: {}", iss);
362                }
363                if let Some(aud) = audience {
364                    authenticator = authenticator.with_audience(aud.clone());
365                    info!("   Audience: {}", aud);
366                }
367
368                let server =
369                    HttpServer::with_auth(processor, agent_info, bind_address, authenticator);
370                server
371                    .start()
372                    .await
373                    .map_err(|e| ServerError::ServerError(e.to_string()))
374            }
375            #[cfg(not(feature = "auth"))]
376            AuthConfig::Jwt { .. } => Err(ServerError::ServerError(
377                "JWT authentication requires the 'auth' feature to be enabled".to_string(),
378            )),
379            #[cfg(feature = "auth")]
380            AuthConfig::OAuth2 {
381                client_id,
382                client_secret,
383                authorization_url,
384                token_url,
385                redirect_url,
386                flow,
387                scopes,
388            } => {
389                info!("🔐 Authentication: OAuth2 (flow: {})", flow);
390                info!("   Authorization URL: {}", authorization_url);
391                info!("   Token URL: {}", token_url);
392
393                let client_id = ClientId::new(client_id.clone());
394                let client_secret = ClientSecret::new(client_secret.clone());
395                let auth_url = AuthUrl::new(authorization_url.clone()).map_err(|e| {
396                    ServerError::ServerError(format!("Invalid authorization URL: {}", e))
397                })?;
398                let token_url = TokenUrl::new(token_url.clone())
399                    .map_err(|e| ServerError::ServerError(format!("Invalid token URL: {}", e)))?;
400
401                let scopes_map: HashMap<String, String> =
402                    scopes.iter().map(|s| (s.clone(), s.clone())).collect();
403
404                let authenticator = if flow == "client_credentials" {
405                    OAuth2Authenticator::new_client_credentials(
406                        client_id,
407                        client_secret,
408                        token_url,
409                        scopes_map,
410                    )
411                } else {
412                    // Authorization code flow
413                    let redirect_url = RedirectUrl::new(
414                        redirect_url
415                            .clone()
416                            .unwrap_or_else(|| "http://localhost:8080/callback".to_string()),
417                    )
418                    .map_err(|e| {
419                        ServerError::ServerError(format!("Invalid redirect URL: {}", e))
420                    })?;
421
422                    info!("   Redirect URL: {}", redirect_url.as_str());
423
424                    OAuth2Authenticator::new_authorization_code(
425                        client_id,
426                        Some(client_secret),
427                        auth_url,
428                        token_url,
429                        redirect_url,
430                        scopes_map,
431                    )
432                };
433
434                let server =
435                    HttpServer::with_auth(processor, agent_info, bind_address, authenticator);
436                server
437                    .start()
438                    .await
439                    .map_err(|e| ServerError::ServerError(e.to_string()))
440            }
441            #[cfg(not(feature = "auth"))]
442            AuthConfig::OAuth2 { .. } => Err(ServerError::ServerError(
443                "OAuth2 authentication requires the 'auth' feature to be enabled".to_string(),
444            )),
445        }
446    }
447
448    /// Start the appropriate server(s) based on configuration
449    pub async fn run(self) -> Result<(), ServerError> {
450        // Check if MCP server mode is enabled
451        if self.config.features.mcp_server.enabled {
452            return self.run_as_mcp_server().await;
453        }
454
455        // Normal A2A server mode
456        if self.config.server.http_port > 0 {
457            self.start_http().await
458        } else {
459            Err(ServerError::ServerNotConfigured(
460                "No servers configured".to_string(),
461            ))
462        }
463    }
464
465    /// Run agent as MCP server.
466    ///
467    /// The MCP bridge calls the configured [`AsyncMessageHandler`] in-process,
468    /// so no HTTP server is spawned. HTTP-only concerns like
469    /// [`AuthConfig`] are not applicable here — if you also want a secured
470    /// HTTP surface, run a normal `start_http()` instance in a separate
471    /// process or task.
472    async fn run_as_mcp_server(self) -> Result<(), ServerError> {
473        use crate::core::mcp;
474        use a2a_rs::services::AgentInfoProvider;
475
476        info!("🔌 Running agent in MCP server mode");
477
478        // `base_url` is what gets stamped onto the agent card (and therefore
479        // into the MCP tool namespace). MCP stdio has no listener of its own,
480        // so this is purely advertising — it doesn't need to be reachable.
481        let base_url = format!(
482            "http://{}:{}",
483            self.config.server.host, self.config.server.http_port
484        );
485
486        let agent_info = self.build_agent_info(base_url);
487        let agent_card = agent_info
488            .get_agent_card()
489            .await
490            .map_err(|e| ServerError::ServerError(format!("Failed to get agent card: {}", e)))?;
491
492        let handler = (*self.handler).clone();
493        mcp::run_mcp_server(&self.config.features.mcp_server, agent_card, handler)
494            .await
495            .map_err(|e| ServerError::ServerError(format!("MCP server error: {}", e)))
496    }
497
498    /// Print agent information
499    fn print_agent_info(&self, server_type: &str, port: &str) {
500        info!("📋 Agent: {}", self.config.agent.name);
501        if let Some(ref desc) = self.config.agent.description {
502            info!("   Description: {}", desc);
503        }
504        info!("   {} port: {}", server_type, port);
505
506        match &self.config.server.storage {
507            StorageConfig::InMemory => info!("💾 Storage: In-memory (non-persistent)"),
508            StorageConfig::Sqlx { url, .. } => info!("💾 Storage: SQLx ({})", url),
509        }
510
511        if !self.config.skills.is_empty() {
512            info!("🛠️  Skills: {}", self.config.skills.len());
513            for skill in &self.config.skills {
514                info!("   - {} ({})", skill.name, skill.id);
515            }
516        }
517    }
518}
519
520/// Errors serving a single agent.
521#[derive(Debug, thiserror::Error)]
522pub enum ServerError {
523    #[error("Server not configured: {0}")]
524    ServerNotConfigured(String),
525
526    #[error("Server error: {0}")]
527    ServerError(String),
528
529    #[error("Storage error: {0}")]
530    StorageError(String),
531}