Quickstart
Prerequisites
Section titled “Prerequisites”-
Create a new project
Terminal window cargo new a2a-echo && cd a2a-echoTerminal window cargo new --lib a2a-echo-wasm && cd a2a-echo-wasm -
Add dependencies
[dependencies]agent_sdk = { workspace = true, features = ["a2a-server", "event-stream"] }tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }anyhow = "1.0"[dependencies]agent_sdk = { workspace = true, features = ["a2a-server"], default-features = false }spin-sdk = "5.2"anyhow = "1.0"[target.'cfg(target_arch = "wasm32")'.dependencies]agent_sdk = { workspace = true, features = ["a2a-server"], default-features = false }Also set
crate-type = ["cdylib"]under[lib]inCargo.toml, as in the example. -
Write the agent
use agent_sdk::{a2a::A2aApp,agent::{AgentConfig, MessageContext, Response, TaskContext},Agent,};#[tokio::main]async fn main() -> anyhow::Result<()> {let mut config = AgentConfig::new("a2a-echo-native", "A2A v1.0 echo agent (native)").with_base_url("http://127.0.0.1:3000");config.streaming = true;let mut agent = Agent::new_with_config(config)?;agent.add_skill("echo").description("Echoes back user messages").register()?;agent.set_message_handler(echo_handler);let app = A2aApp::from_agent(agent)?;println!("A2A echo agent (native) listening on http://127.0.0.1:3000");app.serve("127.0.0.1:3000").await}async fn echo_handler(msg_ctx: MessageContext,task_ctx: Option<TaskContext>,) -> agent_sdk::SdkResult<agent_sdk::agent::RuntimeResponse> {let input = msg_ctx.text_content.as_deref().unwrap_or("(empty)");Response::message_text(format!("Echo: {input}"),None,None,task_ctx.and_then(|t| t.context_id),)}use agent_sdk::{a2a::A2aApp,agent::{AgentConfig, Response},Agent,};use spin_sdk::http_component;static APP: std::sync::OnceLock<A2aApp> = std::sync::OnceLock::new();#[http_component]async fn handle_request(req: spin_sdk::http::Request,) -> anyhow::Result<spin_sdk::http::Response> {let app = APP.get_or_init(|| {let config = AgentConfig::new("a2a-echo-wasm", "A2A v1.0 echo agent (WASM)").with_base_url("http://127.0.0.1:3001");let mut agent = Agent::new_with_config(config).expect("agent init");agent.add_skill("echo").description("Echoes back user messages").register().expect("register echo skill");agent.set_message_handler(|msg_ctx, task_ctx| async move {let input = msg_ctx.text_content.as_deref().unwrap_or("(empty)");Response::message_text(format!("Echo: {input}"),None,None,task_ctx.and_then(|t| t.context_id),)});A2aApp::from_agent(agent).expect("app init")});Ok(app.serve_async(req).await?)} -
Run it
Terminal window cargo runThe agent listens on
http://127.0.0.1:3000.Add a
spin.tomlnext toCargo.toml(fromexamples/a2a-echo-wasm/spin.toml):spin_manifest_version = 2[application]name = "a2a-echo-wasm"version = "1.0.0"description = "Minimal A2A v1.0 echo agent — WASM (Spin)"[[trigger.http]]route = "/..."component = "echo-agent"[component.echo-agent]source = "target/wasm32-wasip1/release/a2a_echo_wasm.wasm"allowed_outbound_hosts = []key_value_stores = ["default"][component.echo-agent.build]command = "cargo build --target wasm32-wasip1 --release"Then build and run the app locally:
Terminal window spin build --upSpin prints the local base URL (often
http://127.0.0.1:3000). Use that host and port in the curl step below. TheAgentConfig::with_base_urlvalue in the example (http://127.0.0.1:3001) is the advertised base URL in metadata (e.g. agent card); adjust it if you want it to match the URL Spin assigns. -
Test with curl
Terminal window curl -X POST http://127.0.0.1:3000 \-H "Content-Type: application/json" \-d '{"jsonrpc": "2.0","id": "1","method": "SendMessage","params": {"message": {"role": "user","parts": [{"text": "Hello, agent!"}]}}}'
What just happened?
Section titled “What just happened?”You configured an Agent with AgentConfig and Agent::new_with_config, registered a skill with add_skill, and installed a user message handler with set_message_handler. That runtime is protocol-neutral. A2aApp::from_agent wraps it as an A2A v1 HTTP app: native code calls serve on Tokio, while the Spin path uses #[http_component] and serve_async with a OnceLock<A2aApp> so the agent is built once per process.
For config-first setups (JSON or files), the SDK also offers AgentBuilder; this tutorial uses explicit AgentConfig to keep the example small.
Next steps
Section titled “Next steps”- SDK Guide — capabilities and crate map
- A2A Protocol — protocol details
- WASM vs Native — deployment targets