Enterprise Java

Exploring the Agent2Agent (A2A) Protocol with Spring AI

AI applications are rapidly moving from single-model applications toward systems composed of multiple specialized agents. One agent may specialize in weather information, another in travel planning, another in financial analysis, while an orchestration agent coordinates their work. The challenge is interoperability. How does an agent implemented using one framework discover and communicate with an agent implemented using another framework? The Agent2Agent Protocol (A2A) addresses this problem by defining a common protocol for agent discovery, communication, task execution, and collaboration. In this article, we will explore the fundamentals of A2A and see how it can be combined with Spring AI to expose a Spring-based AI agent as an A2A server and communicate with it from an A2A client.

1. Overview

Modern AI systems increasingly use specialized agents instead of relying on one large agent to perform every operation. Consider a travel-planning application containing the following agents:

  • A weather agent that provides weather information.
  • A hotel agent that searches for accommodation.
  • A flight agent that retrieves flight options.
  • A coordinator agent that delegates work to the specialized agents.

These agents may not necessarily be written using the same programming language or AI framework. Without a standard communication mechanism, developers would have to create custom APIs and integration logic for every pair of agents. A2A provides a standardized communication layer between such agents. An agent can advertise what it can do, another agent can discover those capabilities, and the two can exchange messages or coordinate longer running tasks. Spring AI provides abstractions such as ChatClient, model integrations, tools, structured output, memory, and related building blocks for developing AI applications in the Spring ecosystem. Spring AI A2A integration connects these Spring AI capabilities with the A2A Java ecosystem. A2A and MCP solve different problems. MCP primarily standardizes how an AI application interacts with tools and resources, whereas A2A standardizes communication between independent agents. A system can therefore use both protocols.

1.1 Understanding the Agent2Agent Protocol

The Agent2Agent Protocol is an open standard designed to allow independent and potentially opaque AI agents to communicate without requiring access to each other’s internal prompts, memory, implementation, or tools. At a high level, A2A communication can be understood through the following concepts.

  • A2A Server: An A2A server represents an agent that exposes its capabilities to other agents. It publishes information describing what it can do and provides endpoints through which A2A clients can communicate with it. For example, a Weather Agent might advertise that it supports a skill called weather_search.
  • A2A Cliet: An A2A client communicates with remote A2A agents. It can discover an agent, inspect its capabilities, create an A2A message, send that message to the remote agent, and process the response.
  • Agent Card: Agent discovery is a fundamental aspect of the Agent2Agent (A2A) Protocol, enabling agents to identify and understand each other’s capabilities before initiating communication. Each agent publishes an Agent Card, which serves as a machine-readable description of the agent and provides essential metadata, including the agent’s name, description, protocol version, supported capabilities, input and output modes, available skills, and the information required to communicate with it. By retrieving this Agent Card, an A2A client can determine what services and skills a remote agent provides before sending a request. In the Spring AI A2A integration demonstrated below, the Agent Card is exposed through a dedicated discovery endpoint: GET /.well-known/agent-card.json.
  • Messages: Agents interact with one another by exchanging A2A messages, which provide a standardized way to transfer information between a client agent and a remote agent. Each message defines a role, such as a user or agent, and consists of one or more parts containing the actual content being exchanged. For example, when a user submits a text-based request, the client agent can represent the request as a text part within an A2A message and send it to the remote A2A agent. The remote agent processes the request and returns a response through the same protocol, which the client agent can then present back to the user. In a typical communication flow, the interaction follows the sequence: User → Client Agent → A2A Message → Remote A2A Agent → Response → Client Agent → User.
  • Tasks: Some requests cannot be completed immediately. A2A therefore also defines a task-oriented interaction model suitable for operations that may take longer to execute. This is particularly useful for agentic workflows in which an agent may need to invoke tools, interact with other systems, or perform several operations before producing its final result.
  • Transport: A2A separates the semantic interaction model from the underlying communication mechanism. The A2A ecosystem supports protocol bindings such as JSON-RPC, HTTP+JSON/REST, and gRPC. The Spring AI A2A example used in this article uses JSON-RPC-based communication.

1.2 A2A vs. MCP

A common question is how A2A relates to the Model Context Protocol (MCP). The protocols are complementary rather than direct replacements for one another.

AspectMCPA2A
Primary purposeConnect AI applications to tools and resourcesConnect independent AI agents
Typical interactionAgent → ToolAgent → Agent
Capability discoveryTools, resources, prompts, and server capabilitiesAgent capabilities and skills
ExampleAn agent queries a database through an MCP serverA travel agent delegates weather analysis to a weather agent

