> ## 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 Copilot Studio agents calling MCP servers

> Route Copilot Studio agents through the TrueFoundry MCP Gateway so every tool call carries a verifiable identity — the signed-in user, the agent, or both.

Use this guide when you have agents in **Microsoft Copilot Studio** and MCP servers hosted in your own Azure subscription. Rather than letting the agent call those servers directly, you route the calls through the TrueFoundry MCP Gateway and enforce governance: it authenticates the caller, resolves it to a registered agent, applies your access policy, and performs the token exchange your MCP server requires.

Your MCP server does not have to do the heavy lifting of trusting each agent call, and every call is attributable in the audit trail.

<Info>
  The distinctive thing about Copilot Studio is that **there is no agent code**. You cannot add token logic to the agent. This is solved by **two OAuth exchanges, neither of which you have to code**:

  1. **The connector performs the first OAuth on-behalf-of exchange.** It does not simply forward a token — Power Platform's Azure API Connections service takes the user's Copilot Studio session and exchanges it for an access token audienced to your agent, using the connector's own client credentials. You configure this on the connector's Security tab.
  2. **The gateway performs the second exchange**, turning that token into one your MCP server accepts. You configure this on the MCP server's Auth Data.

  If you are coming from a stack where the agent performed its own exchange in code, there is no equivalent to that code here.
</Info>

## What you are setting up

| Component                | Who runs it        | Its job in this setup                                                                           |
| ------------------------ | ------------------ | ----------------------------------------------------------------------------------------------- |
| **Copilot Studio agent** | Microsoft          | Calls tools. Reaches them through a Power Platform custom connector.                            |
| **Custom connector**     | You, in Power Apps | Obtains a token for the agent on the signed-in user's behalf, and calls the gateway.            |
| **MCP Gateway**          | TrueFoundry        | Authenticates the caller, 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 Copilot Studio environment, and rights to create custom connectors in the **same Power Platform environment** as the agent.
* An MCP server in your Azure subscription that validates Entra JWTs and speaks **Streamable HTTP** (Server-Sent Events is no longer supported). To write one, see [Create OAuth app registration with Azure Entra](/docs/ai-gateway/mcp/mcp-server-oauth-azure).
* **Entra:** Cloud Application Administrator is the least-privilege role that covers creating the app registrations *and* granting admin consent.
* Entra registered as an identity provider in TrueFoundry. See [Identity Providers](/docs/platform/identity-providers).
* For the CLI path through the app registrations: Azure CLI signed in to the tenant (`az login`), plus `uuidgen`. The portal path needs neither.

## The two identities, and why there are two

"The agent's identity" means two different objects in two different systems.

|              | **Entra identity**                                        | **TrueFoundry agent identity**                                                                |
| ------------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| Lives in     | Microsoft Entra ID                                        | The TrueFoundry Agent Registry                                                                |
| What it does | Lets the agent authenticate — what Entra issues tokens to | Lets the gateway authorize the agent — which servers, which tools, which users it may act for |
| Governed by  | Conditional Access, consent grants                        | Collaborator grants, guardrails, rate limits                                                  |

Entra decides **who the agent is**; TrueFoundry decides **what that agent may do, and on whose behalf**. The registry entry binds them by mapping a claim in the incoming Entra token to a TrueFoundry principal you can write policy against.

### Copilot Studio and Entra Agent ID

Copilot Studio creates an **Entra Agent ID** for each agent automatically. You will find it under **Entra admin center → Agents**, with the person who created the agent recorded as its **sponsor**, and governed by a Microsoft-published **agent identity blueprint**.

<Warning>
  **That Agent ID does not appear in the tokens your gateway sees.** Power Platform's connector infrastructure acquires tokens using the connector's own app registration, and the blueprint behind the Copilot Studio agent identity belongs to Microsoft — so nothing in this path can mint a token as that identity. Map your TrueFoundry agent to the **connector application ID**, which is what actually arrives.
</Warning>

### What this leads to

Because the Copilot Studio agent is a **managed** identity you cannot address, two objects in this setup are stand-ins for it:

