Microsoft Foundry com Managed Identity consumindo um MCP Server protegido pelo Microsoft Entra ID

Microsoft Foundry com Managed Identity consumindo um MCP Server protegido pelo Microsoft Entra ID

7 de agosto de 2026

O Microsoft Foundry é a plataforma da Microsoft para construir, orquestrar e operar agentes de IA, reunindo modelos, ferramentas e serviços de dados em um único lugar. Um dos recursos mais interessantes do Foundry Agent Service é a capacidade de conectar um agente a um Model Context Protocol (MCP) Server remoto, permitindo que o modelo chame ferramentas reais expostas por uma API sua. O problema é que, na prática, esse MCP Server quase nunca pode ficar aberto para qualquer caller. Neste artigo vou mostrar como protegi um MCP Server com Microsoft Entra ID de um jeito que só o próprio Foundry consegue chamar, e como configurar o agente no portal para testar o fluxo completo de criação e listagem de livros.

O cenário aqui é simples de entender: vamos ter um MCP Server, o Books.Mcp, que expõe operações de CRUD sobre livros. Esse servidor está protegido pelo Microsoft Entra ID, e a regra de negócio é clara: só podemos aceitar chamadas vindas do projeto do Foundry configurado, mais especificamente da identidade gerenciada associada a esse projeto. Nenhum outro client, usuário ou aplicação, mesmo dentro do mesmo tenant, consegue chamar as tools desse MCP server. Esse é exatamente o tipo de arquitetura descrita na documentação de MCP authentication do Foundry, que lista quatro formas de autenticar um MCP Server a partir de um agente: Project Managed Identity, Agent Identity, OAuth identity passthrough ou API Key. Para esse post, vamos usar o caminho de Project Managed Identity, onde o próprio projeto do Foundry obtém um token de aplicação (app-only) via client credentials e o repassa nas chamadas ao MCP Server.

Vale entender quando essa integração via Managed Identity, e não via OAuth identity passthrough, faz sentido. O identity passthrough é indicado quando você precisa que o agente atue em nome do usuário final, propagando o contexto de autorização dele para o backend, útil quando cada usuário tem permissões diferentes dentro da API alvo. Já a Project Managed Identity é indicada quando o agente representa a própria aplicação, sem depender de um usuário logado, e você só precisa garantir que a chamada realmente partiu daquele projeto específico do Foundry, e de nenhum outro lugar. É o caso típico de um agente automatizado, rodando em produção, que precisa consultar ou manipular dados através de um MCP Server interno da empresa. Como o Books.Mcp representa justamente esse cenário (um agente de back office manipulando um catálogo de livros), o modelo de app-only token com validação por Object ID foi a escolha mais direta.

Do lado do Entra ID, a base de tudo é o conceito de identidade, que explica como os agentes do Foundry obtêm tokens para chamar serviços externos. Cada projeto do Foundry possui uma identidade própria no Entra ID, e é essa identidade, e não a do usuário que está testando no sandbox, que aparece nos claims do token JWT quando o MCP Server recebe a requisição. Para o MCP Server aceitar esse token, é preciso primeiro registrar uma App Registration no Entra ID.

Depois de criada, o App Registration expõe dois valores essenciais para a configuração da API: o Application (client) ID e o Directory (tenant) ID, ambos visíveis na página de visão geral do registro.


O próximo passo é declarar o Application ID URI do MCP Server, feito na aba Expose an API do App Registration, seguindo o formato api://<client-id>, conforme as regras de formato descritas em identifier URI restrictions. Esse URI é o audience que o token emitido para o Foundry precisa carregar para ser aceito pela API.

Com o App Registration pronto, o próximo pedaço é o projeto do Foundry em si, que já está criado no meu portal.

Cada projeto do Foundry tem um Object ID de identidade próprio, e é justamente esse valor que precisamos capturar para configurar a autorização no lado da API. É esse Object ID que vai para o appsettings do MCP Server como o único caller permitido.