A real enterprise system can use both. For example, a Weather Agent might communicate with a coordinator through A2A, while the Weather Agent itself obtains information from external tools or enterprise systems through MCP.

1.3 Benefits of A2A With Spring AI

  • Interoperability: Agents do not need to use the same framework or programming language. A Spring AI agent can communicate with another A2A-compliant agent through a common protocol boundary.
  • Agent Discovery: Agent Cards provide machine-readable descriptions of agent capabilities, reducing the need to hard-code every remote agent’s functionality into the client.
  • Separation of Responsibilities: Each agent can focus on a particular domain, enabling applications to use smaller, specialized agents instead of relying on a single monolithic agent.
  • Independent Deployment: Specialized agents can be developed, versioned, and deployed independently, provided their external A2A contracts remain compatible with clients.
  • Spring Ecosystem Integration: Spring developers can combine A2A with familiar Spring Boot concepts and Spring AI abstractions such as ChatClient and tools, while the A2A layer manages communication with other agents.

2. Building an A2A Server and Client With Spring AI

To understand how A2A works with Spring AI A2A, let us create a simple example consisting of two applications: a Weather A2A Server, which uses Spring AI to expose a weather agent, and an A2A Client, which discovers the Weather Agent and sends requests to it using the A2A protocol. The example uses the Spring AI A2A community integration demonstrated with the A2A Java SDK 0.3.x generation, providing the necessary components for exposing Spring AI agents as A2A-compatible services and communicating with remote agents. Since the A2A ecosystem and its Java SDK continue to evolve, it is recommended to check the latest Spring AI A2A and A2A Java SDK documentation before selecting or upgrading dependency versions.

2.1 Maven Dependencies

For the server, add the Spring AI A2A server autoconfiguration dependency.

<dependency>
    <groupId>org.springaicommunity</groupId>
    <artifactId>spring-ai-a2a-server-autoconfigure</artifactId>
    <version>stable__jar__version</version>
</dependency>

You will also need the appropriate Spring AI model starter for the LLM provider used by your application. An application acting as an A2A client can add the A2A Java SDK client:

<dependency>
    <groupId>io.github.a2asdk</groupId>
    <artifactId>a2a-java-sdk-client</artifactId>
    <version>stable__jar__version</version>
</dependency>

2.2 Server Configuration

Configure the Spring Boot application by defining the server settings and the LLM that will power the A2A agent. In this example, the application runs on port 8080 with /weather as its context path and uses Anthropic Claude through Spring AI. The API key is obtained from the ANTHROPIC_API_KEY environment variable instead of being stored directly in the application’s source code.

# Server configuration
server.port=8080
server.servlet.context-path=/weather

# Anthropic configuration
spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}

# Claude model used by the agent
spring.ai.anthropic.chat.options.model=claude-sonnet-4-5-20250929

Before starting the application, set the ANTHROPIC_API_KEY environment variable with a valid Anthropic API key. Spring Boot resolves ${ANTHROPIC_API_KEY} from the environment at runtime, and Spring AI uses the configured Claude model when the A2A agent processes incoming requests. With the context path set to /weather, the A2A server endpoints are exposed under the application’s /weather path rather than directly at the server root.

export ANTHROPIC_API_KEY=your-anthropic-api-key

2.3 Creating a Spring AI Tool

To provide the Weather Agent with access to real-time weather information, we will create a Spring AI tool that integrates with the Open-Meteo API. The tool accepts a city name, resolves its geographic coordinates using the Open-Meteo Geocoding API, and then retrieves the current weather conditions for that location.

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.util.List;

@Service
public class WeatherTools {

    private final RestClient restClient;

    public WeatherTools(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.build();
    }

    @Tool(description = """
    Get the current weather for a city. Returns temperature,
    apparent temperature, humidity, precipitation and wind speed.
    """)
    public String getCurrentWeather(String city) {
        // Step 1: Convert the city name into latitude and longitude.
        GeocodingResponse geocodingResponse = restClient.get()
            .uri(uriBuilder -> uriBuilder
                .scheme("https")
                .host("geocoding-api.open-meteo.com")
                .path("/v1/search")
                .queryParam("name", city)
                .queryParam("count", 1)
                .queryParam("language", "en")
                .queryParam("format", "json")
                .build())
            .retrieve()
            .body(GeocodingResponse.class);

        if (geocodingResponse == null || geocodingResponse.results() == null || geocodingResponse.results().isEmpty()) {
            return "Unable to find weather information for " + city + ".";
        }

        Location location = geocodingResponse.results().get(0);

        // Step 2: Retrieve the current weather for those coordinates.
        WeatherResponse weatherResponse = restClient.get()
            .uri(uriBuilder -> uriBuilder
                .scheme("https")
                .host("api.open-meteo.com")
                .path("/v1/forecast")
                .queryParam("latitude", location.latitude())
                .queryParam("longitude", location.longitude())
                .queryParam(
                    "current",
                    "temperature_2m," +
                    "apparent_temperature," +
                    "relative_humidity_2m," +
                    "precipitation," +
                    "wind_speed_10m")
                .queryParam("timezone", "auto")
                .build())
            .retrieve()
            .body(WeatherResponse.class);

        if (weatherResponse == null || weatherResponse.current() == null) {
            return "Weather information is currently unavailable for " + location.name() + ".";
        }

        CurrentWeather current = weatherResponse.current();
        String resolvedLocation = location.name();

        if (location.country() != null) {
            resolvedLocation += ", " + location.country();
        }

        return ""
        "
        Current weather
        for % s:
            Temperature: % .1 f° C
        Feels like: % .1 f° C
        Relative humidity: % d % %
            Precipitation: % .1 f mm
        Wind speed: % .1 f km / h ""
        "
        .formatted(resolvedLocation, current.temperature(), current.apparentTemperature(), current.relativeHumidity(), current.precipitation(), current.windSpeed());
    }

    // Response returned by: https://geocoding-api.open-meteo.com/v1/search
    public record GeocodingResponse(List < Location > results) {}

    public record Location(String name, double latitude, double longitude, String country) {}

    // Response returned by: https://api.open-meteo.com/v1/forecast
    public record WeatherResponse(
        CurrentWeather current) {}

    public record CurrentWeather(

        @com.fasterxml.jackson.annotation.JsonProperty("temperature_2m") double temperature,

        @com.fasterxml.jackson.annotation.JsonProperty("apparent_temperature") double apparentTemperature,

        @com.fasterxml.jackson.annotation.JsonProperty("relative_humidity_2m") int relativeHumidity,

        double precipitation,

        @com.fasterxml.jackson.annotation.JsonProperty("wind_speed_10m") double windSpeed) {}
}

The WeatherTools class is registered as a Spring service, while the @Tool annotation exposes the getCurrentWeather() method to Spring AI so that the model can invoke it when weather information is required. The method first calls the Open-Meteo Geocoding API to translate the supplied city name into latitude and longitude coordinates and then uses those coordinates to call the Open-Meteo Forecast API for current weather data. Spring’s RestClient handles the HTTP requests, while Java records such as GeocodingResponse, Location, WeatherResponse, and CurrentWeather map the returned JSON data into Java objects. The @JsonProperty annotations map Open-Meteo fields such as temperature_2m and relative_humidity_2m to their corresponding Java record components. Finally, the retrieved temperature, apparent temperature, humidity, precipitation, and wind speed are formatted into a readable response that Spring AI can use when generating its answer to the user.

2.4 Defining the Agent Card

For other A2A clients to discover and understand our Weather Agent, the server needs to publish an Agent Card describing the agent and its capabilities. The following configuration creates the Agent Card for the Weather Agent, including its endpoint, supported input and output modes, protocol version, and the weather-related skill it provides.

import io.a2a.spec.AgentCapabilities;
import io.a2a.spec.AgentCard;
import io.a2a.spec.AgentSkill;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class WeatherAgentConfiguration {

    @Bean
    public AgentCard agentCard(
        @Value("${server.port:8080}") int port,
        @Value("${server.servlet.context-path:/}") String contextPath) {

        return new AgentCard.Builder()
            .name("Weather Agent")
            .description("Provides weather information for cities")
            .url("http://localhost:" + port + contextPath + "/")
            .version("1.0.0")
            .capabilities(
                new AgentCapabilities.Builder()
                .streaming(false)
                .build()
            )
            .defaultInputModes(List.of("text"))
            .defaultOutputModes(List.of("text"))
            .skills(List.of(
                new AgentSkill.Builder()
                .id("weather_search")
                .name("Weather Search")
                .description(
                    "Returns weather information for a city")
                .tags(List.of("weather", "temperature"))
                .examples(List.of(
                    "What is the weather in New Delhi?"
                ))
                .build()
            ))
            .protocolVersion("0.3.0")
            .build();
    }
}

