Ваш MCP-сервер не знает, кто вы. И это проблема
Представьте: вы развернули MCP-сервер для работы с корпоративной CRM. Он умеет создавать сделки, тащить контакты и обновлять статусы. Запускаете на нём MCP Orchestrator — и тут выясняется, что каждый AI-агент видит все данные. Без разницы, кто спрашивает: Петя из продаж или Вася из бухгалтерии. MCP-сервер по умолчанию слеп к пользователям. В однопользовательском сценарии это ок, но в мультитенантной среде — выстрел в ногу.
Давайте честно: MCP без аутентификации — это офис с открытыми дверями. Каждый может зайти и взять документы. Но только до первого аудита. Недавнее исследование 2181 MCP-эндпоинтов показало, что 37% из них требуют аутентификации, но лишь единицы делают это per-user. Остальные тупо проверяют статический API-ключ. Это не про безопасность.
Два пути, и один ведёт к Tech Debt
Вариант А: встроить OAuth-логику в каждый MCP-сервер. Прописать Keycloak client, обрабатывать редиректы, хранить токены. Звучит логично, но есть нюанс:
- Меняется протокол — каждый сервер придётся переписывать.
- Усложняется конфигурация — если серверов 10, то OAuth нужно настраивать в каждом.
- Смешивается логика авторизации и бизнес-логики — плохая архитектура в долгосрочной перспективе.
Вариант Б: вынести OAuth в отдельный Auth Proxy. Прокси сидит перед MCP-серверами, проверяет JWT, подставляет заголовок с user_id. MCP-сервер просто читает этот заголовок и работает в контексте пользователя. Никакой магии, только правильный паттерн Sidecar / Gateway. Выбираем Б.
Архитектура Auth Proxy: три кита
Всё строится вокруг трёх компонентов:
| Компонент | Роль |
|---|---|
| Keycloak (IdP) | Выпускает JWT, управляет пользователями, client_id/client_secret |
| Telegram-бот | Инициирует OAuth-флоу от имени пользователя, получает токен и отдаёт его клиенту |
| Auth Proxy | Проверяет JWT, пробрасывает заголовок X-User-ID и X-User-Roles на MCP-сервер |
Важно: Telegram-бот не хранит токены постоянно. Он получает их, отдаёт клиенту (например, Claude Code или вашему кастомному агенту) и забывает. Клиент кеширует токен локально. Это снижает риск компрометации.
1Разворачиваем Keycloak
Берём последнюю версию Keycloak (на апрель 2026 — это v26.x с поддержкой OAuth 2.1 и PKCE). Поднимаем в Docker:
docker run -d --name keycloak \
-p 8443:8443 \
-e KC_HOSTNAME=auth.example.com \
-e KC_HOSTNAME_STRICT=false \
-e KC_HTTPS_CERTIFICATE_FILE=/certs/tls.crt \
-e KC_HTTPS_CERTIFICATE_KEY_FILE=/certs/tls.key \
quay.io/keycloak/keycloak:26.0.0 start --optimized
Создаём realm mcp-realm, клиент telegram-bot с типом confidential, включаем Standard Flow и PKCE. Для production не забудьте настроить HTTPS (самоподписанный для теста, Let's Encrypt — для прода).
Частая ошибка: не ставят Valid Redirect URIs. Укажите https://t.me/your_bot или, если используете WebApp, точный URL. Иначе Keycloak не примет callback.
2Создаём Telegram-бота для OAuth-флоу
Telegram Bot API сам по себе не поддерживает OAuth-редиректы. Решение: бот генерирует ссылку на Keycloak, пользователь открывает её в браузере, а код авторизации пользователь отправляет обратно боту (или через WebApp). Покажу на Pyrogram — но aiogram тоже подойдёт. Код бота (минимум):
import requests
from pyrogram import Client, filters
import secrets
CLIENT_ID = "telegram-bot"
CLIENT_SECRET = "supersecret"
KEYCLOAK_URL = "https://auth.example.com/realms/mcp-realm/protocol/openid-connect"
app = Client("mcp_oauth_bot")
@app.on_message(filters.command("login"))
async def login(client, message):
state = secrets.token_urlsafe(16)
auth_url = f"{KEYCLOAK_URL}/auth?response_type=code&client_id={CLIENT_ID}&state={state}&redirect_uri=myapp://callback"
await message.reply(f"Перейдите по ссылке для входа: {auth_url}")
@app.on_message(filters.text & ~filters.command)
async def handle_code(client, message):
code = message.text.strip()
# обмен authorization code на токен
resp = requests.post(f"{KEYCLOAK_URL}/token", data={
"grant_type": "authorization_code",
"code": code,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"redirect_uri": "myapp://callback"
})
if resp.ok:
token = resp.json()["access_token"]
await message.reply(f"Ваш токен: {token}\nОтправьте его в настройки AI-агента.")
else:
await message.reply("Ошибка при получении токена.")
app.run()
В реальном проекте лучше реализовать WebApp: пользователь нажимает кнопку, открывается WebView с Keycloak login, токен приходит через Telegram WebApp API. Но для MVP достаточно ручного копирования.
3Пишем Auth Proxy (сверяем JWT)
Прокси — самое мяско. Можно взять готовый oauth2-proxy или написать свой на Python с aiohttp. Я покажу свой, потому что хочу полный контроль. Плюс — интеграция с любимыми инструментами вроде MCPHero.
import jwt
from aiohttp import web
KEYCLOAK_PUBLIC_KEY = open("keycloak.pem").read()
async def handle(request):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return web.json_response({"error": "Unauthorized"}, status=401)
token = auth_header.split()[-1]
try:
payload = jwt.decode(token, KEYCLOAK_PUBLIC_KEY, algorithms=["RS256"],
audience="account")
except jwt.PyJWTError as e:
return web.json_response({"error": str(e)}, status=401)
# пробрасываем пользовательские данные на MCP-сервер
headers = {
"X-User-ID": payload["sub"],
"X-User-Roles": ",".join(payload.get("realm_access", {}).get("roles", [])),
"X-User-Email": payload.get("email", "")
}
# здесь ваш клиент MCP-сервера (например, httpx)
async with web.ClientSession() as session:
async with session.get("http://mcp-server:8000/mcp", headers=headers) as resp:
data = await resp.json()
return web.json_response(data)
app = web.Application()
app.router.add_route('*', '/mcp/{path:.*}', handle)
web.run_app(app, port=8080)
Не забудьте настроить CORS, если прокси вызывается из браузера. И используйте PKCE для защиты от перехвата authorization code.
4Учим MCP-сервер читать заголовки
Ваш MCP-сервер (Python, Node, Go) должен брать X-User-ID и фильтровать данные. Например, в FastMCP:
from fastmcp import FastMCP, Context
mcp = FastMCP("crm")
@mcp.tool()
def get_deals(ctx: Context):
user_id = ctx.request.headers.get("X-User-ID")
if not user_id:
raise Exception("Missing user context")
# вытащить сделки только для этого пользователя
return db.query("SELECT * FROM deals WHERE owner_id = ?", user_id)
Теперь агенты, работающие через Claude Code или MCP Chat Studio, будут получать данные строго в контексте пользователя.
Грабли, на которые я наступал (вы — не наступайте)
- Не использовать HTTPS. JWT — не секрет, перехватили — всё, вы в пролёте. Только HTTPS, даже для localhost (самоподписанный сертификат).
- Не валидировать issuer и audience. Если не проверить
issиaud, злоумышленник может подставить JWT от другого Keycloak. Всегда проверяйте. - Игнорировать refresh token. Access token живёт 5-15 минут. Если не сделать refresh, пользователь будет часто перелогиниваться. Реализуйте механизм: клиент хранит refresh token, при 401 пытается обновить через прокси или напрямую в Keycloak.
- Хранить client_secret в коде бота. Используйте переменные окружения или Vault. Как настроить groupPolicy и защититься от промпт-инъекций — тема смежная, советую почитать.
Неочевидный совет: храните токены в Vault, а отдавайте через подписанные short-lived сессии
Бот получает токен — это точка уязвимости. Если кто-то взломает бота, он получит все токены пользователей. Лучше: бот не хранит токен вообще. Он создаёт в HashiCorp Vault временную запись (скажем, на 1 час), а клиент по ID сессии получает доступ. Прокси сверяет сессию с Vault и выдёт JWT. Это усложняет архитектуру, но в enterprise без этого не обойтись.
Ещё один лайфхак: используйте Token Exchange в Keycloak. Пользовательский токен (с ограниченными правами) можно обменять на токен для конкретного MCP-сервера с более узкими скоупами. Это реализуется парой строк конфигурации в Keycloak и одним вызовом API через proxy. mcp-context-proxy как раз умеет такое, но уже с другим подходом.
Прогноз: Auth Proxy станет стандартом для корпоративных MCP
Сейчас MCP-экосистема переживает подростковый возраст. Люди запускают серверы без аутентификации, потому что «это же для агентов, а не для людей». Но когда агенты начнут выполнять реальные бизнес-задачи — финансовые операции, изменение документов, — без per-user контроля безопасности не обойтись. Архитектура Auth Proxy с Keycloak и Telegram-ботом — это не единственное решение, но одно из самых гибких. Telegram даёт удобный интерфейс для пользователя, Keycloak — стандарт IdP, а proxy сохраняет MCP-серверы чистыми.
Кстати, в VK-ботах или LM Studio MCP можно применить тот же паттерн — заменить только фронтенд. Удачи.