From 9ec5f92c13128eac5945091cb429a8d3c78f2d09 Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Wed, 13 May 2026 14:18:10 +0200 Subject: [PATCH] feat: add url and preferredTransport fields to AgentCard for v0.3 compatibility Signed-off-by: Emmanuel Hugonnet --- .../http/A2ACardResolver_v0_3_Test.java | 33 ++++++++++++ .../client/http/JsonMessages_v0_3.java | 50 +++++++++++++++++++ .../sdk/client/http/A2ACardResolver.java | 2 +- .../sdk/grpc/mapper/AgentCardMapper.java | 2 + .../sdk/grpc/utils/JSONRPCUtils.java | 7 ++- .../org/a2aproject/sdk/spec/AgentCard.java | 50 +++++++++++++------ .../server/apps/common/AgentCardProducer.java | 4 +- 7 files changed, 129 insertions(+), 19 deletions(-) diff --git a/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java b/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java index 658b0f49d..1b6e2736e 100644 --- a/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java +++ b/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; @@ -96,6 +97,38 @@ public void testGetAgentCardJsonDecodeError() throws Exception { } + @Test + public void testParseV1AgentCardWithUrlAndPreferredTransport() throws Exception { + TestHttpClient client = new TestHttpClient(); + client.body = JsonMessages_v0_3.V1_AGENT_CARD; + + A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(client, "http://example.com/"); + AgentCard_v0_3 card = resolver.getAgentCard(); + + assertEquals("Test Agent", card.name()); + assertEquals("A test agent", card.description()); + assertEquals("1.0.0", card.version()); + assertEquals("https://agent.example.com/a2a", card.url()); + assertEquals("JSONRPC", card.preferredTransport()); + assertTrue(card.capabilities().streaming()); + } + + @Test + public void testParseV1AgentCardDefaultsPreferredTransportWhenAbsent() throws Exception { + TestHttpClient client = new TestHttpClient(); + client.body = JsonMessages_v0_3.V1_AGENT_CARD_NO_PREFERRED_TRANSPORT; + + A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(client, "http://example.com/"); + AgentCard_v0_3 card = resolver.getAgentCard(); + + assertEquals("Minimal Agent", card.name()); + assertEquals("https://agent.example.com/a2a", card.url()); + // v0.3 compact constructor defaults null preferredTransport to "JSONRPC" + assertEquals("JSONRPC", card.preferredTransport()); + // v1.0-only fields such as supportedInterfaces are unknown to v0.3 and must be ignored + assertNull(card.additionalInterfaces()); + } + @Test public void testGetAgentCardRequestError() throws Exception { TestHttpClient client = new TestHttpClient(); diff --git a/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/JsonMessages_v0_3.java b/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/JsonMessages_v0_3.java index 54ea3f41e..9b9a4ec0c 100644 --- a/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/JsonMessages_v0_3.java +++ b/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/JsonMessages_v0_3.java @@ -83,6 +83,56 @@ public class JsonMessages_v0_3 { ] }"""; + /** + * A v1.0 format agent card returned by a current server. Contains the new {@code supportedInterfaces} + * and {@code securityRequirements} fields (v1.0-only), plus the backward-compat {@code url} and + * {@code preferredTransport} fields that the v0.3 client relies on. + */ + static final String V1_AGENT_CARD = """ + { + "name": "Test Agent", + "description": "A test agent", + "url": "https://agent.example.com/a2a", + "preferredTransport": "JSONRPC", + "version": "1.0.0", + "supportedInterfaces": [ + {"protocolBinding": "JSONRPC", "url": "https://agent.example.com/a2a"}, + {"protocolBinding": "GRPC", "url": "grpc://agent.example.com:9090"} + ], + "capabilities": { + "streaming": true, + "pushNotifications": false, + "stateTransitionHistory": false + }, + "defaultInputModes": ["text/plain"], + "defaultOutputModes": ["text/plain"], + "skills": [], + "securityRequirements": [{"oauth2": ["read"]}] + }"""; + + /** + * A v1.0 format agent card without an explicit {@code preferredTransport}. The v0.3 client must + * fall back to the default transport ("JSONRPC"). + */ + static final String V1_AGENT_CARD_NO_PREFERRED_TRANSPORT = """ + { + "name": "Minimal Agent", + "description": "Minimal agent card", + "url": "https://agent.example.com/a2a", + "version": "2.0.0", + "supportedInterfaces": [ + {"protocolBinding": "JSONRPC", "url": "https://agent.example.com/a2a"} + ], + "capabilities": { + "streaming": false, + "pushNotifications": false, + "stateTransitionHistory": false + }, + "defaultInputModes": ["text/plain"], + "defaultOutputModes": ["text/plain"], + "skills": [] + }"""; + static final String AUTHENTICATION_EXTENDED_AGENT_CARD = """ { "name": "GeoSpatial Route Planner Agent Extended", diff --git a/http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java b/http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java index d1edf49b8..e8ee10397 100644 --- a/http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java +++ b/http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java @@ -184,7 +184,7 @@ public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError { try { org.a2aproject.sdk.grpc.AgentCard.Builder agentCardBuilder = org.a2aproject.sdk.grpc.AgentCard.newBuilder(); - JSONRPCUtils.parseJsonString(body, agentCardBuilder, ""); + JSONRPCUtils.parseJsonString(body, agentCardBuilder, "", true); return ProtoUtils.FromProto.agentCard(agentCardBuilder); } catch (A2AError | JsonProcessingException e) { throw new A2AClientJSONError("Could not unmarshal agent card response", e); diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCardMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCardMapper.java index df9be2b6d..565d6255c 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCardMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCardMapper.java @@ -30,5 +30,7 @@ public interface AgentCardMapper { @Mapping(target = "provider", source = "provider", conditionExpression = "java(proto.hasProvider())") @Mapping(target = "documentationUrl", source = "documentationUrl", conditionExpression = "java(!proto.getDocumentationUrl().isEmpty())") @Mapping(target = "iconUrl", source = "iconUrl", conditionExpression = "java(!proto.getIconUrl().isEmpty())") + @Mapping(target = "url", ignore = true) + @Mapping(target = "preferredTransport", ignore = true) org.a2aproject.sdk.spec.AgentCard fromProto(org.a2aproject.sdk.grpc.AgentCard proto); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/JSONRPCUtils.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/JSONRPCUtils.java index 966c998cc..7af14fa5c 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/JSONRPCUtils.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/JSONRPCUtils.java @@ -439,8 +439,13 @@ protected static void parseRequestBody(JsonElement jsonRpc, com.google.protobuf. } public static void parseJsonString(String body, com.google.protobuf.Message.Builder builder, Object id) throws JsonProcessingException { + parseJsonString(body, builder, id, false); + } + + public static void parseJsonString(String body, com.google.protobuf.Message.Builder builder, Object id, boolean ignoringUnknownFields) throws JsonProcessingException { try { - JsonFormat.parser().merge(body, builder); + JsonFormat.Parser parser = ignoringUnknownFields ? JsonFormat.parser().ignoringUnknownFields() : JsonFormat.parser(); + parser.merge(body, builder); } catch (InvalidProtocolBufferException e) { log.log(Level.FINE, "Protocol buffer parsing failed for JSON: {0}", body); log.log(Level.FINE, "Proto parsing error details", e); diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java b/spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java index af9a9ce2b..b3aaa2275 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java @@ -56,7 +56,9 @@ public record AgentCard( @Nullable List securityRequirements, @Nullable String iconUrl, List supportedInterfaces, - @Nullable List signatures) { + @Nullable List signatures, + @Nullable String url, + @Nullable String preferredTransport) { /** * Compact constructor that validates required fields. @@ -156,6 +158,8 @@ public static class Builder { private @Nullable String iconUrl; private @Nullable List supportedInterfaces; private @Nullable List signatures; + private @Nullable String url; + private @Nullable String preferredTransport; /** * Creates a new Builder with all fields unset. @@ -172,20 +176,22 @@ private Builder() { * @param card the AgentCard to copy values from */ private Builder(AgentCard card) { - this.name = card.name; - this.description = card.description; - this.provider = card.provider; - this.version = card.version; - this.documentationUrl = card.documentationUrl; - this.capabilities = card.capabilities; - this.defaultInputModes = card.defaultInputModes != null ? new ArrayList<>(card.defaultInputModes) : Collections.emptyList(); - this.defaultOutputModes = card.defaultOutputModes != null ? new ArrayList<>(card.defaultOutputModes) : Collections.emptyList(); - this.skills = card.skills != null ? new ArrayList<>(card.skills) : Collections.emptyList(); - this.securitySchemes = card.securitySchemes != null ? Map.copyOf(card.securitySchemes) : Collections.emptyMap(); - this.securityRequirements = card.securityRequirements != null ? new ArrayList<>(card.securityRequirements) : Collections.emptyList(); - this.iconUrl = card.iconUrl; - this.supportedInterfaces = card.supportedInterfaces != null ? new ArrayList<>(card.supportedInterfaces) : Collections.emptyList(); - this.signatures = card.signatures != null ? new ArrayList<>(card.signatures) : null; + this.name = card.name(); + this.description = card.description(); + this.provider = card.provider(); + this.version = card.version(); + this.documentationUrl = card.documentationUrl(); + this.capabilities = card.capabilities(); + this.defaultInputModes = card.defaultInputModes() != null ? new ArrayList<>(card.defaultInputModes()) : Collections.emptyList(); + this.defaultOutputModes = card.defaultOutputModes() != null ? new ArrayList<>(card.defaultOutputModes()) : Collections.emptyList(); + this.skills = card.skills() != null ? new ArrayList<>(card.skills()) : Collections.emptyList(); + this.securitySchemes = card.securitySchemes() != null ? Map.copyOf(card.securitySchemes()) : Collections.emptyMap(); + this.securityRequirements = card.securityRequirements() != null ? new ArrayList<>(card.securityRequirements()) : Collections.emptyList(); + this.iconUrl = card.iconUrl(); + this.supportedInterfaces = card.supportedInterfaces() != null ? new ArrayList<>(card.supportedInterfaces()) : Collections.emptyList(); + this.signatures = card.signatures() != null ? new ArrayList<>(card.signatures()) : null; + this.url = card.url(); + this.preferredTransport= card.preferredTransport(); } /** @@ -376,6 +382,16 @@ public Builder signatures(List signatures) { return this; } + public Builder preferredTransport(String preferredTransport) { + this.preferredTransport = preferredTransport; + return this; + } + + public Builder url(String url) { + this.url = url; + return this; + } + /** * Builds an immutable {@link AgentCard} from the current builder state. *

@@ -399,7 +415,9 @@ public AgentCard build() { securityRequirements, iconUrl, Assert.checkNotNullParam("supportedInterfaces", supportedInterfaces), - signatures); + signatures, + url, + preferredTransport == null ? "JSONRPC" : preferredTransport); } } } diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentCardProducer.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentCardProducer.java index fa90cbfd8..d6d76dcca 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentCardProducer.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentCardProducer.java @@ -58,7 +58,9 @@ public AgentCard agentCard() { .defaultInputModes(Collections.singletonList("text")) .defaultOutputModes(Collections.singletonList("text")) .skills(new ArrayList<>()) - .supportedInterfaces(Collections.singletonList(new AgentInterface(preferredTransport, transportUrl))); + .supportedInterfaces(Collections.singletonList(new AgentInterface(preferredTransport, transportUrl))) + .url(transportUrl) + .preferredTransport(preferredTransport); // Add security configuration if enabled (for authentication tests) if (securityEnabled) {