diff --git a/include/a2a/server/rest_server_transport.h b/include/a2a/server/rest_server_transport.h index 06484a2..ac5305d 100644 --- a/include/a2a/server/rest_server_transport.h +++ b/include/a2a/server/rest_server_transport.h @@ -33,6 +33,7 @@ struct RestServerTransportOptions final { class RestServerTransport final { public: static constexpr std::string_view kAgentCardPath = "/.well-known/agent-card.json"; + static constexpr std::string_view kLegacyAgentCardPath = "/.well-known/agent.json"; RestServerTransport(Dispatcher* dispatcher, lf::a2a::v1::AgentCard agent_card, RestServerTransportOptions options = {}); diff --git a/src/server/rest_server_transport.cpp b/src/server/rest_server_transport.cpp index a34db65..2119162 100644 --- a/src/server/rest_server_transport.cpp +++ b/src/server/rest_server_transport.cpp @@ -1,10 +1,12 @@ #include "a2a/server/rest_server_transport.h" #include +#include #include #include #include "a2a/core/error.h" +#include "a2a/core/protocol_bindings.h" #include "a2a/core/protojson.h" #include "a2a/core/version.h" @@ -84,6 +86,30 @@ google::protobuf::Value* EnsureListField(google::protobuf::Struct* object, std:: return &value; } +std::optional FindInterfaceUrl(const google::protobuf::Struct& card, + std::string_view protocol_binding) { + const auto interfaces_it = card.fields().find("supportedInterfaces"); + if (interfaces_it == card.fields().end() || !interfaces_it->second.has_list_value()) { + return std::nullopt; + } + for (const auto& entry : interfaces_it->second.list_value().values()) { + if (!entry.has_struct_value()) { + continue; + } + const auto& fields = entry.struct_value().fields(); + const auto binding_it = fields.find("protocolBinding"); + const auto url_it = fields.find("url"); + if (binding_it == fields.end() || url_it == fields.end() || + !binding_it->second.has_string_value() || !url_it->second.has_string_value()) { + continue; + } + if (binding_it->second.string_value() == protocol_binding) { + return url_it->second.string_value(); + } + } + return std::nullopt; +} + HttpServerResponse BuildJsonErrorResponse(int status_code, const JsonError& error) { HttpServerResponse response; response.status_code = status_code; @@ -202,7 +228,7 @@ core::Result RestServerTransport::Handle( ? std::string_view(request.target) : std::string_view(request.target).substr(0, query_start); - if (path == kAgentCardPath) { + if (path == kAgentCardPath || path == kLegacyAgentCardPath) { return HandleAgentCard(request); } @@ -330,6 +356,43 @@ core::Result RestServerTransport::HandleAgentCard( EnsureListField(skill.mutable_struct_value(), "tags"); } } + + // Backward-compatible fields for A2A v0.3.0 transport discovery helpers. + if (fields->find("endpoint") == fields->end()) { + const auto jsonrpc_url = + FindInterfaceUrl(card, std::string(a2a::core::protocol_bindings::kJsonRpc)); + const auto rest_url = + FindInterfaceUrl(card, std::string(a2a::core::protocol_bindings::kHttpJson)); + if (jsonrpc_url.has_value()) { + (*fields)["endpoint"].set_string_value(jsonrpc_url.value()); + (*fields)["preferredTransport"].set_string_value("jsonrpc"); + } else if (rest_url.has_value()) { + (*fields)["endpoint"].set_string_value(rest_url.value()); + (*fields)["preferredTransport"].set_string_value("rest"); + } + } + + if (fields->find("additionalInterfaces") == fields->end()) { + google::protobuf::Value additional; + auto* interfaces = additional.mutable_list_value()->mutable_values(); + for (const auto& iface : agent_card_.supported_interfaces()) { + google::protobuf::Value interface_value; + auto* interface_fields = interface_value.mutable_struct_value()->mutable_fields(); + if (iface.protocol_binding() == a2a::core::protocol_bindings::kJsonRpc) { + (*interface_fields)["transport"].set_string_value("jsonrpc"); + } else if (iface.protocol_binding() == a2a::core::protocol_bindings::kHttpJson) { + (*interface_fields)["transport"].set_string_value("rest"); + } else if (iface.protocol_binding() == a2a::core::protocol_bindings::kGrpc) { + (*interface_fields)["transport"].set_string_value("grpc"); + } else { + continue; + } + (*interface_fields)["endpoint"].set_string_value(iface.url()); + interfaces->Add(std::move(interface_value)); + } + (*fields)["additionalInterfaces"] = std::move(additional); + } + const auto normalized = core::MessageToJson(card); if (!normalized.ok()) { return normalized.error(); diff --git a/tests/unit/rest_server_transport_test.cpp b/tests/unit/rest_server_transport_test.cpp index 512ac0d..3867f18 100644 --- a/tests/unit/rest_server_transport_test.cpp +++ b/tests/unit/rest_server_transport_test.cpp @@ -98,6 +98,44 @@ TEST(RestServerTransportTest, ServesAgentCardFromWellKnownEndpoint) { EXPECT_EQ(parsed.supported_interfaces(0).url(), "http://localhost:8080/a2a"); } +TEST(RestServerTransportTest, ServesAgentCardFromLegacyWellKnownEndpoint) { + EchoExecutor executor; + a2a::server::Dispatcher dispatcher(&executor); + a2a::server::RestServerTransport server(&dispatcher, BuildCard(), {.rest_api_base_path = "/a2a"}); + + const auto response = server.Handle({.method = "GET", + .target = "/.well-known/agent.json", + .headers = {}, + .body = {}, + .remote_address = {}}); + + ASSERT_TRUE(response.ok()); + EXPECT_EQ(response.value().status_code, 200); +} + +TEST(RestServerTransportTest, AddsBackwardCompatibleTransportFieldsToAgentCard) { + EchoExecutor executor; + a2a::server::Dispatcher dispatcher(&executor); + a2a::server::RestServerTransport server(&dispatcher, BuildCard(), {.rest_api_base_path = "/a2a"}); + + const auto response = server.Handle({.method = "GET", + .target = "/.well-known/agent-card.json", + .headers = {}, + .body = {}, + .remote_address = {}}); + + ASSERT_TRUE(response.ok()); + google::protobuf::Struct parsed; + ASSERT_TRUE(a2a::core::JsonToMessage(response.value().body, &parsed).ok()); + const auto& fields = parsed.fields(); + ASSERT_TRUE(fields.contains("endpoint")); + EXPECT_EQ(fields.at("endpoint").string_value(), "http://localhost:8080/a2a"); + ASSERT_TRUE(fields.contains("preferredTransport")); + EXPECT_EQ(fields.at("preferredTransport").string_value(), "rest"); + ASSERT_TRUE(fields.contains("additionalInterfaces")); + EXPECT_TRUE(fields.at("additionalInterfaces").has_list_value()); +} + TEST(RestServerTransportTest, RoutesRequestUsingConfiguredBasePath) { EchoExecutor executor; a2a::server::Dispatcher dispatcher(&executor);