Passando para o código, o projeto já está na raiz do repositório, dentro da pasta Books.Mcp. O ponto de entrada é o Program.cs:

using Books.Mcp.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.AddConfigurations();

var app = builder.Build();

app.ConfigureApplication();

app.Run();

public partial class Program { }

Program.cs

  • Cria o WebApplicationBuilder, chama AddConfigurations() para registrar autenticação, autorização, EF Core e o servidor MCP, depois builda e executa a aplicação.

Toda a parte de autenticação e autorização fica concentrada em Extensions/ConfigurationExtensions.cs, que é o coração da proteção do MCP Server:

public static void AddConfigurations(this WebApplicationBuilder builder)
{
    var services = builder.Services;
    var configuration = builder.Configuration;

    var tenantId = configuration["AzureAd:TenantId"]
        ?? throw new InvalidOperationException("AzureAd:TenantId is required.");
    var apiClientId = configuration["AzureAd:ClientId"]
        ?? throw new InvalidOperationException("AzureAd:ClientId is required.");
    var foundryObjectId = configuration["FoundryCaller:ObjectId"]
        ?? throw new InvalidOperationException("FoundryCaller:ObjectId is required.");
    var applicationIdUri = $"api://{apiClientId}";

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApi(configuration.GetSection("AzureAd"));

    services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        options.TokenValidationParameters.ValidateAudience = true;
        options.TokenValidationParameters.ValidAudiences = [apiClientId, applicationIdUri];
    });

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();

        options.AddPolicy("FoundryProjectOnly", policy =>
        {
            policy.RequireAuthenticatedUser();
            policy.RequireAssertion(context => IsFoundryProjectCaller(context.User, tenantId, foundryObjectId));
        });
    });

    services.AddDbContext<BooksDbContext>(options => options.UseInMemoryDatabase("BooksDb"));
    services.AddScoped<BookService>();

    services.AddMcpServer()
        .WithHttpTransport(options => options.Stateless = true)
        .WithTools<BookTools>();
}

public static void ConfigureApplication(this WebApplication app)
{
    app.UseAuthentication();
    app.UseAuthorization();

    app.MapGet("/health", () => Results.Ok(new { status = "Healthy", message = "Books MCP Server is running." }))
        .AllowAnonymous();

    app.MapMcp("/mcp").RequireAuthorization("FoundryProjectOnly");
}

private static bool IsFoundryProjectCaller(ClaimsPrincipal caller, string tenantId, string foundryObjectId)
{
    var tokenTenantId = caller.FindFirstValue("tid")
        ?? caller.FindFirstValue("http://schemas.microsoft.com/identity/claims/tenantid");
    var tokenObjectId = caller.FindFirstValue("oid")
        ?? caller.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier");
    var tokenSubject = caller.FindFirstValue("sub")
        ?? caller.FindFirstValue(ClaimTypes.NameIdentifier);

    var isApplicationOnlyToken = !string.IsNullOrWhiteSpace(tokenObjectId)
        && !string.IsNullOrWhiteSpace(tokenSubject)
        && string.Equals(tokenObjectId, tokenSubject, StringComparison.OrdinalIgnoreCase);

    var isAllowedTenant = string.Equals(tokenTenantId, tenantId, StringComparison.OrdinalIgnoreCase);
    var isAllowedFoundryIdentity = string.Equals(tokenObjectId, foundryObjectId, StringComparison.OrdinalIgnoreCase);

    return isApplicationOnlyToken && isAllowedTenant && isAllowedFoundryIdentity;
}

AddConfigurations()

  • Lê TenantId, ClientId e o ObjectId da identidade do Foundry a partir da configuração, monta o Application ID URI no formato api://<client-id>, registra a autenticação JWT Bearer via AddMicrosoftIdentityWebApi() e restringe os audiences válidos ao client ID puro e ao Application ID URI.
  • Define a política FoundryProjectOnly, que exige usuário autenticado e aplica a validação customizada IsFoundryProjectCaller().
  • Registra o EF Core InMemory para persistir os livros e o servidor MCP com a ferramenta BookTools.

