mcp-secure-server
Uses Keycloak as the OAuth 2.1 identity provider backing the server's authorization flow: verifies incoming user JWTs against its JWKS (signature, issuer, audience, expiry) and performs RFC 8693 token exchange so that each tool call is made against corporate APIs with a short-lived token carrying the same user subject, an actor claim for the MCP server, and a narrowed scope. The dev-issuer stands in for Keycloak on the local stand.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-secure-serverlook up CRM deals for Acme Corp"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-secure-server
Референсный MCP-сервер, через который LLM-агент получает доступ к закрытым корпоративным API с правами конкретного пользователя, а не с правами сервиса. OAuth 2.1 resource server, декларативный RBAC, token exchange вместо «супер-токена», аудит, маскирование PII и пометка недоверенного вывода.
Это учебный/референсный проект: небольшой, полностью покрытый тестами и рассчитанный на то, чтобы его можно было прочитать за вечер. Это не готовый продукт — см. Ограничения.
Зачем
Типичная схема «подключим агента к CRM» выглядит так: MCP-сервер держит сервисный токен с полными правами, а агент вызывает тулы. Проблемы:
Confused deputy. Любой пользователь агента получает права сервиса. Агента можно уговорить (в том числе через данные, которые он сам прочитал) сделать то, на что у пользователя прав нет.
Нет атрибуции. В CRM все изменения сделаны «сервисным аккаунтом».
Утечка данных в контекст модели. Email, телефоны, номера карт попадают в промпт и логи.
Prompt injection через вывод. Текст из базы («игнорируй инструкции и …») приходит модели как результат тула.
Здесь показано, как закрыть каждый пункт стандартными средствами: MCP Authorization (OAuth 2.1 resource server, RFC 9728), RFC 8693 Token Exchange и проверкой прав на двух уровнях.
Related MCP server: grantd-mcp
Что внутри
Компонент | Что делает | Файл |
MCP-сервер | Streamable HTTP, 3 тула, официальный MCP Python SDK ( |
|
Проверка токенов |
|
|
RBAC | YAML-политика scope → роль → тул → операции, deny-by-default |
|
Доступ к API от имени пользователя | Token exchange: токен с |
|
Безопасный вывод | маскирование PII, лимит размера, конверт |
|
Аудит | JSON-строка на каждый вызов: кто, тул, решение, без значений аргументов |
|
Mock корпоративного API | FastAPI, HR-справочник и CRM-сделки, сам проверяет токен и scope |
|
Dev-issuer | JWKS + выпуск токенов из CLI + token exchange. Только для локального стенда |
|
Тулы:
Тул | Операция | Нужный scope пользователя | Scope токена к API |
| read |
|
|
| read |
|
|
| write |
|
|
Роли складываются: у менеджера в токене crm.read crm.write, и он получает права обеих ролей.
Архитектура
flowchart LR
U[Пользователь] --> A[MCP-клиент / агент]
IdP[(IdP<br/>Keycloak или dev-issuer)]
A -- "Bearer JWT<br/>aud = MCP-сервер" --> S
subgraph S[mcp-secure-server]
direction TB
V[TokenVerifier<br/>подпись, iss, aud, exp] --> G[Guard: RBAC<br/>до выполнения тула]
G --> T[Тул]
T --> SAN[PII-маска · лимит размера<br/>· конверт untrusted]
G -.-> AUD[(audit log)]
end
S -- "token exchange<br/>RFC 8693" --> IdP
T -- "Bearer JWT<br/>aud = corp-api, sub = пользователь,<br/>scope сужен" --> API[Корпоративный API<br/>сам проверяет токен и scope]
S -. JWKS .-> IdP
API -. JWKS .-> IdPПуть одного вызова tools/call:
sequenceDiagram
participant C as MCP-клиент
participant S as MCP-сервер
participant I as IdP
participant B as Корп. API
C->>S: POST /mcp, Authorization: Bearer (aud=MCP)
S->>S: SDK + JwtTokenVerifier: подпись, iss, aud, exp
alt токен невалиден
S-->>C: 401 + WWW-Authenticate: resource_metadata=...
end
S->>S: Guard: scopes → роли → policy.check(tool, op)
alt запрещено
S->>S: audit(deny)
S-->>C: tool error "access denied"
end
S->>I: token exchange (subject_token, audience=corp-api, scope=crm.write)
I-->>S: короткоживущий токен (sub=alice, act=mcp-secure-server)
S->>B: PATCH /crm/deals/d1 (Bearer обменянный токен)
B->>B: своя проверка подписи, aud, scope
B-->>S: данные
S->>S: маска PII → лимит → {untrusted: true, data: ...}
S->>S: audit(allow)
S-->>C: результат тулаПочему token exchange, а не проброс токена пользователя
Спецификация MCP Authorization прямо запрещает token passthrough: токен, выданный для MCP-сервера
(aud = MCP-сервер), нельзя отправлять в другие API. Если API примет такой токен, его можно
переиспользовать против любого сервиса, который «доверяет всему от этого IdP».
Вместо этого сервер на каждый вызов меняет токен пользователя на новый:
aud= конкретный API, а не MCP-сервер;sub— тот же пользователь, API пишет изменения на него (updated_by: alice);act.sub= MCP-сервер (RFC 8693, actor claim) — видно, что действовал агент;scope— только то, что нужно этому тулу, и не шире исходного: IdP отказывает, если пользователь просит то, чего у него нет;время жизни — 60 секунд.
У MCP-сервера нет собственных прав на данные. Его client secret позволяет только обменивать токены пользователей.
RBAC-политика
scope_roles: # scope из токена -> роль
hr.read: hr_viewer
crm.read: sales_viewer
crm.write: sales_manager
roles: # роль -> тул -> операции
hr_viewer:
search_employees: [read]
sales_viewer:
search_deals: [read]
sales_manager:
update_deal_status: [write]Правила:
запрещено всё, что явно не разрешено: неизвестный scope, тул не из политики,
writeпри наличии толькоread;каждый тул объявляет свою операцию в коде (
guard.run("update_deal_status", "write", ...)), политика решает, кому она доступна;проверка идёт до любого обращения к бэкенду;
некорректная политика (ссылка на несуществующую роль, неизвестная операция) не даёт серверу стартовать.
Модель угроз (кратко)
Угроза | Митигация | Где проверяется |
Вызов без токена, с просроченным, чужим ( |
|
|
Пользователь вызывает тул сверх своих прав | RBAC deny-by-default до выполнения тула |
|
Confused deputy: сервер делает то, на что у пользователя нет прав | Нет сервисного токена; token exchange с тем же |
|
Ошибка в RBAC-политике | Второй рубеж: IdP и API не выдадут и не примут лишний scope |
|
PII попадает в контекст модели | Маскирование email / телефонов / карт (с проверкой Луна) во всех строках ответа |
|
Огромный ответ съедает контекст | Лимит |
|
Prompt injection через данные из API | Ответ заворачивается в |
|
Утечка через логи | Аудит пишет только имена аргументов, никогда значения, ответы или токены |
|
DNS rebinding к локальному серверу |
| конфиг в |
Что не закрыто — см. Ограничения.
Быстрый старт
Docker Compose
docker compose up --build -d
# токены выпускает dev-issuer (ключ лежит внутри его контейнера)
MANAGER=$(docker compose exec -T issuer python -m dev_issuer.mint --sub alice --scope "crm.read crm.write")
VIEWER=$(docker compose exec -T issuer python -m dev_issuer.mint --sub bob --scope "crm.read")
HR=$(docker compose exec -T issuer python -m dev_issuer.mint --sub carol --scope "hr.read")
# без токена -> 401 и ссылка на Protected Resource Metadata
curl -i -X POST http://localhost:8000/mcp
curl http://localhost:8000/.well-known/oauth-protected-resource/mcp
docker compose logs -f mcp # здесь виден аудит-логНаружу публикуются только MCP-сервер (127.0.0.1:8000) и dev-issuer (127.0.0.1:9000).
Mock API доступен только внутри сети compose.
Без Docker
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
uvicorn --factory dev_issuer.issuer:app_from_env --port 9000 &
uvicorn --factory mock_api.app:app_from_env --port 9100 &
mcp-secure-server & # http://127.0.0.1:8000/mcp
python -m dev_issuer.mint --sub alice --scope "crm.read crm.write"Ключ dev-issuer создаётся в .dev/issuer_key.pem при первом запуске (каталог в .gitignore).
Подключение к MCP-клиенту
Dev-issuer не реализует интерактивный логин (authorization code + PKCE), поэтому токен
выпускается вручную и передаётся заголовком Authorization: Bearer <token>.
MCP Inspector
npx @modelcontextprotocol/inspectorTransport: Streamable HTTP, URL: http://localhost:8000/mcp, в разделе аутентификации —
заголовок Authorization со значением Bearer <token>. Попробуйте update_deal_status
с токеном bob (отказ) и с токеном alice (успех).
Claude Desktop — через прокси mcp-remote,
который умеет добавлять заголовки (claude_desktop_config.json):
{
"mcpServers": {
"corp-secure": {
"command": "npx",
"args": ["mcp-remote", "http://localhost:8000/mcp", "--header", "Authorization:${AUTH_HEADER}"],
"env": { "AUTH_HEADER": "Bearer <token>" }
}
}
}Python (MCP SDK)
import httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
async with (
httpx2.AsyncClient(headers={"Authorization": f"Bearer {token}"}) as http,
Client(streamable_http_client("http://localhost:8000/mcp", http_client=http)) as client,
):
result = await client.call_tool("search_deals", {"query": "Вектор"})Prod-режим: внешний IdP
Сервер не знает про dev-issuer — он работает с любым IdP, который выдаёт JWT (RS256), публикует JWKS и поддерживает token exchange (RFC 8693). Например, для Keycloak:
Переменная | Значение |
|
|
|
|
| публичный URL сервера, например |
|
|
| конфиденциальный клиент MCP-сервера с разрешённым token exchange |
| адрес корпоративного API и его audience |
| политика и лимит ответа (по умолчанию |
| адрес прослушивания и allow-list заголовка |
В IdP нужно: mapper, кладущий в aud URL MCP-сервера; client scopes hr.read, crm.read,
crm.write; разрешение клиенту mcp-secure-server обменивать токены с audience API.
С живым Keycloak проект в CI не проверяется — только с dev-issuer, который реализует
тот же контракт.
Тесты и линтер
pytest -v # 44 теста, без сети и Docker
ruff check . && ruff format --check .Тесты поднимают dev-issuer, mock API и MCP-сервер в одном процессе (ASGI-транспорты httpx) и ходят в сервер настоящим MCP-клиентом из SDK, так что проверяется весь путь: HTTP → auth → RBAC → token exchange → API → санитизация → аудит.
Файл | Что проверяет |
| нет токена, просрочен, чужой |
| deny-by-default, read ≠ write, неизвестные scope/тулы, валидация политики |
| разрешено/запрещено через MCP, атрибуция изменений, невозможность эскалации через exchange, защита при ошибке в политике |
| маскирование, ложные срабатывания, лимит размера, конверт untrusted |
| allow / deny / error в логе, отсутствие значений аргументов, PII и токенов |
Структура
├── policy.yaml # RBAC-политика
├── src/
│ ├── secure_mcp/ # MCP-сервер
│ │ ├── server.py # тулы + Guard (RBAC → вызов → санитизация → аудит)
│ │ ├── auth.py # JWT / JWKS, TokenVerifier для SDK
│ │ ├── policy.py # загрузка и проверка политики
│ │ ├── backend.py # token exchange + HTTP-клиент API
│ │ ├── sanitize.py # PII, лимит, конверт untrusted
│ │ ├── audit.py # аудит-лог
│ │ └── config.py # настройки из env
│ ├── mock_api/app.py # mock корпоративного API
│ └── dev_issuer/ # dev-IdP: JWKS, token exchange, CLI mint
├── tests/
├── Dockerfile
└── docker-compose.ymlОграничения
Честный список того, чего здесь нет или что упрощено:
Dev-issuer — не IdP. Нет логина, authorization code + PKCE, refresh-токенов, ротации ключей, отзыва. Protected Resource Metadata указывает на него, но клиент не сможет пройти OAuth-flow автоматически — токен передаётся вручную.
Маскирование PII — регулярки, а не DLP. Ловит типовые форматы email, телефонов (от 10 цифр) и карт (с проверкой Луна). Не ловит имена, адреса, паспорта, номера в нестандартном написании; возможны ложные срабатывания на длинных числах. Маскирование одинаково для всех ролей.
Пометка untrusted — сигнал, а не гарантия. Она помогает модели и клиенту отличить данные от инструкций, но не делает prompt injection невозможным. Реальная защита — минимальные права (агент с ролью viewer не сможет ничего изменить, что бы ни прочитал) и подтверждение write-операций человеком на стороне клиента.
Обрезка по размеру грубая. При превышении лимита
dataстановится строкой-префиксом JSON. Для продакшена лучше пагинация на уровне API.Token exchange на каждый вызов. Это лишний сетевой запрос; токены можно кэшировать по
(sub, scope)до ихexp. Не сделано ради простоты.Список тулов не фильтруется по ролям.
tools/listпоказывает все тулы, запрет срабатывает при вызове.Нет rate limiting, квот, ABAC (например, «менеджер меняет только свои сделки»), отзыва токенов (проверяется только
exp) и mTLS между сервисами.Аудит пишется в stdout через
logging. Для настоящего аудита нужен неизменяемый приёмник (SIEM, WORM-хранилище) и, возможно, хэшированиеsub.Stateless Streamable HTTP, JSON-ответы — без SSE-стриминга и серверных уведомлений. Для этих тулов они не нужны.
Проект написан под
mcp2.x (классMCPServer; в 1.x он называлсяFastMCP).
Лицензия
This server cannot be deployed
Maintenance
Related MCP Connectors
OAuth 2.1 short-link tools for AI agents with scoped tokens, approvals, audit logs, and revocation.
- FullmaktOAuthai.fullmakt
Credential broker for AI agents: scoped, revocable API access with policy enforcement and audit.
Security gateway for AI agents: policy, approval, and audited execution, no secrets shared.
Zero-trust gateway for AI agents: score tool calls, verify agent cards, enforce policy, audit.
Related MCP Servers
- AlicenseNot gradedqualityFmaintenanceEnables AI agents to securely perform privileged actions like creating GitHub issues by minting short-lived, single-purpose tokens on demand, with policy enforcement and audit logging.MIT
- AlicenseNot gradedqualityDmaintenanceOAuth token broker for AI agents to securely act on a user's behalf across third-party APIs (Gmail, Slack, GitHub, Notion, etc.) by vaulting tokens server-side and never exposing them to the LLM.1MIT
- FlicenseNot gradedqualityBmaintenanceEnables AI agents to call external endpoints under per-endpoint policy enforcement, with credentials and personal data kept inside a hardware enclave and every allowed or denied attempt recorded to an immutable audit ledger.2-
- AlicenseNot gradedqualityBmaintenanceEnables autonomous AI agents to securely access and execute external tools, such as GitHub REST API operations, with per-user authentication, authorization, audit logging, and observability.MIT