> ## 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 **OAuth exchanges you configure rather than write**:

  1. **The connector performs an 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, using the connector's own client credentials. You configure this on the connector's Security tab, and it happens in all three modes below.
  2. **The gateway performs a second exchange**, turning that token into one your MCP server accepts. You configure this on the MCP server's Auth Data. [Token passthrough](#part-1-token-passthrough) is the mode where this second exchange does not happen at all.

  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. 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**.
TrueFoundry agent identity is mirror identity that you add in TrueFoundry agent registry. The registry entry binds them by mapping a claim in the incoming Entra token to a TrueFoundry principal you can write policy against.

|              | **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**.

<Warning>
  **However please note that the Entra Agent ID does not appear in the tokens that is sent by agent to the gateway.** 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*.

## Authentication modes and identity flows

There are three ways in which TrueFoundry gateway supports authentication and token exchanges on this journey

|                                     | Token passthrough                   | On-behalf-of                   | Client credentials                   |
| ----------------------------------- | ----------------------------------- | ------------------------------ | ------------------------------------ |
| **Entra app registrations**         | **2**                               | 3                              | 3                                    |
| The gateway exchanges the token     | No                                  | Yes                            | Yes                                  |
| The gateway holds a secret of yours | **No**                              | Yes                            | Yes                                  |
| End user reaches your MCP server    | Yes                                 | Yes                            | **No**                               |
| The gateway can be bypassed         | **Yes** — network controls only     | No                             | No                                   |
|                                     | [Part 1](#part-1-token-passthrough) | [Part 2](#part-2-on-behalf-of) | [Part 3](#part-3-client-credentials) |

<Note>
  Each mechanism listed above impacts the ability to enforce governance, and hence please go through the [pros and cons](#what-the-gateway-is-worth-in-each-mode) and decide which ones suites you best.
</Note>

<Note>
  The CLI snippets in each part 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>

## Part 1: Token passthrough

The copilot connector obtains a token for your MCP server directly and the gateway forwards it unchanged, so there is no token exchange happening at gateway.

### Token flow

<Frame>
  <img src="https://mintcdn.com/truefoundry/1_g4h0P1Jf_pgfch/images/copilot-studio-passthrough-flow-light.svg?fit=max&auto=format&n=1_g4h0P1Jf_pgfch&q=85&s=1f712e0be8922412362f5e86f7928cdf" alt="Token passthrough flow: the user signs in to Copilot Studio, the Power Platform custom connector performs a single on-behalf-of exchange whose Resource URL is mcp-api rather than agent-service, producing a token already audienced to the MCP server that carries the user in oid and the agent in azp; the TrueFoundry MCP Gateway validates it, resolves the user and the agent, applies RBAC and audit, and forwards the same token unchanged; a dashed arrow shows the same token also reaching the MCP server without the gateway, which only network controls can prevent." className="block dark:hidden" width="860" height="502" data-path="images/copilot-studio-passthrough-flow-light.svg" />

  <img src="https://mintcdn.com/truefoundry/1_g4h0P1Jf_pgfch/images/copilot-studio-passthrough-flow-dark.svg?fit=max&auto=format&n=1_g4h0P1Jf_pgfch&q=85&s=d652e57a29c4f1742bee5da14595eb3d" alt="Token passthrough flow: the user signs in to Copilot Studio, the Power Platform custom connector performs a single on-behalf-of exchange whose Resource URL is mcp-api rather than agent-service, producing a token already audienced to the MCP server that carries the user in oid and the agent in azp; the TrueFoundry MCP Gateway validates it, resolves the user and the agent, applies RBAC and audit, and forwards the same token unchanged; a dashed arrow shows the same token also reaching the MCP server without the gateway, which only network controls can prevent." className="hidden dark:block" width="860" height="502" data-path="images/copilot-studio-passthrough-flow-dark.svg" />
</Frame>

### App registrations you need

| Registration   | Purpose                                                                                                      |
| -------------- | ------------------------------------------------------------------------------------------------------------ |
| `mcp-api`      | The audience of your MCP server. Your MCP server requires this audience on every token.                      |
| `agent-client` | The connector's credentials. Its client ID arrives as `azp` and is what identifies the agent at the gateway. |

<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>`. Add a **delegated scope** `Tool.Write` (`type: User`, state Enabled), and 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)

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

        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` **replaces the whole collection**. If the registration already has scopes, read the current value first and append to it.
        </Warning>
      </Tab>
    </Tabs>

    A v1 token carries `appid` instead of `azp` and a different issuer, which breaks the gateway's issuer match and its agent lookup at once — hence `requestedAccessTokenVersion: 2`.
  </Step>

  <Step title="mcp-api — authorize Azure API Connections">
    <Tabs>
      <Tab title="Portal">
        Under **Expose an API → Authorized client applications**, add `fe053c5f-3692-4f14-aef2-ee34fc081cae`, ticking `Tool.Write`.
      </Tab>

      <Tab title="Azure CLI">
        ```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\": [\"$WRITE_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>
      </Tab>
    </Tabs>

    That `fe053c5f-…` GUID is Microsoft's **Azure API Connections** service — the thing that obtains tokens on your users' behalf. Without it authorized here, 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://<mcp-api-app-id>/Tool.Write`, 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"

        WRITE_ID=$(az ad app show --id "$MCP_API" \
          --query "api.oauth2PermissionScopes[?value=='Tool.Write'].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 "$MCP_API" \
          --api-permissions "$WRITE_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.

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

    Use **Delegated** (`=Scope`), never Application (`=Role`). 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 `agent-client`: **API permissions → Grant admin consent**.
      </Tab>

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

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

    Mandatory. The connector's exchange runs with no user present to click a consent dialog, so an unconsented grant fails outright with `AADSTS65001` rather than prompting.
  </Step>
</Steps>

<Note>
  `agent-client`'s secret goes to the **Power Apps connector**. Nothing in this mode gives a secret to TrueFoundry — the gateway holds no credential of yours at all.
</Note>

### The custom connector

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://<mcp-api-app-id>`                           |
    | **Enable on-behalf-of login** | `true`                                             |
    | Scope                         | `api://<mcp-api-app-id>/Tool.Write offline_access` |

    **Resource URL is what makes this passthrough.** It names your MCP server, so the token comes out already accepted there and nothing downstream needs to change it.

    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. **This is the primary con with token passthrough approach as the connector now gets the broadest scope and there is no way to control that unless we make a token exchange hop through gateway.**
  </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>

### Register the MCP server on TrueFoundry

| Field         | Value                                                      |
| ------------- | ---------------------------------------------------------- |
| URL           | your MCP server's HTTPS endpoint — **not** the gateway URL |
| Auth Data     | **enabled**, set to **Token Passthrough**                  |
| Client ID     | —                                                          |
| Client Secret | —                                                          |
| Scopes        | —                                                          |

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

On the gateway's **Identity Provider**, list your MCP server's audience — both `api://<mcp-api-app-id>` and `<mcp-api-app-id>` — under **Allowed Audiences**. That is what the connector's token is minted for, and without it the gateway returns 401 before it ever calls your MCP server.

<Note>
  **Token Passthrough is a mode, not the absence of one.** Leave the **Auth Data** toggle on and select it — it sits alongside API Key, OAuth2 and AWS SigV4, and its description is exact: *"forward the user's existing auth token directly to the MCP server."* Switching Auth Data *off* is a different setting: the upstream call then carries no credential, your MCP server returns 401, and the symptom points at your server rather than at the gateway entry that caused it.
</Note>

### What your MCP server receives

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

Validate the audience, require `Tool.Write` in `scp`, and key the user on **`oid`** — not `sub`. In Entra `sub` is a pairwise identifier that differs per application, so a per-user store keyed on `sub` gives one person a different record depending on which mode was used to reach it.

`actor_app` is `agent-client`, the connector that actually called. This is the only mode where that is true; the other two overwrite it during their exchange.

<Note>
  `scp` carries **every scope `agent-client` was consented for** on `mcp-api`, not only the one the connector requested — Entra returns all consented scopes for a resource. The connector's consent is therefore the ceiling for every MCP server behind it.
</Note>

### What you can enforce

| Where               | Control                                                                                                                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Entra**           | Delegated permissions on `agent-client`; admin consent; Conditional Access on the user.                                                                                   |
| **Network**         | **Load-bearing here, unlike in the other two modes.** Your MCP server must accept traffic only from the gateway — private endpoint, VNet integration, or IP restrictions. |
| **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 from the verified user claims, plus per-*client* authorization from `azp` if you want it.                                                          |

### The governance gap with token passthrough

| Gap                                                                        | Why it exists                                                                                                                                                                         |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Gateway enforcement is advisory unless your network makes it mandatory** | The token is valid at your MCP server on its own, so RBAC configured at the gateway is only reached by callers who choose to go through the gateway.                                  |
| **Scope cannot be narrowed per MCP server**                                | Entra returns every scope the connector was *consented* for on that resource, not only the requested one. The connector's consent becomes the ceiling for every MCP server behind it. |
| **One connector per MCP server**                                           | `Resource URL` names a single resource, so a second MCP API means a second connector to build, share, and keep in sync.                                                               |

**On-behalf-of closes all three**, and closes them structurally rather than by configuration. The connector's token is audienced to `agent-service`, which your MCP server rejects — so the only route to your tools runs through the gateway, where the exchange happens. Bypass stops being something to prevent and becomes something that cannot be expressed. Scope narrowing moves to the gateway's per-server **Scopes** field, and one connector audience fans out to every MCP server behind it.

## Part 2: On-behalf-of

The signed-in user's identity is preserved all the way to your MCP server, and the gateway is on the token path by construction.

### 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.

### App registrations you need

Three. The third, `agent-service`, is the one passthrough does not need — and it is what makes the gateway unbypassable, because it gives the connector's token an audience your MCP server refuses.

| Registration    | Purpose                                                                                                      |
| --------------- | ------------------------------------------------------------------------------------------------------------ |
| `mcp-api`       | The audience of your MCP server. 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 — which is also why TrueFoundry needs no app registration of its own.
</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>`. Add a **delegated scope** `Tool.Write` (`type: User`, state Enabled), and 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)

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

        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` **replaces the whole collection**. If the registration already has scopes, read the current value first and append to it.
        </Warning>
      </Tab>
    </Tabs>

    Unlike Part 1, do **not** authorize Azure API Connections on `mcp-api` here. In this mode the connector's token targets `agent-service`, and the pre-authorization belongs there instead.
  </Step>

  <Step title="agent-service — the agent's audience, and the gateway's credential">
    <Tabs>
      <Tab title="Portal">
        Create the registration, 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`, plus **Delegated** → Microsoft Graph → `offline_access`. Then create a client secret — this is the one the gateway will hold. Set `requestedAccessTokenVersion: 2`.
      </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 permission from `mcp-api`, plus `offline_access` from Graph, then create the secret the gateway will hold:

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

        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>
      </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**. Keep them apart — the gateway has no reason to hold the connector's credential, and the connector has no reason to hold the gateway's.
</Note>

### The custom connector

Identical to Part 1's except for two fields on the Security tab. If you already built a passthrough connector, build a **second** one rather than editing it: `Resource URL` is single-valued, so changing it converts a connector from one mode to the other. Both can share the same `agent-client` registration.

<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 this MCP server entry'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 your MCP server and you have built Part 1 instead.

    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, then add it to the agent">
    *Can view* for end users, *Can edit* for makers. Then 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.

    Attach only one connector at a time. An agent with both a passthrough and an on-behalf-of connector has two sets of identically-named tools and picks between them unpredictably.
  </Step>
</Steps>

### 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.

On the gateway's **Identity Provider**, list `api://<agent-service-app-id>` and `<agent-service-app-id>` under **Allowed Audiences** — that is what the connector's token is minted for in this mode.

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. This is not theoretical: `sub` changes across the gateway's exchange while `oid` does not, so a per-user store keyed on `sub` gives one person a different record depending on which mode was used to reach it.

`actor_app` is `agent-service`, not the connector: the exchange overwrites it. Your MCP server can prove *an authorized agent* called, but not *which connector* — per-hop attribution lives in the gateway's audit trail.

### What you can enforce

| Where                | Control                                                                                                                                  |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Entra**            | Delegated permissions on `agent-service`; admin consent; Conditional Access on the user.                                                 |
| **The token itself** | Your MCP server rejects the connector's audience, so the gateway cannot be bypassed. No configuration can undo this, which is the point. |
| **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 3: Client credentials

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

Use it for tools that genuinely act under one authority — a shared mailbox, a batch job, a system-of-record write attributed to the service rather than the person. Not as a shortcut past a delegated setup.

### Token flow

Everything up to the gateway is **identical** to Part 2 — 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.

### App registrations you need

The same three as Part 2, plus one app role and one assignment. If you have already built Part 2, this mode adds to it rather than replacing anything — the connector and the inbound token are identical, and only the gateway's outbound call differs.

<Steps>
  <Step title="mcp-api — add an app role">
    <Tabs>
      <Tab title="Portal">
        Alongside the `Tool.Write` delegated scope, add an **app role** `Tool.Invoke` with `allowedMemberTypes: ["Application"]`.
      </Tab>

      <Tab title="Azure CLI">
        ```bash expandable theme={"dark"}
        OBJ=$(az ad app show --id "$MCP_API" --query id -o tsv)
        ROLE_ID=$(uuidgen)
        WRITE_ID=$(az ad app show --id "$MCP_API" \
          --query "api.oauth2PermissionScopes[?value=='Tool.Write'].id | [0]" -o tsv)

        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.\"
              }]
            },
            \"appRoles\": [{
              \"id\": \"$ROLE_ID\",
              \"value\": \"Tool.Invoke\",
              \"isEnabled\": true,
              \"allowedMemberTypes\": [\"Application\"],
              \"displayName\": \"Invoke tools\",
              \"description\": \"Call tools on the MCP server.\"
            }]
          }"
        ```

        <Warning>
          `oauth2PermissionScopes` and `appRoles` are each **replaced wholesale** by a PATCH, so this re-sends `Tool.Write` alongside the new role. If your registration has other scopes or roles, read the current collections and include them too — anything omitted is deleted.
        </Warning>
      </Tab>
    </Tabs>

    An app role, not a scope: application tokens carry `roles`, never `scp`. Your MCP server must accept either shape, or this mode fails validation on a token that is perfectly valid.
  </Step>

  <Step title="agent-service — request and be assigned the app role">
    <Tabs>
      <Tab title="Portal">
        Under **API permissions**, add from `mcp-api`: **Application** → `Tool.Invoke`. Then **Grant admin consent**.

        Make sure it is added as an **Application** permission, not a delegated one. Admin consent turns each Application permission 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.

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

        <Note>
          **`=Role` is what becomes the app-role assignment.** Admin consent 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>

    The permission and the assignment are different things. Without the assignment the client-credentials call fails with `AADSTS501051`, which names neither.
  </Step>

  <Step title="agent-client — unchanged">
    Nothing to do. The connector still obtains an `agent-service`-audienced user token exactly as in Part 2; the user is dropped later, at the gateway's outbound hop.
  </Step>
</Steps>

### The custom connector

**Unchanged from Part 2.** Same connector, same `Resource URL`, same scope. Point its `paths:` key at this MCP server entry's gateway path, or reuse the Part 2 connector and switch entries — the difference between the two modes is entirely on the gateway side.

### Register the MCP server on TrueFoundry

The **same** MCP server, registered again. Only two fields differ from Part 2:

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

## What the gateway is worth in each mode

The gateway does two separable jobs. It is a **control plane** — the catalog of which agent may call which server and tool, on whose behalf, under which guardrails, with an audit record naming both parties. And it is a **token broker** — the thing that converts a token your MCP server would reject into one it accepts.

The first job is identical in all three modes. Only the second changes, and every difference below follows from that.

|                                                 | Token passthrough (Part 1)                                                             | On-behalf-of (Part 2)                                                                    | Client credentials (Part 3)     |
| ----------------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------- |
| **Entra app registrations**                     | **2**                                                                                  | 3                                                                                        | 3                               |
| **The gateway is a token broker**               | **No** — forwards unchanged                                                            | Yes — one exchange per call                                                              | Yes — mints an app token        |
| **Can the gateway be bypassed?**                | **Yes.** The connector's token is accepted directly — only network controls prevent it | No. The connector's token is rejected by your MCP server, so there is no route around it | No. Same reason                 |
| **What enforces "must go through the gateway"** | Your network configuration                                                             | The token's audience                                                                     | The token's audience            |
| **Credentials the gateway holds**               | **None**                                                                               | `agent-service`'s client secret                                                          | `agent-service`'s client secret |
| **Scope narrowing per MCP server**              | No — fixed by the connector's consent                                                  | Yes, in the server entry's **Scopes**                                                    | Yes                             |
| **Connectors needed for N MCP servers**         | **N** — `Resource URL` is per-server                                                   | One                                                                                      | One                             |
| **End user at your MCP server**                 | Named and verified                                                                     | Named and verified                                                                       | **Not present**                 |
| **Authorized by**                               | `scp`                                                                                  | `scp`                                                                                    | `roles`                         |
| **`azp` at your MCP server**                    | **`agent-client` — the real client**                                                   | `agent-service` — the original client is **lost**                                        | `agent-service`                 |
| **Agent resolved at the gateway from**          | `azp`                                                                                  | `azp` — identical                                                                        | `azp` — identical               |
| **RBAC, guardrails, audit, tool catalog**       | Full                                                                                   | Full                                                                                     | Full                            |
| **Per-user policy at your MCP server**          | Yes                                                                                    | Yes                                                                                      | No user to apply it to          |

### Reading the table

**The last four rows are the same in all three modes.** Identity resolution, agent RBAC, Agent Access, guardrails, rate limits and audit do not depend on the gateway performing an exchange. This is the point most easily missed: passthrough does not reduce the gateway to a proxy.

**On-behalf-of buys enforcement you cannot get any other way.** Because your MCP server rejects the connector's token, the gateway is on the path by construction — not by configuration, not by convention, and not by anything an application team can accidentally undo. Nothing else in the table is worth as much in a large tenant, and it is why on-behalf-of is the default recommendation.

**Client credentials buys simplicity at the cost of the user.** There is no user in the token, so per-user authorization can only happen at the gateway. Choose it for tools that genuinely act under one shared authority, not as a shortcut past a delegated setup.

**Passthrough buys two real things, and they are not the ones usually claimed for it.** Not "less configuration" — it needs an extra connector per MCP server, which grows faster than the on-behalf-of setup does. What it buys is that the gateway holds **no credential of yours**, and that your MCP server sees the **real calling client** rather than the app that performed the last exchange. If your threat model cares more about what a compromised gateway could mint than about whether the gateway can be bypassed, that is a coherent choice.

**The two `delegated` modes are not interchangeable.** Passthrough and on-behalf-of both deliver a verified end user, which makes them look equivalent at a glance. They differ on the two rows that matter most — bypassability and `azp` — and they differ in opposite directions. Neither dominates.

## Limits and scaling

### Three 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. This is also why passthrough preserves `agent-client` at your MCP server: with no exchange, there is nothing to overwrite it.

**Passthrough moves one guarantee out of identity and into networking.** No gateway setting can restore it. If you cannot restrict your MCP server's network reachability to the gateway, do not use passthrough — the RBAC you configure will be advisory rather than enforced.

### 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.

Passthrough (Part 1) scales differently, and less well: `Resource URL` names one MCP server, so each MCP server needs its own connector. On-behalf-of adds a delegated permission on the shared `agent-service` instead. With a handful of MCP servers the difference is unimportant; with dozens it is the main argument for on-behalf-of on operational grounds alone.

## 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 **Expose an API → Authorized client applications** — on `agent-service` for Parts 2 and 3, on `mcp-api` for Part 1.                                                                                                    |
| 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.                                                                                                                                                                                                      |
| Passthrough: `401` from the gateway before any upstream call                  | The Identity Provider does not list the token's audience. Part 1's token is audienced to `mcp-api`; if you also run Part 2 or 3, list **both** `mcp-api` and `agent-service` spellings under Allowed Audiences.                                                              |
| Passthrough: the gateway accepts the call, then your MCP server returns `401` | Auth Data was switched **off** rather than set to **Token Passthrough**. Those are different settings, and only the second forwards the inbound header.                                                                                                                      |
| Passthrough: `scp` carries more scopes than the connector requested           | Expected. Entra returns every scope `agent-client` was consented for on that resource. Narrow the consent, not the request.                                                                                                                                                  |
| One mode stopped working after setting up another                             | A connector's `Resource URL` was edited rather than a second connector being created. That field is single-valued: changing it converts the connector from one mode to the other rather than adding a mode.                                                                  |
| 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.
