DEV Community

Cover image for Can Google ADK Talk to Microsoft Foundry on Azure? A Cross-Cloud A2A Benchmark
xbill for Google Developer Experts

Posted on

Can Google ADK Talk to Microsoft Foundry on Azure? A Cross-Cloud A2A Benchmark

This article provides a step-by-step guide to building and testing a
cross-cloud currency agent. A coordinator hosted on Microsoft Foundry in Azure
calls a Google ADK agent on Google Cloud over A2A v1.0, uses an MCP exchange-rate
tool, and records what independent verification costs in latency.

What is This Project Trying to Do?

Most Agent-to-Agent (A2A) protocol demos stop at "look, the request succeeded."
That is a smoke test, not an interoperability benchmark. This project goes
further: a Microsoft Agent Framework coordinator, hosted on Microsoft Foundry
in Azure, discovers and delegates to a Google ADK agent running on Cloud Run in
GCP, then measures what that cross-cloud hop actually costs.

The questions, with numbers instead of vibes:

  1. Can a Foundry-hosted Agent Framework agent invoke a Google ADK agent through an A2A agent card, with no framework-specific glue?
  2. What latency does remote-agent verification add?
  3. Does independently verifying an MCP tool result over A2A improve correctness enough to justify the overhead?

Reduce, Re-Use, Re-Cycle!

This builds directly on the currency agent from the previous articles in this series:

Getting Started with MCP, ADK and A2A | Google Codelabs

GitHub - xbill9/currency-agent

That agent — ADK, Gemini, a FastMCP exchange-rate server backed by the free Frankfurter API — becomes the remote verifier in this project. The new repo wraps it in a benchmark harness:

GitHub - xbill9/foundry-adk-a2a-currency

The Architecture

You (Responses protocol, Entra ID)
      |
Microsoft Foundry hosted agent          (Azure, eastus2, gpt-5-mini)
Microsoft Agent Framework coordinator
      |
      +-- MCP stdio --> Frankfurter rates      (in-container)
      |
      +-- A2A v1.0 --> Cloud Run               (GCP, us-central1)
                          |
                       Google ADK agent        (gemini-2.5-flash)
                          |
                       MCP HTTP --> Frankfurter rates
Enter fullscreen mode Exit fullscreen mode

The coordinator answers every conversion three ways:

Mode What happens Why it exists
mcp_only Coordinator calls the rate tool Baseline
a2a_only Coordinator delegates to the remote ADK agent Measure remote-agent behavior
verified MCP result independently checked over A2A The accuracy/overhead tradeoff

Both sides read the same Frankfurter daily reference rates on purpose: when the two clouds disagree, that measures protocol and model behavior, not data-source skew.

Rule One: the Model Never Does Math

Currency conversion is a terrible job for an LLM and a great job for Decimal. The domain layer is framework-independent Python with pydantic models, and agreement is judged by relative numeric difference — no model is ever asked "do these numbers look the same to you?"

difference = abs(primary.converted_amount - verifier.converted_amount)
relative = difference / abs(primary.converted_amount)
agreed = relative <= tolerance  # default 0.005
Enter fullscreen mode Exit fullscreen mode

The failure policy is explicit rather than emergent:

  • MCP fails, A2A succeeds → return the remote result, labeled unverified.
  • A2A fails, MCP succeeds → return the tool result with a "verification unavailable" warning.
  • Both succeed but disagree → return both and warn. Never silently pick the model's favorite.
  • Both fail → return a typed failure (validation, provider, authentication, transport, timeout, protocol). Never fabricate a rate.

"Which layer broke" is a research question, so every adapter failure is converted into exactly one of those kinds at one boundary.

The Wire Fight: A2A v0.3.0 vs v1.0

Here is the headline interoperability lesson. The first attempt to point the Microsoft Agent Framework A2A client at the existing currency agent died instantly:

a2a.utils.errors.MethodNotFoundError: Method not found
Enter fullscreen mode Exit fullscreen mode

Root cause: a clean protocol-version skew with no negotiation.

  • The Microsoft client (agent-framework-a2a, requires a2a-sdk>=1.0) calls the A2A v1.0 JSON-RPC method SendMessage.
  • The existing ADK agent (a2a-sdk 0.3.x) only routes the v0.3.0 method message/send.
  • The client fetches the agent card — which says protocolVersion: 0.3.0 — and calls the v1.0 method anyway.

Worse, the two ecosystems had mutually exclusive dependency pins at the time of writing:

Package a2a-sdk requirement
agent-framework-a2a 1.0.0b260721 >=1.0.0,<2
google-adk 2.1.0 – 2.4.0 >=0.3.4,<0.4
google-adk 2.5.0 >=0.3.4,<2
a2ui-agent-sdk (through 0.4.0) <0.4.0

