Skip to content

feat: add invoke module with discover-first flow (v0.12.0)#45

Merged
iracic82 merged 1 commit intomainfrom
feat/invoke-module-discover-first
Mar 12, 2026
Merged

feat: add invoke module with discover-first flow (v0.12.0)#45
iracic82 merged 1 commit intomainfrom
feat/invoke-module-discover-first

Conversation

@iracic82
Copy link
Collaborator

Summary

  • New core/invoke.py module — single source of truth for agent invocation (A2A + MCP). Eliminates 3-way protocol logic duplication across CLI, MCP server, and SDK.
  • Discover-first invocation flowsend_a2a_message() accepts domain + name instead of raw endpoint URLs. Resolution: DNS discovery → agent card prefetch → invoke.
  • Agent card prefetch — fetches /.well-known/agent-card.json for canonical endpoint URL and metadata. Host mismatch protection prevents internal URL leakage (e.g., Bedrock AgentCore runtime URLs).
  • Fixed hardcoded 30s timeout_run_async() thread pool wrapper killed requests after 30s regardless of configured timeout. Now passes actual timeout with headroom.
  • Better error handling — SDK path wrapped in try/except, empty error strings eliminated, type guards on A2A response parsing (null text values, non-dict parts).
  • Version bump to 0.12.0

Changes

Code

  • src/dns_aid/core/invoke.py — NEW: send_a2a_message(), call_mcp_tool(), list_mcp_tools(), resolve_a2a_endpoint(), InvokeResult, with structlog observability
  • src/dns_aid/mcp/server.py — Simplified: tools delegate to invoke.py; fixed _run_async timeout; added executor cleanup via atexit; send_a2a_message MCP tool accepts domain/name
  • src/dns_aid/cli/main.py — Simplified: commands delegate to invoke.py; message command accepts --domain/--name
  • src/dns_aid/__init__.py — Version bump 0.11.0 → 0.12.0
  • pyproject.toml — Version bump
  • CITATION.cff — Version bump

Tests

  • tests/unit/core/test_invoke.py — NEW: 28 tests covering all public functions, utility helpers, raw invocation paths, and edge cases
  • tests/unit/test_mcp_server.py — Updated imports for moved functions

Documentation

  • CHANGELOG.md — v0.12.0 entry (Added, Changed, Fixed)
  • README.md — Added Python SDK section with discover-first examples
  • docs/api-reference.md — CLI agent communication commands, invoke module API reference
  • docs/architecture.md — Invocation layer architecture section
  • docs/getting-started.md — High-level invoke API tutorial, updated MCP Claude Desktop examples
  • docs/diagrams/invocation-flow.excalidraw.json — Invocation flow + before/after architecture diagram

Test plan

  • 765 unit tests passing (uv run pytest tests/unit/ -x -q)
  • Lint clean (uv run ruff check src/)
  • Format clean (uv run ruff format --check src/)
  • Type check clean (uv run mypy src/dns_aid/)
  • Live CLI test: dns-aid message --domain ai.infoblox.com --name security-analyzer "hello" — success via discover-first
  • Live Claude Desktop test: send_a2a_message(domain="ai.infoblox.com", name="security-analyzer") — success with 36s latency, full security analysis returned
  • MCP booking agent test: call_agent_tool(endpoint="...", tool_name="search_flights") — success in 1.6s

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>
from dns_aid.sdk import AgentClient, SDKConfig

_sdk_available = True
except ImportError:

Check notice

Code scanning / CodeQL

Empty except Note

'except' clause does nothing but pass and there is no explanatory comment.

Copilot Autofix

AI 7 days ago

In general, to fix empty except blocks you should either remove them (letting exceptions propagate) or add explicit handling: logging, fallback behavior, or at least a clear comment explaining why ignoring the exception is safe. For optional imports, the usual fix is to leave the fallback behavior intact but document it and optionally emit a low-level log.

For this specific code, the best fix without changing existing functionality is:

  • Keep the current behavior where a failed import simply means _sdk_available remains False.
  • Add a short explanatory comment in the except ImportError: block to document that the SDK is optional.
  • Optionally add a logger.debug call to record that the SDK could not be imported, without being noisy at higher log levels.

