Skip to content

Commit 2643395

Browse files
authored
feat: add invoke module with discover-first flow (v0.12.0) (#45)
New core/invoke.py consolidates agent invocation logic into a single source of truth. CLI and MCP server now delegate here instead of duplicating protocol payloads and HTTP handling. Discover-first pattern: callers provide domain+name instead of raw endpoint URLs. DNS discovery resolves the agent, agent card prefetch validates the endpoint and retrieves metadata, then the call proceeds through SDK (with telemetry) or raw httpx fallback. Key fixes: hardcoded 30s future.result() timeout in _run_async, empty error strings from SDK exceptions, missing type guards on A2A response parsing, unhandled SDK exceptions in invoke path. Signed-off-by: Igor Racic <iracic82@gmail.com>
1 parent b033fc1 commit 2643395

File tree

15 files changed

+4427
-637
lines changed

15 files changed

+4427
-637
lines changed

CHANGELOG.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ All notable changes to DNS-AID will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.12.0] - 2026-03-12
9+
10+
### Added
11+
- **`core/invoke.py` module** — Single source of truth for agent invocation (A2A messaging + MCP tool calling). CLI and MCP server now delegate to `invoke.py` instead of duplicating protocol logic. Public API: `send_a2a_message()`, `call_mcp_tool()`, `list_mcp_tools()`, `resolve_a2a_endpoint()`.
12+
- **Discover-first invocation flow**`send_a2a_message()` and MCP tools accept `domain` + `name` instead of requiring a raw endpoint URL. Resolution chain: DNS discovery → agent card fetch → invoke.
13+
- **Agent card prefetch** — Before invoking, fetches `/.well-known/agent-card.json` for canonical endpoint URL and metadata (name, description, skills). Includes host mismatch protection: if the agent card's `url` hostname differs from the DNS endpoint, the DNS endpoint is used and a warning is logged.
14+
- **`dns-aid message --domain --name` options** — Discover-first CLI flow: `dns-aid message --domain ai.infoblox.com --name security-analyzer "hello"`. Existing `--endpoint` option still supported for direct invocation.
15+
16+
### Changed
17+
- **CLI commands delegate to `core/invoke.py`**`dns-aid message`, `dns-aid call`, and `dns-aid list-tools` now call the shared invoke module instead of inlining httpx/SDK logic. Reduces code duplication and ensures consistent behavior across CLI and MCP server.
18+
- **MCP `send_a2a_message` tool enhanced** — Now accepts `domain` + `name` parameters for discover-first invocation from Claude Desktop, in addition to the existing `endpoint` parameter.
19+
20+
### Fixed
21+
- **Hardcoded 30s timeout in `_run_async()`** — The thread pool wrapper used `future.result(timeout=30)`, which killed long-running requests regardless of the user-specified timeout. Now passes the actual timeout value through.
22+
- **Empty error strings in SDK path**`InvokeResult.error` could be an empty string on failure. All exceptions are now wrapped in `InvokeResult` with meaningful error messages.
23+
- **Type guards on A2A response parsing** — Response body is now validated before accessing nested fields, preventing `KeyError` and `TypeError` on unexpected A2A responses.
24+
825
## [0.11.0] - 2026-03-12
926

1027
### Added
@@ -498,7 +515,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
498515
- [RFC 9460 - SVCB and HTTPS Resource Records](https://www.rfc-editor.org/rfc/rfc9460.html)
499516
- [RFC 4033-4035 - DNSSEC](https://www.rfc-editor.org/rfc/rfc4033.html)
500517

501-
[Unreleased]: https://github.com/infobloxopen/dns-aid-core/compare/v0.10.0...HEAD
518+
[Unreleased]: https://github.com/infobloxopen/dns-aid-core/compare/v0.12.0...HEAD
519+
[0.12.0]: https://github.com/infobloxopen/dns-aid-core/compare/v0.11.0...v0.12.0
520+
[0.11.0]: https://github.com/infobloxopen/dns-aid-core/compare/v0.10.1...v0.11.0
521+
[0.10.1]: https://github.com/infobloxopen/dns-aid-core/compare/v0.10.0...v0.10.1
502522
[0.10.0]: https://github.com/infobloxopen/dns-aid-core/compare/v0.9.0...v0.10.0
503523
[0.9.0]: https://github.com/infobloxopen/dns-aid-core/compare/v0.8.0...v0.9.0
504524
[0.8.0]: https://github.com/infobloxopen/dns-aid-core/compare/v0.7.3...v0.8.0

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ repository-code: "https://github.com/infobloxopen/dns-aid-core"
88
authors:
99
- name: "The DNS-AID Authors"
1010

11-
version: "0.11.0"
11+
version: "0.12.0"
1212
date-released: "2026-03-12"
1313

1414
keywords:

README.md

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,21 +184,66 @@ dns-aid publish --name internal-bot --domain example.com --protocol mcp --no-upd
184184
# Agent Communication (talk to discovered agents)
185185
# =============================================================================
186186

187-
# Send a message to an A2A agent (Google A2A protocol)
188-
dns-aid message https://security-analyzer.ai.infoblox.com "Analyze DNS-AID security posture"
187+
# Discover-first: find agent via DNS, fetch agent card, then invoke
188+
dns-aid message --domain ai.infoblox.com --name security-analyzer \
189+
"Analyze security of _marketing._a2a._agents.ai.infoblox.com"
190+
191+
# Direct endpoint (skip discovery)
192+
dns-aid message --endpoint https://security-analyzer.ai.infoblox.com \
193+
"Analyze DNS-AID security posture"
189194

190195
# Send a message with JSON output
191-
dns-aid message https://chat.example.com "Hello" --json
196+
dns-aid message --endpoint https://chat.example.com "Hello" --json
197+
198+
# List tools on an MCP agent (discover-first)
199+
dns-aid list-tools --domain example.com --name network-specialist
192200

193-
# List tools on an MCP agent
194-
dns-aid list-tools https://mcp.example.com/mcp
201+
# List tools via direct endpoint
202+
dns-aid list-tools --endpoint https://mcp.example.com/mcp
195203

196204
# Call a specific tool on an MCP agent
197-
dns-aid call https://mcp.example.com/mcp search_flights \
205+
dns-aid call --endpoint https://mcp.example.com/mcp search_flights \
198206
--arguments '{"origin": "SFO", "destination": "JFK"}'
199207

208+
# Note: discover-first flow fetches /.well-known/agent-card.json to resolve
209+
# the canonical endpoint URL and agent metadata before invoking. If the agent
210+
# card's url hostname differs from the DNS endpoint, DNS takes precedence.
200211
```
201212

213+
### Python SDK
214+
215+
```python
216+
import asyncio
217+
from dns_aid.core.invoke import send_a2a_message, call_mcp_tool, list_mcp_tools
218+
219+
async def main():
220+
# Discover-first: find agent via DNS, fetch agent card, invoke
221+
result = await send_a2a_message(
222+
domain="ai.infoblox.com",
223+
name="security-analyzer",
224+
message="Analyze security of _marketing._a2a._agents.ai.infoblox.com",
225+
)
226+
print(result.data["response_text"])
227+
228+
# Direct endpoint invocation
229+
result = await send_a2a_message(
230+
endpoint="https://chat.example.com",
231+
message="Hello",
232+
)
233+
234+
# MCP tool calling
235+
tools = await list_mcp_tools("https://mcp.example.com/mcp")
236+
result = await call_mcp_tool(
237+
"https://mcp.example.com/mcp",
238+
"search_flights",
239+
{"origin": "SFO", "destination": "JFK"},
240+
)
241+
242+
asyncio.run(main())
243+
```
244+
245+
For advanced usage with telemetry, connection reuse, and ranking, see the [SDK documentation](docs/getting-started.md#sdk-agent-invocation--telemetry).
246+
202247
### Agent Index Records
203248

204249
DNS-AID automatically maintains an index record at `_index._agents.{domain}` for efficient discovery:

docs/api-reference.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ Complete API documentation for DNS-AID - DNS-based Agent Identification and Disc
3030
- [Validation Utilities](#validation-utilities)
3131
- [CLI Reference](#cli-reference)
3232
- [MCP Server](#mcp-server)
33+
- [Invocation Module](#invocation-module-coreinvokepy)
34+
- [send_a2a_message()](#send_a2a_message)
35+
- [call_mcp_tool()](#call_mcp_tool)
36+
- [list_mcp_tools()](#list_mcp_tools)
37+
- [resolve_a2a_endpoint()](#resolve_a2a_endpoint)
38+
- [InvokeResult](#invokeresult)
3339
- [SDK: Invocation & Telemetry](#sdk-invocation--telemetry)
3440
- [AgentClient](#agentclient)
3541
- [SDKConfig](#sdkconfig)
@@ -759,6 +765,48 @@ dns-aid index list example.com # List agents in domain's index
759765
dns-aid index sync example.com # Sync index with actual DNS records
760766
```
761767

768+
### Agent Communication Commands
769+
770+
```bash
771+
# Send a message to an A2A agent (discover-first: DNS → agent card → invoke)
772+
dns-aid message --domain ai.infoblox.com --name security-analyzer \
773+
"Analyze security of _marketing._a2a._agents.ai.infoblox.com"
774+
775+
# Send a message to an A2A agent (direct endpoint)
776+
dns-aid message --endpoint https://security-analyzer.ai.infoblox.com \
777+
"Analyze DNS-AID security posture"
778+
779+
# JSON output
780+
dns-aid message --endpoint https://chat.example.com "Hello" --json
781+
782+
# Custom timeout (seconds)
783+
dns-aid message --domain example.com --name chat "Hello" --timeout 60
784+
```
785+
786+
| Option | Description |
787+
|--------|-------------|
788+
| `--domain` | Domain for DNS discovery (used with `--name`) |
789+
| `--name` | Agent name for DNS discovery (used with `--domain`) |
790+
| `--endpoint` | Direct endpoint URL (skips discovery) |
791+
| `--json` | Output raw JSON response |
792+
| `--timeout` | Request timeout in seconds (default: 30) |
793+
794+
```bash
795+
# List tools on a remote MCP agent (discover-first)
796+
dns-aid list-tools --domain example.com --name network-specialist
797+
798+
# List tools via direct endpoint
799+
dns-aid list-tools --endpoint https://mcp.example.com/mcp
800+
801+
# Call a tool on a remote MCP agent
802+
dns-aid call --endpoint https://mcp.example.com/mcp search_flights \
803+
--arguments '{"origin": "SFO", "destination": "JFK"}'
804+
805+
# Call with discover-first
806+
dns-aid call --domain example.com --name network-specialist get_subnets \
807+
--arguments '{"network": "10.0.0.0/8"}'
808+
```
809+
762810
### Environment Variables
763811

764812
**General:**
@@ -838,6 +886,7 @@ dns-aid-mcp --transport http --host 0.0.0.0 --port 8000
838886
| `delete_agent_from_dns` | Delete an agent from DNS (auto-updates index) |
839887
| `list_agent_index` | List agents in domain's index |
840888
| `sync_agent_index` | Sync index with actual DNS records |
889+
| `send_a2a_message` | Send a message to an A2A agent. Accepts `domain` + `name` (discover-first) or `endpoint` (direct). |
841890

842891
### Health Endpoints (HTTP Transport)
843892

@@ -887,6 +936,101 @@ except Exception as e:
887936
```
888937

889938

939+
## Invocation Module (`core/invoke.py`)
940+
941+
Single source of truth for agent invocation. Both CLI and MCP server delegate to these functions.
942+
943+
### send_a2a_message()
944+
945+
Send a message to an A2A agent using discover-first or direct endpoint.
946+
947+
```python
948+
from dns_aid.core.invoke import send_a2a_message, InvokeResult
949+
950+
# Discover-first (DNS → agent card → invoke)
951+
result: InvokeResult = await send_a2a_message(
952+
message="Analyze DNS-AID security posture",
953+
domain="ai.infoblox.com",
954+
name="security-analyzer",
955+
timeout=30.0,
956+
)
957+
958+
# Direct endpoint (skip discovery)
959+
result = await send_a2a_message(
960+
message="Hello",
961+
endpoint="https://chat.example.com",
962+
)
963+
964+
print(result.text) # Extracted text response
965+
print(result.raw) # Full JSON-RPC response dict
966+
print(result.error) # Error message if failed (None on success)
967+
```
968+
969+
| Parameter | Type | Required | Description |
970+
|-----------|------|----------|-------------|
971+
| `message` | `str` | Yes | Message text to send |
972+
| `domain` | `str` | No | Domain for DNS discovery (used with `name`) |
973+
| `name` | `str` | No | Agent name for DNS discovery (used with `domain`) |
974+
| `endpoint` | `str` | No | Direct endpoint URL (skips discovery). Either `domain`+`name` or `endpoint` required. |
975+
| `timeout` | `float` | No | Request timeout in seconds (default: 30) |
976+
977+
### call_mcp_tool()
978+
979+
Call a tool on a remote MCP agent via JSON-RPC `tools/call`.
980+
981+
```python
982+
from dns_aid.core.invoke import call_mcp_tool
983+
984+
result = await call_mcp_tool(
985+
endpoint="https://mcp.example.com/mcp",
986+
tool_name="search_flights",
987+
arguments={"origin": "SFO", "destination": "JFK"},
988+
)
989+
```
990+
991+
### list_mcp_tools()
992+
993+
List available tools on a remote MCP agent via JSON-RPC `tools/list`.
994+
995+
```python
996+
from dns_aid.core.invoke import list_mcp_tools
997+
998+
result = await list_mcp_tools(
999+
endpoint="https://mcp.example.com/mcp",
1000+
)
1001+
```
1002+
1003+
### resolve_a2a_endpoint()
1004+
1005+
Resolve an A2A agent endpoint via DNS discovery and agent card fetch.
1006+
1007+
```python
1008+
from dns_aid.core.invoke import resolve_a2a_endpoint
1009+
1010+
endpoint_url = await resolve_a2a_endpoint(
1011+
domain="ai.infoblox.com",
1012+
name="security-analyzer",
1013+
)
1014+
# Returns: "https://security-analyzer.ai.infoblox.com:443"
1015+
```
1016+
1017+
Resolution chain:
1018+
1. DNS discovery (`discover(domain, protocol="a2a", name=name)`)
1019+
2. Agent card fetch (`/.well-known/agent-card.json`) for canonical URL
1020+
3. Host mismatch protection: if agent card URL hostname differs from DNS endpoint, DNS wins
1021+
1022+
### InvokeResult
1023+
1024+
Returned by all invocation functions.
1025+
1026+
| Field | Type | Description |
1027+
|-------|------|-------------|
1028+
| `text` | `str \| None` | Extracted text from response |
1029+
| `raw` | `dict \| None` | Full response payload |
1030+
| `error` | `str \| None` | Error message if invocation failed |
1031+
1032+
---
1033+
8901034
## SDK: Invocation & Telemetry
8911035

8921036
The Tier 1 SDK provides agent invocation with automatic telemetry capture, and community-wide ranking queries.

docs/architecture.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,87 @@ and gracefully skips hosts that don't serve `.well-known/agent-card.json`.
237237

238238
---
239239

240+
## Invocation Layer (`core/invoke.py`)
241+
242+
The invocation module is the single source of truth for agent communication.
243+
Both the CLI (`dns-aid message`, `dns-aid call`, `dns-aid list-tools`) and the
244+
MCP server (`send_a2a_message` tool) delegate to `core/invoke.py` instead of
245+
duplicating protocol logic.
246+
247+
### Resolution Chain
248+
249+
```
250+
send_a2a_message(domain="ai.infoblox.com", name="security-analyzer", message="...")
251+
252+
├─ 1. DNS Discovery
253+
│ discover(domain, protocol="a2a", name=name)
254+
│ → AgentRecord with endpoint_url
255+
256+
├─ 2. Agent Card Prefetch
257+
│ GET https://{endpoint_host}/.well-known/agent-card.json
258+
│ → canonical URL, name, description, skills
259+
│ │
260+
│ └─ Host mismatch check:
261+
│ card.url hostname != DNS endpoint hostname?
262+
│ YES → log warning, use DNS endpoint (DNS is authoritative)
263+
│ NO → use agent card URL (may include path)
264+
265+
└─ 3. Invoke
266+
POST {resolved_endpoint}
267+
JSON-RPC 2.0: {"method": "message/send", "params": {...}}
268+
→ InvokeResult(text, raw, error)
269+
```
270+
271+
### SDK vs Raw httpx Paths
272+
273+
```
274+
invoke.py
275+
├─ SDK available? (dns_aid.sdk importable + AgentRecord available)
276+
│ YES → AgentClient.invoke(agent, method="message/send", ...)
277+
│ → telemetry capture, signal collection, ranking
278+
│ → InvokeResult from InvocationResult
279+
280+
└─ NO → Raw httpx.AsyncClient POST
281+
→ JSON-RPC 2.0 envelope, manual response parsing
282+
→ InvokeResult from httpx.Response
283+
```
284+
285+
The SDK path is preferred when available — it captures telemetry signals and
286+
feeds the ranking system. The raw httpx path exists as a fallback for minimal
287+
installations without the `[sdk]` extra.
288+
289+
### Interface Delegation
290+
291+
```
292+
┌──────────────────┐ ┌──────────────────┐
293+
│ CLI (Typer) │ │ MCP Server │
294+
│ │ │ │
295+
│ dns-aid message │ │ send_a2a_message │
296+
│ dns-aid call │ │ (MCP tool) │
297+
│ dns-aid list-tools│ │ │
298+
└────────┬─────────┘ └────────┬─────────┘
299+
│ │
300+
└───────────┬────────────┘
301+
302+
┌──────▼──────┐
303+
│ core/invoke │
304+
│ │
305+
│ send_a2a_message() │
306+
│ call_mcp_tool() │
307+
│ list_mcp_tools() │
308+
│ resolve_a2a_endpoint()│
309+
└──────┬──────┘
310+
311+
┌───────────┴───────────┐
312+
│ │
313+
┌────▼─────┐ ┌──────▼──────┐
314+
│ SDK path │ │ httpx path │
315+
│ (prefer) │ │ (fallback) │
316+
└──────────┘ └─────────────┘
317+
```
318+
319+
---
320+
240321
## Community Rankings (Optional)
241322

242323
The SDK can fetch community-wide telemetry rankings when a telemetry API is configured:

0 commit comments

Comments
 (0)