diff --git a/pyproject.toml b/pyproject.toml index 25825b21a..be8c98ef2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ - "adcp==2.12.0", # Official AdCP Python client for external agent communication and adagents.json validation + "adcp==2.12.1", # Official AdCP Python client for external agent communication and adagents.json validation "fastmcp>=2.13.0", # Required for context.get_http_request() support "google-generativeai>=0.5.4", "google-cloud-iam>=2.19.1", diff --git a/src/a2a_server/README.md b/src/a2a_server/README.md index a92978021..2bd3f2915 100644 --- a/src/a2a_server/README.md +++ b/src/a2a_server/README.md @@ -101,6 +101,31 @@ Automatically fetches and parses Agent Cards to: - Get correct RPC endpoint URLs - Display agent metadata and descriptions +### AdCP 2.5 Extension Support + +The agent card includes the AdCP extension (per AdCP 2.5 spec) in `capabilities.extensions`: + +```json +{ + "capabilities": { + "extensions": [ + { + "uri": "https://adcontextprotocol.org/schemas/2.5.0/protocols/adcp-extension.json", + "description": "AdCP protocol version and supported domains", + "params": { + "adcp_version": "2.5.0", + "protocols_supported": ["media_buy"] + } + } + ] + } +} +``` + +This extension declares: +- **adcp_version**: The AdCP specification version implemented by this agent (currently "2.5.0") +- **protocols_supported**: Which AdCP protocol domains are supported (currently only "media_buy") + ### Skill Invocation Patterns (AdCP PR #48) The server supports two invocation patterns for maximum flexibility: diff --git a/src/a2a_server/adcp_a2a_server.py b/src/a2a_server/adcp_a2a_server.py index 8d5c35c63..5259a1f35 100644 --- a/src/a2a_server/adcp_a2a_server.py +++ b/src/a2a_server/adcp_a2a_server.py @@ -30,6 +30,7 @@ from a2a.server.request_handlers.request_handler import RequestHandler from a2a.types import ( AgentCard, + AgentExtension, Artifact, DataPart, InternalError, @@ -2200,6 +2201,20 @@ def create_agent_card() -> AgentCard: server_url = get_a2a_server_url() from a2a.types import AgentCapabilities, AgentSkill + from adcp import get_adcp_version + + # Create AdCP extension (AdCP 2.5 spec) + # As of adcp 2.12.1, get_adcp_version() returns the protocol version (e.g., "2.5.0") + # Previously it returned the schema version (e.g., "v1"), but this was fixed upstream + protocol_version = get_adcp_version() + adcp_extension = AgentExtension( + uri=f"https://adcontextprotocol.org/schemas/{protocol_version}/protocols/adcp-extension.json", + description="AdCP protocol version and supported domains", + params={ + "adcp_version": protocol_version, + "protocols_supported": ["media_buy"], # Only media_buy protocol is currently supported + }, + ) # Create the agent card with minimal required fields agent_card = AgentCard( @@ -2207,7 +2222,10 @@ def create_agent_card() -> AgentCard: description="AI agent for programmatic advertising campaigns via AdCP protocol", version="1.0.0", protocol_version="1.0", - capabilities=AgentCapabilities(push_notifications=True), + capabilities=AgentCapabilities( + push_notifications=True, + extensions=[adcp_extension], + ), default_input_modes=["message"], default_output_modes=["message"], skills=[ diff --git a/tests/e2e/test_a2a_endpoints_working.py b/tests/e2e/test_a2a_endpoints_working.py index c5a8103a8..1d4eb0d5f 100644 --- a/tests/e2e/test_a2a_endpoints_working.py +++ b/tests/e2e/test_a2a_endpoints_working.py @@ -55,6 +55,24 @@ def test_well_known_agent_json_endpoint_live(self): # Note: A2A spec uses security/securitySchemes instead of simple authentication field assert "security" in data or "securitySchemes" in data + # AdCP 2.5: Should have AdCP extension in capabilities + assert "capabilities" in data + assert "extensions" in data["capabilities"] + extensions = data["capabilities"]["extensions"] + assert isinstance(extensions, list) + assert len(extensions) > 0 + + # Find AdCP extension + adcp_ext = None + for ext in extensions: + if "adcp-extension" in ext.get("uri", ""): + adcp_ext = ext + break + + assert adcp_ext is not None, "AdCP extension not found in live agent card" + assert adcp_ext["params"]["adcp_version"] == "2.5.0" + assert "media_buy" in adcp_ext["params"]["protocols_supported"] + except (requests.ConnectionError, requests.Timeout): pytest.skip("A2A server not running on localhost:8091") @@ -157,6 +175,43 @@ def test_create_agent_card_function(self): assert hasattr(skill, "name") assert hasattr(skill, "description") + def test_agent_card_adcp_extension(self): + """Test that agent card includes AdCP 2.5 extension.""" + from src.a2a_server.adcp_a2a_server import create_agent_card + + agent_card = create_agent_card() + + # Check capabilities has extensions + assert hasattr(agent_card, "capabilities") + assert agent_card.capabilities is not None + assert hasattr(agent_card.capabilities, "extensions") + assert agent_card.capabilities.extensions is not None + assert len(agent_card.capabilities.extensions) > 0 + + # Find AdCP extension + adcp_ext = None + for ext in agent_card.capabilities.extensions: + if "adcp-extension" in ext.uri: + adcp_ext = ext + break + + assert adcp_ext is not None, "AdCP extension not found in capabilities.extensions" + + # Validate AdCP extension structure + assert adcp_ext.uri == "https://adcontextprotocol.org/schemas/2.5.0/protocols/adcp-extension.json" + assert adcp_ext.params is not None + assert "adcp_version" in adcp_ext.params + assert "protocols_supported" in adcp_ext.params + + # Validate AdCP extension values + assert adcp_ext.params["adcp_version"] == "2.5.0" + protocols = adcp_ext.params["protocols_supported"] + assert isinstance(protocols, list) + assert len(protocols) >= 1 + # Currently only media_buy protocol is supported + assert "media_buy" in protocols + assert set(protocols) == {"media_buy"}, "Only media_buy protocol is currently supported" + def test_agent_card_skills_coverage(self): """Test that agent card includes expected AdCP skills.""" from src.a2a_server.adcp_a2a_server import create_agent_card diff --git a/uv.lock b/uv.lock index e342703b8..a9c301ef6 100644 --- a/uv.lock +++ b/uv.lock @@ -62,7 +62,7 @@ http-server = [ [[package]] name = "adcp" -version = "2.12.0" +version = "2.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "a2a-sdk" }, @@ -72,9 +72,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/e5/c0582bcc0b0bf0caf30ca32c5124710c7091934efbac4b36d8983869a27b/adcp-2.12.0.tar.gz", hash = "sha256:b2b5035ffc013c4f9e82115f076538a5528a4fc1f6f5435702b4e2562370f8cf", size = 153689, upload-time = "2025-11-22T21:56:24.488Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/cb/58df8fb99dc448751b156712745b713384dcf822f68e54e9aed6dbb8bbe2/adcp-2.12.1.tar.gz", hash = "sha256:a385c4987b713bdbadf7527797962ea685981770f11931aac376785d60ef3502", size = 153710, upload-time = "2025-11-24T11:23:36.059Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/e3/3f4293096d5a8372ccc820f345635d87770e608ee20ac1e966dd246e76f3/adcp-2.12.0-py3-none-any.whl", hash = "sha256:9ed5a14ae52d9150177af676713f0caf4269b08d4f8d2783e2b17c00bb2575ae", size = 190486, upload-time = "2025-11-22T21:56:22.815Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/93b7d0e2b80ccf06777a0f812d2c652712809b21582fea0579adb529d9bf/adcp-2.12.1-py3-none-any.whl", hash = "sha256:51165f9c6b0d86d08e21dce73f28c976294c043c3021d1c1aa386797fe9e082c", size = 190435, upload-time = "2025-11-24T11:23:34.397Z" }, ] [[package]] @@ -165,7 +165,7 @@ dev = [ requires-dist = [ { name = "a2a-cli", specifier = ">=0.2.0" }, { name = "a2a-sdk", extras = ["http-server"], specifier = ">=0.3.10" }, - { name = "adcp", specifier = "==2.12.0" }, + { name = "adcp", specifier = "==2.12.1" }, { name = "aiohttp", specifier = ">=3.9.0" }, { name = "alembic", specifier = ">=1.13.0" }, { name = "allure-pytest", marker = "extra == 'ui-tests'", specifier = "==2.13.5" },