This requires only editing the except ImportError: block around lines 39–44 in src/dns_aid/core/invoke.py. No new imports or other definitions are needed because logger is already defined earlier in the file.


Suggested changeset 1
src/dns_aid/core/invoke.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/dns_aid/core/invoke.py b/src/dns_aid/core/invoke.py
--- a/src/dns_aid/core/invoke.py
+++ b/src/dns_aid/core/invoke.py
@@ -41,7 +41,8 @@
 
     _sdk_available = True
 except ImportError:
-    pass
+    # SDK is optional; fall back to raw HTTP client without telemetry.
+    logger.debug("dns_aid.sdk not available; telemetry will be disabled.")
 
 
 # ---------------------------------------------------------------------------
EOF
@@ -41,7 +41,8 @@

_sdk_available = True
except ImportError:
pass
# SDK is optional; fall back to raw HTTP client without telemetry.
logger.debug("dns_aid.sdk not available; telemetry will be disabled.")


# ---------------------------------------------------------------------------
Copilot is powered by AI and may make mistakes. Always verify output.
the timeout passed to the underlying network call so the
thread pool does not abandon a still-in-flight request.
"""
import concurrent.futures

Check notice

Code scanning / CodeQL

Module is imported with 'import' and 'import from' Note

Module 'concurrent.futures' is imported with both 'import' and 'import from'.

Copilot Autofix

AI 7 days ago

In general, to fix “Module is imported with both import and from ... import ...,” consolidate imports so that either the module or its members are imported consistently. Here, we already use from concurrent.futures import ThreadPoolExecutor at the top of the file, and inside _run_async we only need concurrent.futures.TimeoutError. The best fix is to import TimeoutError at the top along with ThreadPoolExecutor, then remove the inner import concurrent.futures and update the exception handler to use TimeoutError directly.

Concretely:

  • Edit src/dns_aid/mcp/server.py near line 32 to change from concurrent.futures import ThreadPoolExecutor into from concurrent.futures import ThreadPoolExecutor, TimeoutError.
  • Inside _run_async, remove the local import concurrent.futures.
  • Change except concurrent.futures.TimeoutError: to except TimeoutError: so it uses the newly imported name.
    This does not alter functionality: the same exception class is caught, just referenced via a different imported name.
Suggested changeset 1
src/dns_aid/mcp/server.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/dns_aid/mcp/server.py b/src/dns_aid/mcp/server.py
--- a/src/dns_aid/mcp/server.py
+++ b/src/dns_aid/mcp/server.py
@@ -29,7 +29,7 @@
 import logging
 import sys
 import time
-from concurrent.futures import ThreadPoolExecutor
+from concurrent.futures import ThreadPoolExecutor, TimeoutError
 from typing import Literal
 
 # Configure logging BEFORE importing any dns_aid modules to ensure
@@ -135,7 +135,6 @@
                  the timeout passed to the underlying network call so the
                  thread pool does not abandon a still-in-flight request.
     """
-    import concurrent.futures
 
     try:
         loop = asyncio.get_running_loop()
@@ -148,7 +147,7 @@
         future = executor.submit(asyncio.run, coro)
         try:
             return future.result(timeout=timeout)
-        except concurrent.futures.TimeoutError:
+        except TimeoutError:
             raise TimeoutError(f"Operation did not complete within {timeout:.0f}s") from None
     else:
         return asyncio.run(coro)
EOF
@@ -29,7 +29,7 @@
import logging
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from typing import Literal

# Configure logging BEFORE importing any dns_aid modules to ensure
@@ -135,7 +135,6 @@
the timeout passed to the underlying network call so the
thread pool does not abandon a still-in-flight request.
"""
import concurrent.futures

try:
loop = asyncio.get_running_loop()
@@ -148,7 +147,7 @@
future = executor.submit(asyncio.run, coro)
try:
return future.result(timeout=timeout)
except concurrent.futures.TimeoutError:
except TimeoutError:
raise TimeoutError(f"Operation did not complete within {timeout:.0f}s") from None
else:
return asyncio.run(coro)
Copilot is powered by AI and may make mistakes. Always verify output.
@iracic82 iracic82 merged commit 2643395 into main Mar 12, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant