18 de junho de 202616 min de leitura

Controlando Acesso a Ferramentas com APIM MCP Gateway

samcogan

Azure

Banner - Controlando Acesso a Ferramentas com APIM MCP Gateway

Controlando Acesso a Ferramentas com APIM MCP Gateway

TL;DR: MCP servers entregam todas as ferramentas de uma vez, sem forma nativa de desligar itens individuais. Isso é um problema de segurança, custo e governança. O artigo mostra como o Azure API Management (APIM) pode atuar como um gateway MCP, inspecionando o tráfego JSON-RPC para filtrar tools/list e bloquear tools/call. Duas estratégias são apresentadas: allowlist estática (mais segura, exige manutenção manual) e deny-list dinâmica (menos manutenção, mas mexe no corpo da resposta). A conclusão principal é que, para ambientes corporativos brasileiros que adotam agentes de IA, essa abordagem traz controle granular sem alterar o servidor MCP.

Se você começou a trabalhar com servidores MCP no GitHub Copilot, Claude ou qualquer outro agente em um ambiente corporativo, provavelmente já esbarrou num problema parecido. Você quer dar acesso a um servidor MCP útil para seus desenvolvedores, mas não a todas as ferramentas que ele entrega. Talvez uma ferramenta seja ruidosa e consuma contexto sem necessidade. Talvez outra chame uma API paga. Ou talvez uma ferramenta faça algo que seu time de segurança não considera aceitável. O servidor MCP é tudo ou nada: instale e leve tudo.

A maioria dos servidores MCP não oferece uma forma de desligar ferramentas individuais. A especificação MCP também não define uma. Então, se você quer controle granular, precisa colocar algo na frente do servidor que consiga ver o tráfego JSON-RPC e tomar decisões sobre ele. Esse algo é um MCP gateway, e o Azure API Management (APIM) pode fazer o trabalho.

O que é um MCP gateway?

Um MCP gateway fica entre o agente (cliente) e o servidor MCP (backend) e inspeciona o tráfego do protocolo MCP que flui entre eles. Como MCP é apenas JSON-RPC sobre HTTP ou SSE, qualquer ferramenta que faça reverse-proxy mais inspeção de payload pode, em tese, atuar como gateway. O interessante é o que você faz com essa posição na rede.

Se você já trabalhou com API gateways, o modelo mental é o mesmo: colocar um ponto de entrada gerenciado na frente de um ou mais backends e usar políticas para controlar o que passa. As diferenças são que o protocolo é JSON-RPC sobre uma conexão longa, e as coisas que você filtra não são rotas, mas ferramentas, prompts e recursos.

Arquitetura do MCP Gateway

Fora da caixa, o MCP oferece muito pouco controle operacional. O protocolo assume uma relação confiável de um para um entre cliente e servidor. Não há história de autenticação embutida além do que o transporte oferece, nem rate limiting, nem audit trail além dos logs do próprio agente, e nenhuma forma de compartilhar um servidor MCP com segurança entre múltiplos usuários ou times. Um gateway resolve tudo isso sem modificar o servidor MCP em si.

Por que usar um MCP gateway? Funcionalidades

O problema de acesso a ferramentas não é a única razão para colocar um gateway na frente de um servidor MCP. A lista se parece muito com as razões para colocar qualquer API atrás de um gateway, com alguns twists específicos do MCP:

  • Autenticação e autorização centralizadas. Adicione Entra ID, mTLS ou tokens com escopo na frente de servidores MCP que vêm com pouco mais que uma API key.
  • Rate limiting e quota. Impedir que um loop descontrolado de agente sobrecarregue uma API upstream paga e acumule uma conta em minutos.
  • Logging, audit e observabilidade. Capturar qual usuário invocou qual ferramenta com quais argumentos e enviar para Log Analytics ou seu SIEM.
  • Isolamento de rede. Manter as máquinas dos desenvolvedores fora da internet pública, colocando servidores MCP externos atrás de private endpoints e IPs de egress fixos.
  • Compartilhamento de um servidor MCP entre muitos clientes. Transformar um servidor MCP single-tenant em uma porta multi-tenant com identidade por usuário e limites por time.
  • Política e governança. Controlar quais ferramentas são expostas, redigir campos, validar argumentos ou transformar respostas antes de chegarem ao agente.
  • Agregação de múltiplos servidores MCP. Apresentar um endpoint MCP lógico que se distribui para vários backends, de modo que os agentes tenham um único ponto de conexão.
  • Tratamento de falhas e resiliência. Adicionar retries, circuit breakers e caching de tools/list em vez de cada agente ter o seu próprio.
  • Gerenciamento de custos. Colocar limites, alertas e chargeback por time em servidores MCP que envolvam APIs pagas por chamada.

