> ## Content Index
> Fetch the complete content index at: https://www.azurebrasil.cloud/llms.txt
> Use this file to discover other available public pages before exploring further.

# OAuth Identity Passthrough no Foundry Agent Service com MCP Server customizado em .NET - Parte 1
- URL: https://www.azurebrasil.cloud/blog/oauth-identity-passthrough-no-foundry-agent-service-com-mcp-server-customizado-em-net-parte-1/
- Published: 2026-07-02T13:25:00.000Z
- Updated: 2026-07-02T13:25:00.000Z
- Author: Talles Valiatti

O [OAuth Identity Passthrough](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/mcp-authentication?view=foundry&wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud) permite que o Foundry Agent Service chame um MCP Server em nome do usuário logado, preservando a identidade e os scopes que aquele usuário consentiu. A diferença em relação a uma integração comum é conceitualmente simples: o agente não chama o MCP Server usando uma API Key ou uma identidade genérica de aplicação. Ele chama o MCP Server usando um access token delegado do próprio usuário, emitido pelo [Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud). Isso significa que cada tool do MCP roda com as permissões reais da pessoa que está conversando com o agente, e não com um crachá compartilhado.

O fluxo de identidade que vamos montar percorre a seguinte cadeia, começando no usuário autenticado, passando pelo Foundry Agent Service, depois pelo App Registration client que representa o Foundry como OAuth client, seguindo para o Microsoft Entra ID que emite os tokens e terminando no MCP Server protegido, que aqui chamaremos de **custom-mcp-cars**. Essa cadeia é o coração do artigo, então vale ter ela em mente enquanto avançamos.

Antes de abrir o portal, é importante entender que o cenário usa dois [**App Registrations**](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud) distintos, e confundir os dois é a causa mais comum de erro nesse tipo de integração. O primeiro deles é o **custom-mcp-cars**, que representa o MCP Server como uma API protegida no **Microsoft Entra ID**. Ele é usado pela aplicação .NET Web API publicada no [**Azure App Service**](https://learn.microsoft.com/en-us/azure/app-service/overview?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud) e é responsável por expor a API, definir o Application ID URI, declarar os **scopes** e determinar o audience esperado nos tokens que o MCP Server recebe. O segundo é o **custom-mcp-cars-client**, que representa o OAuth client usado pelo Foundry. Ele é usado pelo [Foundry Agent Service](https://learn.microsoft.com/pt-br/azure/foundry/agents/overview?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud) e concentra o Client ID, o Client Secret, o Redirect URI, as API permissions e a solicitação de tokens em nome do usuário. Resumindo em uma linha, o **custom-mcp-cars** é o server e o **custom-mcp-cars-client** é o client. A diferença entre App Registration, application object e service principal está bem detalhada na documentação sobre [apps and service principals](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud), caso você queira aprofundar

Começamos criando o App Registration do server, que vamos chamar de custom-mcp-cars. Esse registro representa o MCP Server como recurso protegido no Microsoft Entra ID, e o passo a passo oficial para criar um App Registration está no [quickstart de registro de aplicações](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud).  

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/1.png)

Com o registro criado, o próximo passo é ir até a seção Expose an API do custom-mcp-cars e configurar o Application ID URI no formato **api://<server-app-client-id>**. Esse valor é fundamental, porque será exatamente o audience esperado nos tokens recebidos pelo MCP Server. O [guia oficial para expor uma Web API e criar scopes cobre esse ponto em detalhe](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud).

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/2.png)

  
Nessa mesma tela vamos criar apenas três scopes próprios da API, que são **Cars.Read**, **Cars.Write** e **Cars.Delete**. O Cars.Read permite consultar carros, o Cars.Write permite criar e atualizar carros e o Cars.Delete permite excluir carros. Ao criar cada scope, deixamos o campo de consentimento como Admins and users, para que o usuário comum também consiga consentir sem depender de um administrador.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/3-1.png)

Depois de criar os três, a tela de scopes deve listar exatamente **Cars.Read**, **Cars.Write** e **Cars.Delete**. Vale reforçar aqui um detalhe que costuma confundir: a API **custom-mcp-cars** possui somente esses três scopes próprios, e o offline\_access que aparecerá mais adiante não é um scope da API, e sim um scope [OIDC padrão](https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud) usado para habilitar refresh token.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/4-1.png)

