> ## Documentation Index
> Fetch the complete documentation index at: https://www.truefoundry.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Govern Microsoft Foundry agents calling MCP servers

> Route Microsoft Foundry agents through the TrueFoundry MCP Gateway so every tool call carries a verifiable identity — the Entra agent identity, the end user, or both.

Use this guide when you have agents in **Microsoft Foundry** (formerly Azure AI Foundry) and MCP servers hosted in your own Azure subscription. Instead of letting agents call those servers directly, you route the calls through the TrueFoundry MCP Gateway. The gateway authenticates the caller, resolves it to a registered agent, applies your access policy, and performs the token exchange the MCP server requires.

Your MCP server never has to trust a raw agent token, and every call is attributable in the audit trail.

<Info>
  Entra Agent ID and its Foundry integration are evolving quickly, and parts are in preview. The flows below reflect the documented model as of mid-2026 — verify feature status against [Microsoft's Entra Agent ID documentation](https://learn.microsoft.com/en-us/entra/agent-id/) before depending on specifics.
</Info>

## What you are setting up

| Component              | Who runs it   | Its job in this setup                                                                          |
| ---------------------- | ------------- | ---------------------------------------------------------------------------------------------- |
| **Foundry agent**      | Microsoft     | Calls tools. Foundry gives it an Entra agent identity automatically.                           |
| **MCP Gateway**        | TrueFoundry   | Authenticates the agent, enforces policy, exchanges the token for one your MCP server accepts. |
| **MCP server**         | You, in Azure | Validates the token it receives and serves tools.                                              |
| **Microsoft Entra ID** | You           | Issues every token in the chain and holds the permission grants.                               |

## Prerequisites

* A Microsoft Foundry project with at least one agent, and permission to create app registrations in the same Entra tenant.
* An MCP server running in your Azure subscription that validates Entra JWTs. To write one, see [Create an OAuth MCP server with Azure Entra](/docs/ai-gateway/mcp/mcp-server-oauth-azure).
* Azure CLI signed in to the tenant (`az login`), plus `jq` and `uuidgen` for the commands below.
* Entra registered as an identity provider in TrueFoundry. See [Identity Providers](/docs/platform/identity-providers).
* An MCP Server Group in TrueFoundry. See [MCP Gateway getting started](/docs/ai-gateway/mcp/mcp-server-getting-started).

## Mapping an Entra Agent ID to a TrueFoundry agent

When you create an agent in Foundry, Entra mints an **agent identity** for it — a service principal that represents that agent, governed by an **agent identity blueprint**. Find its object ID in the Azure portal on your project or agent resource, under **JSON view**.

To make the gateway aware of that agent, create an entry in the [Agent Registry](/docs/agent-platform/agent-governance/agent-registry#registering-an-agent):

| Field                 | Value                                                    |
| --------------------- | -------------------------------------------------------- |
| **Identity type**     | Identity provider-backed                                 |
| **Identity Provider** | Your Microsoft Entra identity provider                   |
| **Subject value**     | The agent identity object ID, for example `e877c2c0-...` |

On every call the gateway reads the identity out of the incoming token and matches it against this entry. Once an agent resolves, you can:

* Scope tool access per agent — which MCP servers, and which tools on them, this agent may call.
* Control who it may act for — in on-behalf-of mode, which users this agent is allowed to represent.
* Attribute every call — the audit trail records the agent, not just the application that authenticated.
* Apply guardrails and limits per agent rather than per credential.

<Warning>
  The claim the gateway matches on differs between the two modes: **client credentials** matches the token subject, and **on-behalf-of** matches `azp`. Each part below states which to use.
</Warning>

### The blueprint, and governing agents as a class

Every agent identity is created from an **agent identity blueprint** — the governing template for a class of agents. Foundry provisions one for your project and creates each agent identity from it. The blueprint holds the credentials; the agent identities themselves hold none, which is why an agent identity cannot be used as a sign-in client.

The blueprint is where class-level controls apply:

* Conditional Access policies applied to the blueprint cover every agent created from it.
* Disable or revoke once to affect the whole class, rather than chasing individual agents.
* Audit and inventory agents by class as their number grows.

The blueprint's application ID also travels in the token, in the `xms_par_app_azp` claim. That gives you a tier-level check independent of any individual agent: a gateway policy or an MCP server can require that a caller was minted from your blueprint, rather than accepting any agent identity that happens to exist in the tenant.

The blueprint ID appears alongside the agent identity in the same **JSON view** on your project or agent resource. It is also what authorizes your agents against the `agent-service` audience in the first place — see step 5 of [Part 1](#part-1-client-credentials).

<Tip>
  Register individual agents in the Agent Registry for day-to-day scoping and attribution; use the blueprint for controls that should apply to an entire class at once. They are complementary, not alternatives.
</Tip>

## App registrations you need

Foundry creates the agent identity and its blueprint for you. You create the rest in Entra.

| Registration    | Purpose                                                                                                                                                                        | Needed for        |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- |
| `mcp-api`       | The audience of your MCP server. Exposes a delegated scope (`Tool.Write`) and an app role (`Tool.Invoke`). Your MCP server validates that incoming tokens carry this audience. | Both              |
| `agent-service` | The audience your agents' tokens target, and the credential the gateway uses to perform the exchange. Nothing is hosted here — it exists to be an audience.                    | Both              |
| `agent-client`  | The agent's client identity for the OAuth sign-in. This is what identifies the agent in a delegated token.                                                                     | On-behalf-of only |

<Note>
  Entra requires that the application performing an on-behalf-of exchange is the audience of the token it redeems. The gateway therefore exchanges as `agent-service`, using its client secret. There is no TrueFoundry application registration in your tenant.
</Note>

## Choosing between client credentials and on-behalf-of

| If your agent…                                                         | Use                | The MCP server sees                          |
| ---------------------------------------------------------------------- | ------------------ | -------------------------------------------- |
| Runs unattended — schedules, pipelines, background work                | Client credentials | The agent, acting under its own authority    |
| Acts for a signed-in person, and permissions should follow that person | On-behalf-of       | The user, plus the agent that acted for them |

Both modes run through the same gateway and the same MCP server. You can register both and run them side by side.

## Part 1: Client credentials

The agent authenticates with its own Entra agent identity. No user is involved, and the MCP server authorizes the call on an application permission.

<Frame>
  <img src="https://mintcdn.com/truefoundry/UIRypaj_zMgEzXPS/images/foundry-client-credentials-flow-light.svg?fit=max&auto=format&n=UIRypaj_zMgEzXPS&q=85&s=264692c18f299e0271c468d3b0887397" alt="Client credentials flow: the Foundry agent obtains an agent identity token whose subject is the agent identity and which carries no user, the TrueFoundry MCP Gateway resolves the agent from that subject and exchanges the token as agent-service against api://mcp-api/.default, and the MCP server receives an application token carrying the Tool.Invoke role." className="block dark:hidden" width="800" height="452" data-path="images/foundry-client-credentials-flow-light.svg" />

  <img src="https://mintcdn.com/truefoundry/UIRypaj_zMgEzXPS/images/foundry-client-credentials-flow-dark.svg?fit=max&auto=format&n=UIRypaj_zMgEzXPS&q=85&s=fb0171ca7486ab9ae9a95365689e91de" alt="Client credentials flow: the Foundry agent obtains an agent identity token whose subject is the agent identity and which carries no user, the TrueFoundry MCP Gateway resolves the agent from that subject and exchanges the token as agent-service against api://mcp-api/.default, and the MCP server receives an application token carrying the Tool.Invoke role." className="hidden dark:block" width="800" height="452" data-path="images/foundry-client-credentials-flow-dark.svg" />
</Frame>

The agent identity is proven end to end. Because no user ever enters the chain, there is no user identity to lose.

<Steps>
  <Step title="Create the two application registrations">
    ```bash theme={"dark"}
    MCP_API=$(az ad app create --display-name mcp-api \
      --sign-in-audience AzureADMyOrg --query appId -o tsv)
    AGENT_SVC=$(az ad app create --display-name agent-service \
      --sign-in-audience AzureADMyOrg --query appId -o tsv)

    az ad sp create --id "$MCP_API"
    az ad sp create --id "$AGENT_SVC"
    ```

    The service principals are required. Without one, an application cannot be consented to and cannot be named as a resource in a token request.
  </Step>

  <Step title="Configure agent-service with an identifier URI, v2 tokens, and a scope">
    ```bash expandable theme={"dark"}
    OBJ=$(az ad app show --id "$AGENT_SVC" --query id -o tsv)
    SCOPE_ID=$(uuidgen)

    az rest --method PATCH --headers "Content-Type=application/json" \
      --url "https://graph.microsoft.com/v1.0/applications/$OBJ" \
      --body "{
        \"identifierUris\": [\"api://$AGENT_SVC\"],
        \"api\": {
          \"requestedAccessTokenVersion\": 2,
          \"oauth2PermissionScopes\": [{
            \"id\": \"$SCOPE_ID\",
            \"value\": \"access_as_user\",
            \"type\": \"User\",
            \"isEnabled\": true,
            \"adminConsentDisplayName\": \"Act as the signed-in user through the agent\",
            \"adminConsentDescription\": \"Allows a client to obtain a token for the agent on behalf of the signed-in user.\"
          }]
        }
      }"
    ```

    <Warning>
      **Set the token version to 2.** This is the easiest setting to miss and the hardest to diagnose. Version 1 tokens name the calling application in `appid` rather than `azp`, and carry a different issuer (`sts.windows.net`). Both break gateway configuration that is otherwise correct — and on-behalf-of agent resolution depends on `azp` specifically.
    </Warning>

    The identifier URI must exist before you can use `api://$AGENT_SVC` as an audience anywhere else.
  </Step>

  <Step title="Expose the application permission on mcp-api">
    ```bash expandable theme={"dark"}
    OBJ=$(az ad app show --id "$MCP_API" --query id -o tsv)
    ROLE_ID=$(uuidgen)

    az rest --method PATCH --headers "Content-Type=application/json" \
      --url "https://graph.microsoft.com/v1.0/applications/$OBJ" \
      --body "{
        \"identifierUris\": [\"api://$MCP_API\"],
        \"api\": { \"requestedAccessTokenVersion\": 2 },
        \"appRoles\": [{
          \"id\": \"$ROLE_ID\",
          \"value\": \"Tool.Invoke\",
          \"isEnabled\": true,
          \"allowedMemberTypes\": [\"Application\"],
          \"displayName\": \"Invoke tools\",
          \"description\": \"Call tools on the MCP server.\"
        }]
      }"
    ```
  </Step>

  <Step title="Grant the permission to agent-service and admin-consent it">
    ```bash theme={"dark"}
    ROLE_ID=$(az ad app show --id "$MCP_API" \
      --query "appRoles[?value=='Tool.Invoke'].id | [0]" -o tsv)

    az ad app permission add --id "$AGENT_SVC" --api "$MCP_API" \
      --api-permissions "$ROLE_ID=Role"

    az ad app permission admin-consent --id "$AGENT_SVC"
    ```

    <Warning>
      **Consent, or nothing changes.** Without it Entra still issues the token — just without the `roles` claim, so the call fails at your MCP server, not the gateway. Allow a moment for it to propagate.
    </Warning>
  </Step>

  <Step title="Authorize your agents on agent-service, through the blueprint">
    Entra issues an agent identity a token for `agent-service` only if that audience authorizes it. List your blueprints, then pre-authorize them:

    ```bash expandable theme={"dark"}
    az rest --method GET --headers "OData-Version=4.0" \
      --url "https://graph.microsoft.com/v1.0/servicePrincipals/microsoft.graph.agentIdentityBlueprintPrincipal" \
      --query "value[].{name:displayName, appId:appId}" -o table

    BLUEPRINT=<the blueprint appId for your project>
    SCOPE_ID=$(az ad app show --id "$AGENT_SVC" \
      --query "api.oauth2PermissionScopes[?value=='access_as_user'].id | [0]" -o tsv)
    OBJ=$(az ad app show --id "$AGENT_SVC" --query id -o tsv)

    az rest --method PATCH --headers "Content-Type=application/json" \
      --url "https://graph.microsoft.com/v1.0/applications/$OBJ" \
      --body "{
        \"api\": {
          \"preAuthorizedApplications\": [
            { \"appId\": \"$BLUEPRINT\", \"delegatedPermissionIds\": [\"$SCOPE_ID\"] }
          ]
        }
      }"
    ```

    <Note>
      **Why the blueprint and not the agent.** Agent identities hold no credentials of their own and cannot be consented interactively — an attempt returns `AADSTS82014`. Authorization is granted once at the class level, on the blueprint, and every agent identity created from it inherits the grant. Add an agent to the project later and it works with no further Entra change.

      This is the tier-level control in practice: the blueprint decides which agents may obtain a token for your agent tier at all, while the permission granted in the previous step decides what the gateway may then do downstream. The two are independent, and you need both.
    </Note>

    <Warning>
      **Send this as a second call.** Graph validates `preAuthorizedApplications` against already-persisted scopes, so creating a scope and pre-authorizing it in one request fails with `InvalidValue`.
    </Warning>
  </Step>

  <Step title="Give the gateway a credential">
    ```bash theme={"dark"}
    az ad app credential reset --id "$AGENT_SVC" --append \
      --display-name gateway --years 1 --query password -o tsv
    ```

    <Warning>
      **Always pass `--append`.** Without it, `credential reset` deletes every existing secret on the application, silently breaking anything else already using it.
    </Warning>
  </Step>

  <Step title="Configure the Foundry MCP connection">
    Set the auth type to **Microsoft Entra — agent identity**, the audience to `api://$AGENT_SVC`, and the server URL to your TrueFoundry MCP Gateway URL.

    <Warning>
      **Audience, not URL.** The audience is the OAuth resource identifier of the exchanging application — not the address of the MCP server. An incorrect audience fails authentication even when every role assignment is right.
    </Warning>
  </Step>

  <Step title="Register the MCP server in TrueFoundry">
    Register the server against your MCP server URL:

    * **Inbound**: your Entra identity provider.
    * **Outbound**: **OAuth2 → Microsoft Entra → Client Credentials**, with the `agent-service` client ID and secret, and scope `api://$MCP_API/.default`.

    For the full field reference on this auth type, see [Microsoft Entra app setup](/docs/ai-gateway/mcp/entra-app-setup).

    ```yaml expandable theme={"dark"}
    type: mcp-server/remote
    name: foundry-tools
    description: MCP server called by Foundry agents under their own identity
    url: https://<your-mcp-server-url>/mcp
    auth_data:
      type: oauth2
      client_id: <agent-service client ID>
      client_secret: <agent-service client secret>
      grant_type: client_credentials
      provider: microsoft-entra
      scopes:
        - api://<mcp-api client ID>/.default
      token_url: >-
        https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
    ```

    Store `client_secret` as a [TrueFoundry Secret](/docs/manage-secrets) FQN rather than a raw value.

    <Warning>
      **Client credentials uses `/.default`**, not a named scope. The application permissions you granted determine what the token carries; requesting `api://$MCP_API/Tool.Write` here fails, because that is a delegated scope and there is no user.
    </Warning>
  </Step>

  <Step title="Register the agent in TrueFoundry">
    Add an [Agent Registry](/docs/agent-platform/agent-governance/agent-registry#registering-an-agent) entry:

    | Field                 | Value                                                      |
    | --------------------- | ---------------------------------------------------------- |
    | **Identity type**     | Identity provider-backed                                   |
    | **Identity Provider** | Your Microsoft Entra identity provider                     |
    | **Subject value**     | The agent identity object ID, matched on the token subject |

    The registry entry **shadows** the Foundry agent. It creates no new credential and no second principal — it is a governance record keyed to the *same* Entra agent ID that Foundry already minted, so the agent Microsoft runs and the agent TrueFoundry governs are one identity seen from two sides.

    That is what makes the attribution trustworthy: the gateway is not labelling the call from configuration you typed, it is reading the agent identity out of the token Entra signed and looking up the entry that claims it. Grants, tool scope, guardrails, and audit all attach to that entry, and they apply to the Foundry agent itself rather than to a stand-in for it.

    <Note>
      Register one entry per Foundry agent whose calls you want scoped and attributed separately. Agents you leave unregistered do not resolve, so their calls are refused rather than silently passed through — which is what makes registration the chokepoint.
    </Note>
  </Step>
</Steps>

### What your MCP server receives

```json theme={"dark"}
{
  "mode": "application",
  "user": null,
  "aud": "<mcp-api>",
  "roles": ["Tool.Invoke"],
  "actor_app": "<agent-service>"
}
```

Validate the audience, then require `Tool.Invoke` in `roles`.

### What you can enforce

| Where           | Control                                                                                                                                                                                         |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Entra**       | Application permissions granted to `agent-service`; Conditional Access policies targeting agent identities; blueprint-level disable or revoke, which affects every agent of that class at once. |
| **Azure**       | Azure RBAC role assignments made directly to the agent identity, for any Azure resource the MCP server fronts.                                                                                  |
| **MCP Gateway** | Which servers and tools this agent may call; rate limits; guardrails; per-agent audit.                                                                                                          |

All of it is agent-scoped. There is no user in this mode, so no user-scoped policy applies.

## Part 2: On-behalf-of

A signed-in user consents once, and the agent then calls tools with that user's delegated permissions. The token carries both the user and the agent, so the gateway can attribute and authorize on both.

<Frame>
  <img src="https://mintcdn.com/truefoundry/UIRypaj_zMgEzXPS/images/foundry-obo-flow-light.svg?fit=max&auto=format&n=UIRypaj_zMgEzXPS&q=85&s=3a53f3dec1164282776f541052207a6a" alt="On-behalf-of flow: the user consents once through the agent-client application, producing a token that carries the user in oid and the agent in azp. Both identities arrive at the TrueFoundry MCP Gateway together, which resolves the user from oid and the agent from azp, then performs an on-behalf-of exchange as agent-service so the MCP server receives a delegated token naming the user with the Tool.Write scope." className="block dark:hidden" width="800" height="498" data-path="images/foundry-obo-flow-light.svg" />

  <img src="https://mintcdn.com/truefoundry/UIRypaj_zMgEzXPS/images/foundry-obo-flow-dark.svg?fit=max&auto=format&n=UIRypaj_zMgEzXPS&q=85&s=9211bdb2c6f262a0051c83dd67153fa9" alt="On-behalf-of flow: the user consents once through the agent-client application, producing a token that carries the user in oid and the agent in azp. Both identities arrive at the TrueFoundry MCP Gateway together, which resolves the user from oid and the agent from azp, then performs an on-behalf-of exchange as agent-service so the MCP server receives a delegated token naming the user with the Tool.Write scope." className="hidden dark:block" width="800" height="498" data-path="images/foundry-obo-flow-dark.svg" />
</Frame>

The agent is identified by `azp` — the OAuth client that obtained the token. This is why on-behalf-of needs its own `agent-client` registration.

Part 2 builds on Part 1: complete steps 1 through 6 above first, then continue here.

<Steps>
  <Step title="Expose a delegated scope on mcp-api">
    ```bash expandable theme={"dark"}
    OBJ=$(az ad app show --id "$MCP_API" --query id -o tsv)
    WRITE_ID=$(uuidgen)

    az rest --method PATCH --headers "Content-Type=application/json" \
      --url "https://graph.microsoft.com/v1.0/applications/$OBJ" \
      --body "{
        \"api\": {
          \"requestedAccessTokenVersion\": 2,
          \"oauth2PermissionScopes\": [{
            \"id\": \"$WRITE_ID\",
            \"value\": \"Tool.Write\",
            \"type\": \"User\",
            \"isEnabled\": true,
            \"adminConsentDisplayName\": \"Call tools as the signed-in user\",
            \"adminConsentDescription\": \"Allows tools to be invoked on behalf of the signed-in user.\"
          }]
        }
      }"
    ```

    <Warning>
      Sending `oauth2PermissionScopes` replaces the whole collection. Include any scopes you already have.
    </Warning>
  </Step>

  <Step title="Create the agent-client registration">
    ```bash theme={"dark"}
    AGENT_CLIENT=$(az ad app create --display-name agent-client \
      --sign-in-audience AzureADMyOrg --query appId -o tsv)
    az ad sp create --id "$AGENT_CLIENT"

    az ad app credential reset --id "$AGENT_CLIENT" --append \
      --display-name foundry-passthrough --years 1 --query password -o tsv
    ```

    <Warning>
      **Use a registration that represents the agent.** `azp` names whichever client obtained the token, and it is the only place an agent can appear in a delegated token. Reuse a general-purpose client and the gateway sees that client, not the agent.
    </Warning>
  </Step>

  <Step title="Give agent-client its delegated permissions and consent them">
    ```bash expandable theme={"dark"}
    SCOPE_ID=$(az ad app show --id "$AGENT_SVC" \
      --query "api.oauth2PermissionScopes[?value=='access_as_user'].id | [0]" -o tsv)

    # Microsoft Graph, and the well-known id of its offline_access permission
    GRAPH=00000003-0000-0000-c000-000000000000
    OFFLINE=7427e0e9-2fba-42fe-b0c0-848c9e6a8182

    az ad app permission add --id "$AGENT_CLIENT" --api "$AGENT_SVC" \
      --api-permissions "$SCOPE_ID=Scope"
    az ad app permission add --id "$AGENT_CLIENT" --api "$GRAPH" \
      --api-permissions "$OFFLINE=Scope"

    az ad app permission admin-consent --id "$AGENT_CLIENT"
    ```

    `offline_access` is what yields a refresh token. Without it the user is re-prompted every time the access token expires.
  </Step>

  <Step title="Pre-authorize agent-client on agent-service">
    ```bash expandable theme={"dark"}
    OBJ=$(az ad app show --id "$AGENT_SVC" --query id -o tsv)
    EXISTING=$(az ad app show --id "$AGENT_SVC" \
      --query "api.preAuthorizedApplications" -o json)

    # append to what is already there — do not replace the blueprint entry
    az rest --method PATCH --headers "Content-Type=application/json" \
      --url "https://graph.microsoft.com/v1.0/applications/$OBJ" \
      --body "{
        \"api\": {
          \"preAuthorizedApplications\":
            $(echo "$EXISTING" | jq -c --arg a "$AGENT_CLIENT" --arg s "$SCOPE_ID" \
              '. + [{appId:$a, delegatedPermissionIds:[$s]}]')
        }
      }"
    ```

    This step is optional but recommended: it suppresses a per-user consent prompt on this scope, so users are not asked to approve something an administrator has already approved.

    <Warning>
      **This field replaces, it does not merge.** Sending only the new entry drops the Part 1 blueprint authorization and silently breaks client-credentials mode. Read the current value and append.
    </Warning>
  </Step>

  <Step title="Grant agent-service the delegated scope on mcp-api">
    ```bash theme={"dark"}
    WRITE_ID=$(az ad app show --id "$MCP_API" \
      --query "api.oauth2PermissionScopes[?value=='Tool.Write'].id | [0]" -o tsv)

    az ad app permission add --id "$AGENT_SVC" --api "$MCP_API" \
      --api-permissions "$WRITE_ID=Scope"
    az ad app permission admin-consent --id "$AGENT_SVC"
    ```

    This is what lets the gateway complete the on-behalf-of exchange. It is the delegated form of the same permission you granted as an application role in Part 1 — an application needs both if it supports both modes.
  </Step>

  <Step title="Configure the Foundry MCP connection">
    Set the auth type to **OAuth identity passthrough → custom OAuth**, using the `agent-client` ID and secret, and your tenant's v2.0 endpoints:

    | Field           | Value                                                             |
    | --------------- | ----------------------------------------------------------------- |
    | **Auth URL**    | `https://login.microsoftonline.com/$TENANT/oauth2/v2.0/authorize` |
    | **Token URL**   | `https://login.microsoftonline.com/$TENANT/oauth2/v2.0/token`     |
    | **Refresh URL** | `https://login.microsoftonline.com/$TENANT/oauth2/v2.0/token`     |
    | **Scopes**      | `api://$AGENT_SVC/access_as_user offline_access`                  |

    Register the redirect URL Foundry returns after saving:

    ```bash theme={"dark"}
    az ad app update --id "$AGENT_CLIENT" \
      --web-redirect-uris "<the redirect URL Foundry gave you>"
    ```

    <Warning>
      **Scopes are space-separated**, not comma-separated. A comma is accepted by the form and fails later at token time, where it reads like a permissions problem.
    </Warning>

    <Note>
      **Use an audience you control.** Foundry refuses to send a token with a well-known Microsoft audience to a custom MCP endpoint. Because the scope above resolves to `agent-service` — your own registration — this is the supported shape.
    </Note>
  </Step>

  <Step title="Register the MCP server in TrueFoundry">
    Set **Outbound** to **OAuth2 → Microsoft Entra → JWT Bearer**, with the `agent-service` client ID and secret, and scopes `api://$MCP_API/Tool.Write offline_access`.

    For the full field reference on this auth type, see [Microsoft Entra On-Behalf-Of for MCP](/docs/ai-gateway/mcp/mcp-server-oauth-azure-obo).

    ```yaml expandable theme={"dark"}
    type: mcp-server/remote
    name: foundry-tools-obo
    description: MCP server called by Foundry agents on behalf of a signed-in user
    url: https://<your-mcp-server-url>/mcp
    auth_data:
      type: oauth2
      client_id: <agent-service client ID>
      client_secret: <agent-service client secret>
      grant_type: jwt_bearer
      provider: microsoft-entra
      scopes:
        - api://<mcp-api client ID>/Tool.Write
        - offline_access
      token_url: >-
        https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
    ```

    Store `client_secret` as a [TrueFoundry Secret](/docs/manage-secrets) FQN rather than a raw value.
  </Step>

  <Step title="Register the agent in TrueFoundry">
    Add an [Agent Registry](/docs/agent-platform/agent-governance/agent-registry#registering-an-agent) entry:

    | Field                 | Value                                               |
    | --------------------- | --------------------------------------------------- |
    | **Identity type**     | Identity provider-backed                            |
    | **Identity Provider** | Your Microsoft Entra identity provider              |
    | **Subject value**     | The `agent-client` application ID, matched on `azp` |
    | **May act for**       | The users this agent is permitted to represent      |

    Unlike Part 1, there is no single Entra object here that *is* the agent. The Entra agent identity cannot appear in a delegated token at all, so the Foundry agent is represented by an **`agent-service` / `agent-client` pair**: `agent-client` is the OAuth client that signs the user in and therefore lands in `azp`, and `agent-service` is the audience it targets and the credential that performs the downstream exchange. Between them the pair stands in for the Foundry agent — one half naming it on the way in, the other acting for it on the way out.

    <Warning>
      **A different claim from Part 1.** On a delegated token the subject is the user, not the agent. An entry matched on the subject will never resolve the agent here — it must match `azp`.
    </Warning>

    <Note>
      Running both modes for the same Foundry agent means two registry entries — one keyed to the Entra agent identity for unattended calls, one keyed to `agent-client` for delegated calls. Give them matching owners so the two legs of the same agent stay attributable to one team.
    </Note>
  </Step>
</Steps>

### What your MCP server receives

```json theme={"dark"}
{
  "mode": "delegated",
  "user": "alex@contoso.com",
  "aud": "<mcp-api>",
  "scp": "Tool.Write",
  "actor_app": "<agent-service>"
}
```

Validate the audience, then require `Tool.Write` in `scp`. The user is in the standard claims — use `oid` as the stable key rather than `sub`, which is unique per application.

### What you can enforce

| Where               | Control                                                                                                                             |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Entra**           | Delegated permissions on `agent-service`; per-user consent; Conditional Access on both the user and the agent identity.             |
| **MCP Gateway**     | Which servers and tools the agent may call; which users the agent may act for; guardrails and limits; audit recording both parties. |
| **Your MCP server** | Per-user authorization using the verified user claims — the same checks you would apply to a direct API call.                       |

<Tip>
  **Permissions intersect.** A delegated token can only carry permissions the user actually has and that the agent has been granted. Neither party can exceed its own access by going through the other — which is the main reason to choose on-behalf-of over client credentials.
</Tip>

## Comparison, limits, and scale

| At the MCP server                  | Client credentials               | On-behalf-of                           |
| ---------------------------------- | -------------------------------- | -------------------------------------- |
| End user                           | Not present                      | Named and verified                     |
| Authorized by                      | `roles`                          | `scp`                                  |
| An authorized agent called         | Provable                         | Provable                               |
| Agent resolved at the gateway from | `sub` — the Entra agent identity | `azp` — the `agent-client` application |
| Per-user policy applies            | No user to apply it to           | Yes                                    |

### One limit worth knowing

A single Entra token cannot carry both an end user and an Entra agent identity. Agent identities appear only in application tokens, where there is no user; a delegated token names its agent through `azp`, which is an ordinary application registration.

If you need both, run the modes side by side — the gateway records the agent on every call in either case, so the two legs can be correlated in the audit trail.

### Scaling to many agents

Point every agent at the same `agent-service` audience. The audience identifies the *tier* of callers, while each agent is still distinguished by its own identity in the token, so the number of gateway registrations stays proportional to your MCP servers rather than to your agents. Adding an agent means one Agent Registry entry, not a new application registration.

<Note>
  With a shared audience, per-agent permissions are enforced at the gateway rather than by separate Entra grants. Keep separate audiences only where a requirement demands that the identity provider itself draws the line.
</Note>

## Troubleshooting

Most failures in this chain surface a hop or two away from their cause.

| Symptom                                         | What it usually means                                                                                                                                                                                                   |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `missing_token`                                 | The gateway's outbound exchange failed and the request was forwarded without a token — not a caller problem. Most often an on-behalf-of exchange attempted against an application token, which has no user to delegate. |
| `insufficient_scope`                            | A delegated token arrived without the required scope. Check the gateway's outbound scopes and that the delegated permission is admin-consented.                                                                         |
| `insufficient_role`                             | An application token arrived without the required app role — the application permission is missing or was never admin-consented.                                                                                        |
| Audience mismatch, though the value looks right | v2.0 tokens carry the bare application ID in `aud`, not the `api://` URI. Accept both forms.                                                                                                                            |
| Consent succeeded, calls still fail             | A token minted before consent was granted may still be cached. Wait out its lifetime, or register the server under a new name to force a fresh exchange.                                                                |
| The agent reports it has no tools               | Tool approval defaults to requiring confirmation on every call. Set the connection's [approval policy](/docs/ai-gateway/mcp/mcp-tool-approval) explicitly.                                                              |
| No rules produced a session                     | The gateway could not resolve the caller to a registered agent — usually the Agent Registry subject or the matched claim is wrong for the mode. Client credentials matches the subject; on-behalf-of matches `azp`.     |

For gateway-side connectivity and OAuth debugging, see the [MCP troubleshooting guide](/docs/ai-gateway/mcp/mcp-troubleshooting-guide).

## Next steps

* **Restrict which tools an agent sees**: [MCP tool management](/docs/ai-gateway/mcp/mcp-tool-management)
* **Understand the governance model behind this setup**: [Agent Governance with Microsoft Entra](/docs/agent-platform/agent-governance/scenarios/microsoft-entra)
* **Apply content and behavior policy per agent**: [Agent guardrails](/docs/agent-platform/agent-governance/agent-guardrails)
* **Trace and audit agent tool calls**: [Agent observability](/docs/agent-platform/agent-governance/agent-observability)