APIM como gateway

O APIM oferece tudo isso, e a Microsoft vem adicionando suporte específico para MCP nas últimas releases. Os pontos relevantes para este post:

  1. Existe uma seção dedicada MCP servers no APIM, separada da blade de APIs padrão. É lá que ficam as funcionalidades voltadas para MCP.
  2. Você pode registrar um servidor MCP existente como external MCP server. O APIM faz o proxy do tráfego do protocolo e aplica políticas a ele.
  3. Você também pode expor uma REST API normal como um servidor MCP, adicionando uma camada MCP sobre uma API existente.
  4. O policy engine padrão do APIM funciona no tráfego MCP.

Qual é a forma do problema?

O protocolo MCP tem dois métodos que importam aqui:

  1. tools/list é o que o cliente chama ao conectar. O servidor retorna o catálogo de ferramentas disponíveis, com nomes, descrições e schemas de entrada. O agente usa isso para decidir o que pode fazer.
  2. tools/call é o que o cliente envia quando o agente quer de fato invocar uma ferramenta.

Se você quer ocultar uma ferramenta, precisa lidar com ambos. Filtrar tools/list impede que o agente saiba que a ferramenta existe, que é o que normalmente se deseja. Bloquear tools/call impede que um cliente determinado (ou uma ferramenta que o agente adivinhou) a chame diretamente. Você precisa de ambos para ter certeza de que a ferramenta está realmente fora dos limites.

Para o resto deste post, usarei o Microsoft Learn MCP server como exemplo, porque é público, útil e vem com três ferramentas:

  • microsoft_docs_search
  • microsoft_docs_fetch
  • microsoft_code_sample_search

Digamos que eu queira permitir as duas primeiras e bloquear a terceira. Eis como fazer.

Como configurar o APIM como um MCP gateway?

Vou pular a parte de provisionar o APIM. A configuração específica para MCP é:

  1. No seu APIM, vá para MCP Servers na navegação à esquerda (é uma seção própria, não dentro de APIs).

Menu MCP Servers

  1. Adicione um novo MCP server. Escolha External MCP server como tipo.
  2. Aponte para o endpoint do Microsoft Learn MCP server. O transporte será HTTP ou SSE, dependendo do upstream — o APIM lida com isso para você.
  3. Salve. O APIM agora faz o proxy do tráfego MCP. Configure o agente para apontar para a URL exposta pelo APIM, em vez do upstream.

Configuração do MCP server

Neste ponto, você tem um passthrough funcional. O próximo passo é a política.

Duas formas de controlar o acesso a ferramentas

Existem dois padrões que funcionam, e a escolha certa depende de quanto trabalho de manutenção você quer assumir.

Opção A: Allowlist estática

Você codifica manualmente a lista de ferramentas que o APIM vai expor. O APIM intercepta tools/list e retorna sua lista fixa, ignorando o que o backend diz. Também bloqueia tools/call para qualquer coisa que não esteja na lista.

Prós: previsível, não lê o corpo da resposta, não se importa com mudanças no upstream.
Contras: você mesmo precisa manter os schemas. Se a Microsoft adicionar uma ferramenta útil ao Learn MCP server, você não a verá até atualizar a política. Se mudarem o schema de entrada de uma ferramenta existente, precisará atualizar também.

É a opção que eu escolheria primeiro se quisesse controle estrito e não esperasse muitas mudanças no upstream.

Opção B: Deny-list dinâmica

Você deixa tools/list fluir, depois reescreve a resposta na saída, removendo as ferramentas que não deseja. Também bloqueia tools/call para essas ferramentas.

Prós: menor manutenção. Novas ferramentas aparecem automaticamente. Mudanças de schema são capturadas.
Contras: lê e reescreve context.Response.Body, e a própria documentação da Microsoft para políticas MCP alerta que o acesso ao corpo da resposta pode interferir com streaming. Na prática, tools/list é JSON-RPC não-streaming e funciona bem, mas é preciso testar cuidadosamente no seu ambiente, especialmente se o agente for exigente quanto ao tratamento da resposta. Este método também exige que você fique de olho quando novas ferramentas forem adicionadas e as inclua na lista se não quiser que sejam usadas — caso contrário, elas estarão automaticamente disponíveis para os usuários.

Escolha esta opção quando você confia no upstream e quer minimizar o trabalho operacional.

Opção A: a política de allowlist

Aqui está a política completa para a allowlist estática. Ela vai no editor de políticas do MCP Server no APIM. Você notará que não há <base /> nas seções. Alguns editores de política de MCP Server não os incluem, e a política funciona bem sem eles.

<policies>
  <inbound>
    <choose>
      <when condition="@{
        var body = context.Request.Body.As<Newtonsoft.Json.Linq.JObject>(preserveContent: true);
        if (body == null) { return false; }
        var rpcMethod = (string)body["method"];
        return string.Equals(rpcMethod, "tools/list", System.StringComparison.OrdinalIgnoreCase);
      }">
        <return-response>
          <set-status code="200" reason="OK" />
          <set-header name="Content-Type" exists-action="override">
            <value>application/json</value>
          </set-header>
          <set-body>@{
            var req = context.Request.Body.As<Newtonsoft.Json.Linq.JObject>(preserveContent: true);
            var id = req != null ? req["id"] : Newtonsoft.Json.Linq.JValue.CreateNull();
            var tools = new Newtonsoft.Json.Linq.JArray(
              new Newtonsoft.Json.Linq.JObject(
                new Newtonsoft.Json.Linq.JProperty("name", "microsoft_docs_search"),
                new Newtonsoft.Json.Linq.JProperty("description", "Search official Microsoft/Azure documentation"),
                new Newtonsoft.Json.Linq.JProperty("inputSchema", new Newtonsoft.Json.Linq.JObject(
                  new Newtonsoft.Json.Linq.JProperty("type", "object"),
                  new Newtonsoft.Json.Linq.JProperty("properties", new Newtonsoft.Json.Linq.JObject(
                    new Newtonsoft.Json.Linq.JProperty("query", new Newtonsoft.Json.Linq.JObject(
                      new Newtonsoft.Json.Linq.JProperty("type", "string")
                    ))
                  ))
                ))
              ),
              new Newtonsoft.Json.Linq.JObject(
                new Newtonsoft.Json.Linq.JProperty("name", "microsoft_docs_fetch"),
                new Newtonsoft.Json.Linq.JProperty("description", "Fetch a Microsoft documentation page as markdown"),
                new Newtonsoft.Json.Linq.JProperty("inputSchema", new Newtonsoft.Json.Linq.JObject(
                  new Newtonsoft.Json.Linq.JProperty("type", "object"),
                  new Newtonsoft.Json.Linq.JProperty("properties", new Newtonsoft.Json.Linq.JObject(
                    new Newtonsoft.Json.Linq.JProperty("url", new Newtonsoft.Json.Linq.JObject(
                      new Newtonsoft.Json.Linq.JProperty("type", "string")
                    ))
                  )),
                  new Newtonsoft.Json.Linq.JProperty("required", new Newtonsoft.Json.Linq.JArray("url"))
                ))
              )
            );
            var response = new Newtonsoft.Json.Linq.JObject(
              new Newtonsoft.Json.Linq.JProperty("jsonrpc", "2.0"),
              new Newtonsoft.Json.Linq.JProperty("id", id),
              new Newtonsoft.Json.Linq.JProperty("result", new Newtonsoft.Json.Linq.JObject(
                new Newtonsoft.Json.Linq.JProperty("tools", tools)
              ))
            );
            return response.ToString(Newtonsoft.Json.Formatting.None);
          }</set-body>
        </return-response>
      </when>
      <when condition="@{
        var body = context.Request.Body.As<Newtonsoft.Json.Linq.JObject>(preserveContent: true);
        if (body == null) { return false; }
        var rpcMethod = (string)body["method"];
        var toolName = (string)body["params"]?["name"];
        return string.Equals(rpcMethod, "tools/call", System.StringComparison.OrdinalIgnoreCase)
          && string.Equals(toolName, "microsoft_code_sample_search", System.StringComparison.OrdinalIgnoreCase);
      }">
        <return-response>
          <set-status code="200" reason="OK" />
          <set-header name="Content-Type" exists-action="override">
            <value>application/json</value>
          </set-header>
          <set-body>@{
            var req = context.Request.Body.As<Newtonsoft.Json.Linq.JObject>(preserveContent: true);
            var id = req != null ? req["id"] : Newtonsoft.Json.Linq.JValue.CreateNull();
            var error = new Newtonsoft.Json.Linq.JObject(
              new Newtonsoft.Json.Linq.JProperty("jsonrpc", "2.0"),
              new Newtonsoft.Json.Linq.JProperty("id", id),
              new Newtonsoft.Json.Linq.JProperty("error", new Newtonsoft.Json.Linq.JObject(
                new Newtonsoft.Json.Linq.JProperty("code", -32601),
                new Newtonsoft.Json.Linq.JProperty("message", "Tool disabled by API Management policy: microsoft_code_sample_search")
              ))
            );
            return error.ToString(Newtonsoft.Json.Formatting.None);
          }</set-body>
        </return-response>
      </when>
    </choose>
  </inbound>
  <backend />
  <outbound />
  <on-error />
</policies>

É um monte de XML, mas está fazendo apenas três coisas. Veja o que cada parte faz.

O <choose> externo e os dois <when>. Isso é apenas um if/else if. O APIM examina a requisição JSON-RPC de entrada e decide qual branch executar. O primeiro branch lida com tools/list. O segundo lida com tools/call para a ferramenta específica que quero bloquear. Se nenhum corresponder, nada acontece na política e a requisição flui normalmente para o backend. Então tools/call para as ferramentas permitidas, além de initialize, ping e qualquer outra coisa que o MCP jogue no gateway, tudo passa sem alteração.

O branch tools/list: sintetiza o catálogo. Quando o agente pede a lista de ferramentas, o APIM nunca encaminha a chamada. Em vez disso, a política lê o id da requisição de entrada (para que a resposta se correlacione corretamente), constrói um JArray contendo apenas as ferramentas que quero expor, com seus nomes, descrições e blocos inputSchema, envolve em um envelope de resultado JSON-RPC e retorna diretamente com <return-response>. O backend nunca vê a requisição. Essa é a parte que torna a Opção A à prova de balas: não há comportamento upstream que possa vazar, porque o upstream não está envolvido.

O branch tools/call: bloqueio rígido da ferramenta proibida. Quando o agente tenta chamar microsoft_code_sample_search diretamente (seja por adivinhação, cache de execução anterior ou alguém colocou no arquivo de configuração), a política faz um short-circuit com um erro JSON-RPC. O código de erro é -32601, que a especificação define como "Method not found", e a mensagem diz claramente que a ferramenta foi desabilitada pelo APIM. Agentes bem-comportados exibirão isso ao usuário e seguirão em frente. Sem esse branch, ocultar a ferramenta de tools/list só pararia clientes honestos.

Algumas coisas incidentais que vale a pena saber. O argumento preserveContent: true em context.Request.Body.As<...>() é importante: sem ele, ler o corpo o consome e o resto do pipeline fica sem nada. E as comparações OrdinalIgnoreCase são cinto e suspensórios: os nomes dos métodos MCP são case-sensitive na especificação, mas agentes na vida real são inconsistentes.

Opção B: a política de deny-list

Mesmo cenário, abordagem diferente. Deixe tools/list fluir para o backend e reescreva a resposta na saída. Ainda bloqueie tools/call para a ferramenta proibida.

<policies>
  <inbound>
    <choose>
      <when condition="@{
        var body = context.Request.Body.As<Newtonsoft.Json.Linq.JObject>(preserveContent: true);
        if (body == null) { return false; }
        var rpcMethod = (string)body["method"];
        var toolName = (string)body["params"]?["name"];
        return string.Equals(rpcMethod, "tools/call", System.StringComparison.OrdinalIgnoreCase)
          && string.Equals(toolName, "microsoft_code_sample_search", System.StringComparison.OrdinalIgnoreCase);
      }">
        <return-response>
          <set-status code="200" reason="OK" />
          <set-header name="Content-Type" exists-action="override">
            <value>application/json</value>
          </set-header>
          <set-body>@{
            var req = context.Request.Body.As<Newtonsoft.Json.Linq.JObject>(preserveContent: true);
            var id = req != null ? req["id"] : Newtonsoft.Json.Linq.JValue.CreateNull();
            var error = new Newtonsoft.Json.Linq.JObject(
              new Newtonsoft.Json.Linq.JProperty("jsonrpc", "2.0"),
              new Newtonsoft.Json.Linq.JProperty("id", id),
              new Newtonsoft.Json.Linq.JProperty("error", new Newtonsoft.Json.Linq.JObject(
                new Newtonsoft.Json.Linq.JProperty("code", -32601),
                new Newtonsoft.Json.Linq.JProperty("message", "Tool disabled by API Management policy: microsoft_code_sample_search")
              ))
            );
            return error.ToString(Newtonsoft.Json.Formatting.None);
          }</set-body>
        </return-response>
      </when>
    </choose>
  </inbound>
  <backend />
  <outbound>
    <choose>
      <when condition="@{
        var req = context.Request.Body.As<Newtonsoft.Json.Linq.JObject>(preserveContent: true);
        if (req == null) { return false; }
        var rpcMethod = (string)req["method"];
        return string.Equals(rpcMethod, "tools/list", System.StringComparison.OrdinalIgnoreCase);
      }">
        <set-body>@{
          var bodyText = context.Response.Body.As<string>(preserveContent: true);
          if (string.IsNullOrEmpty(bodyText)) { return bodyText; }
          var resp = Newtonsoft.Json.Linq.JObject.Parse(bodyText);
          var tools = resp["result"]?["tools"] as Newtonsoft.Json.Linq.JArray;
          if (tools == null) { return bodyText; }
          var filtered = new Newtonsoft.Json.Linq.JArray();
          foreach (var t in tools)
          {
            var name = (string)t?["name"];
            if (!string.Equals(name, "microsoft_code_sample_search", System.StringComparison.OrdinalIgnoreCase))
            {
              filtered.Add(t);
            }
          }
          ((Newtonsoft.Json.Linq.JObject)resp["result"])["tools"] = filtered;
          return resp.ToString(Newtonsoft.Json.Formatting.None);
        }</set-body>
      </when>
    </choose>
  </outbound>
  <on-error />
</policies>

Esta política divide o trabalho entre inbound e outbound. A forma é diferente da Opção A, mas novamente está fazendo apenas algumas coisas.

Inbound: bloquear chamadas diretas à ferramenta proibida. Este bloco é idêntico ao da Opção A. Se o agente tentar invocar microsoft_code_sample_search diretamente, o APIM retorna o erro JSON-RPC -32601 e a requisição nunca chega ao backend. Todo o resto (incluindo tools/list e chamadas às ferramentas permitidas) segue para o servidor MCP upstream.

Backend: nada personalizado. O <backend /> vazio é intencional. Queremos que o servidor upstream lide com todas as requisições que não foram bloqueadas no inbound, incluindo tools/list. O APIM encaminha, o servidor retorna seu catálogo completo, e temos a chance de reescrever a resposta na volta.

Outbound: filtrar o catálogo na saída. Esta é a parte que faz o trabalho dinâmico. A condição <when> verifica a requisição original (não a resposta) para saber se foi uma chamada tools/list, porque só queremos reescrever respostas para esse método específico. Se foi, a política lê o corpo da resposta como string, converte em JSON, percorre o array result.tools, constrói um novo array contendo tudo exceto a ferramenta da deny-list, coloca de volta no objeto de resposta e escreve o JSON modificado com <set-body>. Para qualquer outro método, a resposta flui sem alteração.

A razão pela qual esta opção é mais frágil que a Opção A está nesse último parágrafo: ela lê e reescreve context.Response.Body. A própria documentação da Microsoft para políticas MCP alerta que o acesso ao corpo da resposta pode interferir com streaming. Para JSON-RPC não-streaming como tools/list, isso funciona bem na prática, mas se você estender esse padrão para filtrar respostas em streaming (atualizações de resources, chamadas de ferramentas longas), precisará pensar muito mais. Para o caso restrito de podar tools/list, é um trade-off razoável pelo custo de manutenção mais baixo.

Validando que funciona

Depois de salvar a política, aponte o endpoint MCP exposto pelo APIM para o agente de sua escolha e verifique três coisas:

  1. tools/list retorna apenas microsoft_docs_search e microsoft_docs_fetch. A terceira ferramenta não deve aparecer.
  2. tools/call para microsoft_code_sample_search retorna um erro JSON-RPC com código -32601.
  3. As duas ferramentas permitidas ainda funcionam de ponta a ponta.

Qual usar?

Opte pela Opção A (allowlist estática) a menos que tenha um motivo específico para não usar. É a mais previsível e não toca no corpo da resposta, o que te mantém longe da ressalva de streaming. Você terá que atualizá-la quando o catálogo de ferramentas upstream mudar, mas isso te dá maior controle sobre quando disponibilizar essas mudanças e cria uma decisão consciente de permiti-las ou não.

Recorra à Opção B quando o upstream mudar com frequência, ou quando você preferir bloquear ferramentas específicas conhecidas como ruins e deixar todo o resto passar. Teste cuidadosamente e observe o comportamento do agente em busca de qualquer sinal de problemas de streaming ou framing.

Se você tem um servidor MCP totalmente interno sob seu controle, a resposta mais limpa é corrigir a lista de ferramentas na origem e pular as alterações no gateway. Mas para servidores MCP de terceiros, essa é uma abordagem que você pode aplicar sem modificar o servidor MCP subjacente.

Perguntas Frequentes

  • O que é um MCP gateway e por que preciso de um?
    Um MCP gateway fica entre o agente (cliente) e o servidor MCP, inspecionando o tráfego JSON-RPC. Você precisa dele porque o protocolo MCP não oferece controle granular sobre quais ferramentas são expostas — é tudo ou nada. Com um gateway, você pode filtrar tools/list e bloquear tools/call indesejados sem modificar o servidor.

  • Qual a diferença entre as opções de allowlist e deny-list apresentadas?
    A allowlist (Opção A) constrói manualmente a lista de ferramentas permitidas e não consulta o backend — é a mais previsível e não mexe no corpo da resposta, mas exige atualização quando o catálogo upstream muda. A deny-list (Opção B) deixa o tools/list fluir e filtra as ferramentas indesejadas na resposta de saída, sendo mais dinâmica, porém mais frágil por ler o corpo da resposta (pode interferir com streaming).

  • O Azure API Management suporta MCP nativamente?
    Sim. O APIM possui uma seção dedicada chamada 'MCP servers' (separada do blade de APIs padrão). Você pode registrar um servidor MCP externo, expor uma REST API como MCP ou aplicar políticas padrão do APIM sobre o tráfego MCP. Isso inclui autenticação, rate limiting, logging e, como mostrado no artigo, controle de ferramentas.

  • Preciso modificar o servidor MCP original para usar o gateway?
    Não. O gateway fica na frente do servidor MCP e o agente aponta para a URL exposta pelo APIM. Nenhuma alteração no código ou configuração do servidor original é necessária. Isso é particularmente útil para servidores MCP de terceiros (como o Microsoft Learn MCP server) que você não controla.

  • Essa abordagem funciona com qualquer MCP server?
    Sim, desde que o servidor MCP utilize JSON-RPC sobre HTTP ou SSE (Server-Sent Events). O APIM faz o proxy do tráfego e aplica as políticas. A técnica de filtrar tools/list e tools/call é genérica e funciona para qualquer servidor MCP que siga o protocolo padrão.


Artigo originalmente publicado por samcogan em Azure Updates - Latest from Azure Charts.

Gostou? Compartilhe:
Precisa de ajuda?Fale com nossos especialistas 👋
Avatar Walcew - Headset