Skip to content

Commit 869241a

Browse files
IngmarVG-IBclaude
andcommitted
feat: add DNS-AID integration for agent discovery via DNS
Add native DNS-AID tools that enable AI agents to discover, publish, and unpublish other agents using DNS SVCB records (IETF draft-mozleywilliams-dnsop-dnsaid-01). DNS-AID provides decentralized agent discovery without centralized registries, using existing DNS infrastructure. Requires: dns-aid>=0.12.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 22fc332 commit 869241a

2 files changed

Lines changed: 186 additions & 0 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Bridge between DNS-AID discovered agents and Google ADK A2A.
2+
3+
When DNS-AID discovers an agent published with protocol='a2a', the agent's
4+
endpoint serves an A2A Agent Card at /.well-known/agent-card.json.
5+
This module provides utilities to convert those records to ADK-compatible refs.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from typing import Any
11+
12+
13+
async def a2a_agent_from_record(agent_record: Any) -> dict[str, Any]:
14+
"""Convert a DNS-AID AgentRecord with A2A protocol to an ADK-compatible reference.
15+
16+
Args:
17+
agent_record: An AgentRecord from dns_aid.discover() with protocol=a2a.
18+
19+
Returns:
20+
Dictionary with agent metadata suitable for ADK RemoteAgent construction.
21+
22+
Raises:
23+
ValueError: If the agent record is not using A2A protocol.
24+
"""
25+
from dns_aid.core.models import Protocol
26+
27+
if agent_record.protocol != Protocol.A2A:
28+
raise ValueError(
29+
f"Agent {agent_record.name} uses protocol {agent_record.protocol}, not A2A"
30+
)
31+
32+
base_url = agent_record.endpoint_url.rstrip("/")
33+
return {
34+
"name": agent_record.name,
35+
"url": agent_record.endpoint_url,
36+
"capabilities": agent_record.capabilities or [],
37+
"description": agent_record.description,
38+
"protocol": "a2a",
39+
"a2a_card_url": f"{base_url}/.well-known/agent-card.json",
40+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""DNS-AID tools for Google ADK.
2+
3+
Google ADK wraps plain async functions as FunctionTool objects.
4+
Tool descriptions are derived from docstrings.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import json
10+
from typing import Any, Optional
11+
12+
13+
def _resolve_backend(backend_name: str | None) -> Any:
14+
"""Resolve a DNS backend by name, or return None."""
15+
if not backend_name:
16+
return None
17+
from dns_aid.backends import create_backend
18+
19+
return create_backend(backend_name)
20+
21+
22+
async def discover_agents(
23+
domain: str,
24+
protocol: Optional[str] = None,
25+
name: Optional[str] = None,
26+
require_dnssec: bool = False,
27+
) -> str:
28+
"""Discover AI agents at a domain via DNS-AID SVCB records.
29+
30+
Queries DNS to find published agents, optionally filtering by protocol or name.
31+
Returns JSON with agent names, endpoints, capabilities, and protocols.
32+
33+
Args:
34+
domain: Domain to search for agents (e.g. 'agents.example.com').
35+
protocol: Filter by protocol: 'a2a', 'mcp', 'https', or None for all.
36+
name: Filter by specific agent name.
37+
require_dnssec: Require DNSSEC-validated responses.
38+
"""
39+
import dns_aid
40+
41+
result = await dns_aid.discover(
42+
domain=domain, protocol=protocol, name=name, require_dnssec=require_dnssec
43+
)
44+
return json.dumps(result.model_dump(), default=str)
45+
46+
47+
async def publish_agent(
48+
agent_name: str,
49+
domain: str,
50+
protocol: str = "mcp",
51+
endpoint: str = "",
52+
port: int = 443,
53+
capabilities: Optional[list[str]] = None,
54+
version: str = "1.0.0",
55+
description: Optional[str] = None,
56+
ttl: int = 3600,
57+
backend_name: Optional[str] = None,
58+
) -> str:
59+
"""Publish an AI agent to DNS using DNS-AID protocol.
60+
61+
Creates SVCB and TXT records so the agent becomes discoverable.
62+
63+
Args:
64+
agent_name: Agent identifier in DNS label format.
65+
domain: Domain to publish under.
66+
protocol: Protocol ('a2a', 'mcp', 'https').
67+
endpoint: Hostname where the agent is reachable.
68+
port: Port number.
69+
capabilities: List of agent capabilities.
70+
version: Agent version.
71+
description: Human-readable description.
72+
ttl: DNS TTL in seconds.
73+
backend_name: DNS backend name (e.g. 'route53', 'cloudflare').
74+
"""
75+
import dns_aid
76+
77+
result = await dns_aid.publish(
78+
name=agent_name,
79+
domain=domain,
80+
protocol=protocol,
81+
endpoint=endpoint,
82+
port=port,
83+
capabilities=capabilities,
84+
version=version,
85+
description=description,
86+
ttl=ttl,
87+
backend=_resolve_backend(backend_name),
88+
)
89+
return json.dumps(result.model_dump(), default=str)
90+
91+
92+
async def unpublish_agent(
93+
agent_name: str,
94+
domain: str,
95+
protocol: str = "mcp",
96+
backend_name: Optional[str] = None,
97+
) -> str:
98+
"""Remove an AI agent's DNS-AID records.
99+
100+
Args:
101+
agent_name: Agent identifier to remove.
102+
domain: Domain the agent is published under.
103+
protocol: Protocol.
104+
backend_name: DNS backend name.
105+
"""
106+
import dns_aid
107+
108+
deleted = await dns_aid.unpublish(
109+
name=agent_name, domain=domain, protocol=protocol, backend=_resolve_backend(backend_name)
110+
)
111+
if deleted:
112+
return json.dumps(
113+
{"success": True, "message": f"Agent '{agent_name}' unpublished from {domain}"}
114+
)
115+
return json.dumps(
116+
{"success": False, "message": f"Agent '{agent_name}' not found at {domain}"}
117+
)
118+
119+
120+
def get_dns_aid_tools(backend_name: Optional[str] = None) -> list:
121+
"""Return DNS-AID tools wrapped as Google ADK FunctionTool objects.
122+
123+
Args:
124+
backend_name: Optional DNS backend for publish/unpublish operations.
125+
"""
126+
from google.adk.tools import FunctionTool
127+
128+
tools = [FunctionTool(discover_agents)]
129+
if backend_name:
130+
# Create closures that bind the backend_name
131+
async def _publish(**kwargs): # type: ignore[no-untyped-def]
132+
return await publish_agent(backend_name=backend_name, **kwargs)
133+
134+
async def _unpublish(**kwargs): # type: ignore[no-untyped-def]
135+
return await unpublish_agent(backend_name=backend_name, **kwargs)
136+
137+
_publish.__name__ = "publish_agent"
138+
_publish.__doc__ = publish_agent.__doc__
139+
_unpublish.__name__ = "unpublish_agent"
140+
_unpublish.__doc__ = unpublish_agent.__doc__
141+
tools.append(FunctionTool(_publish))
142+
tools.append(FunctionTool(_unpublish))
143+
else:
144+
tools.append(FunctionTool(publish_agent))
145+
tools.append(FunctionTool(unpublish_agent))
146+
return tools

0 commit comments

Comments
 (0)