MCP Authentication
How MCP requests authenticate via Personal Access Tokens -- the bearer flow, SHA-256 validation, and tenant resolution.
MCP requests authenticate using Personal Access Tokens (PATs) sent as bearer tokens in the Authorization header. This page covers the server-side validation flow. For PAT creation, revocation, hashing, and the security model, see Identity & Access -- Personal Access Tokens.
Authentication Flow
Claude Code (MCP client)
|
| Authorization: Bearer trov_a1b2c3d4...
v
authenticateRequest(request) packages/mcp/src/auth.ts
|
| 1. Extract Authorization header
| 2. Verify "Bearer " prefix
| 3. Verify "trov_" token prefix
| 4. SHA-256 hash the raw token
| 5. Query personal_access_token + user tables
| 6. Check revokedAt is null
| 7. Check expiresAt is in the future
| 8. Fire-and-forget: update lastUsedAt
|
v
McpAuthContext { userId, userName, userEmail }
|
v
resolveOrganizationId(userId) packages/mcp/src/resolve-org.ts
|
| Query member table for first org
|
v
organizationId
|
v
withTenantContext(orgId, userId, handler) Every tool callback
The McpAuthContext Type
The authentication step produces a context object passed to every tool registration function:
interface McpAuthContext {
userId: string;
userName: string;
userEmail: string;
}
This context is not the same as a tRPC context. MCP tools do not go through the tRPC procedure chain. Instead, each tool callback manually calls resolveOrganizationId() and withTenantContext() to establish tenant scope.
Token Validation Details
The authenticateRequest() function in packages/mcp/src/auth.ts performs these checks in order:
- Header present -- the
Authorizationheader must exist and start withBearer - Token prefix -- the raw token must start with
trov_(rejects tokens from other systems) - Hash lookup -- the raw token is SHA-256 hashed using
crypto.subtle.digest, then looked up in thepersonal_access_tokentable joined touser - Not revoked -- the query filters for
revoked_at IS NULL - Not expired -- if
expiresAtis set, it must be in the future - Last-used update -- a fire-and-forget
UPDATEsetslastUsedAtto the current timestamp (does not block the response)
If any check fails, authenticateRequest() returns null, and the server responds with HTTP 401:
{ "error": "Unauthorized" }
Organization Resolution
After authentication identifies the user, resolveOrganizationId() determines which tenant to scope database operations to. For MVP, this returns the user's first organization membership:
const [row] = await db
.select({ organizationId: member.organizationId })
.from(member)
.where(eq(member.userId, userId))
.limit(1);
This query uses the bare db client (not withTenantContext) because the member table lookup happens before tenant scope is established. See Data & Storage -- Non-Tenant Queries for why certain queries bypass RLS.
Per-Request Server Model
Unlike typical MCP servers that maintain long-lived connections, Trovella creates a new McpServer instance per HTTP request:
const server = createMcpServer(auth);
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: undefined, // No session tracking
enableJsonResponse: true,
});
await server.connect(transport);
const response = await transport.handleRequest(request);
await transport.close();
await server.close();
Setting sessionIdGenerator: undefined disables the SDK's built-in session management. All state lives in the database, not in server memory.
Why PATs Instead of Sessions
MCP clients like Claude Code operate outside the browser. They cannot participate in the cookie-based session flow that the web application uses. PATs provide a simpler alternative:
- No redirect-based OAuth flow required
- Token is configured once in the MCP client settings
- Same user identity as browser sessions (same
usertable row) - Tenant resolution works identically after authentication
For the long-term plan to support OAuth 2.1 (for Claude Web/Chat/Work integration), see Identity & Access -- Personal Access Tokens.
Related Pages
- Identity & Access -- Personal Access Tokens -- PAT lifecycle, creation, revocation, security model
- Identity & Access -- Session Management -- the browser-based auth path (contrast)
- Data & Storage -- Non-Tenant Queries -- why PAT and org-resolution queries use bare
db - Tool Catalog -- how authenticated context flows into each tool