The WeatherAgentConfiguration class defines the AgentCard as a Spring bean so that the A2A server can use it to advertise the Weather Agent to clients. The name and description fields identify the agent and describe its purpose, while url constructs the agent endpoint from the configured Spring Boot server port and context path. The version value represents the version of the Weather Agent itself, whereas protocolVersion identifies the A2A protocol version targeted by this example. The AgentCapabilities configuration declares that streaming is disabled, and defaultInputModes and defaultOutputModes specify that the agent exchanges text content. Finally, the weather_search skill advertises the weather functionality provided by the agent, together with descriptive tags and an example request that helps A2A clients understand how the skill can be used. Once registered, this metadata can be exposed through the A2A Agent Card discovery endpoint, allowing clients to inspect the Weather Agent before sending requests to it.

2.5 Connecting A2A to Spring AI ChatClient

After defining the Agent Card and the weather tool, the next step is to connect incoming A2A requests to Spring AI. The AgentExecutor acts as the bridge between the A2A server and Spring AI’s ChatClient, allowing messages received through the A2A protocol to be processed by the configured language model and its registered tools.

import io.a2a.server.agentexecution.AgentExecutor;

import org.springaicommunity.a2a.server.DefaultAgentExecutor;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WeatherAgentExecutorConfiguration {

    @Bean
    public AgentExecutor agentExecutor(
        ChatClient.Builder chatClientBuilder,
        WeatherTools weatherTools) {

        ChatClient chatClient = chatClientBuilder
            .clone()
            .defaultSystem("""
                You are a weather assistant.
                Use the available weather tool whenever the user asks
                for weather or temperature information.
                """)
            .defaultTools(weatherTools)
            .build();

        return new DefaultAgentExecutor(
            chatClient,
            (chat, requestContext) -> {
                String userMessage = DefaultAgentExecutor.extractTextFromMessage(requestContext.getMessage());
                return chat
                    .prompt()
                    .user(userMessage)
                    .call()
                    .content();
            });
    }
}

The WeatherAgentExecutorConfiguration class creates an AgentExecutor that connects the A2A request-processing layer with Spring AI. A dedicated ChatClient is created from the injected ChatClient.Builder, and the system instruction defines the model’s role as a weather assistant and directs it to use the available weather tool for weather-related requests. The defaultTools(weatherTools) call registers the previously created WeatherTools service with the ChatClient, enabling the model to invoke getCurrentWeather() when appropriate. The resulting ChatClient is passed to DefaultAgentExecutor, which receives the A2A request through the requestContext. The extractTextFromMessage() helper extracts the user’s text from the incoming A2A message, after which the text is supplied to the ChatClient through prompt().user(...). Spring AI then sends the request to the configured language model, allows the model to invoke the registered weather tool when required, and returns the generated content to DefaultAgentExecutor, which makes the result available to the A2A server for delivery back to the requesting client.

2.6 Creating an A2A Client

With the Weather A2A Server configured, the next step is to create a client that can discover the remote agent before communicating with it. The A2A Java SDK provides the A2A.getAgentCard() method for retrieving the Agent Card published by the server, allowing the client to inspect the Weather Agent’s metadata and capabilities.

import io.a2a.A2A;
import io.a2a.spec.AgentCard;

public class AgentDiscovery {

    public AgentCard discoverWeatherAgent() {

        String agentUrl = "http://localhost:8080/weather/";

        return A2A.getAgentCard(
            agentUrl,
            "/weather/.well-known/agent-card.json",
            null
        );
    }
}

The AgentDiscovery class is responsible for discovering the Weather Agent by retrieving its published AgentCard. The agentUrl variable identifies the base URL of the Weather A2A Server running on port 8080 with the /weather context path. The A2A.getAgentCard() method sends a request to the Agent Card discovery endpoint at /weather/.well-known/agent-card.json and converts the returned metadata into an AgentCard object. This object gives the client access to information advertised by the remote agent, such as its name, description, protocol version, supported input and output modes, capabilities, and available skills. The client can then use the discovered Agent Card when creating an A2A client and sending messages to the Weather Agent, keeping agent discovery separate from the actual message exchange.

2.7 Sending an A2A Message

After discovering the Weather Agent and obtaining its AgentCard, the client can communicate with it by creating and sending an A2A message. The following implementation creates a text-based user message, configures an A2A client to use the JSON-RPC transport, sends the request to the remote Weather Agent, and waits for the text response returned by the server.

