> ## 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 2
- URL: https://www.azurebrasil.cloud/blog/oauth-identity-passthrough-no-foundry-agent-service-com-mcp-server-customizado-em-net-parte-2/
- Published: 2026-07-07T11:59:18.000Z
- Updated: 2026-07-07T11:59:17.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 aquela pessoa consentiu. Na [Parte 1](https://www.azurebrasil.cloud/blog/oauth-identity-passthrough-no-foundry-agent-service-com-mcp-server-customizado-em-net-parte-1/) percorremos todo esse fluxo pelo portal e pelo Agent Playground, criando os dois App Registrations, expondo a API, publicando o MCP Server no Azure App Service e consentindo como usuário para executar chamadas reais ao MCP pelo chat do agente. 

Nesta Parte 2 saímos do Playground e construímos uma .NET Web App de verdade, com um backend em ASP.NET Core conversando com o Foundry e um frontend em HTML, CSS e JavaScript exibindo um chatbot. A ideia é que a própria aplicação envie as mensagens ao agente, detecte quando o Foundry pede consentimento OAuth e conduza o usuário por esse consentimento sem depender do portal.

Antes de escrever qualquer linha de código, vale recuperar rapidamente o que já existe desde a [Parte 1](https://www.azurebrasil.cloud/blog/oauth-identity-passthrough-no-foundry-agent-service-com-mcp-server-customizado-em-net-parte-1/). O MCP Server customizado já está publicado no Azure App Service, o App Registration server é o custom-mcp-cars, o App Registration client é o custom-mcp-cars-client e a API expõe os três scopes Cars.Read, Cars.Write e Cars.Delete. O Foundry Agent já foi configurado com um Custom MCP usando OAuth Identity Passthrough, e o teste dentro do portal foi feito lá, então aqui esse teste não será repetido. O foco agora é o consumo programático desse mesmo fluxo.

O primeiro passo prático da Parte 2 é remover a connection antiga que criamos no Foundry durante a Parte 1\. O motivo é começar esta parte com uma connection limpa, garantindo que o consentimento seja pedido de novo a partir da aplicação e que o comportamento observado seja fiel a um cenário recém configurado, sem estado de consentimento herdado do Playground. Para deletar uma connection de projeto no Foundry precisamos de cinco informações, que são o subscriptionId, o resourceGroupName, o accountName, o projectName e o connectionName. O accountName corresponde ao recurso do Foundry, o projectName ao projeto dentro dele e o connectionName ao nome que demos à connection MCP na Parte 1.  
Com esses dados em mãos, a remoção é feita chamando a Project Connections [Delete REST API](https://learn.microsoft.com/en-us/rest/api/aifoundry/accountmanagement/project-connections/delete?view=rest-aifoundry-accountmanagement-2025-06-01&wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud) diretamente pela Azure CLI. O comando a seguir dispara um DELETE no endpoint de management do Azure Resource Manager, apontando exatamente para a connection do projeto.

Com a connection antiga removida, recriamos a Custom MCP connection no Foundry Project, agora do zero. A configuração é a mesma que descrevemos na Parte 1 ao conectar uma [MCP tool remota a um agente](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/model-context-protocol?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud), apontando para o endpoint público do MCP Server publicado, algo como **https://<app-service-name>.azurewebsites.net/mcp**, e escolhendo OAuth Identity Passthrough como método de autenticação. O Client ID e o Client Secret continuam sendo os do **custom-mcp-cars-client**, a Auth URL fica em **https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize** e a Token URL junto com a Refresh URL apontam ambas para **https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token**, que são os endpoints padrão do [Authorization Code Flow do 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 voltam a ser configurados no 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, o **offline\_access,** que é o scope OIDC [responsável por habilitar o refresh token](https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud). Continua valendo o cuidado de separar os scopes por espaço, e não por vírgula. Ao salvar essa nova connection, o Foundry gera um novo Redirect URI próprio, normalmente no formato **https://global.consent.azure-apim.net/redirect/<id>**, e esse valor precisa ser copiado, porque ele muda em relação ao da Parte 1.

Como o Redirect URI é novo, precisamos registrá-lo no App Registration client. Abrimos o **custom-mcp-cars-client**, vamos até Authentication e adicionamos essa URL como Redirect URI da plataforma Web, 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). Vale reforçar o ponto que mais gera confusão nesse cenário, que é o fato de o Redirect URI ser cadastrado sempre no **custom-mcp-cars-client**, o client, e nunca no **custom-mcp-cars**, o server. Aproveitamos também para conferir que as permissões delegadas **Cars.Read, Cars.Write e Cars.Delete** continuam presentes no client, já que é por meio delas que o 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/1.1.png)

Ainda no portal do Foundry, há um ajuste importante na configuração da MCP tool do agente antes de partir para a aplicação. Ao abrir a configuração da tool MCP, marcamos a opção Always auto-approve all tools, para que o Foundry possa usar as tools do MCP sem exigir uma aprovação manual adicional a cada chamada. Sem esse ajuste, a web app pode receber erros quando o agente tenta usar uma tool que não foi previamente habilitada, então deixar o **auto-approve** ativo evita esse tipo de tropeço durante os testes.

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

Resolvida a parte de portal, entramos na construção da aplicação em si, que é o coração desta Parte 2\. Adicionamos à solution um novo projeto Web App chamado Chat.WebApp, que passa a ser a interface final do usuário. O backend em ASP.NET Core conversa com o Foundry usando o SDK do [Azure AI Projects](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.projects-readme?view=azure-dotnet&wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud), enquanto o frontend em HTML, CSS e JavaScript exibe o chatbot e trata a experiência de consentimento. A partir daqui o usuário não usa mais o Agent Playground, e toda a conversa acontece dentro da nossa aplicação. O projeto referencia quatro pacotes principais, visíveis no arquivo de projeto.

```xml
<ItemGroup>
  <PackageReference Include="Azure.AI.Extensions.OpenAI" Version="2.0.0" />
  <PackageReference Include="Azure.AI.Projects" Version="2.0.1" />
  <PackageReference Include="Azure.Identity" Version="1.21.0" />
  <PackageReference Include="OpenAI" Version="2.9.1" />
</ItemGroup>
```

O Azure.AI.Projects traz o AIProjectClient e o acesso aos recursos do projeto Foundry, o Azure.AI.Extensions.OpenAI junto com o pacote OpenAI dão suporte ao Responses API que usamos para conversar com o agente, e o Azure.Identity fornece o DefaultAzureCredential que autentica a aplicação no Foundry.  
A composição da aplicação vive no Program.cs, e é nele que registramos o AIProjectClient e amarramos a autenticação da aplicação ao Foundry.

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

builder.Services.AddControllersWithViews();

builder.Services.Configure<FoundryOptions>(
    builder.Configuration.GetSection("Foundry"));

builder.Services.AddSingleton(sp =>
{
    var configuration = sp.GetRequiredService<IConfiguration>();
    var projectEndpoint = configuration["Foundry:ProjectEndpoint"];

    if (string.IsNullOrWhiteSpace(projectEndpoint))
        throw new InvalidOperationException("Missing configuration: Foundry:ProjectEndpoint");

    return new AIProjectClient(
        endpoint: new Uri(projectEndpoint),
        tokenProvider: new DefaultAzureCredential());
});

builder.Services.AddScoped<FoundryChatService>();

var app = builder.Build();
```

- **AddControllersWithViews()** habilita os controllers MVC e as Views Razor, servindo tanto a API interna do chat quanto a página que renderiza o chatbot.  
**Configure<FoundryOptions>()** vincula a seção Foundry da configuração à classe de opções tipada FoundryOptions, seguindo o [options pattern do ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-10.0&wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud).
- [AddSingleton<AIProjectClient>()](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-10.0&wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud) registra o [AIProjectClient](https://learn.microsoft.com/en-us/dotnet/api/azure.ai.projects.aiprojectclient?view=azure-dotnet&wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud) como singleton, construído a partir do endpoint do projeto e autenticado com o DefaultAzureCredential.
- **AddScoped<FoundryChatService>()** registra o serviço de aplicação que concentra toda a conversa com o Foundry, injetado depois no controller.

Aqui está o ponto conceitual mais importante de toda a Parte 2, então vale insistir nele. O backend usa o **DefaultAzureCredential** para autenticar a própria aplicação no plano de dados do Foundry. Em desenvolvimento local, essa credencial resolve automaticamente a identidade do desenvolvedor logado na Azure CLI ou no [Visual Studio Code](https://learn.microsoft.com/en-us/dotnet/azure/sdk/authentication/local-development-dev-accounts?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud), e em produção o mesmo código passaria a usar uma Managed Identity sem alteração. Essa identidade da aplicação é o que dá acesso ao projeto Foundry para criar conversas e respostas, e ela é diferente da identidade delegada do usuário que trafega até o MCP Server via OAuth Identity Passthrough. Em outras palavras, o DefaultAzureCredential autentica o app no Foundry, enquanto o consentimento OAuth que veremos adiante autoriza o Foundry a chamar o MCP em nome do usuário.

A configuração consumida por esse código fica na seção Foundry do appsettings.json e aponta para o endpoint do projeto e para o nome do agente que já existe no Foundry.

```json
{
  "Foundry": {
    "ProjectEndpoint": "https://<foundry-resource-name>.services.ai.azure.com/api/projects/<project-name>",
    "AgentName": "my-agent"
  }
}
```

Essa seção é materializada em uma classe de opções simples, com as duas propriedades obrigatórias que o serviço precisa para funcionar.

```csharp
public sealed class FoundryOptions
{
    public required string ProjectEndpoint { get; init; }
    public required string AgentName { get; init; }
}
```

**FoundryOptions** representa a configuração tipada do Foundry, expondo o ProjectEndpoint usado para construir o AIProjectClient e o AgentName usado para direcionar as respostas ao agente correto.

Antes de olhar o serviço, precisamos entender o contrato de dados que ele troca com o frontend, definido em um único arquivo de records. Esses tipos descrevem o pedido de criação de conversa, o envio de mensagem, a continuação após o consentimento e a resposta unificada que o frontend interpreta.

```csharp
public sealed record CreateConversationResponse(string ConversationId);

public sealed record SendMessageRequest(string Message);

public sealed record ContinueAfterConsentRequest(string Message, string PreviousResponseId);

public static class ChatStatus
{
    public const string Completed = "completed";
    public const string OAuthConsentRequired = "oauth_consent_required";
}

public sealed record SendMessageResponse(
    string ConversationId,
    string ResponseId,
    string Status,
    string? OutputText = null,
    string? ConsentLink = null,
    string? ConsentServerLabel = null);
```

- **CreateConversationResponse** devolve ao frontend o ConversationId recém criado, que passa a identificar toda a conversa.
- **SendMessageRequest** e **ContinueAfterConsentRequest** carregam a mensagem do usuário, sendo que o segundo ainda transporta o PreviousResponseId necessário para retomar a chamada MCP depois do consentimento.
- **ChatStatus** define as duas situações possíveis de uma resposta, Completed quando o agente já respondeu e OAuthConsentRequired quando o Foundry pede consentimento antes de continuar.
- **SendMessageResponse** é a resposta unificada, que ora traz o OutputText final, ora traz o ConsentLink e o ConsentServerLabel usados para montar o card de consentimento na tela.

Com o contrato definido, chegamos ao FoundryChatService, que é onde a aplicação realmente fala com o Foundry usando o [Responses API](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/responses-api?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud). O serviço recebe o AIProjectClient e as [FoundryOptions por injeção de dependência](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-10.0&wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud) e guarda ambos para uso nas operações seguintes.

```csharp
public sealed class FoundryChatService
{
    private readonly AIProjectClient _projectClient;
    private readonly FoundryOptions _options;

    public FoundryChatService(
        AIProjectClient projectClient,
        IOptions<FoundryOptions> options)
    {
        _projectClient = projectClient;
        _options = options.Value;
    }
```

  
A primeira responsabilidade do serviço é criar uma conversa no projeto, que funciona como o contêiner de estado onde as mensagens e respostas vão sendo acumuladas.

```csharp
public async Task<CreateConversationResponse> CreateConversationAsync()
{
    var conversationsClient =
        _projectClient.ProjectOpenAIClient.GetProjectConversationsClient();

    ProjectConversation conversation =
        await conversationsClient.CreateProjectConversationAsync(
            new ProjectConversationCreationOptions());

    return new CreateConversationResponse(ConversationId: conversation.Id);
}
```

**CreateConversationAsync()** obtém o client de conversas do projeto, cria uma nova ProjectConversation e devolve o identificador dela para o frontend, que passará a usar esse ConversationId em todas as mensagens seguintes.

O envio de uma mensagem comum acontece no método a seguir, que direciona a solicitação ao agente configurado e trata a resposta de forma uniforme.

```csharp
public async Task<SendMessageResponse> SendMessageAsync(
    string conversationId,
    SendMessageRequest request)
{
    if (string.IsNullOrWhiteSpace(conversationId))
        throw new ArgumentException("Conversation id is required.", nameof(conversationId));

    if (string.IsNullOrWhiteSpace(request.Message))
        throw new ArgumentException("Message is required.", nameof(request));

    ProjectResponsesClient responsesClient =
        _projectClient.ProjectOpenAIClient
            .GetProjectResponsesClientForAgent(_options.AgentName);

    CreateResponseOptions responseOptions = new()
    {
        AgentConversationId = conversationId
    };

    responseOptions.InputItems.Add(
        ResponseItem.CreateUserMessageItem(request.Message));

    ResponseResult response =
        await responsesClient.CreateResponseAsync(responseOptions);

    return BuildResponse(conversationId, response);
}
```

**SendMessageAsync()** valida a entrada, obtém um ProjectResponsesClient vinculado ao agente pelo AgentName, monta um CreateResponseOptions apontando para a conversa atual, adiciona a mensagem do usuário como input item e chama o [Responses API](https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/responses?wt.mc%5Fid=MVP%5F407589&ref=azurebrasil.cloud), delegando a interpretação do resultado ao **BuildResponse**.

O ponto que torna esse fluxo especial é o que acontece quando o agente precisa usar a MCP tool pela primeira vez. Nesse instante o Foundry não devolve a resposta final, e sim um item de consentimento, sinalizando que o usuário ainda não autorizou o custom-mcp-cars-client a acessar o MCP em seu nome. O método ContinueAfterConsentAsync existe justamente para retomar a conversa depois que esse consentimento foi concedido.

```csharp
public async Task<SendMessageResponse> ContinueAfterConsentAsync(
    string conversationId,
    ContinueAfterConsentRequest request)
{
    if (string.IsNullOrWhiteSpace(conversationId))
        throw new ArgumentException("Conversation id is required.", nameof(conversationId));

    if (string.IsNullOrWhiteSpace(request.Message))
        throw new ArgumentException("Message is required.", nameof(request));

    if (string.IsNullOrWhiteSpace(request.PreviousResponseId))
        throw new ArgumentException("Previous response id is required.", nameof(request));

    ProjectResponsesClient responsesClient =
        _projectClient.ProjectOpenAIClient
            .GetProjectResponsesClientForAgent(_options.AgentName);

    CreateResponseOptions responseOptions = new()
    {
        PreviousResponseId = request.PreviousResponseId,
        ToolChoice = ResponseToolChoice.CreateRequiredChoice()
    };

    responseOptions.InputItems.Add(
        ResponseItem.CreateUserMessageItem(request.Message));

    ResponseResult response =
        await responsesClient.CreateResponseAsync(responseOptions);

    return BuildResponse(conversationId, response);
  }
```

**ContinueAfterConsentAsync()** retoma o fluxo após o consentimento usando o PreviousResponseId da resposta que pediu consentimento e forçando o uso da tool com ResponseToolChoice.CreateRequiredChoice(), de modo que o Foundry volte a executar a chamada MCP agora com a identidade do usuário já consentida, encadeando a nova resposta a partir da anterior.

A leitura da resposta do Foundry fica isolada em um único método privado, que decide se o retorno é um pedido de consentimento ou a resposta final do agente.

```csharp
private static SendMessageResponse BuildResponse(
    string conversationId,
    ResponseResult response)
{
    foreach (ResponseItem item in response.OutputItems)
    {
        if (item.AsAgentResponseItem() is OAuthConsentRequestResponseItem consentRequest)
        {
            return new SendMessageResponse(
                ConversationId: conversationId,
                ResponseId: response.Id,
                Status: ChatStatus.OAuthConsentRequired,
                ConsentLink: consentRequest.ConsentLink?.ToString(),
                ConsentServerLabel: consentRequest.ServerLabel);
        }
    }

    return new SendMessageResponse(
        ConversationId: conversationId,
        ResponseId: response.Id,
        Status: ChatStatus.Completed,
        OutputText: response.GetOutputText()?.Trim());
```

**BuildResponse()** percorre os output items da resposta e, ao encontrar um OAuthConsentRequestResponseItem, devolve o status OAuthConsentRequired junto com o ConsentLink e o ServerLabel, e quando nenhum item de consentimento aparece, devolve o status Completed com o texto final produzido pelo agente. É esse ResponseId retornado que o frontend guarda para conseguir chamar a continuação depois do consentimento.

Vale registrar um detalhe do arquivo do serviço, que traz a diretiva **#pragma warning disable OPENAI001** no topo. Isso acontece porque parte dos tipos do **Responses API** ainda está marcada como experimental no SDK, e a diretiva apenas silencia esse aviso de recurso em avaliação, sem alterar o comportamento em runtime.

O serviço é exposto ao frontend por um controller de API enxuto, com três endpoints que espelham exatamente os três métodos que acabamos de ver.

```csharp
[ApiController]
[Route("api/chat")]
public sealed class ChatApiController : ControllerBase
{
    private readonly FoundryChatService _chatService;

    public ChatApiController(FoundryChatService chatService)
    {
        _chatService = chatService;
    }

    [HttpPost("conversations")]
    public async Task<IActionResult> CreateConversation()
    {
        CreateConversationResponse response =
            await _chatService.CreateConversationAsync();

        return Created(
            $"/api/chat/conversations/{response.ConversationId}",
            response);
    }

    [HttpPost("conversations/{conversationId}/responses")]
    public async Task<ActionResult<SendMessageResponse>> SendMessage(
        [FromRoute] string conversationId,
        [FromBody] SendMessageRequest request)
    {
        SendMessageResponse response =
            await _chatService.SendMessageAsync(conversationId, request);

        return Ok(response);
    }

    [HttpPost("conversations/{conversationId}/responses/continue")]
    public async Task<ActionResult<SendMessageResponse>> ContinueAfterConsent(
        [FromRoute] string conversationId,
        [FromBody] ContinueAfterConsentRequest request)
    {
        SendMessageResponse response =
            await _chatService.ContinueAfterConsentAsync(conversationId, request);

        return Ok(response);
    }
}
```

- **CreateConversation()** cria uma nova conversa e responde 201 com o ConversationId no corpo e no header de localização.
- **SendMessage()** recebe o texto do usuário para uma conversa existente e devolve a resposta do agente, que pode ser o texto final ou o pedido de consentimento.
- **ContinueAfterConsent()** recebe a mensagem original junto com o PreviousResponseId e retoma a chamada MCP depois que o usuário consentiu, devolvendo então o resultado real da tool.

Do lado do navegador, a página do chat concentra a lógica de conversa em JavaScript, e o trecho mais relevante é o que envia a mensagem e decide o que fazer com a resposta. Quando o usuário manda um texto, o script faz um POST para o endpoint de responses e entrega o resultado para uma função que interpreta o status.

```javascript
const res = await fetch(`/api/chat/conversations/${encodeURIComponent(conversationId)}/responses`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message: text })
});
removeTyping(typingId);
if (!res.ok) throw new Error(`Server error: ${res.status}`);
const data = await res.json();
handleChatResponse(data, text);
```

Essa chamada envia a mensagem digitada para a conversa atual e passa a resposta recebida, junto com o texto original, para a função **handleChatResponse**, que precisa saber o texto para conseguir reenviá-lo caso o consentimento seja necessário.

```javascript
function handleChatResponse(data, originalMessage) {
    if (data && data.status === 'oauth_consent_required') {
        appendConsentCard(data, originalMessage);
    } else {
        appendMessage('assistant', (data && data.outputText) || '');
    }
    scrollToBottom();
}
```

**openConsentAndContinue()** abre o ConsentLink em um popup, troca os botões do card por um estado de espera com a opção de continuar manualmente e, ao detectar que o popup foi fechado, chama automaticamente a continuação da conversa. O botão **I've finished, continue** existe como caminho alternativo para os casos em que o popup é bloqueado pelo navegador.

O fechamento do ciclo acontece na continuação, que chama o endpoint de continue enviando a mensagem original e o PreviousResponseId, exatamente os dados que o backend precisa para retomar a chamada MCP com a identidade já consentida.

```javascript
async function continueAfterConsent(previousResponseId, message, wrapper) {
    if (wrapper.dataset.continuing === '1') return;
    wrapper.dataset.continuing = '1';
    wrapper.remove();
    hideError();

    const typingId = appendTyping();
    try {
        const res = await fetch(`/api/chat/conversations/${encodeURIComponent(conversationId)}/responses/continue`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ message, previousResponseId })
        });
        removeTyping(typingId);
        if (!res.ok) throw new Error(`Server error: ${res.status}`);
        const data = await res.json();
        handleChatResponse(data, message);
    } catch (e) {
        removeTyping(typingId);
        showError('Failed to continue after consent: ' + e.message);
    } finally {
        scrollToBottom();
    }
}
```

  
**continueAfterConsent()** evita continuações duplicadas com uma trava simples, remove o card de consentimento da tela, chama o endpoint de continue com a mensagem original e o previousResponseId e reaproveita o \`handleChatResponse\` para renderizar o resultado, que agora deve ser a resposta real vinda do MCP Server.

Com backend e frontend prontos, é hora de testar tudo rodando localmente.

```bash
dotnet run
```

O desenvolvedor sobe a web app na própria máquina, garantindo antes que está logado na Azure CLI para que o **DefaultAzureCredential** resolva a identidade corretamente, e abre o chat no navegador. Na caixa de mensagem, envia algo simples que dependa do MCP, como listar todos os carros, digitando liste todos os carros. Como a **connection** foi recriada e o consentimento ainda não foi concedido nesta aplicação, o Foundry devolve um **OAuthConsentRequestResponseItem**, e a web app exibe o item de consentimento na conversa em vez da resposta final.

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

  
O usuário então clica em **Open consent**, e o navegador abre a tela de consentimento do Microsoft Entra ID, onde ele autoriza o client usado pelo Foundry a acessar o MCP Server em seu nome. A experiência de consentimento passa pelas telas de permissões solicitadas e pela confirmação final exigida pelo Foundry, momento em que o Microsoft Entra ID devolve o authorization code que o Foundry troca por access token e refresh token.

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

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

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

Concluído o consentimento, o usuário volta para a aplicação. Se o popup foi fechado, a continuação dispara sozinha, e se preferir, o usuário pode clicar em I've finished, continue para retomar manualmente. Nesse ponto a web app chama o endpoint de continue, passando o PreviousResponseId, e o Foundry finalmente executa a tool **cars\_list** em nome do usuário.  

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

A resposta então retorna à conversa, e a lista de carros aparece dentro do próprio chat da aplicação, mostrando que o fluxo delegado funcionou de ponta a ponta fora do Playground. A partir dessa primeira autorização, o usuário não precisa consentir de novo, e as próximas mensagens que dependem do MCP são atendidas diretamente.  

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

Chegamos assim ao fim da Parte 2, na qual removemos a connection antiga do Foundry pela Project Connections Delete REST API, recriamos a Custom MCP connection com OAuth Identity Passthrough, atualizamos o Redirect URI no custom-mcp-cars-client, habilitamos o auto-approve das tools no agente e construímos a Chat.WebApp com backend em ASP.NET Core e frontend em HTML, CSS e JavaScript. A grande diferença em relação à Parte 1 é que o fluxo agora acontece dentro de uma aplicação real, com o Foundry Playground deixando de ser o ponto de teste e a nossa web app assumindo o controle da experiência do usuário. O chatbot consegue detectar o pedido de consentimento, conduzir o usuário pelo fluxo OAuth e continuar a conversa, enquanto o MCP Server segue sendo chamado em nome do usuário via OAuth Identity Passthrough, com cada tool rodando sob os scopes reais que aquela pessoa consentiu.

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!