| Object                      | What it stands in for           | Why it exists                                                                                                                                            |
| --------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent-client` (Entra app)  | the agent's **client identity** | The connector needs OAuth credentials and its client ID doubles as the agent's identity at the gateway.                                                  |
| The TrueFoundry agent entry | the agent itself                | It is connected to `agent-client` and acts as a **mirror agent** for the Copilot Studio agent, which Microsoft manages and you cannot register directly. |

* **Use the Agent ID for** — inventory, the sponsor relationship, and Conditional Access on the agent's own sign-ins within Microsoft 365.
* **Use the TrueFoundry agent identity for the call path** — which MCP servers and tools the agent may reach, and whom it may act for. That is the part the Agent ID cannot express.

<Info>
  If you need the agent's Entra identity to reach the MCP server itself, the agent has to be one that can perform its own token exchange — an Azure AI Foundry or custom-hosted agent, not Copilot Studio.
</Info>

### Mapping to a TrueFoundry 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-client`** application ID — what arrives as `azp`                  |
| **Config**            | Embedded — no callable URL                                                     |
| **Access Control**    | Owner team, plus the users or teams the agent may act for, as **Agent Access** |

**Embedded** because Microsoft hosts the agent: the gateway is not in front of the agent, it is in front of the agent's *tools*. One registry entry covers both modes below — unlike some other agent platforms, the claim the gateway matches on is the same either way.

## App registrations you need

Three, and all three are needed for both modes.

| Registration    | Purpose                                                                                                                                                           |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mcp-api`       | The audience of your MCP server. Exposes a delegated scope (`Tool.Write`) and an app role (`Tool.Invoke`). Your MCP server requires this audience on every token. |
| `agent-service` | The audience the connector's token targets, and the credential the gateway exchanges with.                                                                        |
| `agent-client`  | The connector's credentials. Its client ID arrives as `azp` and is what identifies the agent at the gateway.                                                      |

<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.
</Note>

### Creating them

Each step below can be done in the Entra portal or with the Azure CLI — the two tabs produce the same result, so pick one and stay on it.

<Note>
  The CLI snippets are written to run in **one shell session**, in order: later steps reuse `$MCP_API`, `$AGENT_SVC`, and `$AGENT_CLIENT` from earlier ones. If you start a new shell, re-derive them with `az ad app list --display-name <name> --query "[0].appId" -o tsv`.
</Note>

<Steps>
  <Step title="mcp-api — the tool's audience">
    <Tabs>
      <Tab title="Portal">
        Create the registration and set its Application ID URI to `api://<mcp-api-app-id>`. Then add:

        * a **delegated scope** `Tool.Write` (`type: User`, state Enabled) — used by on-behalf-of
        * an **app role** `Tool.Invoke` with `allowedMemberTypes: ["Application"]` — used by client credentials

        Set `requestedAccessTokenVersion: 2` in the manifest.
      </Tab>

      <Tab title="Azure CLI">
        ```bash expandable theme={"dark"}
        MCP_API=$(az ad app create --display-name mcp-api \
          --sign-in-audience AzureADMyOrg --query appId -o tsv)
        az ad sp create --id "$MCP_API"

        OBJ=$(az ad app show --id "$MCP_API" --query id -o tsv)
        WRITE_ID=$(uuidgen)
        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,
              \"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.\"
              }]
            },
            \"appRoles\": [{
              \"id\": \"$ROLE_ID\",
              \"value\": \"Tool.Invoke\",
              \"isEnabled\": true,
              \"allowedMemberTypes\": [\"Application\"],
              \"displayName\": \"Invoke tools\",
              \"description\": \"Call tools on the MCP server.\"
            }]
          }"
        ```

        The service principal is required. Without one the application cannot be consented to, and cannot be named as a resource in a token request.

        <Warning>
          Sending `oauth2PermissionScopes` or `appRoles` **replaces the whole collection**. If the registration already has either, read the current value first and append to it.
        </Warning>
      </Tab>
    </Tabs>

    The delegated scope serves on-behalf-of and the app role serves client credentials, so `mcp-api` needs both even if you only run one mode today.
  </Step>

  <Step title="agent-service — the agent's audience, and the gateway's credential">
    <Tabs>
      <Tab title="Portal">
        Create the registration and set its Application ID URI to `api://<agent-service-app-id>` and add a delegated scope `access_as_user`. Then, under **Expose an API → Authorized client applications**, add `fe053c5f-3692-4f14-aef2-ee34fc081cae`, ticking `access_as_user`.

        Under **API permissions**, add from `mcp-api`:

        * **Delegated** → `Tool.Write` (for on-behalf-of)
        * **Application** → `Tool.Invoke` (for client credentials)

        plus **Delegated** → Microsoft Graph → `offline_access`. Then create a client secret — this is the one the gateway will hold. Set `requestedAccessTokenVersion: 2`.

        Make sure `Tool.Invoke` is added as an **Application** permission, not a delegated one. Admin consent in the last step turns it into the app-role assignment on `agent-service`'s own service principal — which is what the client-credentials path needs. There is no separate screen for that assignment: **Enterprise applications → `mcp-api` → Users and groups** takes only users and groups, so the API-permission plus admin-consent pair above is how an application gets assigned.

        <Note>
          **To verify:** on `agent-service` → **API permissions**, the `Tool.Invoke` row should read type **Application** with Status **"Granted for \<tenant>"**. A "Not granted" warning there is the same condition that surfaces later as `AADSTS501051`. Granting only the delegated `Tool.Write` leaves it missing, because a delegated grant contributes `scp` to a *user* token and nothing at all to an app-only one.
        </Note>
      </Tab>

      <Tab title="Azure CLI">
        Create it, then set the identifier URI, v2 tokens, and the `access_as_user` scope:

        ```bash expandable theme={"dark"}
        AGENT_SVC=$(az ad app create --display-name agent-service \
          --sign-in-audience AzureADMyOrg --query appId -o tsv)
        az ad sp create --id "$AGENT_SVC"

        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.\"
              }]
            }
          }"
        ```

        Pre-authorize Microsoft's Azure API Connections service on that scope — the portal equivalent of **Authorized client applications**:

        ```bash expandable theme={"dark"}
        # Microsoft's Azure API Connections service
        APIM_CONNECTIONS=fe053c5f-3692-4f14-aef2-ee34fc081cae

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

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

        Add the permissions from `mcp-api`, plus `offline_access` from Graph:

        ```bash expandable theme={"dark"}
        WRITE_ID=$(az ad app show --id "$MCP_API" \
          --query "api.oauth2PermissionScopes[?value=='Tool.Write'].id | [0]" -o tsv)
        ROLE_ID=$(az ad app show --id "$MCP_API" \
          --query "appRoles[?value=='Tool.Invoke'].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_SVC" --api "$MCP_API" \
          --api-permissions "$WRITE_ID=Scope" "$ROLE_ID=Role"
        az ad app permission add --id "$AGENT_SVC" --api "$GRAPH" \
          --api-permissions "$OFFLINE=Scope"
        ```

        Then the secret the gateway will hold:

        ```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>

        <Note>
          **`$ROLE_ID=Role` above is what becomes the app-role assignment.** Admin consent in the last step turns each `Role`-type permission into an assignment on `agent-service`'s own service principal — that is the `Tool.Invoke` assignment the client-credentials path needs. Granting only the delegated `Tool.Write` leaves it missing, and the failure is `AADSTS501051`.
        </Note>
      </Tab>
    </Tabs>

    That `fe053c5f-…` GUID is Microsoft's **Azure API Connections** service — the thing that obtains tokens on your users' behalf. Without it authorized on `access_as_user`, users get an interactive sign-in prompt on *every call* instead of a silent hand-off, which presents as a broken connector rather than a missing grant.
  </Step>

  <Step title="agent-client — the connector's credentials">
    <Tabs>
      <Tab title="Portal">
        Create the registration and add a **Delegated** permission on `api://<agent-service>/access_as_user`, plus Microsoft Graph `offline_access`. Create a client secret. Under **Authentication → Web → Redirect URIs**, add `https://global.consent.azure-apim.net/redirect`.
      </Tab>

      <Tab title="Azure CLI">
        ```bash expandable 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"

        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 update --id "$AGENT_CLIENT" \
          --web-redirect-uris "https://global.consent.azure-apim.net/redirect"

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

        That redirect URI is fixed — it is where Power Platform returns the consent response, not a URL of yours.
      </Tab>
    </Tabs>

    Use **Delegated** (`=Scope`), never Application (`=Role`), throughout this chain. An application permission yields a token with no user to delegate — it validates cleanly and silently drops the person you were representing.
  </Step>

  <Step title="Grant admin consent">
    <Tabs>
      <Tab title="Portal">
        On **both** `agent-service` and `agent-client`: **API permissions → Grant admin consent**.
      </Tab>

      <Tab title="Azure CLI">
        ```bash theme={"dark"}
        az ad app permission admin-consent --id "$AGENT_SVC"
        az ad app permission admin-consent --id "$AGENT_CLIENT"
        ```

        Allow a moment for the grants to propagate before your first call.
      </Tab>
    </Tabs>

    This is mandatory. A missing grant surfaces as `AADSTS65001` from the token endpoint.
  </Step>
</Steps>

<Note>
  `agent-service`'s secret goes to **TrueFoundry**. `agent-client`'s secret goes to the **Power Apps connector**.
</Note>

## The custom connector

The connector is where the agent's token is obtained. Build it in **Power Apps**, not with Copilot Studio's MCP onboarding wizard — the wizard's OAuth options are all authorization-code and have no on-behalf-of switch, so the user's identity does not carry through.

<Steps>
  <Step title="Import a definition">
    ```yaml theme={"dark"}
    swagger: '2.0'
    info:
      title: My MCP Server
      version: '1.0.0'
    host: <gateway-host>
    basePath: /
    schemes: [https]
    paths:
      /<gateway-path-for-this-server>:
        post:
          summary: MCP server
          operationId: InvokeMCP
          x-ms-agentic-protocol: mcp-streamable-1.0
          responses:
            '200':
              description: Success
    ```

    Split the gateway URL from the MCP server's **How To Use** tab into `host` and the key under `paths:`. Keep `basePath: /`.

    <Warning>
      Put the whole path under `paths:` and leave `basePath: /`. Folding the path into `basePath` and leaving `paths: /` yields an identical URL but places the operation at the **root** path, which Power Platform does not register as an MCP operation — the connector imports cleanly, the connection succeeds, and the agent's tool list is silently empty.
    </Warning>
  </Step>

  <Step title="Configure the Security tab">
    | Field                         | Value                                                        |
    | ----------------------------- | ------------------------------------------------------------ |
    | Authentication type           | **OAuth 2.0**                                                |
    | Identity Provider             | **Microsoft Entra ID**                                       |
    | Client ID / secret            | **`agent-client`**                                           |
    | Authorization URL             | `https://login.microsoftonline.com`                          |
    | Tenant ID                     | your tenant GUID                                             |
    | **Resource URL**              | `api://<agent-service-app-id>`                               |
    | **Enable on-behalf-of login** | `true`                                                       |
    | Scope                         | `api://<agent-service-app-id>/access_as_user offline_access` |

    **Resource URL is the field to get right.** It is `agent-service` — not your MCP server, and not the gateway. Point it at the MCP and the gateway has nothing left to exchange.

    One resource only in `Scope`: Entra derives the audience from the resource of the requested scopes and rejects a request naming two. `offline_access` is resource-agnostic and yields the refresh token.
  </Step>

  <Step title="Share the connector">
    *Can view* for end users, *Can edit* for makers. Without this the connector works for its author and fails for everyone else, with an error that says nothing about sharing. (Sharing is not needed for yourself — the owner can always use their own connector.)
  </Step>

  <Step title="Add it to the agent">
    In Copilot Studio: **Settings → Generative AI → Orchestration = Generative** *first*, then **Tools → Add a tool → Connector →** your connector → **Add to agent**, and create a connection.

    With classic orchestration the agent never calls MCP tools at all — the tool appears attached and is silently ignored. And because on-behalf-of is on, creating the connection shows a short **consent card** rather than a full sign-in page; a full sign-in page means on-behalf-of is not active.
  </Step>
</Steps>

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

Both modes share the same connector, the same agent, and the same MCP server. **Only the gateway's outbound exchange differs** — so you register the MCP server twice and point the connector at whichever entry you want.

| If the tool should…                         | Use                | The MCP server sees                          |
| ------------------------------------------- | ------------------ | -------------------------------------------- |
| Apply the signed-in person's permissions    | On-behalf-of       | The user, plus the agent that acted for them |
| Act under one shared authority for everyone | Client credentials | The agent, acting under its own authority    |

## Part 1: On-behalf-of

The signed-in user's identity is preserved all the way to your MCP server.

### Token flow

<Frame>
  <img src="https://mintcdn.com/truefoundry/P57EnmleOCNnD-vs/images/copilot-studio-obo-flow-light.svg?fit=max&auto=format&n=P57EnmleOCNnD-vs&q=85&s=dabeff97ebd56c9670b34282227ecba6" alt="On-behalf-of flow: the user signs in to Copilot Studio, the Power Platform custom connector performs an OAuth on-behalf-of exchange via Azure API Connections producing a token audienced to agent-service that carries the user in oid and the agent in azp, the TrueFoundry MCP Gateway resolves both identities and performs a second on-behalf-of exchange as agent-service, and the MCP server receives a delegated token whose audience is mcp-api and whose user is unchanged." className="block dark:hidden" width="860" height="512" data-path="images/copilot-studio-obo-flow-light.svg" />

  <img src="https://mintcdn.com/truefoundry/P57EnmleOCNnD-vs/images/copilot-studio-obo-flow-dark.svg?fit=max&auto=format&n=P57EnmleOCNnD-vs&q=85&s=7721f292612b4b5d4b5f484f9d94292b" alt="On-behalf-of flow: the user signs in to Copilot Studio, the Power Platform custom connector performs an OAuth on-behalf-of exchange via Azure API Connections producing a token audienced to agent-service that carries the user in oid and the agent in azp, the TrueFoundry MCP Gateway resolves both identities and performs a second on-behalf-of exchange as agent-service, and the MCP server receives a delegated token whose audience is mcp-api and whose user is unchanged." className="hidden dark:block" width="860" height="512" data-path="images/copilot-studio-obo-flow-dark.svg" />
</Frame>

Point to note: **the audience moves to your MCP server, the user stays the same.** That is delegation. If the user changes, something is impersonating rather than delegating; if the user disappears, you have an application token and the wrong grant type.

### Register the MCP server on Truefoundry

| Field          | Value                                                             |
| -------------- | ----------------------------------------------------------------- |
| URL            | your MCP server's HTTPS endpoint — **not** the gateway URL        |
| Auth Data      | **OAuth2**                                                        |
| OAuth Provider | **Microsoft Entra**                                               |
| Grant Type     | **JWT Bearer**                                                    |
| Token URL      | `https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token` |
| Client ID      | **`agent-service`**                                               |
| Client Secret  | `agent-service`'s secret — store as a `tfy-secret://` FQN         |
| Scopes         | `api://<mcp-api-app-id>/Tool.Write`                               |

Then add `agent:<your-agent>` as an **MCP Server User** collaborator.

Three fields are easy to get wrong:

* **Grant Type = JWT Bearer**, not *Token Exchange*. Token Exchange is the RFC 8693 profile other identity providers use; Entra implements RFC 7523 (`jwt-bearer` + `requested_token_use=on_behalf_of`) and rejects the other shape.
* **Client ID = `agent-service`**, matching the inbound token's audience. Anything else gives `AADSTS50013`.
* **Scopes names the destination** — your MCP server. Point it elsewhere and the exchange *succeeds* while your MCP server rejects the audience, which reads like an MCP bug.

### 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`. Use **`oid`** as the stable key for the user, not `sub` — in Entra `sub` is a pairwise identifier that differs per application, so the same person gets a different `sub` at every audience.

### What you can enforce

| Where               | Control                                                                                                                          |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **Entra**           | Delegated permissions on `agent-service`; admin consent; Conditional Access on the user.                                         |
| **MCP Gateway**     | Which servers and tools the agent may call; **which users it 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 — the main reason to choose on-behalf-of over client credentials.
</Tip>

## Part 2: Client credentials

The gateway calls your MCP server under one shared authority. The user is authenticated at the gateway and then deliberately not propagated.

### Token flow

Everything up to the gateway is **identical** to Part 1 — same connector, same TOKEN A, same inbound validation and RBAC. Only exchange 2 changes.

<Frame>
  <img src="https://mintcdn.com/truefoundry/P57EnmleOCNnD-vs/images/copilot-studio-client-credentials-flow-light.svg?fit=max&auto=format&n=P57EnmleOCNnD-vs&q=85&s=0dc5f4ae57dd89df89b33a077a225cee" alt="Client credentials flow: everything above the gateway is identical to on-behalf-of, the TrueFoundry MCP Gateway resolves both the user and the agent and applies RBAC, then exchanges via client credentials as agent-service so the MCP server receives an application token carrying the Tool.Invoke role and no user claims." className="block dark:hidden" width="860" height="348" data-path="images/copilot-studio-client-credentials-flow-light.svg" />

  <img src="https://mintcdn.com/truefoundry/P57EnmleOCNnD-vs/images/copilot-studio-client-credentials-flow-dark.svg?fit=max&auto=format&n=P57EnmleOCNnD-vs&q=85&s=8796d6f804c37ac43748c056d62b889b" alt="Client credentials flow: everything above the gateway is identical to on-behalf-of, the TrueFoundry MCP Gateway resolves both the user and the agent and applies RBAC, then exchanges via client credentials as agent-service so the MCP server receives an application token carrying the Tool.Invoke role and no user claims." className="hidden dark:block" width="860" height="348" data-path="images/copilot-studio-client-credentials-flow-dark.svg" />
</Frame>

The user is lost at the gateway's outbound hop, not earlier. That matters for two reasons: the gateway's audit trail still records **which user** made every call, and per-user policy still applies *at the gateway* even though your MCP server cannot see the person.

An MCP server that reports "no user" here is behaving correctly, not failing.

### Register the MCP server

The **same** MCP server, registered a second time. Only two fields differ from Part 1:

| Field              | Value                                 |
| ------------------ | ------------------------------------- |
| URL                | the same endpoint as Part 1           |
| Grant Type         | **Client Credentials**                |
| Client ID / Secret | `agent-service` — the same credential |
| Scopes             | `api://<mcp-api-app-id>/.default`     |

**`/.default`, not `Tool.Write`.** An app-only grant cannot request named delegated scopes; it resolves whatever app roles the service principal holds — which is why `Tool.Invoke` had to be *assigned* to `agent-service`, not merely granted.

Add the same agent as an **MCP Server User** here too.

### 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**           | The `Tool.Invoke` app-role assignment on `agent-service`; Conditional Access on the user's sign-in to Copilot Studio.                    |
| **Azure**           | Azure RBAC assignments made to `agent-service`'s service principal, for any Azure resource your MCP server fronts.                       |
| **MCP Gateway**     | Which servers and tools the agent may call; which users may drive it; rate limits; guardrails; audit naming both the user and the agent. |
| **Your MCP server** | Application-level authorization only. There is no user in the token to key on.                                                           |

## RBAC you can set using the agent identity

Once an agent resolves at the gateway it becomes a first-class principal in your access policy, alongside users, teams and virtual accounts.

| Grant                                                                           | Where you set it           | What it decides                                           |
| ------------------------------------------------------------------------------- | -------------------------- | --------------------------------------------------------- |
| **MCP Server User** on a server                                                 | The server's Collaborators | Which MCP servers this agent may call at all              |
| Tool subset via a [Virtual MCP Server](/docs/ai-gateway/mcp/virtual-mcp-server) | Virtual server definition  | Which *tools* on a server the agent may call              |
| **Agent Access** on the agent                                                   | The agent's Access Control | Which users the agent may **act for**                     |
| **Agent Manager** on the agent                                                  | The agent's Access Control | Who may edit the agent's registration                     |
| **Owner** team                                                                  | The agent's Access Control | Who is accountable for it, separately from who may use it |
| Guardrails and tool approvals                                                   | Per server or per tool     | Content checks and human-in-the-loop on sensitive tools   |

Two properties are what a per-credential model cannot give you:

**Access and delegation are separate grants.** Being a collaborator on an MCP server lets the agent reach it. **Agent Access** decides whom it may act for once there. A user with no Agent Access cannot be acted for — even if that same user could reach the MCP server directly. Both must hold.

**Every hop is checked against the agent actually calling.** In a chain of agents, each needs its own access to the target and its own permission to act for the user. Nothing is inherited from the caller, so a chain cannot accumulate reach that no single agent was granted.

## Comparison and limits

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

### Two limits worth knowing

**The Copilot Studio Entra Agent ID never reaches your MCP server** — and cannot be registered directly anywhere in this chain. If you need the agent's own Entra identity to travel with the call, the agent must be one that performs its own token exchange — Azure AI Foundry or custom-hosted, not Copilot Studio.

**Entra emits no nested delegation chain.** `azp` is single-valued and is overwritten at each exchange, so the token your MCP server receives names the app that performed the *last* exchange — `agent-service` — not every hop before it. Per-hop attribution lives in the gateway's 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 `agent-client` ID in the token. A second agent needs its own `agent-client` plus one Agent Registry entry; `mcp-api` and `agent-service` are shared.

## Troubleshooting

| Symptom                                         | Cause                                                                                                                                                                                                                                                                        |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No tools appear in the agent                    | The connector operation is at the root path, or `x-ms-agentic-protocol` was dropped when saving, or generative orchestration is off. All three present identically as an empty list.                                                                                         |
| Interactive sign-in on every call               | Azure API Connections (`fe053c5f-…`) not listed under `agent-service` → Expose an API → Authorized client applications.                                                                                                                                                      |
| Users re-prompted roughly hourly                | `offline_access` not requested or not admin-consented.                                                                                                                                                                                                                       |
| `401` from the gateway before any upstream call | The token validated but resolved to no principal. Check the issuer matches exactly (including `/v2.0`), that allowed audiences list `agent-service`, that the registry Subject value is the `agent-client` ID, and that the resolved user exists in your TrueFoundry tenant. |
| `403` from the gateway                          | The agent is not an **MCP Server User** on that server, or the user has no **Agent Access** on the agent.                                                                                                                                                                    |
| `AADSTS50013`                                   | The inbound token's audience is not the exchanging client. The connector's Resource URL and the gateway's Client ID must both be `agent-service`.                                                                                                                            |
| `AADSTS65001`                                   | Admin consent missing.                                                                                                                                                                                                                                                       |
| `AADSTS501051`                                  | The `Tool.Invoke` app role is not assigned to `agent-service`'s service principal (client-credentials path).                                                                                                                                                                 |
| MCP server rejects the audience                 | The gateway's outbound **Scopes** is not pointing at `api://<mcp-api>`.                                                                                                                                                                                                      |
| MCP server rejects the issuer                   | The registration is still emitting v1 tokens — set `requestedAccessTokenVersion: 2`.                                                                                                                                                                                         |
| On-behalf-of yields a token with no user        | An Application permission was granted where a Delegated one was needed.                                                                                                                                                                                                      |
| Worked, then broke after rotating a secret      | Re-save the MCP server entry. Credentials bind when the entry is saved, so updating only the stored secret leaves the gateway using the old one.                                                                                                                             |

## Next steps

* [Agent Registry](/docs/agent-platform/agent-governance/agent-registry) — the full registration flow.
* [Agent and its identity with TrueFoundry](/docs/ai-gateway/agents/agent-identity) — how agent identity differs from a user or a virtual account, and how delegation is bounded.
* [Agent Governance with Microsoft Entra](/docs/agent-platform/agent-governance/scenarios/microsoft-entra) — the wider Entra scenario.
* [MCP Gateway authentication and security](/docs/ai-gateway/mcp/mcp-gateway-auth-security) — every inbound and outbound option, not just these two.
