- Дата публикации
Как срезать лишние инструменты MCP через Azure API Management
Что нового
Microsoft добавила в Azure API Management (APIM) полноценную роль MCP‑шлюза. Теперь APIM умеет работать не только с REST, но и с MCP‑серверами, которые используют GitHub Copilot, Claude и другие агентные платформы.
Конкретно появилось:
- Отдельный раздел MCP Servers в интерфейсе APIM — не в общем списке API, а как самостоятельный тип сущности.
- Возможность подключить внешний MCP‑сервер (External MCP server): APIM выступает как reverse‑proxy, понимает MCP‑протокол (JSON‑RPC поверх HTTP или Server‑Sent Events) и вешает на него политики.
- Возможность поверх обычного REST API включить MCP‑слой и выдавать его агентам как MCP‑сервер.
- Поддержка политик APIM для MCP‑трафика: можно инспектировать и переписывать JSON‑RPC‑сообщения, фильтровать инструменты, валидировать аргументы, логировать вызовы.
Ключевой сценарий: вы можете дать разработчикам доступ к MCP‑серверу, но точечно отключить отдельные инструменты, которые:
- сжигают контекст без пользы,
- ходят в платные API,
- не проходят требования безопасности.
MCP‑спецификация не умеет отключать инструменты поштучно. Для тонкого контроля приходится ставить перед MCP‑сервером шлюз, который видит JSON‑RPC и может его фильтровать. Эту роль теперь берёт на себя APIM.
Как это работает
Базовая идея MCP‑шлюза
MCP‑шлюз сидит между агентом (клиентом) и MCP‑сервером (бэкендом). MCP — это JSON‑RPC поверх HTTP или SSE. Любой reverse‑proxy, который умеет смотреть и менять тело запроса/ответа, теоретически может играть роль MCP‑шлюза.
APIM делает именно это:
- Принимает соединение от агента (Copilot, Claude и т.п.).
- Понимает, что внутри идёт MCP‑протокол.
- Применяет к JSON‑RPC‑пакетам политики: аутентификация, фильтрация инструментов, логирование, квоты и т.д.
- Прокидывает допустимые вызовы на реальный MCP‑сервер или возвращает синтетический ответ.
MCP по сути опирается на два метода:
tools/list— агент при подключении спрашивает у сервера список инструментов: имена, описания, схемы входных данных. На этом основе он решает, что может делать.tools/call— агент вызывает конкретный инструмент с аргументами.
Если вы хотите спрятать инструмент, нужно обработать оба метода:
- отфильтровать его из ответа
tools/list, чтобы агент вообще не знал о его существовании; - заблокировать
tools/callпо имени инструмента, чтобы не сработал прямой вызов (например, если кто‑то жёстко прописал имя в конфиг).
Что даёт MCP‑шлюз на базе APIM
APIM переносит привычные фичи API‑шлюза в мир MCP:
- Централизованная аутентификация и авторизация. Можно поставить Entra ID, mTLS или токены с областями поверх MCP‑серверов, которые сами по себе знают только про API‑ключ.
- Rate limiting и квоты. Ограничить количество вызовов, чтобы зациклившийся агент не спалил бюджет на платном апстриме за пару минут.
- Логирование и аудит. Сохранять, какой пользователь вызвал какой инструмент и с какими аргументами. Отправлять это в Log Analytics или SIEM.
- Сетевую изоляцию. Спрятать внешние MCP‑сервера за приватными endpoint’ами и фиксированными egress‑IP. Машины разработчиков при этом не торчат в интернет напрямую.
- Шаринг одного MCP‑сервера между многими клиентами. Из single‑tenant MCP сделать общий вход с пер‑юзер идентичностью и лимитами на уровне команд.
- Политики и комплаенс. Управлять тем, какие инструменты видны, редактировать поля, валидировать аргументы, трансформировать ответы до того, как они попадут в агента.
- Агрегация MCP‑серверов. Собирать несколько бэкендов за одним логическим MCP‑endpoint’ом, чтобы агенты подключались только к одной точке.
- Устойчивость. Добавлять ретраи, circuit‑breaker’ы, кэшировать
tools/list, чтобы не реализовывать всё это в каждом агент‑хосте. - Контроль стоимости. Вводить лимиты, алерты и внутренний чарджбек для MCP‑серверов, которые ходят в платные по‑запросу сервисы.
MCP в интерфейсе APIM
В APIM для MCP выделен отдельный раздел MCP Servers. Там можно:
- зарегистрировать существующий внешний MCP‑сервер (External MCP server);
- включить MCP‑поверх обычного REST API.
Политики для MCP применяются в том же движке, что и для REST, но к JSON‑RPC‑сообщениям MCP. Можно читать context.Request.Body, context.Response.Body, проверять method, аргументы и возвращать свои ответы.
Пример: Microsoft Learn MCP‑сервер
В примере используется публичный MCP‑сервер Microsoft Learn. Он предоставляет три инструмента:
microsoft_docs_searchmicrosoft_docs_fetchmicrosoft_code_sample_search
Задача: оставить первые два и заблокировать третий.
Шаги настройки MCP‑шлюза в APIM:
- В APIM открыть раздел MCP Servers.
- Добавить новый MCP‑сервер.
- Выбрать тип External MCP server.
- Указать endpoint Microsoft Learn MCP.
- APIM сам подберёт транспорт (HTTP или SSE) в зависимости от апстрима.
- Сохранить.
- Настроить агента (Copilot, Claude и т.п.) на URL, который выдаёт APIM, а не прямой URL MCP‑сервера.
На этом этапе APIM работает как прозрачный прокси. Дальше всё решают политики.
Установка / Как запустить
Ниже — два готовых варианта политик APIM для MCP‑серверов.
Вариант A: статический allowlist инструментов
Вы жёстко задаёте список инструментов, которые MCP‑сервер «как бы» поддерживает. APIM:
- перехватывает
tools/listи возвращает свой список, игнорируя ответ бэкенда; - блокирует
tools/callдля запрещённых инструментов.
Плюсы:
- предсказуемое поведение;
- не нужно читать тело ответа от апстрима;
- изменения на стороне MCP‑сервера не просачиваются к пользователям, пока вы сами не обновите политику.
Минусы:
- нужно вручную поддерживать схемы входных данных;
- если на MCP‑сервере появится новый полезный инструмент, его надо отдельно добавить в политику;
- при изменении схемы входа у существующего инструмента политику тоже придётся обновлять.
Политика для варианта A (добавляется в редактор MCP Server policy в APIM):
<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>
Что делает эта политика:
- В первом
whenAPIM ловитtools/list, не отправляет его на MCP‑сервер, а возвращает собственный JSON‑RPC‑ответ с двумя инструментами. ID запроса берётся из входящего сообщения, чтобы клиенту всё сошлось. - Во втором
whenAPIM перехватываетtools/callсname = microsoft_code_sample_searchи отвечает ошибкой JSON‑RPC с кодом-32601(Method not found) и явным текстом, что инструмент отключён политикой APIM. - Все остальные вызовы (
tools/callдля разрешённых инструментов,initialize,pingи т.д.) проходят на бэкенд без изменений.
Ключевой момент — preserveContent: true при чтении тела запроса. Без него чтение тела «съедает» поток, и дальше по конвейеру уже нечего проксировать.
Вариант B: динамический deny‑list
Здесь вы доверяете списку инструментов от MCP‑сервера и вырезаете только нежелательные. Логика:
tools/listидёт на бэкенд, APIM получает полный список;- на выходе APIM переписывает ответ, удаляя инструменты из deny‑листа;
tools/callдля запрещённых инструментов по‑прежнему блокируется.
Плюсы:
- меньше ручной работы: новые инструменты появляются автоматически;
- изменения схем входных данных MCP‑сервером подхватываются без правки политики.
Минусы:
- политика читает и переписывает
context.Response.Body, а это может конфликтовать со стримингом; - придётся следить за новыми инструментами: если вы не добавите их в deny‑лист, они сразу станут доступны пользователям.
Политика для варианта B:
<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>
Разбор логики:
- Inbound: тот же блок, что и в варианте A. Блокирует прямые вызовы
microsoft_code_sample_searchс ошибкой-32601. - Backend: пустой — все разрешённые запросы идут на настоящий MCP‑сервер, включая
tools/list. - Outbound: если исходный запрос был
tools/list, политика читает тело ответа, парсит JSON, фильтрует массивresult.tools, выкидывая инструмент из deny‑листа, и возвращает обновлённый JSON.
Весь остальной трафик идёт без переписывания.
Как проверить, что всё работает
После сохранения политики подключите к APIM‑endpoint’у любой агент, который умеет MCP, и проверьте три вещи:
- В ответе на
tools/listвидны толькоmicrosoft_docs_searchиmicrosoft_docs_fetch.microsoft_code_sample_searchотсутствует. - Вызов
tools/callсname = microsoft_code_sample_searchвозвращает ошибку JSON‑RPC с кодом-32601. - Вызовы разрешённых инструментов проходят до реального MCP‑сервера и возвращают корректный результат.
Что это значит для вас
Когда стоит использовать APIM как MCP‑шлюз
Это решение полезно, если вы:
- запускаете GitHub Copilot, Claude или другие агенты в корпоративной среде;
- подключаете MCP‑сервера, которые вы не контролируете (SaaS, сторонние команды, публичные сервисы);
- хотите дать разработчикам мощные инструменты, но обязаны ограничить риски — финансовые и по безопасности.
Конкретные кейсы:
- Платные апстримы. MCP‑инструмент ходит в дорогой API по вызову. Через APIM вы ставите квоты, лимиты по пользователям и командам, алерты, а при необходимости — полностью отключаете инструмент.
- Секьюрити‑политики. Какой‑то инструмент умеет, например, вытаскивать чувствительные ресурсы. Вы скрываете его из
tools/listи блокируете прямые вызовы. - Общий доступ к MCP‑серверу. Один MCP‑сервер может обслуживать несколько команд и продуктов. APIM добавляет аутентификацию, разделяет лимиты и ведёт раздельный аудит.
- Безопасный выход в интернет. Разработчики сидят за NAT без прямого выхода. APIM выступает единой точкой выхода к внешним MCP‑серверам с контролируемыми IP и правилами.
Когда лучше не усложнять
- Если MCP‑сервер полностью ваш, и вы можете просто изменить список инструментов в его коде, проще починить всё на источнике. Шлюз не нужен.
- Если у вас один маленький внутренний проект без жёстких требований по аудиту и бюджету, MCP‑шлюз через APIM может быть избыточным.
- Если ваш агент активно использует стриминг ответов MCP (особенно для долгих операций), вариант с переписыванием тела ответа (deny‑list) нужно тестировать очень аккуратно. В спорных случаях лучше остаться на статическом allowlist.
Доступность и ограничения
APIM — облачный сервис Microsoft Azure. Для полноценной работы требуется доступ к Azure. Если ваша организация ограничивает использование зарубежных облаков или трафик в Azure, придётся согласовать это с безопасностью и IT.
Если доступ к Azure в вашей сети закрыт или вы не можете использовать облачные сервисы Microsoft по юридическим причинам, развернуть APIM не получится. В этом случае придётся искать альтернативный шлюз (self‑hosted reverse‑proxy с кастомной логикой для MCP).
Место на рынке
На практике роль MCP‑шлюза может выполнять любой достаточно умный reverse‑proxy:
- Nginx или Envoy с Lua/Wasmtime;
- собственный сервис на Node.js, Go или .NET;
- коммерческие API‑шлюзы других вендоров.
Ключевое отличие APIM — глубокая интеграция в экосистему Azure и готовый движок политик:
- те же механизмы аутентификации (Entra ID, mTLS);
- общая система логирования и мониторинга (Log Analytics, SIEM);
- единая точка управления квотами и тарифами для REST и MCP.
Числовых сравнений по скорости, задержкам или стоимости по отношению к другим решениям в материале нет. Но важно понимать: APIM добавляет собственный хоп в сеть и слой политик, поэтому задержка будет выше, чем при прямом подключении агента к MCP‑серверу. В обмен вы получаете управляемость, аудит и контроль инструментов.
Как выбирать между вариантами политик
- Если вам нужна жёсткая и предсказуемая конфигурация, список инструментов меняется редко, а стриминг критичен — берите Option A (allowlist). Вы полностью контролируете, что видит агент, и не трогаете тело ответов.
- Если MCP‑сервер часто обновляется, добавляет инструменты и меняет схемы, а вы хотите минимум ручной работы — используйте Option B (deny‑list), но внимательно тестируйте поведение агентов и стриминг.
Для полностью внутренних MCP‑серверов, которыми владеет ваша команда, самый чистый путь — вообще не трогать APIM, а настраивать инструменты прямо в сервере. APIM как MCP‑шлюз особенно полезен там, где вы интегрируете сторонние MCP‑сервера и не можете менять их поведение, но хотите держать управление на своей стороне.