google-adk 2.5.0 is the first release that allows a2a-sdk 1.x — but the A2UI extension from the previous article still pins the old protocol. So the benchmark agent is the currency agent with A2UI removed, pinned to google-adk 2.5.0 + a2a-sdk 1.1.2. Separate uv virtualenvs keep both worlds installable on one machine; the wire protocol is what has to match.

Two more v1.0 observations worth knowing:

  • The v1.0 agent card moved url/protocolVersion into a supportedInterfaces array.
  • ADK auto-advertises MCP tools (get_exchange_rate) as A2A skills on the card — your remote agent's card documents its toolbox for free.

After the version alignment, the answer to research question 1 is yes: the Microsoft Agent Framework client consumed the ADK-generated card and delegated over A2A v1.0 with no schema translation beyond prompting the agent to answer in parseable JSON.

Hosting the Coordinator on Microsoft Foundry

Versions observed working (preview software; pin what works, expect drift): Azure Developer CLI 1.28.1, microsoft.foundry azd extension 1.0.0-beta.2, Python 3.13, agent-framework-core 1.12.1, agent-framework-foundry 1.10.3, agent-framework-foundry-hosting 1.0.0b260722, region eastus2, model gpt-5-mini (2025-08-07, Global Standard).

The entire hosted agent is one file. FoundryChatClient authenticates with DefaultAzureCredential — no API keys anywhere on the Azure side — and ResponsesHostServer exposes it over the Responses protocol:

agent = Agent(
    client=FoundryChatClient(
        project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
        model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
        credential=DefaultAzureCredential(),
    ),
    name="currency-interoperability-coordinator",
    instructions="For every conversion request, call run_currency_benchmark. "
                 "Never calculate or verify arithmetic yourself. ...",
    tools=[run_currency_benchmark],
)
ResponsesHostServer(agent).run()
Enter fullscreen mode Exit fullscreen mode

The adapters behind that tool are selected by environment variables (CURRENCY_A2A_ENDPOINT, CURRENCY_RATE_PROVIDER, CURRENCY_RATE_TRANSPORT, CURRENCY_TIMEOUT_SECONDS), passed through azure.yaml. Version 1 shipped with deterministic fixtures; version 2 flipped the env vars and went live without touching code.

The gotcha that will probably bite you too: control-plane roles (Owner/Contributor) are not enough to deploy. The deploying identity also needs the Foundry Project Manager data-plane role (definition eadc314b-1a2d-4efa-be10-5d325db5065e) at the Foundry account scope.

The Google Side: ADK on Cloud Run

The benchmark agent container colocates two processes: the FastMCP Frankfurter server on localhost and the A2A app on $PORT. The Gemini key comes from Secret Manager — never an env literal:

gcloud secrets create gemini-api-key --data-file="$HOME/gemini.key"
gcloud run deploy currency-adk-a2a \
  --source adk_agent --region us-central1 \
  --allow-unauthenticated --min-instances=0 --max-instances=2 \
  --set-secrets "GOOGLE_API_KEY=gemini-api-key:latest" \
  --set-env-vars "MCP_SERVER_URL=http://127.0.0.1:8081/mcp,GENAI_MODEL=gemini-2.5-flash"
Enter fullscreen mode Exit fullscreen mode