Com a API exposta e os scopes definidos no portal, faz sentido olhar para o outro lado dessa relação, que é o código do MCP Server que vai validar esses tokens. A aplicação .NET já está pronta no repositório, então a ideia não é criar nada do zero, e sim entender como esse código existente funciona como um MCP Server HTTP protegido pelo Microsoft Entra ID. Ela expõe um endpoint /mcp, valida tokens emitidos para o custom-mcp-cars, confere o audience, trabalha com os scopes Cars.Read, Cars.Write e Cars.Delete e aplica autorização específica em cada tool. As tools formam um CRUD de carros e são **cars\_list**, **cars\_get**, **cars\_create**, **cars\_update** e **cars\_delete**, mapeadas respectivamente para **Cars.Read** nas leituras, **Cars.Write** nas escritas e **Cars.Delete** na exclusão.

O ponto de partida da aplicação é o **Program.cs**, onde a autenticação, a autorização e o próprio MCP Server são configurados.

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(options => options.AddCarsPolicies());

builder.Services.AddSingleton<CarStore>();

builder.Services
    .AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.Stateless = true;
    })
    .AddAuthorizationFilters()
    .WithToolsFromAssembly();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/healthz", () => Results.Ok(new
{
    status = "healthy",
    service = "cars-mcp"
}))
.AllowAnonymous();

app.MapMcp("/mcp")
   .RequireAuthorization("McpAccess");

app.Run();
```

- **AddMicrosoftIdentityWebApi()** protege a Web API validando os tokens do Microsoft Entra ID com base na seção AzureAd da configuração, conforme a [referência oficial do método](https://learn.microsoft.com/en-us/dotnet/api/microsoft.identity.web.microsoftidentitywebapiauthenticationbuilderextensions.addmicrosoftidentitywebapi?view=msal-model-dotnet-latest&wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud);
- **AddCarsPolicies()** registra as policies de autorização baseadas em scope que controlam o acesso ao MCP e a cada tool;
- **AddMcpServer()** com WithHttpTransport() configura o MCP Server usando o transport HTTP do [SDK C# do MCP](https://csharp.sdk.modelcontextprotocol.io/concepts/getting-started.html?ref=azurebrasil.cloud);
- **AddAuthorizationFilters()** liga os filtros de autorização do MCP para que os atributos \[Authorize\] das tools sejam respeitados, conforme documentado nos filters do SDK;
- **WithToolsFromAssembly()** descobre automaticamente todas as tools anotadas no assembly;
- **MapMcp("/mcp")** publica o endpoint do MCP Server exigindo a policy McpAccess, enquanto o /healthz permanece anônimo para health checks.

A configuração da seção **AzureAd** usada por esse código vive no appsettings.json e é o que amarra a aplicação ao App Registration server.

```json
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<Your_Tenant_Id>",
    "ClientId": "<Your_Client_Id>",
    "Audience": "<Your_Audience>"
  },
  "AllowedHosts": "*"
}
```

Aqui vale destacar que o TenantId, o ClientId e o Audience se referem ao [custom-mcp-cars](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-protected-web-api-app-configuration?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud), e não ao custom-mcp-cars-client. O MCP Server não valida tokens pelo Client ID do Foundry: ele valida tokens emitidos para o App Registration server, com audience igual ao Application ID URI do custom-mcp-cars. Essa é a essência da configuração de uma [Web API protegida em ASP.NET Core](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-protected-web-api-app-configuration?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud).

As policies mencionadas no Program.cs ficam concentradas em um método de extensão, e é nele que a validação de scope realmente acontece.

```csharp
public static void AddCarsPolicies(this AuthorizationOptions options)
{
    options.AddPolicy("McpAccess", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireAssertion(context =>
            context.User.HasAnyScope(
                CarsScopes.Read,
                CarsScopes.Write,
                CarsScopes.Delete));
    });

    options.AddPolicy("CarsRead", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireAssertion(context =>
            context.User.HasScope(CarsScopes.Read));
    });

    options.AddPolicy("CarsWrite", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireAssertion(context =>
            context.User.HasScope(CarsScopes.Write));
    });

    options.AddPolicy("CarsDelete", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireAssertion(context =>
            context.User.HasScope(CarsScopes.Delete));
    });
```

- **McpAccess** exige um usuário autenticado que tenha ao menos um dos três scopes, servindo como porta de entrada geral do endpoint /mcp.
- **CarsRead** exige especificamente o scope Cars.Read para operações de leitura.
- **CarsWrite** exige o scope Cars.Write para criação e atualização.
- **CarsDelete** exige o scope Cars.Delete para exclusão.

Esse desenho segue a authorization baseada em **policies do ASP.NET Core** e a recomendação de validar scopes em uma [API protegida](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-protected-web-api-verification-scope-app-roles?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud). A leitura do scope em si é feita por um par de extensões sobre o ClaimsPrincipal, que inspecionam a claim scp presente no token.

```csharp
public static bool HasScope(this ClaimsPrincipal user, string requiredScope)
{
    var scopeClaim =
        user.FindFirst("scp")?.Value ??
        user.FindFirst("http://schemas.microsoft.com/identity/claims/scope")?.Value;

    if (string.IsNullOrWhiteSpace(scopeClaim))
        return false;

    var scopes = scopeClaim.Split(' ', StringSplitOptions.RemoveEmptyEntries);

    return scopes.Contains(requiredScope, StringComparer.OrdinalIgnoreCase);
}

public static bool HasAnyScope(this ClaimsPrincipal user, params string[] requiredScopes)
{
    return requiredScopes.Any(user.HasScope);
}
```

- **HasScope()** lê a claim scp do token, separa os scopes por espaço e verifica se o scope exigido está presente, ignorando diferença de maiúsculas e minúsculas.
- **HasAnyScope()** retorna verdadeiro se qualquer um dos scopes informados estiver presente, e é usado pela policy McpAccess.

Com as policies prontas, cada tool declara qual delas precisa. A tool de listagem é o exemplo mais direto, porque é somente leitura e depende apenas do Cars.Read.

```csharp
[McpServerTool(Name = "cars_list", ReadOnly = true, Destructive = false)]
[Authorize(Policy = "CarsRead")]
[Description("Lists all cars. Requires the Cars.Read scope.")]
public static IReadOnlyCollection<Car> ListCars(CarStore store)
{
    return store.List();
}
```

**ListCars()** retorna todos os carros do CarStore e só executa se o token do usuário contiver o scope **Cars.Read**, graças ao atributo **\[Authorize(Policy = "CarsRead")\]**. O atributo **\[McpServerTool\]** marca o método como uma tool do MCP e traz metadados como ReadOnly e Destructive, descritos na referência do [McpServerTool](https://csharp.sdk.modelcontextprotocol.io/api/ModelContextProtocol.Server.McpServerTool.html?ref=azurebrasil.cloud).

A tool de criação eleva o requisito para o scope Cars.Write e ainda valida os dados de entrada antes de gravar.

```csharp
[McpServerTool(Name = "cars_create", ReadOnly = false, Destructive = false)]
[Authorize(Policy = "CarsWrite")]
[Description("Creates a new car. Requires the Cars.Write scope.")]
public static object CreateCar(
    CarStore store,
    [Description("The car brand. Example: Toyota.")] string brand,
    [Description("The car model. Example: Corolla.")] string model,
    [Description("The manufacturing year.")] int year)
{
    var validation = ValidateCarInput(brand, model, year);

    if (validation is not null)
        return validation;

    var car = store.Create(brand, model, year);

    return new
    {
        created = true,
        car
    };
}
```

**CreateCar()** valida marca, modelo e ano, cria o carro no CarStore e devolve o registro criado, exigindo o scope **Cars.Write** por meio da policy **CarsWrite**.  

Por fim, a tool de exclusão é a única marcada como Destructive e exige o scope mais restrito, o **Cars.Delete**.

```csharp
[McpServerTool(Name = "cars_delete", ReadOnly = false, Destructive = true)]
[Authorize(Policy = "CarsDelete")]
[Description("Deletes a car. Requires the Cars.Delete scope.")]
public static object DeleteCar(
    CarStore store,
    [Description("The car id.")] Guid id)
{
    var deleted = store.Delete(id);

    return new
    {
        deleted
    };
}
```

**DeleteCar()** remove o carro pelo identificador e retorna se a exclusão ocorreu, exigindo o scope Cars.Delete por meio da policy CarsDelete. 

Os dados ficam em um CarStore em memória que já vem populado com dez carros de exemplo, o que é suficiente para demonstrar o fluxo sem precisar de banco de dados.

  
Entendido o código, precisamos de um lugar público para hospedá-lo, já que o Foundry acessa o MCP Server por um endpoint HTTP remoto. Por isso criamos um Azure App Service que vai receber a aplicação .NET, e a configuração específica de runtime para ASP.NET Core está descrita na documentação de [configuração de apps .NET no App Service](https://learn.microsoft.com/en-us/azure/app-service/configure-language-dotnetcore?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud). O endpoint final esperado do MCP será algo como **https://<app-service-name>.azurewebsites.net/mcp.**

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/5.png)

Com o App Service criado, o deploy da aplicação é feito por um script que já existe no repositório, o **deploy.sh**. Ele publica o projeto em modo Release, empacota o resultado em um zip e envia para o App Service já existente, sem tocar em nenhuma configuração de ambiente.

```bash
#!/usr/bin/env bash
set -euo pipefail

# ─── Configuration ────────────────────────────────────────────────────────────
RESOURCE_GROUP_NAME="rg-cars-mcp"
APP_SERVICE_NAME="cars-mcp-app"
PROJECT_PATH="."
PUBLISH_DIR="./publish"
ZIP_PATH="./publish.zip"
DOTNET_CONFIGURATION="Release"
# ──────────────────────────────────────────────────────────────────────────────

echo "==> Checking Azure CLI login..."
az account show >/dev/null 2>&1 || az login

echo "==> Cleaning previous publish artifacts..."
rm -rf "$PUBLISH_DIR" "$ZIP_PATH"

echo "==> Publishing Cars.Mcp ($DOTNET_CONFIGURATION)..."
dotnet publish "$PROJECT_PATH" \
  --configuration "$DOTNET_CONFIGURATION" \
  --output "$PUBLISH_DIR"

echo "==> Creating deployment zip..."
(cd "$PUBLISH_DIR" && zip -r -q "../$(basename "$ZIP_PATH")" .)

echo "==> Deploying to App Service: $APP_SERVICE_NAME"
az webapp deploy \
  --resource-group "$RESOURCE_GROUP_NAME" \
  --name "$APP_SERVICE_NAME" \
  --src-path "$ZIP_PATH" \
  --type zip
