"""Контекст MCP-запроса: какой пользователь за Bearer-ключом.
McpTokenGuard резолвит токен (см. server.py) и кладёт AuthenticatedUser в
ContextVar — тулы (`app/mcp/tools.py`) читают его через current_mcp_user():
полный набор тулов действуют ролью владельца ключа, админ-тулы требуют
роли admin (require_mcp_admin). ContextVar прокидывается FastMCP в
threadpool-исполнение тулов, так что sync-функции его видят.
MCP_USER — прежний синтетический superadmin: остаётся бэкдором статического
env-токена (MCP_TOKEN) и дефолтом для прямых вызовов тулов в тестах.
"""
from contextvars import ContextVar
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.auth.deps import AuthenticatedUser
from app.models import McpToken
# Синтетический superadmin: ключ статического env-токена MCP_TOKEN (docs/07).
MCP_USER = AuthenticatedUser(
user_id="mcp", email="mcp@synapse.local", email_verified=True, system_role="superadmin"
)
_mcp_user: ContextVar[AuthenticatedUser | None] = ContextVar("mcp_user", default=None)
def set_mcp_user(user: AuthenticatedUser) -> object:
"""Для гейда: установить пользователя на время запроса, вернуть token для reset()."""
return _mcp_user.set(user)
def reset_mcp_user(token: object) -> None:
_mcp_user.reset(token) # type: ignore[arg-type]
def current_mcp_user() -> AuthenticatedUser:
"""Пользователь (или роль) текущего MCP-вызова; None — прямой вызов вне запроса."""
found = _mcp_user.get()
if found is not None:
return found
return MCP_USER
def resolve_personal_token(db: Session, raw_token: str) -> AuthenticatedUser | None:
"""Bearer `mcp_...` -> владелец ключа; None — промах/отзыв/блокировка владельца.
Вызывается гейтом на каждом запросе /mcp. Заблокированный в gnexus-auth
владелец блокирует и его токены (mcp.md) — флаг ставят вебхуки
(user.blocked и др.) в user_prefs. last_used_at — единственный столбец,
который MCP-путь когда-либо пишет (коммит здесь: у гейта нет общей
транзакции роутера).
"""
from datetime import UTC, datetime
from app.auth.apikeys import token_hash_of
from app.models import UserPrefs
row = db.execute(
select(McpToken).where(
McpToken.token_hash == token_hash_of(raw_token),
McpToken.revoked_at.is_(None),
)
).scalar_one_or_none()
if row is None:
return None
prefs = db.get(UserPrefs, row.user_id)
if prefs is not None and prefs.blocked:
return None
row.last_used_at = datetime.now(UTC)
db.commit()
return AuthenticatedUser(
user_id=row.user_id,
email=row.user_email or "",
email_verified=True,
system_role=row.system_role,
)