--min-instances=0 means the verifier costs nothing while idle. (Ask me how I know to check min-instances on old GPU services. Actually, don't.)

How to Run It

Everything local runs credential-free on fixtures first:

git clone https://github.com/xbill9/foundry-adk-a2a-currency
cd foundry-adk-a2a-currency
pip3 install --user -e ".[dev]"
pytest                      # 35 deterministic tests, no cloud, no model calls
currency-benchmark 100 USD EUR --mode verified --transport mcp-stdio
Enter fullscreen mode Exit fullscreen mode

To go live locally, start the ADK agent (needs GOOGLE_API_KEY):

cd adk_agent && uv sync
MCP_SERVER_URL=http://127.0.0.1:8081/mcp uv run uvicorn agent:a2a_app --port 10001
Enter fullscreen mode Exit fullscreen mode

and point the benchmark at it:

CURRENCY_RATE_PROVIDER=frankfurter currency-benchmark 250 GBP USD JPY \
  --mode verified --transport mcp-stdio \
  --a2a-endpoint http://127.0.0.1:10001 --timeout-seconds 60
Enter fullscreen mode Exit fullscreen mode

The full evaluation matrix (fault-free cases live, fault-injection cases deterministic — you cannot order a live agent to time out on demand):

currency-evaluate --a2a-endpoint http://127.0.0.1:10001 --live-rates \
  --output results.jsonl --summary summary.json
Enter fullscreen mode Exit fullscreen mode

The full cross-cloud deployment is one script — Secret Manager, Cloud Run, then azd deploy:

bash infra/deploy_live.sh
Enter fullscreen mode Exit fullscreen mode

Results

The 38-case matrix (validation, cross-rates, precision, injected timeouts, stale rates, disagreement, malicious tool text) ran in all three modes — 114 records, raw JSONL retained in the repo so every number below is regenerable.

Mode Success Median latency p95 latency
mcp_only 100% 297 ms 1.09 s
a2a_only (live Gemini) 100% 1.69 s 4.82 s
verified (live) 100% 1.71 s 4.15 s

The two numbers I care most about:

  • All 60 live-adapter records agreed within the 0.5% tolerance. The only disagreement in the entire run was the deliberately injected fault case — which the verifier flagged, exactly as designed.
  • Verified mode costs roughly what the A2A call costs, because MCP and A2A run concurrently. Independent verification added ~1.4 s of median latency over the MCP baseline, not the sum of both paths.

On the fully hosted path (Azure coordinator → GCP agent), the remote Foundry endpoint completed all three modes: mcp_only in 0.71 s, verified in 2.7 s with agreed: true and relative_difference: 0. One warm-path anecdote, not a distribution — repeated warm/cold trials and token/cost accounting are the remaining evaluation work.

Lessons Learned

  1. A2A version skew fails loud but late. The client happily fetched a v0.3.0 card and then called v1.0 methods. Check the a2a-sdk major version on both sides before you debug anything else; MethodNotFoundError is the signature.
  2. Extension packages can pin you to an old protocol. A2UI held the whole agent at A2A v0.3.0. Protocol-adjacent extras deserve a dependency audit before you commit to them.
  3. Default timeouts assume one cloud. The coordinator's 10 s per-adapter timeout was generous locally and fatal cross-cloud: a Cloud Run cold start plus multi-target Gemini generation blew straight through it. It's now configurable, set to 60 s for remote endpoints.
  4. Cold starts don't just add latency — they change behavior. One cold-start reply omitted one of the requested targets entirely. Because parsing is strict, it surfaced as a typed protocol failure instead of a silently incomplete answer. Design for partial replies.
  5. Preview deploy tooling has transient moods. One azd deploy died with AzureDeveloperCLICredential: signal: killed and succeeded unchanged on retry. Retry before you bisect.
  6. IPv6 can hang quietly. Frankfurter calls from some sandboxed environments hang on IPv6 and surface as empty-message timeouts. Pinning the client socket to IPv4 fixed it — the same workaround the original currency agent shipped with, which I removed as "sandbox-only" and then reinstated a day later. Respect the workarounds you inherit.
  7. Keep the model out of the arithmetic. Both gpt-5-mini and gemini-2.5-flash respected "call the tool, never calculate" — and because all math is Decimal in deterministic code, agreement checks compare numbers, not model prose.
  8. Same-source verification isolates the protocol. Pointing both paths at Frankfurter means a disagreement can only come from the protocol, the model, or the code. With live data on both sides: zero false disagreements in 60 records.

So When is A2A Verification Worth It?

Based on what's measured so far: the accuracy delta on the happy path is zero — both paths read the same rates and agree. What you're actually buying for your ~1.4 s is independent failure detection: a second implementation, on a second cloud, that will loudly disagree when a tool is compromised, stale, or wrong. The injected-fault case shows the mechanism works; the malicious-tool-text and hosted-injection cases are where that budget earns its keep. If your tool result feeds a decision that matters, one extra second for a cross-checked answer is cheap. If it's a chatbot rate lookup — mcp_only at 297 ms is your friend.

Source

Everything — coordinator, benchmark agent, deploy script, evaluation cases, and the raw results behind every table above:

GitHub - xbill9/foundry-adk-a2a-currency

Upstream agent pinned at xbill9/currency-agent@aeef3c4.

If you've wired a Microsoft Agent Framework agent to a Google ADK agent over A2A — especially if you hit card or version mismatches I didn't — I'd genuinely like to hear what broke.

Top comments (1)

Collapse
 
zira125 profile image
Zira

The strongest part here is treating “verified” as an explicit mode instead of letting the model decide whether two results look similar. The v0.3.0/v1.0 failure also suggests a useful preflight: fetch both agent cards, record protocolVersion and supportedInterfaces, then fail the deployment if the advertised method set cannot satisfy the client. I would also keep cold-start and warm-path latency distributions separate before choosing the 60-second timeout. How are you planning to turn the tolerance, timeout, and version checks into a repeatable CI contract test?