Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Important
MCP in Azure IoT Operations is in preview. Use a nonproduction environment to evaluate the preview and the examples in this article.
Connect to Azure IoT Operations MCP through MCP Gateway, which authenticates clients and applies the connection's authorization policy before forwarding tool calls. The following settings let you connect your own MCP-compatible agent. An optional sample agent is also provided to demonstrate tool use with a model. The sample isn't required to use MCP.
Prerequisites
- Deploy MCP and verify the
aioconnection and its authorization policy. - Review MCP authentication and authorization.
Configure your MCP client
For an agent running in the cluster, configure its MCP client with the following settings:
| Setting | Value |
|---|---|
| MCP endpoint | https://aio-mcp-frontend.azure-iot-operations:443/mcp |
| Authentication | A projected Kubernetes service account token with audience aio-mcp-frontend, sent as a bearer token. The client must reload the token when Kubernetes rotates it. |
| CA trust | The CA bundle from ConfigMap azure-iot-operations-aio-ca-trust-bundle. Keep TLS certificate verification enabled. |
| Authorization | A dedicated ServiceAccount with annotation aio-mcp-auth/roles: '["Reader"]', or the roles required by your connection's policy. |
Configure token projection and CA mounting through your agent's deployment. The agent's model configuration and credentials are separate from its MCP authentication. You don't need to install the sample agent Helm chart.
For an agent outside the cluster, see Connect your own agent through Azure Relay for a GitHub Copilot CLI example. The client connects through APIM and the relay to MCP Gateway; the sample agent isn't required.
Deploy the sample AI agent
This section is optional. Skip it if you use your own MCP-compatible agent.
For the first tool call, use the sample agent with the built-in aio connection
and a tool-calling model. You don't need Kubernetes MCP or Azure Relay.
The walkthrough uses MQTT to ask the agent to list assets through MCP Gateway.
The sample AI agent runs in the cluster and uses an OpenAI-compatible model backend.
The aio-ai-agent Helm chart published in ghcr.io
deploys the sample with MQTT receiver and executor topics. Don't connect the
agent directly to the backend or deploy another MCP server.
The chart connects to the existing MCP Gateway and doesn't install MCP backends. It configures the tools endpoint, projected token, CA trust, and dedicated agent ServiceAccount directly from Helm values.
Sample agent prerequisites
- An Azure IoT Operations installation in the
azure-iot-operationsnamespace, including the ready MCP Gateway,aioconnection, authorization policy, andazure-iot-operations-aio-ca-trust-bundlefrom Deploy MCP. - Foundry Local installed as an Azure Arc extension, with a model that supports tool calling deployed and reachable from the agent's namespace. Install the extension and deploy a model before installing the agent.
helmandkubectl, with thekubectlcontext set to the target cluster.- Access to the agent chart and image in
ghcr.io.
Enable service-account-token (SAT) authentication on the Foundry Local model
and authorize the agent's ServiceAccount. Add this authentication method to
the model's ModelDeployment configuration, preserving its other settings:
spec:
authenticationMethods:
- method: serviceAccountToken
enabled: true
roles:
- role: GeneralInferenceUser
authorizedSubjects:
- serviceAccountName: aio-ai-agent-agent
serviceAccountNamespace: azure-iot-operations
Apply the updated model deployment before invoking the agent. The
ServiceAccount name corresponds to the aio-ai-agent Helm release used below.
Foundry Local authorization is separate from the MCP Reader role.
Save the following as agent-frontend-values.yaml. The dedicated
ServiceAccount has only the Reader MCP role; don't annotate the namespace's
default ServiceAccount or the gateway's own identity.
agent:
enabled: true
image:
containerRegistry: ghcr.io
repository: azure-samples/explore-iot-operations/aio-ai-agent
backend:
auth: sat
satAudience: foundry-local
frontend:
enabled: true
url: https://aio-mcp-frontend.azure-iot-operations:443/mcp
audience: aio-mcp-frontend
trustBundle: azure-iot-operations-aio-ca-trust-bundle
serviceAccountAnnotations:
aio-mcp-auth/roles: '["Reader"]'
Install the published chart from the directory containing
agent-frontend-values.yaml. No repository checkout or local chart build is
required:
helm upgrade --install aio-ai-agent oci://ghcr.io/azure-samples/explore-iot-operations/helm/aio-ai-agent \
--version 0.1.3 \
--namespace azure-iot-operations \
--values agent-frontend-values.yaml \
--set agent.backend.url=<MODEL_BACKEND_URL> \
--set agent.backend.model=<MODEL_NAME> \
--wait --timeout 5m
The chart and values file use the image repository
ghcr.io/azure-samples/explore-iot-operations/aio-ai-agent.
Use the image tag supplied by chart version 0.1.3 rather than overriding it
with a development build.
The backend URL is the OpenAI-compatible base URL (for example,
https://<MODEL_HOST>/v1). The chart projects a short-lived token with audience
foundry-local at /var/run/secrets/tokens/backend-sat. Kubernetes rotates
the token, and the agent reloads it for model requests. No shared credential
Secret or manually generated token is needed. Set
agent.backend.trustBundle=<BACKEND_CA_CONFIGMAP>
when using a private CA.
The model token is separate from the aio-mcp-frontend token used for tools
and the aio-internal token used for MQTT.
agent.frontend.enabled=true configures tools through the existing
gateway. Its audience must match the gateway's inbound SAT settings, not
the backend's audience. Include the values file on subsequent Helm upgrades.
The agent Deployment and ServiceAccount are named aio-ai-agent-agent.
If MQTT authorization is enabled, grant this ServiceAccount the required broker
permissions for its executor and receiver topics; the MCP Reader role
doesn't grant MQTT access.
Verify the agent, gateway, and backend pods, their restart counts, and the
agent logs; a Helm STATUS: deployed result doesn't establish agent health:
kubectl -n azure-iot-operations get pods
kubectl -n azure-iot-operations logs deployment/aio-ai-agent-agent --tail=100
kubectl -n azure-iot-operations logs deployment/aio-mcp-frontend --tail=100
kubectl -n azure-iot-operations get serviceaccount aio-ai-agent-agent -o yaml
Make your first tool call
Ask the agent to call aio_list_assets, a read-only tool that lists assets
in your instance. Gateway tool names include the aio_ connection prefix.
An empty asset list is a valid result; you don't need to create an asset to
run this example.
The request follows this path:
MQTT client -> sample agent -> model selects aio_list_assets
-> MCP Gateway -> Azure IoT Operations MCP
MQTT client <- sample agent <- model summarizes the tool result
Use two local Bash terminals with the same Kubernetes context. This example
uses the default executor topic, akri/agent/executor. If you configured a
different topic, substitute it in the publish command.
Prepare an MQTT client
Use the test client from Test MQTT connectivity. If you already have that client, reuse it. Otherwise, deploy it:
kubectl apply -f https://raw.githubusercontent.com/Azure-Samples/explore-iot-operations/main/samples/quickstarts/mqtt-client.yaml
kubectl -n azure-iot-operations wait --for=condition=Ready pod/mqtt-client --timeout=180s
kubectl -n azure-iot-operations exec mqtt-client -- sh -c '
for attempt in $(seq 1 60); do
if command -v mosquitto_pub >/dev/null && command -v mosquitto_sub >/dev/null; then
exit 0
fi
sleep 2
done
echo "MQTT client tools did not become available" >&2
exit 1
'
The pod mounts the broker's CA certificate and a projected SAT with audience
aio-internal. Keep TLS verification and SAT authentication enabled.
Don't enable anonymous access or change the default listener.
If broker authorization is enabled, authorize the mqtt-client ServiceAccount
to publish to akri/agent/executor and subscribe to
mcp/examples/agent/replies/#. The agent must be authorized to subscribe to
its executor topic and publish responses to that reply-topic prefix.
These broker permissions are separate from the agent's MCP Reader role.
Subscribe to the response
In terminal 1, run:
kubectl -n azure-iot-operations exec -i mqtt-client -- sh <<'SH'
set -eu
REQUEST_ID="$(cat /proc/sys/kernel/random/uuid)"
printf '%s\n' "$REQUEST_ID" > /tmp/aio-agent-request-id
CORRELATION_DATA="$(printf '%s' "$REQUEST_ID" | tr -d '-' | cut -c1-16)"
printf 'Expected correlation: %s\n' "$CORRELATION_DATA"
mosquitto_sub -h aio-broker -p 18883 -V mqttv5 -q 1 \
--cafile /var/run/certs/ca.crt \
-D CONNECT authentication-method K8S-SAT \
-D CONNECT authentication-data "$(cat /var/run/secrets/tokens/broker-sat)" \
-t "mcp/examples/agent/replies/$REQUEST_ID" \
-C 1 -W 180 -d \
-F 'correlation=%D properties=%P payload=%p'
SH
Wait until the subscriber reports a successful SUBACK before publishing.
It waits up to 180 seconds for one response. If it times out, rerun this step
before publishing again.
Publish the request
In terminal 2, run:
kubectl -n azure-iot-operations exec -i mqtt-client -- sh <<'SH'
set -eu
REQUEST_ID="$(cat /tmp/aio-agent-request-id)"
CORRELATION_DATA="$(printf '%s' "$REQUEST_ID" | tr -d '-' | cut -c1-16)"
CLIENT_ID="agent-example-$CORRELATION_DATA"
cat > /tmp/aio-agent-request.json <<JSON
{
"messageId": "$REQUEST_ID",
"role": "ROLE_USER",
"parts": [{
"text": "Call aio_list_assets and list the returned asset names. If no assets are returned, say 'No assets found'. If the call fails or access is denied, report that instead. Do not invent asset names."
}]
}
JSON
mosquitto_pub -h aio-broker -p 18883 -V mqttv5 -q 1 \
-i "$CLIENT_ID" \
--cafile /var/run/certs/ca.crt \
-D CONNECT authentication-method K8S-SAT \
-D CONNECT authentication-data "$(cat /var/run/secrets/tokens/broker-sat)" \
-t akri/agent/executor -f /tmp/aio-agent-request.json \
-D PUBLISH response-topic "mcp/examples/agent/replies/$REQUEST_ID" \
-D PUBLISH correlation-data "$CORRELATION_DATA" \
-D PUBLISH content-type application/json \
-D PUBLISH payload-format-indicator 1 \
-D PUBLISH message-expiry-interval 120 \
-D PUBLISH user-property __protVer 1.0 \
-D PUBLISH user-property __srcId "$CLIENT_ID" \
-D PUBLISH user-property __ts "$(date +%s)000:0:$CLIENT_ID"
SH
Use ROLE_USER, not lowercase user. The response topic and mRPC properties
are required to receive and correlate the reply. This example uses 16 ASCII
bytes for correlation data; don't replace them with a 36-character UUID string.
Don't run overlapping examples in the same client pod because they share
the request-ID file.
Check the result
Terminal 1 prints the correlation data, mRPC response properties, and JSON
payload. Verify that the correlation matches the value printed before
subscribing, __stat is 200, and the response role is ROLE_AGENT.
The following payload is illustrative; identifiers, asset names, and model wording vary:
{
"messageId": "<agent-generated-uuid>",
"contextId": "<conversation-id>",
"role": "ROLE_AGENT",
"parts": [{
"text": "The instance has two assets: boiler-01 and compressor-01."
}]
}
If there are no assets, expect a response such as No assets found. A timeout,
authentication failure, or tool error isn't an empty asset list.
Check the agent and gateway logs for the request time:
kubectl -n azure-iot-operations logs deployment/aio-ai-agent-agent --since=5m
kubectl -n azure-iot-operations logs deployment/aio-mcp-frontend --since=5m \
| grep 'tool="list_assets"' \
| grep 'principal="azure-iot-operations:aio-ai-agent-agent"'
The gateway should record an authorization decision like this:
backend="aio" tool="list_assets" principal="azure-iot-operations:aio-ai-agent-agent" decision=allow
A model-generated answer or mRPC status 200 alone doesn't prove that a tool
succeeded. The authorization log confirms that the agent attempted the tool
call, but decision=allow alone doesn't establish backend success either.
Check for tool/backend errors and compare the reported asset names with the
assets configured in your instance. If the model didn't call the tool, or its
answer doesn't match your instance, don't treat the walkthrough as passed.
Restrict access and repeat the request (optional)
Use a nonproduction instance. Changing the aio connection's policy affects
all clients using that connection.
This example starts with the default aio-mcp-policy-v1 policy. Follow
Configure an authorization policy with Bicep
to create and associate mcp-reader-policy-v1, initially allowing
documentation_overview and list_assets. Keep that article's deployment
variables and Bicep files. Don't edit the versioned default policy in place.
If your connection already uses a different custom policy, don't replace it
with this example.
Restrict the custom policy to list_devices, removing the grant for
list_assets. Run this command from the directory containing the Bicep files:
az deployment group create \
--resource-group "$RESOURCE_GROUP" \
--name configure-mcp-policy \
--template-file mcp-authorization-policy.bicep \
--parameters aioInstanceName="$AIO_INSTANCE_NAME" \
customLocationName="$CUSTOM_LOCATION_NAME" \
readerTools='["list_devices"]'
Follow Verify the deployment to confirm the reconciled policy and connection and wait for the gateway rollout. Then repeat the subscribe and publish steps, which generate a fresh request ID.
The agent should report that it can't use aio_list_assets. Tool discovery
filters unauthorized tools, so it might report that the tool is unavailable
rather than attempting a call. A successfully delivered agent response can
still have mRPC status 200; don't expect MQTT status 403.
An illustrative agent response is:
I couldn't list assets because aio_list_assets isn't available to me.
Check the agent and gateway logs again. If the agent attempts the restricted
tool, the gateway should record decision=deny. If discovery filters the
tool out, there might be no attempted call or denial log. A model-generated
refusal alone isn't proof of policy enforcement. If aio_list_assets still
succeeds, stop and check the applied policy, connection, and any additional grants.
Restore the connection's original default policy after the test:
az deployment group create \
--resource-group "$RESOURCE_GROUP" \
--name configure-mcp-connection \
--template-file mcp-connection.bicep \
--parameters aioInstanceName="$AIO_INSTANCE_NAME" \
customLocationName="$CUSTOM_LOCATION_NAME" \
authorizationPolicyName=aio-mcp-policy-v1
Wait for reconciliation and rollout again, then repeat the MQTT subscribe and
publish steps to confirm that the agent can use aio_list_assets again.
Clean up the test client
If you created the mqtt-client pod and ServiceAccount solely for this
walkthrough, remove them when finished. Don't delete an existing shared client.
kubectl -n azure-iot-operations delete pod mqtt-client
kubectl -n azure-iot-operations delete serviceaccount mqtt-client
Optional extensions
- Add Kubernetes MCP if the agent needs Kubernetes tools.
- Use Azure Relay if a client outside the cluster needs access to MCP Gateway.
Neither extension is required for the aio_list_assets example.