ConfigureApplication()

  • Ativa autenticação e autorização, expõe um endpoint /health anônimo para health checks do App Service, e mapeia o endpoint /mcp exigindo a política FoundryProjectOnly.IsFoundryProjectCaller()
  • Compara o oid (Object ID) com o sub (Subject) do token para confirmar que se trata de um token de aplicação (app-only), já que nesse tipo de token os dois claims são iguais, ao contrário de um token de usuário delegado.
  • Valida se o tid (Tenant ID) do token corresponde ao tenant configurado e se o oid corresponde exatamente ao Object ID da identidade do projeto do Foundry, o mesmo valor visto na imagem5.
  • Só retorna true quando as três condições são verdadeiras, garantindo que nenhum outro chamador, mesmo com um token válido do mesmo tenant, consiga acessar o /mcp.

Essa validação por claims é o que a documentação de access token claims reference descreve em detalhes, explicando o significado de aud, tid, oid, sub, appid e roles dentro de um token emitido pelo Entra ID. Combinando ValidAudiences com a checagem manual de oid e tid, o Books.Mcp implementa uma autorização baseada em ACL em vez de depender de app roles, o que é suficiente quando existe apenas um chamador confiável.

As entidades e o restante da aplicação seguem um desenho enxuto. O modelo de domínio é a classe Book:

public class Book
{
    public Guid Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public string Description { get; set; } = string.Empty;
}

A classe Book representa um livro com Id, Name e Description, persistido via EF Core InMemory pelo BooksDbContext.

O BooksDbContext apenas registra esse mapeamento simples:

public class BooksDbContext(DbContextOptions<BooksDbContext> options) : DbContext(options)
{
    public DbSet<Book> Books => Set<Book>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Book>(entity =>
        {
            entity.HasKey(book => book.Id);
            entity.Property(book => book.Name).IsRequired();
            entity.Property(book => book.Description).IsRequired();
        });
    }
}

BooksDbContext expõe o DbSet<Book> e configura a chave primária e a obrigatoriedade dos campos Name e Description.

A regra de negócio do CRUD fica isolada no BookService, que o BookTools apenas invoca:

public class BookService(BooksDbContext dbContext)
{
    public async Task<Book> CreateAsync(string name, string description, CancellationToken cancellationToken)
    {
        var book = new Book
        {
            Id = Guid.NewGuid(),
            Name = name,
            Description = description,
        };

        dbContext.Books.Add(book);
        await dbContext.SaveChangesAsync(cancellationToken);

        return book;
    }

    public async Task<List<Book>> GetAllAsync(CancellationToken cancellationToken)
    {
        return await dbContext.Books.AsNoTracking().ToListAsync(cancellationToken);
    }

    public async Task<Book?> GetByIdAsync(Guid id, CancellationToken cancellationToken)
    {
        return await dbContext.Books.AsNoTracking().FirstOrDefaultAsync(book => book.Id == id, cancellationToken);
    }

    public async Task<Book?> UpdateAsync(Guid id, string? name, string? description, CancellationToken cancellationToken)
    {
        var book = await dbContext.Books.FirstOrDefaultAsync(book => book.Id == id, cancellationToken);
        if (book is null)
        {
            return null;
        }

        if (!string.IsNullOrWhiteSpace(name))
        {
            book.Name = name;
        }

        if (!string.IsNullOrWhiteSpace(description))
        {
            book.Description = description;
        }

        await dbContext.SaveChangesAsync(cancellationToken);

        return book;
    }

    public async Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken)
    {
        var book = await dbContext.Books.FirstOrDefaultAsync(book => book.Id == id, cancellationToken);
        if (book is null)
        {
            return false;
        }

        dbContext.Books.Remove(book);
        await dbContext.SaveChangesAsync(cancellationToken);

        return true;
    }
}
  • CreateAsync() - Cria um novo Book com Guid gerado, adiciona ao contexto e persiste.
  • GetAllAsync() - Retorna todos os livros sem tracking, já que é uma operação somente leitura.
  • GetByIdAsync() - Busca um único livro pelo Id, retornando null quando não encontrado.
  • UpdateAsync() - Localiza o livro pelo Id e atualiza Name e/ou Description apenas quando os novos valores não são vazios, mantendo o restante intacto.
  • DeleteAsync() - Remove o livro pelo Id e retorna um booleano indicando se a exclusão aconteceu.

