Files
forge/apps/api/forge/routers/mcp_tokens.py
T
nihalashetty ae67bff5a3 feat: deep-agent canvas, live observability, and multi-environment tooling
Self-hosted platform for building, testing, and shipping LangChain/LangGraph agents. Deep-agent sub-agents on the canvas, a live tracing/observability timeline, auto-provisioned built-in tools with import/export, per-environment tool variables, streamed evaluations, and per-user auth token forwarding.
2026-07-28 01:49:19 +05:30

61 lines
2.9 KiB
Python

"""Personal access tokens for a project's MCP server.
A PAT is a per-user, pasteable bearer token (`forge_pat_…`) that authenticates an individual over
`POST /v1/mcp/{project_id}` AS their end_user - the portable "use anywhere" identity for generic MCP
clients (Claude Desktop, Cursor, VS Code). Bound to the current user and scoped to this project; it
is deliberately NOT a general-API credential.
"""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from forge.deps import CurrentUser, get_current_user, get_session
from forge.models.entities import ApiKey
from forge.schemas.dto import McpTokenCreate, McpTokenOut
from forge.services.apikeys import ApiKeyService
router = APIRouter(prefix="/v1/projects/{project_id}/mcp-tokens", tags=["mcp-tokens"])
def _out(k: ApiKey, *, token: str | None = None) -> McpTokenOut:
return McpTokenOut(
id=k.id, name=k.name, prefix=k.prefix, project_id=k.project_id, status=k.status,
created_at=k.created_at, last_used_at=k.last_used_at, expires_at=k.expires_at, token=token,
)
def _require_real_user(user: CurrentUser) -> None:
# A PAT must bind a real end user; service / api-key / dev-fallback principals have no user id.
if user.is_fallback or str(user.id).startswith(("apikey:", "service")):
raise HTTPException(status.HTTP_403_FORBIDDEN, "personal tokens require a logged-in user")
@router.get("", response_model=list[McpTokenOut])
async def list_mcp_tokens(project_id: str, session: AsyncSession = Depends(get_session),
user: CurrentUser = Depends(get_current_user)):
_require_real_user(user)
rows = await ApiKeyService.list_personal(session, user.tenant_id, user.id)
# Show this project's tokens plus any tenant-wide (unscoped) personal tokens.
return [_out(k) for k in rows if k.project_id in (None, project_id)]
@router.post("", response_model=McpTokenOut, status_code=201)
async def create_mcp_token(project_id: str, body: McpTokenCreate, session: AsyncSession = Depends(get_session),
user: CurrentUser = Depends(get_current_user)):
_require_real_user(user)
key, plaintext = await ApiKeyService.create_personal(
session, tenant_id=user.tenant_id, user_id=user.id,
name=body.name or "MCP token", project_id=project_id, ttl_days=body.ttl_days,
)
return _out(key, token=plaintext)
@router.delete("/{token_id}", status_code=204)
async def revoke_mcp_token(project_id: str, token_id: str, session: AsyncSession = Depends(get_session),
user: CurrentUser = Depends(get_current_user)):
_require_real_user(user)
if not await ApiKeyService.revoke_personal(session, tenant_id=user.tenant_id, user_id=user.id, key_id=token_id):
raise HTTPException(status.HTTP_404_NOT_FOUND, "token not found")