You can build the A2A Java SDK using mvn:
mvn clean install
We copy https://github.com/a2aproject/A2A/blob/main/specification/grpc/a2a.proto to the spec-grpc/ project, and adjust the java_package option to be as follows:
option java_package = "org.a2aproject.sdk.grpc";
Then build the spec-grpc module with mvn clean install -Dskip.protobuf.generate=false to regenerate the gRPC classes in the org.a2aproject.sdk.grpc package.
You can find examples of how to use the A2A Java SDK in the a2a-samples repository.
More examples will be added soon.
The A2A Java SDK provides a Java server implementation of the Agent2Agent (A2A) Protocol. To run your agentic Java application as an A2A server, simply follow the steps below.
Adding a dependency on an A2A Java SDK Server will provide access to the core classes that make up the A2A specification and allow you to run your agentic Java application as an A2A server agent.
The A2A Java SDK provides reference A2A server implementations based on Quarkus for use with our tests and examples. However, the project is designed in such a way that it is trivial to integrate with various Java runtimes.
Server Integrations contains a list of community contributed integrations of the server with various runtimes. You might be able to use one of these for your target runtime, or you can use them as inspiration to create your own.
The A2A Java SDK Reference Server implementations support the following transports:
To use the reference implementation with the JSON-RPC protocol, add the following dependency to your project:
<dependency>
<groupId>org.a2aproject.sdk</groupId>
<artifactId>a2a-java-sdk-reference-jsonrpc</artifactId>
<version>${org.a2aproject.sdk.version}</version>
</dependency>
To use the reference implementation with the gRPC protocol, add the following dependency to your project:
<dependency>
<groupId>org.a2aproject.sdk</groupId>
<artifactId>a2a-java-sdk-reference-grpc</artifactId>
<version>${org.a2aproject.sdk.version}</version>
</dependency>
To use the reference implementation with the HTTP+JSON/REST protocol, add the following dependency to your project:
<dependency>
<groupId>org.a2aproject.sdk</groupId>
<artifactId>a2a-java-sdk-reference-rest</artifactId>
<version>${org.a2aproject.sdk.version}</version>
</dependency>
Note that you can add more than one of the above dependencies to your project depending on the transports you'd like to support.
import org.a2aproject.sdk.server.PublicAgentCard;
import org.a2aproject.sdk.spec.AgentCapabilities;
import org.a2aproject.sdk.spec.AgentCard;
import org.a2aproject.sdk.spec.AgentInterface;
import org.a2aproject.sdk.spec.AgentSkill;
import org.a2aproject.sdk.spec.TransportProtocol;
...
@ApplicationScoped
public class WeatherAgentCardProducer {
private static final String AGENT_URL = "http://localhost:10001";
@Produces
@PublicAgentCard
public AgentCard agentCard() {
return AgentCard.builder()
.name("Weather Agent")
.description("Helps with weather")
.supportedInterfaces(List.of(
new AgentInterface(TransportProtocol.JSONRPC.asString(), AGENT_URL)))
.version("1.0.0")
.capabilities(AgentCapabilities.builder()
.streaming(true)
.pushNotifications(false)
.build())
.defaultInputModes(Collections.singletonList("text"))
.defaultOutputModes(Collections.singletonList("text"))
.skills(Collections.singletonList(AgentSkill.builder()
.id("weather_search")
.name("Search weather")
.description("Helps with weather in cities or states")
.tags(Collections.singletonList("weather"))
.examples(List.of("weather in LA, CA"))
.build()))
.build();
}
}
import org.a2aproject.sdk.server.agentexecution.AgentExecutor;
import org.a2aproject.sdk.server.agentexecution.RequestContext;
import org.a2aproject.sdk.server.events.EventQueue;
import org.a2aproject.sdk.server.tasks.AgentEmitter;
import org.a2aproject.sdk.spec.JSONRPCError;
import org.a2aproject.sdk.spec.Message;
import org.a2aproject.sdk.spec.Part;
import org.a2aproject.sdk.spec.Task;
import org.a2aproject.sdk.spec.TaskNotCancelableError;
import org.a2aproject.sdk.spec.TaskState;
import org.a2aproject.sdk.spec.TextPart;
...
@ApplicationScoped
public class WeatherAgentExecutorProducer {
@Inject
WeatherAgent weatherAgent;
@Produces
public AgentExecutor agentExecutor() {
return new WeatherAgentExecutor(weatherAgent);
}
private static class WeatherAgentExecutor implements AgentExecutor {
private final WeatherAgent weatherAgent;
public WeatherAgentExecutor(WeatherAgent weatherAgent) {
this.weatherAgent = weatherAgent;
}
@Override
public void execute(RequestContext context, AgentEmitter agentEmitter) throws JSONRPCError {
// mark the task as submitted and start working on it
if (context.getTask() == null) {
agentEmitter.submit();
}
agentEmitter.startWork();
// extract the text from the message
String userMessage = extractTextFromMessage(context.getMessage());
// call the weather agent with the user's message
String response = weatherAgent.chat(userMessage);
// create the response part
TextPart responsePart = new TextPart(response);
List<Part<?>> parts = List.of(responsePart);
// add the response as an artifact and complete the task
agentEmitter.addArtifact(parts);
agentEmitter.complete();
}
@Override
public void cancel(RequestContext context, AgentEmitter agentEmitter) throws JSONRPCError {
Task task = context.getTask();
if (task.getStatus().state() == TaskState.CANCELED) {
// task already cancelled
throw new TaskNotCancelableError();
}
if (task.getStatus().state() == TaskState.COMPLETED) {
// task already completed
throw new TaskNotCancelableError();
}
// cancel the task
agentEmitter.cancel();
}
private String extractTextFromMessage(Message message) {
StringBuilder textBuilder = new StringBuilder();
for (Part<?> part : message.parts()) {
if (part instanceof TextPart textPart) {
textBuilder.append(textPart.text());
}
}
return textBuilder.toString();
}
}
}
The A2A Java SDK uses a flexible configuration system that works across different frameworks.
Default behavior: Configuration values come from META-INF/a2a-defaults.properties files on the classpath (provided by core modules and extras). These defaults work out of the box without any additional setup.
Customizing configuration:
- Quarkus/MicroProfile Config users: Add the microprofile-config integration to override defaults via application.properties, environment variables, or system properties
- Spring/other frameworks: See the integration module README for how to implement a custom A2AConfigProvider
- Reference implementations: Already include the MicroProfile Config integration
Executor Settings (Optional)
The SDK uses a dedicated executor for async operations like streaming. Default: 5 core threads, 50 max threads.
# Core thread pool size for the @Internal executor (default: 5)
a2a.executor.core-pool-size=5
# Maximum thread pool size (default: 50)
a2a.executor.max-pool-size=50
# Thread keep-alive time in seconds (default: 60)
a2a.executor.keep-alive-seconds=60
Blocking Call Timeouts (Optional)
# Timeout for agent execution in blocking calls (default: 30 seconds)
a2a.blocking.agent.timeout.seconds=30
# Timeout for event consumption in blocking calls (default: 5 seconds)
a2a.blocking.consumption.timeout.seconds=5
Why this matters: - Streaming Performance: The executor handles streaming subscriptions. Too few threads can cause timeouts under concurrent load. - Resource Management: The dedicated executor prevents streaming operations from competing with the ForkJoinPool. - Concurrency: In production with high concurrent streaming, increase pool sizes accordingly. - Agent Timeouts: LLM-based agents may need longer timeouts (60-120s) compared to simple agents.
Note: The reference server implementations (Quarkus-based) automatically include the MicroProfile Config integration, so properties work out of the box in application.properties.
The SDK includes an opt-in SPI for per-user task authorization. When enabled, every RequestHandler operation checks whether the authenticated user is allowed to access the target task before proceeding. When no provider is present, all operations are permitted (the default).
⚠ Security note: For multi-user deployments, a
TaskAuthorizationProvidermust be configured. Without one, all operations are permitted regardless of authentication — any authenticated user can read, modify, or cancel any task. Production deployments should use a fail-closed ownership policy (deny access when ownership is unknown).
Create an @ApplicationScoped CDI bean that implements TaskAuthorizationProvider:
import jakarta.enterprise.context.ApplicationScoped;
import org.a2aproject.sdk.server.auth.TaskAuthorizationProvider;
import org.a2aproject.sdk.server.auth.TaskOperation;
import org.a2aproject.sdk.server.ServerCallContext;
@ApplicationScoped
public class MyTaskAuthorizationProvider implements TaskAuthorizationProvider {
@Override
public boolean checkRead(ServerCallContext context, String taskId, TaskOperation op) {
// Return true to allow, false to deny.
// Denied reads throw TaskNotFoundError — the caller cannot distinguish
// "not found" from "not authorized", preventing information leakage.
String owner = ownershipStore.get(taskId);
if (owner == null) {
return false; // fail-closed: unknown ownership → deny
}
return owner.equals(context.getUser().getUsername());
}
@Override
public boolean checkWrite(ServerCallContext context, String taskId, TaskOperation op) {
return checkRead(context, taskId, op);
}
@Override
public boolean checkCreate(ServerCallContext context, TaskOperation op) {
return context.getUser().isAuthenticated();
}
@Override
public boolean isTaskRecorded(String taskId) {
return ownershipStore.contains(taskId);
}
@Override
public void recordOwnership(ServerCallContext context, String taskId, TaskOperation op) {
ownershipStore.put(taskId, context.getUser().getUsername());
}
}
No additional configuration is required — the SDK automatically discovers the bean via CDI and wires it into the request pipeline. See the TaskAuthorizationProvider javadoc for the full contract, including thread-safety requirements and ownership-recording semantics.
Authorization decisions rely on context.getUser() returning the authenticated user. How the user is populated depends on the transport:
rc.userContext()) and sets it on ServerCallContext directly.QuarkusCallContextFactory CDI bean that injects the Quarkus SecurityIdentity and maps it to the ServerCallContext User. T$ claude mcp add a2a-java \
-- python -m otcore.mcp_server <graph>