Por fim, o BookTools é a camada que o Foundry realmente enxerga como ferramentas MCP, decorada com os atributos do pacote ModelContextProtocol.AspNetCore:

[McpServerToolType]
public sealed class BookTools
{
    [McpServerTool(Name = "create_book"), Description("Creates a new book with a name and description.")]
    public static async Task<object> CreateBook(
        BookService bookService,
        [Description("Name of the book. Required.")] string name,
        [Description("Description of the book. Required.")] string description,
        CancellationToken cancellationToken)
    {
        var book = await bookService.CreateAsync(name, description, cancellationToken);
        return book;
    }

    [McpServerTool(Name = "get_books"), Description("Lists all books.")]
    public static async Task<object> GetBooks(BookService bookService, CancellationToken cancellationToken)
    {
        return await bookService.GetAllAsync(cancellationToken);
    }
}

Toda essa configuração de tenant, client e chamador permitido vem do appsettings.json, com os valores reais preenchidos por ambiente:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<tenant-guid>",
    "ClientId": "<mcp-api-client-id>",
    "AllowWebApiToBeAuthorizedByACL": true
  },
  "FoundryCaller": {
    "ObjectId": "<foundry-project-managed-identity-object-id>"
  }
}

Com o código pronto, falta colocar o Books.Mcp no ar. O deploy é feito em um Azure App Service já existente, através do deploy.sh, que primeiro garante uma sessão ativa via az login, depois publica o projeto em modo Release e envia o pacote via ZIP Deploy:

chmod +x deploy.sh
./deploy.sh

Rodando o script, ele valida se o Azure CLI está autenticado, confirma que o App Service de destino existe no resource group informado, publica o Books.Mcp.csproj, compacta a saída e chama az webapp deploy com o pacote gerado, reiniciando a aplicação ao final. O resultado é o endpoint https://<nome-do-app>.azurewebsites.net/mcp pronto para receber chamadas autenticadas do Foundry.

Com o MCP Server no ar, a última etapa é configurar o agente dentro do Foundry Agent Service. Primeiro, criamos um agente novo, usando o modelo gpt-5.6-luna como LLM por trás das respostas.

Depois, dentro das configurações do agente, adicionamos Books.Mcp como uma ferramenta do tipo MCP Server, informando a URL do endpoint /mcp publicado no App Service e selecionando o modo de autenticação Project Managed Identity, exatamente o que o código em ConfigurationExtensions.cs espera validar.

Com o agente configurado, testamos diretamente no sandbox do Foundry, pedindo para ele criar um novo livro através da ferramenta create_book. A resposta confirma que o token emitido para o projeto do Foundry passou pela política FoundryProjectOnly sem nenhum erro 401 ou 403.

Em seguida, pedimos para o agente listar os livros cadastrados, o que aciona a ferramenta get_books e devolve, entre outros, o livro criado no teste anterior. Isso comprova que o mesmo agente, com a mesma identidade, consegue tanto escrever quanto ler dados através do MCP Server protegido.

Em cenários onde o agente precisa herdar as permissões de um usuário específico, vale considerar o modelo de OAuth identity passthrough descrito na documentação de MCP authentication, que já explorei em dois artigos anteriores: OAuth Identity Passthrough no Foundry Agent Service com MCP Server customizado em .NET, Parte 1 e Parte 2.

Você já pode baixar o projeto por esse link, e não esquece de me seguir no LinkedIn!

Até a próxima, abraços!

Confira mais:

Fique por dentro das novidades

Assine nossa newsletter e receba as últimas atualizações e artigos diretamente em seu email.

Assinar gratuitamente