import io.a2a.client.Client;
import io.a2a.client.ClientConfig;
import io.a2a.client.transport.jsonrpc.JSONRPCTransport;
import io.a2a.client.transport.jsonrpc.JSONRPCTransportConfig;

import io.a2a.spec.AgentCard;
import io.a2a.spec.Message;
import io.a2a.spec.TextPart;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class WeatherA2AClient {

    public String askWeatherAgent(AgentCard agentCard, String question) throws Exception {

        Message message = new Message.Builder()
            .role(Message.Role.USER)
            .parts(List.of(
                new TextPart(question, null)
            ))
            .build();

        CompletableFuture < String > response = new CompletableFuture < > ();

        Client client = Client.builder(agentCard)
            .clientConfig(
                new ClientConfig.Builder()
                .setAcceptedOutputModes(List.of("text"))
                .build()
            )
            .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig())
            .addConsumers(List.of(result -> {
                if (result instanceof TextPart textPart) {
                    response.complete(textPart.getText());
                }
            }))
            .build();

        client.sendMessage(message);

        return response.get(60, TimeUnit.SECONDS);
    }
}

The askWeatherAgent() method receives the previously discovered AgentCard together with the user’s question and converts the question into an A2A Message whose role is USER and whose content is represented by a TextPart. A CompletableFuture is used to capture the asynchronous response from the remote agent. The A2A Client is then created from the Agent Card, configured to accept text output, and instructed to communicate using JSONRPCTransport. The consumer registered through addConsumers() processes results received from the server and, when a TextPart is returned, extracts its text and completes the future. Calling client.sendMessage(message) sends the A2A request to the Weather Agent, where it is processed by the DefaultAgentExecutor, Spring AI ChatClient, and the registered WeatherTools when required. Finally, response.get(60, TimeUnit.SECONDS) waits for the result for up to 60 seconds and returns the Weather Agent’s text response to the calling application.

2.8 Code Run and Output

With the A2A server and client implementations complete, we can now run the applications and verify the complete agent-to-agent communication flow. Start the Weather A2A Server first so that its Agent Card and A2A endpoint are available, and then run the client application to discover the Weather Agent and send a weather-related request.

# Start the Weather A2A Server
mvn spring-boot:run

Once the server is running, its Agent Card can be retrieved from the discovery endpoint. Opening the following URL or invoking it with curl verifies that the Weather Agent is available for discovery.

curl http://localhost:8080/weather/.well-known/agent-card.json

Next, invoke the A2A client with a weather question such as What is the current weather in New Delhi?. The client retrieves the Agent Card, creates an A2A message, and sends it to the Weather Agent using JSON-RPC. The server passes the message to Spring AI’s ChatClient, which can invoke the WeatherTools implementation to obtain current weather data from Open-Meteo before generating the response.

Request:
What is the current weather in New Delhi?

Example Response:
Current weather for New Delhi, India:
Temperature: 31.4 °C
Feels like: 34.2 °C
Relative humidity: 58%
Precipitation: 0.0 mm
Wind speed: 10.8 km/h

The exact weather values in the response depend on the live data returned by Open-Meteo at the time the example is executed, so they should not be hard-coded in the expected output. This execution demonstrates the complete flow of the application: the client discovers the remote agent through its Agent Card, sends an A2A message over JSON-RPC, the A2A server delegates the request to Spring AI, the model invokes the registered weather tool when necessary, and the resulting real-time weather information is returned to the client as an A2A response.

3. Conclusion

The Agent2Agent (A2A) Protocol provides a standardized approach for enabling independent AI agents to discover each other, advertise their capabilities, exchange messages, and collaborate without exposing their internal implementation details. In this article, we explored the core concepts of A2A and demonstrated how it can be integrated with Spring AI by building a Weather A2A Server and an A2A Client. The server publishes its capabilities through an Agent Card, processes incoming A2A requests using Spring AI’s ChatClient, and uses a Spring AI tool to retrieve real-time weather information from Open-Meteo. On the client side, the A2A Java SDK is used to discover the Weather Agent, create an A2A message, and communicate with the remote agent using JSON-RPC. This approach allows Spring-based AI agents to participate in distributed multi-agent architectures while keeping individual agents independently developed and deployed. As the A2A ecosystem continues to evolve, it can provide an important interoperability layer for building applications in which specialized agents collaborate across different services, frameworks, and technology stacks.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button