Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/a2a/server/rest_server_transport.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {});
Expand Down
65 changes: 64 additions & 1 deletion src/server/rest_server_transport.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#include "a2a/server/rest_server_transport.h"

#include <cctype>
#include <optional>
#include <string>
#include <string_view>

#include "a2a/core/error.h"
#include "a2a/core/protocol_bindings.h"
#include "a2a/core/protojson.h"
#include "a2a/core/version.h"

Expand Down Expand Up @@ -84,6 +86,30 @@ google::protobuf::Value* EnsureListField(google::protobuf::Struct* object, std::
return &value;
}

std::optional<std::string> 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;
Expand Down Expand Up @@ -202,7 +228,7 @@ core::Result<HttpServerResponse> 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);
}

Expand Down Expand Up @@ -330,6 +356,43 @@ core::Result<HttpServerResponse> 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();
Expand Down
38 changes: 38 additions & 0 deletions tests/unit/rest_server_transport_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading