ESPN DeportesFederación Portuguesa suspende a Cristiano Ronaldo tras abandonar la selecciónThe Jerusalem PostWWII air-raid shelter buried beneath German playground rediscovered during construction workESPNThe WNBA Finals are set! First look at how Valkyries, Dream match upPunchRonaldo will return to Portugal squad in November — OfficialComplete SportsArsenal Make 27-Year History During Leeds Comeback WinZDF heuteAktuelle Pressemitteilungen des ZDFRTP DesportoMarco Silva reconhece que Benfica não tem outra solução além de ganharFootball ItaliaItaly doctor hits back at Fabregas’ Kean comments: ‘We constantly keep clubs updated’DW EnglishTrump slams Zelenskyy, says Ukraine should elect new presidentAntara NewsPolice foils illegal trade of protected mobula ray species in BaliMyJoyOnlineHincapie injury adds to Arsenal issues after Guimaraes rescue actVariety‘Children of Blood and Bone’ Author Tomi Adeyemi Quietly Cancels New York Comic Con Panel
The Daily Newsstand · Free, Always
Saturday, October 10, 2026

У нас есть MCP дома

Translate

Всем привет! Меня зовут Петрович и я работаю техлидом java команды в небольшом международном финтехе. В прошлый раз я рассказывал, как мы меняли Redis на Hazelcast, а сегодня расскажу как мы делали свой MCP-сервер для Jira, Confluence и GitLab (все self-hosted). От пустого репозитория до прода прошло две недели, а сейчас у сервера уже почти 100 инструментов. Расскажу, почему не взяли готовое решение, как все устроено внутри и на какие грабли мы наступили (спойлер: почти все грабли назывались null).

Немного матчасти: что такое MCP

Model Context Protocol - это открытый стандарт от Anthropic (2024 года рождения), с помощью которого AI-агент обращается к внешним данным и инструментам. Прокладка между агентом и REST API. Агент сам не ходит в Jira: он подключается к отдельному MCP-серверу, получает от него список инструментов с описаниями и JSON-схемами параметров, а дальше сам решает, какой инструмент и с какими аргументами вызвать. Экономия токенов и контекста получается огромная. Но экономия токенов не главное преимущество MCP.

Главная ценность протокола лучше всего видна на картинке:

N×M против N+M

N×M против N+M

Без MCP каждый AI-клиент интегрируется с каждой системой отдельно: три клиента и три системы дают девять уникальных интеграций, и каждую кто-то должен написать и поддерживать. С MCP посередине получается N + M: сервер подключили один раз и им сразу может пользоваться любой MCP-клиент без необходимости адаптировать под него. Стандарт MCP поддерживают все вендоры.

Почему не взяли готовое

Первое что я сделал - пошел смотреть что уже есть. MCP-серверов для Atlassian и GitLab на GitHub десятки, большую часть из них объединяет одно: они рассчитаны на одного пользователя или на общий сервисный токен. Поднял локально, положил свой токен в переменную окружения, работаешь. Для пет-проекта приемлемо, но для компании с сотнями сотрудников и разграничением доступа - нет.

Если копнуть глубже, то вырисовывается следующий список недостатков опенсорсных решений:

  1. Нет авторизации. Общий сервисный аккаунт в Jira, через который агент любого сотрудника видит все, это явно не то за что погладит по головке безопасник. Хочется иметь гранулярный доступ на каждого сотрудника в отдельности. И закрыть авторизацией сам MCP.

  2. Плохая расширяемость. Публичное решение заточено под общий случай. Нужный инструмент можно добавить только форком чужого репозитория, а потом вечно поддерживать свой форк. Спасибо, я уже насмотрелся на форки hibernate, Camunda в других компаниях.

  3. Неконтролируемый стек. Транспорт, хранение секретов, версии зависимостей выбирает автор проекта и совмещать это с внутренними стандартами либо больно, либо вовсе невозможно.

  4. Чужой непроверенный код с доступом к токенам. Сервер получает токены и данные пользователя, но код никто в компании не ревьюил. Заметная часть таких серверов сгенерирована LLM почти без ревью, и независимые исследования на предмет уязвимостей находят эксплуатируемые проблемы в 30–82% публичных MCP-серверов (разброс зависит от исследования и методики, но даже нижняя граница не радует).

  5. Промпт-инъекции. Вредоносный или скомпрометированный сервер может хранить инструкции в описании инструмента и незаметно управлять действиями агента (привет, tool poisoning). Описание инструмента модель читает как часть контекста и, как правило, читает внимательно.

  6. Неясные перспективы поддержки. На момент подготовки статьи 485 репозиториев с тегом mcp-server уже были помечены на GitHub как archived. Закрываются даже проекты крупных компаний: у Roblox studio-rust-mcp-server архивирован с формулировкой «Product discontinued».

И вишенка на торте - реальные критические CVE в экосистеме, причем не где-то на задворках, а в самых популярных компонентах:

  1. CVE-2025-49596 - RCE в официальном MCP Inspector из-за отсутствия аутентификации между Inspector и его proxy;

  2. CVE-2025-6514 (CVSS 9.6) - command injection в mcp-remote при подключении к недоверенному MCP-серверу.

Официальные серверы вендоров тоже не помогают: тот же Atlassian делает MCP для облака, а у нас Jira и Confluence Data Center. Я в целом за минимализм и минимальное приложение усилий. Если кто-то сделал работающее решение за меня, то я рад был бы сказать что нельзя велосипедить свое, но, вы не поверите, это именно тот случай когда можно и нужно писать свое.

Требования к своему

Смекнув что к чему мы быстро cформулировали следующие требования к собственному MCP:

  1. Один сервер и множество пользователей (и изоляция). Каждый пользователь хранит и видит только свои токены. Даже администратор чужие не видит. Меньше видишь - крепче спишь.

  2. Авторизация через корпоративный AD. К AD у нас подключен keycloak. Сотрудники ходят в keycloak. Сервисы ходят в keycloak. Роли подтягиваются из AD в keycloak. Плюс должен быть единый личный API-ключ для подключения агентов. Один или более.

  3. Шифрование секретов. Токены Jira/GitLab/Confluence лежат в базе зашифрованными, сырой ключ шифрования не покидает свой уютный дом (Vault).

  4. Поддержка двух видов транспорта MCP. streamable-http и stdio, причем клиенту не нужно уметь в OIDC/JWT - у него токен.

  5. Самообслуживание. Пользователь сам добавляет токены и выпускает ключи в веб-интерфейсе, без заявок в поддержку. Прямо как в столовой. Столовой самообслуживания.

  6. Расширяемость. В первую очередь Jira, Confluence, GitLab. Затем любые внутренние сервисы, причем добавление новой интеграции должно быть быстрым.

  7. Деплой как у нас принято - Kubernetes, GitOps - все как у остальных сервисов.

Стек выбрали свежий: Java 25, Spring Boot 4.1, Spring AI 2.0 (там появилась аннотационная модель @McpTool/@McpToolParam), PostgreSQL 17, Liquibase, Vault, Keycloak. Короче, все как мы любим. Клиенты внешних API генерируются из OpenAPI-спецификаций, образы собирает Jib, раскатывает GitOps-агент из GitOps-репозитория.

Потанцуем про архитектуру

Общая схема

Общая схема

В кластере два Java-сервиса, для отказоустойчивости развернули по сине-зеленой схеме деплоя:

  1. Бэкенд - реактивный MCP-сервер плюс REST API для интерфейса. Здесь живут все проверки токенов, обработка запросов пользователй и т.д.

  2. Веб-UI - простенький веб-интерфейс: авторизация, токены интеграций, MCP-ключи.

Интеграции с Jira, Confluence и GitLab - стартеры. Плодить микросервис на каждую систему ради пары десятков HTTP-вызовов не хотелось: деплой один, пул соединений один (общий на все интеграции) и мониторить необходимо тоже один под.

Трассировка вызова

Самое интересное что происходит под капотом, когда агент вызывает MCP, например, при запросе вида:

покажи мои задачи в Jira

Трассировка вызова от агента до REST API

Трассировка вызова от агента до REST API

  1. Клиент (cli или агент) присылает запрос вместе с личным MCP-ключом пользователя. Ключ показывается один раз при выпуске, а в базе хранится только его хэш.

  2. Сервер по хэшу находит владельца ключа и проверяет, что ключ не отозван и не истек. Ключ отвечает только на вопрос «кто это».

  3. Сервер расшифровывает токен Jira текущего пользователя. Владелец извлекается только из ключа хранящегося в бд и никогда из параметров запроса. В запросе нет информации о владельце.

  4. Токен тоже лежит в базе зашифрованным, расшифровывает его Vault.

  5. Сервер идет во внешнюю систему с токеном пользователя.

  6. Ответ сжимается, в целях экономии контекстного окна. Сырой ответ Jira на поиск легко съедает бОльшую часть контекстногоокна модели.

Расшифрованный токен живет только в рамках одного вызова и нигде не логируется. В базе он никогда не лежит в открытом виде, а ключ шифрования не покидает Vault. Сам сервер авторизуется в Vault через Kubernetes, так что статичных токенов Vault, которые можно скопроментировать, тоже нет.

Если у пользователя несколько токенов одной системы (например, два инстанса GitLab), в промпте можно указать лейблы и это будет работать.

Contract first

Казалось бы: зачем Contract first, когда можно взять библиотеки и интегрироваться через добавление зависимости. Так-то оно так, но загибайте пальцы:

  1. Все клиенты внешних API генерируются при сборке единообразно (мы не следим за библиотеками, транзитивными зависимостями, а просто обновляем один yaml файл)

  2. Процессом генерации управляем мы (захотели поменять генератор - поменяли)

  3. Авторизация у систем тоже разная: Jira и Confluence принимают Personal Access Token, а GitLab хочет свой заголовок PRIVATE-TOKEN. В спеке это просто разные security-схемы, но нам-то нужно остальной код научить правильно обращаться со всем эти зоопарком.

stdio-прокси: как два байта переслать

Не все MCP-клиенты умеют в HTTP. Некоторые умеют, но не дружат с авторизационными заголовками. Для таких случае есть stdio-прокси - начиналось это как локальный fat jar, который пользователь (как правило разработчик) запускает у себя. А закончилось компиляцией в нативный исполняемый файл и необходимость ставить локально что-либо исчезла.

Прокси не реализует ни одного инструмента. На старте он вызывает tools/list у бэкенда и динамически регистрирует у себя те же инструменты, а каждый tools/call один в один пересылает по HTTP с ключом пользователя. Просто донельзя.

Поэтому сделан он с использованием MCP Java SDK, на чистой, незамутненной Java без Spring. Добавили интеграцию на бэкенде, и прокси у всех подхватил ее при следующем запуске, без обновления jar.

Безопасность: изолируй и властвуй

В одном приложении живут два независимых механизма аутентификации, и это, пожалуй, самое важное решение в плане безопасности (после шифрования секретов, конечно).

Изолируй и властвуй наглядно

Изолируй и властвуй наглядно

  1. Эндпоинт /mcp/* - только личный MCP API-ключ. MCP-клиенту не нужно уметь в OIDC, а нам не нужно учить Claude Desktop обновлять JWT.

  2. Остальной REST - JWT: пользователь входит учетку из AD, получает токен в кeycloak, сам себе создает и отзывает токены доступа.

Грабли

Каждую интеграцию после реализации мы проверяем живыми вызовами инструментов через MCP-клиент против реального инстанса системы и по итогам пишем отчет: сценарии, найденные дефекты, исправления. Почти все грабли ниже найдены именно так, а не юнит-тестами. Почему - станет понятно из второго пункта.

  1. null - главный враг человечества. Инструмент jira_update_issue падал на каждом вызове: сгенерированный клиент сериализовал непереданные поля как явный null, и в тело PUT всегда попадало "priority": null, которое Jira отклоняет как невалидное значение. Хуже того, поле labels в сгенерированной модели по умолчанию инициализировался пустым списком, то есть уходил "labels": [], и после очевидного фикса первой проблемы инструмент начал бы молча стирать метки задачи при каждом обновлении описания. У GitLab еще веселее: на PUT непереданное поле со значением null не отклоняется валидацией, а реально затирает поле задачи или MR на сервере. Решение добавить везде - NON_NULL в настройках ObjectMapper.

  2. Тесты зеленые, на проде 500. Инструмент confluence_add_comment падал в 100% случаев с пустым HTTP 500 без внятной ошибки. Оказалось, Confluence требует у поля container поле type, которого не было в спеке, написанной по документации. Юнит-тесты на WireMock этого не поймали: заглушки строились по той же неполной спеке, что и клиент. С тех пор прогон на проде - обязательный этап тест-плана.

  3. Vault в dev-режиме теряет все что создал ранее при рестарте. Для локальной разработки это особенно занятно, когда transit-ключ пересоздается, а ciphertext в базе остается. Перешли на volume и init-контейнер, который сам инициализирует Vault и создает ключ при docker compose up.

Вместо заключения

Если вам нужен MCP для себя, берите готовый сервер, их много, многие из них достаточно хорошие.

Если же агентом будет пользоваться команда или компания, где важно, от чьего имени агент ходит в Jira и где лежат приватные токены, то свой сервер на знакомом конкретно вашей команде стеке уже не кажется дорогим удовольствием: две недели и вы в дамках.

Если статья понравилась - подписывайтесь. В комментариях делитесь опытом: используете ли вы в компании свой или опенсорсный MCP и как решили вопрос с токенами доступа к внутренним системам?

P.S. Да, не передать словами каким был вау-эффект, когда во время демо агент с подключенным MCP за 3 минуты собрал ошибки с логов за последние 6 часов, удалил дубликаты, создал задачу, написал в общий чат и потратил пару сотен токенов на все про все.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.