use std::net::SocketAddr;
use std::sync::Arc;
use axum::Router;
use futures_core::Stream;
use protocol_droid::adapters::primary::a2a::serve_agent::serve_a2a_agent;
use protocol_droid::adapters::primary::a2a::streaming_serve::A2aStreamingRequestHandler;
use protocol_droid::adapters::secondary::a2a::A2aMethod;
use protocol_droid::application::a2a::A2aRequestHandler;
use protocol_droid::application::a2a::{A2aError, A2aRequestContext};
use protocol_droid::domain::a2a::agent_card::{
AgentCapabilities, AgentCard, AgentCardBuilder, AgentInterface, AgentSkill, JSONRPC,
};
use protocol_droid::domain::a2a::streaming_events::StreamingEvent;
use protocol_droid::domain::a2a::version::A2aVersion;
use serde_json::Value;
struct Echo;
impl A2aRequestHandler for Echo {
fn dispatch(
&self,
method: A2aMethod,
_params: Option<Value>,
_context: &A2aRequestContext,
) -> std::pin::Pin<Box<dyn futures_core::Future<Output = Result<Value, A2aError>> + Send + '_>>
{
Box::pin(async move { Err(A2aError::MethodNotImplemented { method }) })
}
}
impl A2aStreamingRequestHandler for Echo {
fn dispatch_stream(
&self,
_method: A2aMethod,
_params: Option<Value>,
_context: &A2aRequestContext,
) -> std::pin::Pin<Box<dyn Stream<Item = Result<StreamingEvent, A2aError>> + Send>> {
Box::pin(futures_util::stream::empty())
}
}
fn build_card(addr: SocketAddr) -> Result<Arc<AgentCard>, Box<dyn std::error::Error>> {
Ok(Arc::new(
AgentCardBuilder::new()
.with_name("echo")
.with_description("A minimal A2A echo agent.")
.with_version("0.1.0")
.with_interface(AgentInterface {
url: url::Url::parse(&format!("http://{addr}/a2a"))?,
protocol_binding: JSONRPC.clone(),
protocol_version: A2aVersion::V1_0,
tenant: None,
})
.with_capabilities(AgentCapabilities::default())
.with_input_mode("text/plain")
.with_output_mode("text/plain")
.with_skill(AgentSkill {
id: "echo".into(),
name: "Echo".into(),
description: "Echoes a message back.".into(),
tags: Vec::new(),
})
.build()?,
))
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = bind_addr()?;
let card = build_card(addr)?;
let app: Router = serve_a2a_agent("/a2a", Arc::new(Echo), Arc::new(Echo), card);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(serve(app, addr))
}
async fn serve(app: Router, addr: SocketAddr) -> Result<(), Box<dyn std::error::Error>> {
let listener = tokio::net::TcpListener::bind(addr).await?;
eprintln!(
"a2a_minimal_server: listening on http://{}",
listener.local_addr()?
);
eprintln!(" well-known: /.well-known/agent-card.json");
eprintln!(" json-rpc: POST /a2a");
eprintln!(" streaming: POST /a2a/stream");
axum::serve(listener, app).await?;
Ok(())
}
fn bind_addr() -> Result<SocketAddr, Box<dyn std::error::Error>> {
let raw = std::env::var("A2A_ADDR").unwrap_or_else(|_| "127.0.0.1:8080".to_owned());
match raw.parse() {
Ok(addr) => Ok(addr),
Err(error) => {
eprintln!(
"a2a_minimal_server: A2A_ADDR={raw:?} is not a valid socket address: {error}"
);
Ok("127.0.0.1:8080".parse()?)
}
}
}