```

O script primeiro garante o login no Azure, executando az login apenas se ainda não houver sessão ativa. Em seguida limpa artefatos de publicações anteriores, roda o dotnet publish em Release, gera o pacote zip e finalmente usa az webapp deploy para enviar o pacote ao App Service. Para rodar o deploy, basta abrir o terminal na pasta do projeto e executar o script.

```bash
./deploy.sh
```

Depois da publicação, dá para validar que a aplicação subiu acessando o endpoint **https://<app-service-name>.azurewebsites.net/healthz**, que responde de forma anônima, enquanto o endpoint protegido do MCP fica em **https://<app-service-name>.azurewebsites.net/mcp**.

Antes de conectar o Foundry, a aplicação publicada precisa saber a qual App Registration ela pertence, e isso é feito por variáveis de ambiente no Azure App Service, conforme a [documentação de configuração de app settings](https://learn.microsoft.com/en-us/azure/app-service/configure-common?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud). As quatro variáveis usadas são AzureAd\_\_Audience, AzureAd\_\_ClientId, AzureAd\_\_Instance e AzureAd\_\_TenantId, que correspondem exatamente à seção AzureAd do appsettings.json.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/Screenshot-2026-07-02-at-09.53.41.png)

O AzureAd\_\_Audience recebe o Application ID URI do custom-mcp-cars, no formato api://<server-app-client-id>. O AzureAd\_\_ClientId recebe o Client ID do custom-mcp-cars. O AzureAd\_\_Instance recebe https://login.microsoftonline.com/ e o AzureAd\_\_TenantId recebe o Tenant ID onde o App Registration foi criado. 

O ponto crítico aqui é o mesmo de antes: todos esses valores pertencem ao App Registration server **custom-mcp-cars**, e não ao **custom-mcp-cars-client**.

Com o server pronto e publicado, entramos na segunda metade da configuração, que é o lado do client. Criamos então o segundo App Registration, o custom-mcp-cars-client, que será usado pelo Foundry como OAuth client responsável por solicitar tokens para acessar o MCP Server em nome do usuário.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/Screenshot-2026-07-02-at-09.54.54.png)

Dentro do custom-mcp-cars-client, vamos até API permissions, escolhemos Add a permission, depois My APIs e selecionamos o **custom-mcp-cars**. Esse é o passo que conecta o client à API que criamos, e o procedimento está documentado no [guia de configuração de acesso a Web APIs](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-access-web-apis?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud).

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/7-1.png)

Na sequência, marcamos os três scopes que a API expõe, que são Cars.Read, Cars.Write e Cars.Delete. Essas são permissões delegadas, ou seja, permitem que o custom-mcp-cars-client solicite tokens para acessar o custom-mcp-cars em nome do usuário, e o conceito de delegated permissions e consentimento está detalhado na [visão geral de permissions and consent](https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud).

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/8.png)

Depois de adicionar, a lista de permissões mostra os três scopes delegados, ainda sem admin consent, o que é esperado neste cenário, já que o próprio usuário poderá consentir no momento da primeira chamada.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/9.png)

Como o Foundry precisa se autenticar como esse client, criamos também um Client Secret no custom-mcp-cars-client. O Foundry usará o Client ID em conjunto com esse Client Secret para executar o fluxo OAuth, e as boas práticas de criação e gestão de credenciais estão na documentação de [application credentials](https://learn.microsoft.com/en-us/entra/identity-platform/how-to-add-credentials?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud).

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/10.png)

Agora que temos server publicado e client configurado, partimos para o Foundry Agent Service, partindo do princípio de que o Foundry Project, o agent e o model deployment já existem e que o MCP Server já está publicado. Adicionamos uma tool MCP no Foundry apontando para o endpoint remoto e escolhendo OAuth Identity Passthrough como autenticação, seguindo o [guia de conexão de MCP tools remotas a agentes](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/model-context-protocol?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud). 

O nome da tool é custom-mcp-cars e o endpoint é **https://<app-service-name>.azurewebsites.net/mcp**. O Client ID e o Client Secret são os do custom-mcp-cars-client. A Auth URL fica em **https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize**, enquanto a Token URL e a Refresh URL apontam ambas para **https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token**, que são os endpoints padrão do [protocolo OAuth 2.0 no Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud).

Os scopes configurados no Foundry usam o formato completo com o Application ID URI, ficando **api://<server-app-client-id>/Cars.Read, api://<server-app-client-id>/Cars.Write, api://<server-app-client-id>/Cars.Delete** e, por último, **offline\_access**. Os três primeiros são os scopes próprios da API custom-mcp-cars e o offline\_access habilita o refresh token no fluxo OAuth. Um detalhe prático que evita dor de cabeça: os scopes devem ser separados por espaço, e não por vírgula.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/11.png)

Ao salvar essa configuração, o Foundry gera um Redirect URI próprio, normalmente no formato **https://global.consent.azure-apim.net/redirect/<id>**. Esse Redirect URI é o endereço para onde o fluxo OAuth vai devolver o authorization code que o Foundry troca por tokens, e ele precisa ser cadastrado no App Registration. Voltamos então ao **custom-mcp-cars-client**, entramos em Authentication, escolhemos Add platform, depois Web e adicionamos essa URL como Redirect URI, seguindo o procedimento oficial de [adição de redirect URI](https://learn.microsoft.com/en-us/entra/identity-platform/how-to-add-redirect-uri?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud) e respeitando as boas práticas de [reply URLs](https://learn.microsoft.com/en-us/entra/identity-platform/reply-url?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud). Vale reforçar que o Redirect URI é cadastrado no [custom-mcp-cars-client](https://learn.microsoft.com/en-us/entra/identity-platform/reply-url?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud), e nunca no [custom-mcp-cars.](https://learn.microsoft.com/en-us/entra/identity-platform/reply-url?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud)

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/12.1-1.png)

Com tudo conectado, é hora de testar no Agent Playground do Foundry. Pedimos algo simples que dependa do MCP, como listar todos os carros, e na primeira vez o Foundry responde pedindo consentimento, exibindo um botão de consent no próprio chat. Esse botão aparece porque o usuário ainda não autorizou o custom-mcp-cars-client a acessar o custom-mcp-cars em seu nome.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/13.png)

Ao clicar em Open consent, abre-se o fluxo de consentimento OAuth no navegador. Nesse momento o usuário está autorizando o client usado pelo Foundry a acessar o MCP Server em seu nome, e a experiência de consentimento no Microsoft Entra ID está descrita na [documentação de consent experience](https://learn.microsoft.com/en-us/entra/identity-platform/application-consent-experience?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud).

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/14.png)

A tela seguinte mostra os scopes solicitados e, dependendo da configuração do tenant, o consentimento pode ser individual ou para toda a organização, um tema aprofundado no guia de requesting [**permissions and consent**](https://learn.microsoft.com/en-us/entra/identity-platform/consent-types-developer?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud).

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/15.png)

Depois vem uma confirmação final exigida pelo Foundry. Assim que ela é aceita, o Microsoft Entra ID devolve o authorization code e o Foundry, atuando como Credential Manager, troca esse code por access token e refresh token.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/16.png)

Concluído esse passo, a tela indica que a autenticação foi finalizada, e a partir daí o Foundry consegue obter access tokens para chamar o MCP Server em nome do usuário sempre que precisar.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/17.png)

Voltando ao chat do Foundry, o consentimento já aparece concedido e o agente pode prosseguir com a solicitação original.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/18.png)

Voltando ao chat do Foundry, o consentimento já aparece concedido e o agente pode prosseguir com a solicitação original. Na sequência, o agente executa a chamada ao MCP Server e traz de volta os dados dos carros, mostrando que o fluxo delegado funcionou de ponta a ponta.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/19.png)

Para exercitar uma operação de escrita, pedimos ao agente que crie um novo carro, por exemplo um Fiat Argo Drive 1.0 (hatch) 2021\. Essa operação exige o scope Cars.Write, e o Foundry chama o MCP usando o token delegado do usuário, que por sua vez tem seu scope validado pela policy CarsWrite antes de executar a tool cars\_create.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/20-1.png)

O agente coleta os dados informados e monta a chamada correspondente à tool de criação.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/21-1.png)

Em seguida, o MCP Server responde com o resultado da criação, confirmando que o carro foi persistido no CarStore.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/22.png)

Por fim, pedimos novamente a listagem de todos os carros, agora já com o novo registro incluído. Como o usuário já consentiu anteriormente, o Foundry não precisa pedir consentimento de novo, e a operação, que exige apenas o Cars.Read, é atendida diretamente.

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/07/23.png)

Vale entender o que o MCP Server efetivamente recebe nessas chamadas. O access token entregue à API carrega claims importantes, sendo o aud igual a api://<server-app-client-id>, o scp contendo os scopes delegados concedidos como Cars.Read, Cars.Write e Cars.Delete, o tid identificando o tenant e o oid identificando o usuário. O aud indica para qual API o token foi emitido, o scp indica os scopes delegados presentes, o tid identifica o tenant e o oid identifica a pessoa. O MCP Server valida o audience e os scopes, e se o token não contiver o scope exigido pela tool, a chamada simplesmente não é autorizada.

Olhando o cenário como um todo, cada componente tem um papel bem definido. O Microsoft Entra ID autentica o usuário e emite os tokens. O Foundry Agent Service, atuando também como Credential Manager, executa o fluxo OAuth e gerencia consentimento, access token e refresh token. O custom-mcp-cars-client representa o app usado pelo Foundry para pedir os tokens. O custom-mcp-cars representa o MCP Server como API protegida. O Azure App Service hospeda a aplicação .NET, o MCP Server valida token, audience e scopes, e o código .NET autoriza cada tool conforme o scope necessário.

Chegamos assim ao fim da Parte 1, na qual criamos o Server App Registration custom-mcp-cars, expusemos a API, criamos os três scopes Cars.Read, Cars.Write e Cars.Delete, apresentamos o código existente do MCP Server, criamos o Azure App Service, fizemos o deploy via deploy.sh usando az login, configuramos as variáveis AzureAd\_\_ no App Service, criamos o Client App Registration custom-mcp-cars-client, configuramos as API permissions delegadas, geramos o client secret, configuramos o MCP no Foundry, registramos o Redirect URI no client, passamos pelo consentimento OAuth e executamos chamadas reais ao MCP pelo Foundry Agent Service. Na Parte 2, vamos sair do portal e criar aplicações que consomem esse mesmo fluxo de forma programática, integrando o Foundry Agent Service ao MCP Server com OAuth Identity Passthrough.

Você já pode baixar o projeto por esse [link](https://github.com/TallesValiatti/McpOAuthIdentityPassthrough?ref=azurebrasil.cloud), e não esquece de me seguir no [LinkedIn](https://www.linkedin.com/in/tallesvaliatti/?ref=azurebrasil.cloud)!

Até a próxima, abraços!