Azure AI Foundry gives you a managed agent runtime backed by Azure’s model catalog — GPT-4o, Mistral, Llama, and more — with built-in thread state, persistent message history, and enterprise auth via Entra ID. What it does not give you is live blockchain data. Ask a Foundry agent for your current WETH balance and it will either refuse or guess. That number lives on-chain, changes every block, and has never been in any training corpus.
A single custom function tool fixes this. In under 40 lines you can give any Azure AI Foundry agent the ability to read ERC-20 balances, DeFi positions, and oracle prices directly from the chain.
TL;DR
pip install azure-ai-projects azure-ai-agents azure-identity requests, define an evmquery_read function with a docstring, pass it to FunctionTool(functions={evmquery_read}), and your Azure AI Foundry agent can answer questions about live token balances, ETH prices, and Aave health factors. Free tier: 2,000 credits/month, no credit card needed.
How Azure AI Foundry function tools work
Azure AI Foundry agents follow an OpenAI Assistants-style thread and run model. Instead of one-shot completions, the agent operates on a persistent thread (conversation history) and executes each user turn as a run. During a run the model can emit tool calls; your application handles them and submits the outputs back before the run continues.
The FunctionTool class from azure-ai-agents bridges your Python functions and that dispatch loop. Pass a set of callables to FunctionTool(functions={...}) and the SDK introspects each function’s type annotations and docstring to build the JSON schema the model sees. The execute(tool_call) method handles the dispatch: it looks up the function by name, deserializes the arguments, calls the function, and returns the string output to pass back to the run.
The polling loop is more explicit than in frameworks like Agno or LangChain, but it gives you full control: you decide when tool calls execute, can log each invocation, and can reject or transform inputs before they reach the external API.
The evmquery_read tool function
evmquery’s REST API takes a chain identifier, a named contract map, and a CEL expression. The response includes the decoded result and the block number it was read from. One format detail: the API requires contract entries to be {"address": "0x..."} objects, not bare strings. The function below accepts plain address strings from the model and converts them internally.
import os
import time
from typing import Optional
import requests
from azure.ai.projects import AIProjectClient
from azure.identity import DefaultAzureCredential
from azure.ai.agents.models import (
FunctionTool,
RequiredFunctionToolCall,
SubmitToolOutputsAction,
ToolOutput,
)
EVMQUERY_API = "https://api.evmquery.com/api/v1/query"
def evmquery_read(
chain: str,
contracts: dict,
expression: str,
context: Optional[dict] = None,
) -> str:
"""Read live data from an EVM smart contract.
Use for current token balances, DeFi positions, oracle prices, or any
contract view function on Ethereum, Base, or BNB Smart Chain.
Supported chains: evm_ethereum, evm_base, evm_bnb_mainnet.
Do NOT use for historical data or event logs.
Args:
chain: Chain identifier. One of evm_ethereum, evm_base, evm_bnb_mainnet.
contracts: Map of short name to contract address (0x hex string).
Example: {"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}
expression: CEL expression evaluating contract state. Contract short
names become variables. Example:
"formatUnits(usdc.balanceOf(wallet), usdc.decimals())"
context: Optional map of variable names to wallet addresses used in
the expression. Pass a list of addresses for multi-wallet
expressions.
Example: {"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"}
"""
def _type(v) -> str:
return "list<sol_address>" if isinstance(v, list) else "sol_address"
contract_schema = {k: {"address": v} for k, v in contracts.items()}
body: dict = {
"chain": chain,
"schema": {"contracts": contract_schema},
"expression": expression,
}
if context:
body["schema"]["context"] = {k: _type(v) for k, v in context.items()}
body["context"] = context
resp = requests.post(
EVMQUERY_API,
json=body,
headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]},
timeout=10,
)
resp.raise_for_status()
data = resp.json()
return f"{data['result']['value']} (block {data['meta']['blockNumber']})"
Two implementation notes:
- The docstring is the schema. The Azure AI Agents SDK passes it directly to the model as the tool description. Scope constraints (“Do NOT use for historical data or event logs”) reduce misrouted calls without any extra routing code.
- Type inference for context variables. evmquery needs to know whether a context value is a single address (
sol_address) or a list (list<sol_address>). The_typehelper infers this from the Python value at call time.
Wiring the tool into an agent
With the function defined, the rest is standard Foundry boilerplate: create the FunctionTool, create the agent, open a thread, post a message, run it, poll until done.
user_functions = {evmquery_read}
functions = FunctionTool(functions=user_functions)
project_client = AIProjectClient(
endpoint=os.environ["PROJECT_ENDPOINT"],
credential=DefaultAzureCredential(),
)
with project_client:
agents_client = project_client.agents
agent = agents_client.create_agent(
model=os.environ["MODEL_DEPLOYMENT_NAME"],
name="evm-data-agent",
instructions=(
"You are a blockchain data assistant. When asked about token balances, "
"DeFi positions, or current protocol metrics, use evmquery_read to fetch "
"live data from the chain. Always include the block number in your reply."
),
tools=functions.definitions,
)
thread = agents_client.threads.create()
agents_client.messages.create(
thread_id=thread.id,
role="user",
content="What is the current ETH/USD price from the Chainlink oracle?",
)
run = agents_client.runs.create(thread_id=thread.id, agent_id=agent.id)
while run.status in ("queued", "in_progress", "requires_action"):
time.sleep(1)
run = agents_client.runs.get(thread_id=thread.id, run_id=run.id)
if run.status == "requires_action" and isinstance(
run.required_action, SubmitToolOutputsAction
):
tool_outputs = []
for tool_call in run.required_action.submit_tool_outputs.tool_calls:
if isinstance(tool_call, RequiredFunctionToolCall):
output = functions.execute(tool_call)
tool_outputs.append(
ToolOutput(tool_call_id=tool_call.id, output=output)
)
agents_client.runs.submit_tool_outputs(
thread_id=thread.id, run_id=run.id, tool_outputs=tool_outputs
)
messages = agents_client.messages.list(thread_id=thread.id)
for msg in messages:
if msg.role == "assistant" and msg.text_messages:
print(msg.text_messages[-1].text.value)
agents_client.delete_agent(agent.id)
Set three environment variables before running:
export PROJECT_ENDPOINT="https://<resource>.ai.azure.com/api/projects/<project>"
export MODEL_DEPLOYMENT_NAME="gpt-4o"
export EVMQUERY_API_KEY="eq_..."
PROJECT_ENDPOINT is found on the Overview page of your Azure AI Foundry project. DefaultAzureCredential authenticates via az login in development; in production it picks up managed identity automatically.
Install
pip install azure-ai-projects azure-ai-agents azure-identity requests
Three live queries to try
All expressions below were validated against the live chain before publication. Block numbers will differ when you run them.
ETH/USD price via Chainlink
evmquery_read(
chain="evm_ethereum",
contracts={"eth_usd": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"},
expression="formatUnits(eth_usd.latestAnswer(), eth_usd.decimals())",
)
# → "2106.9577767 (block 25133961)"
No context needed — this is a pure contract read. The address is the canonical Chainlink ETH/USD aggregator on Ethereum mainnet.
ERC-20 balance check
evmquery_read(
chain="evm_ethereum",
contracts={"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"},
expression="formatUnits(usdc.balanceOf(wallet), usdc.decimals())",
context={"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"},
)
# → "0.11674 (block 25133961)"
formatUnits reads decimals() directly from the contract — USDC uses 6, DAI uses 18, WBTC uses 8. The expression adapts without hard-coding the scale.
USDC total supply on Base
evmquery_read(
chain="evm_base",
contracts={"usdc": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"},
expression="formatUnits(usdc.totalSupply(), usdc.decimals())",
)
# → "4273730639.504659 (block 46230421)"
Switching chains is a single field change. The same CEL expression pattern works across Ethereum, Base, and BNB Smart Chain without modification.
Struct reads and Aave positions
evmquery expressions can traverse struct return values using dot notation. Aave’s getUserAccountData returns a six-field struct; you can read the health factor in isolation:
evmquery_read(
chain="evm_ethereum",
contracts={"aave": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"},
expression="formatUnits(aave.getUserAccountData(wallet).healthFactor, 18)",
context={"wallet": "0x..."},
)
A healthFactor above 1.0 is safe; below 1.0 triggers liquidation. For wallets with no active borrow, evmquery returns the maximum uint256 divided by 1e18 — approximately 1.16e59 — which represents infinite health, not an error. Include a note in your agent’s system prompt so the model interprets this correctly.
To read the complete position — collateral, debt, available borrows, LTV, and liquidation threshold — drop the .healthFactor field:
expression="aave.getUserAccountData(wallet)"
The response is a JSON object with all six fields. The model can compute ratios, flag risk, or summarise the position without any additional parsing in your code.
Developers building DeFi tooling can find more expression patterns in the developer resources. For agentic use cases that span multiple frameworks, the AI users page covers the REST tool and MCP surfaces side by side.
Multi-wallet scans
Pass a list to context and use the CEL map macro to scan multiple wallets in a single API call:
evmquery_read(
chain="evm_ethereum",
contracts={"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"},
expression="wallets.map(w, formatUnits(usdc.balanceOf(w), usdc.decimals()))",
context={
"wallets": [
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8",
"0x40B38765696e3d5d8d9d834D8AaD4bB6e418E489",
]
},
)
Passing a Python list triggers list<sol_address> automatically via the _type helper. The CEL map macro executes in a single round-trip — not one call per wallet.
Azure AI Foundry tool vs MCP
| Foundry function tool (this post) | MCP server | |
|---|---|---|
| Language | Python | Any MCP client |
| Auth | Azure Entra ID or managed identity | OIDC or API key |
| Setup | ~40 lines in your agent file | One config block |
| Control | Full: schema, error handling, logging | Client manages conversation |
| Multi-tool agents | Yes, composable | Limited to MCP surface |
| Best for | Production Azure workloads | Claude Desktop, Cursor, VS Code |
If you are building a production Python agent on Azure infrastructure, the function tool gives full control over schema design, error surfacing, and result formatting. If you want to query the chain interactively from your IDE without writing code, the evmquery MCP server guide gets you there in under five minutes.
Next steps
- Set up the evmquery MCP server in Claude Desktop and Cursor — no code required
- Add a live EVM tool to LangChain — same function pattern,
@tooldecorator style - Monitor Aave health factors with a Python polling script
- Browse the evmquery REST API docs for the full expression reference and multi-chain support