apcore-a2a is the A2A (Agent-to-Agent) protocol adapter for the apcore ecosystem.
It solves a common problem: you've built AI capabilities with apcore modules, but you need them to talk to other AI agents over a standard protocol. apcore-a2a bridges that gap — it reads your existing module metadata (schemas, descriptions, examples) and automatically exposes them as a standards-compliant A2A server. No hand-written Agent Cards, no JSON-RPC boilerplate, no manual task lifecycle management.
In short: apcore modules + apcore-a2a = a fully functional A2A agent, ready to be discovered and invoked by any A2A-compatible client.
Also available in: Python | TypeScript
- One-call server — launch a compliant A2A server with
serve(source, config) - Automatic Agent Card —
/.well-known/agent-card.json(A2A 1.0;/.well-known/agent.jsonkept as a 0.3 alias) generated from module metadata - Skill mapping — apcore modules become A2A Skills with names, descriptions, tags, and examples;
metadata.display.a2aoverrides surface-facing fields (§5.13) - Full task lifecycle — submitted, working, completed, failed, canceled, input-required
- JWT authentication — tokens bridged to apcore's Identity context
- Built-in client —
A2AClientfor calling remote A2A agents - SSE streaming —
message/streamserver-sent events;A2AClient::stream_messageon the client - Push notifications —
tasks/pushNotificationConfig/{set,get,delete}with webhook delivery - OpenAPI backend (cargo feature
openapi) — point it at an OpenAPI 3.0/3.1 document and every operation becomes an A2A Skill, proxied over HTTP, with no apcore project on the other end - CLI support —
apcore-a2a --extensions-dir ./extensions(or--from-openapi <url|path>, or anapcore-a2a.openapi.specin your apcore config) for zero-code startup - Pluggable storage —
TaskStore/PushConfigStoretraits for custom backends, owner-scoped per call - Observability —
/healthendpoint - Config Bus — registers
apcore-a2anamespace withAPCORE_A2Aenv prefix (apcore 0.22) - Error Formatter Registry — registers A2A error formatter with apcore ecosystem (§8.8)
Note: Metrics (
/metrics) are Python/TypeScript-only — the Rust crate does not serve a/metricsendpoint. SSE streaming (message/stream) and push notifications (tasks/pushNotificationConfig/*) are fully supported.
- Rust edition 2021
apcore>= 0.30apcore-toolkit>= 0.11.1
[dependencies]
apcore-a2a = "0.7"
# Or, to serve an OpenAPI document instead of (or alongside) an extensions
# directory. The feature pulls in apcore-toolkit's `http-proxy` support, which
# is what fetches the spec and registers each operation as an HTTP proxy.
apcore-a2a = { version = "0.7", features = ["openapi"] }use apcore_a2a::{APCoreA2A, APCoreA2AConfig, BackendSource};
use std::path::PathBuf;
#[tokio::main]
async fn main() {
let source = BackendSource::ExtensionsDir(PathBuf::from("./extensions"));
let config = APCoreA2AConfig::default();
apcore_a2a::serve(source, config).await.unwrap();
}No apcore project required. Build with --features openapi, then either name the document on
the command line:
apcore-a2a --from-openapi https://petstore3.swagger.io/api/v3/openapi.json \
--openapi-prefix petstore...or declare it in your apcore config and run apcore-a2a with no flags at all:
apcore-a2a:
openapi:
spec: ./openapi.json # URL, or a path relative to Config::project_root
prefix: petstore
include_deprecated: false
timeout: 30.0 # spec-fetch timeout; Config-Bus only, no flag
headers: # spec fetch only, never sent on proxied calls
X-Api-Key: "${PETSTORE_SPEC_KEY}"Precedence is per key, and an explicit flag wins: --openapi-prefix zoo above overrides
prefix and leaves spec, include_deprecated, timeout and headers exactly as
configured. timeout, include, exclude and acknowledge_unapproved_writes have no flag
in any of the three SDKs and are set through the Config Bus only.
Warning: an OpenAPI document describes an API's shape, not the consequences of calling it, so
requires_approvalis never inferred — aPOST /chargesthat moves money is annotated exactly like aPOST /echo, and is therefore advertised on the public Agent Card, which is served without authentication. apcore-a2a warns about this at startup, and the warning is not silenced by merely attaching an ACL.
use apcore_a2a::A2AClient;
use serde_json::json;
#[tokio::main]
async fn main() {
let client = A2AClient::new("http://remote-agent:8000");
// `message` is an A2A message object; `metadata.skillId` selects the module.
let task = client
.send_message(
json!({
"messageId": "m1",
"role": "ROLE_USER",
"parts": [{ "data": { "name": "Tercel" } }]
}),
Some(json!({ "skillId": "demo.greet" })),
None, // optional contextId
)
.await
.unwrap();
println!("Result: {}", task);
}use apcore_a2a::{JWTAuthenticator, ClaimMapping};
let auth = JWTAuthenticator::new("your-secret-key")
.with_claim_mapping(ClaimMapping {
id_claim: "sub".into(),
roles_claim: "roles".into(),
..Default::default()
});| A2A Concept | apcore Mapping |
|---|---|
| Agent Card | Derived from Registry configuration |
| Skill id | module_id |
| Skill name | metadata.display.a2a.alias or humanized module_id |
| Skill desc | metadata.display.a2a.description or module.description |
| Skill tags | metadata.display.tags or module.tags |
| Task | Managed execution via ApCoreAgentExecutor::call / stream_channel |
| Security | Bridged to apcore's Identity context |
src/
adapters/ AgentCardBuilder, SkillMapper, SchemaConverter, ErrorMapper, PartConverter
auth/ JWTAuthenticator, AuthMiddleware, Authenticator trait
server/ A2AServerFactory, ApCoreAgentExecutor
client/ A2AClient, AgentCardFetcher
storage/ TaskStore + PushConfigStore traits, in-memory implementations, CallContext
explorer/ Explorer UI HTML + card handler
apcore_a2a.rs APCoreA2A builder, serve(), async_serve()
cli.rs CLI entrypoint
A self-contained example lives in examples/run/main.rs. It
registers a tiny in-code demo.greet module and serves it as an A2A agent — no
extensions directory or deployed modules required:
# Serve the demo module on port 8000
cargo run --example run
# Bind to a different port if 8000 is taken (e.g. by a Docker container)
A2A_URL=http://localhost:8001 cargo run --example runPort 8000 already in use? If
curl http://localhost:8000/...returns a non-apcore response such as{"detail":"Not Found"}with aserver: uvicornheader, another process (often a Docker container) owns the port. Find it withlsof -nP -iTCP:8000 -sTCP:LISTEN, then either stop it or setA2A_URLto a free port as shown above.
Once it's running, probe the agent from another terminal:
curl http://localhost:8000/.well-known/agent-card.json # Agent Card (A2A 1.0; lists demo.greet)
# (0.3 alias) curl http://localhost:8000/.well-known/agent.json
curl http://localhost:8000/health # Health check
open http://localhost:8000/explorer # Explorer UI (browser)
# Invoke the skill — inputs go in a `data` part; metadata.skillId picks the module:
curl -X POST http://localhost:8000/ -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":"1","method":"message/send",
"params":{"message":{"messageId":"m1","role":"ROLE_USER",
"parts":[{"data":{"name":"Tercel"}}]},
"metadata":{"skillId":"demo.greet"}}}'
# => artifacts[0].parts[0].data == {"greeting":"Hello, Tercel!"}To serve your own modules instead, build an apcore::registry::Registry,
register your modules, and pass BackendSource::Registry(Arc::new(registry)) to
serve — or point a BackendSource::ExtensionsDir at a directory of deployed
modules. See the serve entry point for all backend sources.
To verify the example compiles as part of a check, build all examples:
cargo build --examplesgit clone https://github.com/aiperceivable/apcore-a2a-rust.git
cd apcore-a2a-rust
cargo test # run the test suite
cargo build --examples # ensure examples still compileApache 2.0 — see LICENSE.