A2A protocol integration for Embabel Agent Framework enabling agent-to-agent communication
Automatic web endpoint registration for A2A communication at application startup.
Component that dynamically registers web endpoints for AgentCardHandler beans.
/**
* Registers A2A endpoints at application startup.
* Creates GET and POST mappings for each AgentCardHandler bean.
*
* @param agentCardHandlers List of all AgentCardHandler beans
* @param requestMappingHandlerMapping Spring MVC request mapping
* @param objectMapper Jackson ObjectMapper for JSON
*/
@Component
class A2AEndpointRegistrar(
private val agentCardHandlers: List<AgentCardHandler>,
private val requestMappingHandlerMapping: RequestMappingHandlerMapping,
private val objectMapper: ObjectMapper
) {
/**
* Triggered when Spring application context fully initialized.
* Registers web endpoints for all AgentCardHandler beans.
*/
@EventListener
fun onApplicationReady(event: ApplicationReadyEvent)
}For each AgentCardHandler with path $path, two endpoints are created:
URL Pattern: /$path/.well-known/agent.json
HTTP Method: GET
Content-Type: application/json
Purpose: Returns A2A AgentCard for agent discovery
Example:
GET http://localhost:8080/a2a/.well-known/agent.jsonResponse:
{
"name": "MyAgent",
"description": "Agent description",
"url": "http://localhost:8080/a2a",
"provider": {
"name": "Embabel",
"url": "https://embabel.com"
},
"skills": [...],
"capabilities": {
"streaming": true,
"pushNotifications": false
},
"protocolVersion": "0.3.0"
}URL Pattern: /$path
HTTP Method: POST
Content-Type: application/json
Produces: application/json or text/event-stream
Purpose: Handles JSON-RPC requests (streaming and non-streaming)
Example:
POST http://localhost:8080/a2a
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": "msg-123",
"method": "message/send",
"params": {
"message": {
"messageId": "m1",
"role": "user",
"parts": [{"text": "Find news for Scorpios"}]
}
}
}Spring Boot Application Starts
ApplicationReadyEvent Fired
Endpoint Registration
.well-known/agent.jsonEndpoints Active
POST endpoint dynamically routes based on method:
Returns SseEmitter:
message/stream → handleJsonRpcStream(SendStreamingMessageRequest)task/resubscribe → handleCustomStreamingRequest(TaskResubscriptionRequest)Returns JSONRPCResponse:
message/send → handleJsonRpc(SendMessageRequest)task/get → handleJsonRpc(GetTaskRequest)task/cancel → handleJsonRpc(CancelTaskRequest)HTTP POST /$path
↓
AgentCardHandlerWebFacade.handleJsonRpc(requestMap)
↓
Parse method field
↓
├─ If "message/stream" or "task/resubscribe":
│ ↓
│ Convert to streaming request type
│ ↓
│ Call agentCardHandler.handleJsonRpcStream(request)
│ ↓
│ Return SseEmitter (Spring handles SSE)
│
└─ Else (non-streaming):
↓
Convert to request type
↓
Call agentCardHandler.handleJsonRpc(request)
↓
Wrap in ResponseEntity<JSONRPCResponse>
↓
Return response@Configuration
class MultiHandlerConfig {
@Bean
fun publicHandler(...): AgentCardHandler =
EmbabelServerGoalsAgentCardHandler(path = "public", ...)
@Bean
fun privateHandler(...): AgentCardHandler =
EmbabelServerGoalsAgentCardHandler(path = "private", ...)
}
// Registered endpoints:
// GET /public/.well-known/agent.json
// POST /public
// GET /private/.well-known/agent.json
// POST /privatecatch (e: Exception) {
logger.error("Failed to deserialize request", e)
ResponseEntity.status(500)
.contentType(MediaType.APPLICATION_JSON)
.body(
JSONRPCErrorResponse(
requestId,
JSONRPCError(500, "Internal server error: ${e.message}", null)
)
)
}else -> {
logger.warn("Unsupported method: {}", method)
throw UnsupportedOperationException("Method ${method} is not supported")
}Registrar logs detailed information:
logger.info("Registering ${agentCardHandlers.size} A2A endpoints")
logger.info(
"Registering web endpoint under {} for {}",
endpointPath,
agentCardHandler.infoString(verbose = true)
)Example Log Output:
INFO c.e.a.a.s.A2AEndpointRegistrar - Registering 2 A2A endpoints
INFO c.e.a.a.s.A2AEndpointRegistrar - Registering web endpoint under /a2a-public/.well-known/agent.json for EmbabelServerGoalsAgentCardHandler(path='a2a-public')
INFO c.e.a.a.s.A2AEndpointRegistrar - Registering web endpoint under /a2a-internal/.well-known/agent.json for EmbabelServerGoalsAgentCardHandler(path='a2a-internal')curl http://localhost:8080/a2a/.well-known/agent.jsoncurl -X POST http://localhost:8080/a2a \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "test-1",
"method": "message/send",
"params": {
"message": {
"messageId": "m1",
"role": "user",
"parts": [{"text": "Hello"}]
}
}
}'curl -X POST http://localhost:8080/a2a \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "stream-1",
"method": "message/stream",
"params": {
"message": {
"messageId": "m2",
"role": "user",
"parts": [{"text": "Research topic"}]
}
}
}'Uses Spring MVC's RequestMappingHandlerMapping for dynamic registration:
val mapping = RequestMappingInfo.paths(endpointPath)
.methods(RequestMethod.GET)
.produces(MediaType.APPLICATION_JSON_VALUE)
.build()
requestMappingHandlerMapping.registerMapping(
mapping,
facade,
facade::class.java.getMethod("agentCard", ServletRequest::class.java)
)Benefits:
Handler paths are relative to servlet context:
path = "a2a" → /a2a
path = "api/v1/agent" → /api/v1/agentIf application uses context path:
server:
servlet:
context-path: /appEndpoints become:
/app/a2a/.well-known/agent.json/app/a2atessl i tessl/maven-com-embabel-agent--embabel-agent-a2a@0.3.3