Session Tokens

Session tokens are opaque strings that represent an authenticated user session. The hub exchanges auth codes for session tokens and uses them to validate user identity on every MCP tool call.

Applies to Tenant Delegated connections only. Identity Brokering and Token Exchange connections don't use these session tokens.

This is a how-to guide. The Contract Specification is the normative reference for token formats, TTLs, and lifecycle rules.

Token format

mcp:{64-hex-chars}

A session token is 32 random bytes, hex-encoded, with the mcp: prefix. Total length: 68 characters.

The mcp: prefix allows your app to distinguish MCP session tokens from other token types in your system.

Lifecycle

  1. Create — after a successful code exchange, generate a new token and store it with an expiry
  2. Validate — on each hub request, look up the token and check expiry
  3. Refresh — atomically rotate: delete old token, create new token (same user, fresh expiry)
  4. Revoke — delete the token from storage (logout)

Implementation

javascript
// lib/mcp-contract/session-token.ts
import { randomBytes } from "crypto";
import { prisma } from "@/lib/prisma";

const MCP_TOKEN_PREFIX = "mcp:";

export async function createMcpSession(
  userId: string,
  ttlSec: number
): Promise<{ token: string; expiresIn: number }> {
  const rawToken = randomBytes(32).toString("hex");
  const token = `${MCP_TOKEN_PREFIX}${rawToken}`;
  const expiresAt = new Date(Date.now() + ttlSec * 1000);

  await prisma.session.create({
    data: {
      token,
      userId,
      expiresAt,
      ipAddress: "mcp-hub",
      userAgent: "MCP-Delegated-Contract",
    },
  });

  return { token, expiresIn: ttlSec };
}

export async function findMcpSession(token: string) {
  if (!token.startsWith(MCP_TOKEN_PREFIX)) return null;

  const session = await prisma.session.findUnique({ where: { token } });
  if (!session) return null;
  if (session.expiresAt <= new Date()) return null;

  return session;
}

export async function refreshMcpSession(
  oldToken: string,
  ttlSec: number
): Promise<{ token: string; expiresIn: number } | null> {
  if (!oldToken.startsWith(MCP_TOKEN_PREFIX)) return null;

  return await prisma.$transaction(async (tx) => {
    const existing = await tx.session.findUnique({
      where: { token: oldToken },
    });
    if (!existing || existing.expiresAt <= new Date()) return null;

    // Verify user still exists
    const user = await tx.user.findUnique({
      where: { id: existing.userId },
      select: { id: true },
    });
    if (!user) return null;

    // Atomic swap: delete old, create new
    await tx.session.delete({ where: { token: oldToken } });

    const rawToken = randomBytes(32).toString("hex");
    const newToken = `${MCP_TOKEN_PREFIX}${rawToken}`;
    const expiresAt = new Date(Date.now() + ttlSec * 1000);

    await tx.session.create({
      data: {
        token: newToken,
        userId: existing.userId,
        expiresAt,
        ipAddress: "mcp-hub",
        userAgent: "MCP-Delegated-Contract",
      },
    });

    return { token: newToken, expiresIn: ttlSec };
  });
}

export async function revokeMcpSession(token: string): Promise<void> {
  if (!token.startsWith(MCP_TOKEN_PREFIX)) return;
  await prisma.session.deleteMany({ where: { token } });
}
javascript
// lib/mcp-contract/session-token.js
import { randomBytes } from "crypto";
import { prisma } from "@/lib/prisma";

const MCP_TOKEN_PREFIX = "mcp:";

export async function createMcpSession(userId, ttlSec) {
  const rawToken = randomBytes(32).toString("hex");
  const token = `${MCP_TOKEN_PREFIX}${rawToken}`;
  const expiresAt = new Date(Date.now() + ttlSec * 1000);

  await prisma.session.create({
    data: {
      token,
      userId,
      expiresAt,
      ipAddress: "mcp-hub",
      userAgent: "MCP-Delegated-Contract",
    },
  });

  return { token, expiresIn: ttlSec };
}

export async function findMcpSession(token) {
  if (!token.startsWith(MCP_TOKEN_PREFIX)) return null;

  const session = await prisma.session.findUnique({ where: { token } });
  if (!session) return null;
  if (session.expiresAt <= new Date()) return null;

  return session;
}

export async function refreshMcpSession(oldToken, ttlSec) {
  if (!oldToken.startsWith(MCP_TOKEN_PREFIX)) return null;

  return prisma.$transaction(async (tx) => {
    const existing = await tx.session.findUnique({ where: { token: oldToken } });
    if (!existing || existing.expiresAt <= new Date()) return null;

    // Verify user still exists
    const user = await tx.user.findUnique({
      where: { id: existing.userId },
      select: { id: true },
    });
    if (!user) return null;

    // Atomic swap: delete old, create new
    await tx.session.delete({ where: { token: oldToken } });

    const rawToken = randomBytes(32).toString("hex");
    const newToken = `${MCP_TOKEN_PREFIX}${rawToken}`;
    const expiresAt = new Date(Date.now() + ttlSec * 1000);

    await tx.session.create({
      data: {
        token: newToken,
        userId: existing.userId,
        expiresAt,
        ipAddress: "mcp-hub",
        userAgent: "MCP-Delegated-Contract",
      },
    });

    return { token: newToken, expiresIn: ttlSec };
  });
}

export async function revokeMcpSession(token) {
  if (!token.startsWith(MCP_TOKEN_PREFIX)) return;
  await prisma.session.deleteMany({ where: { token } });
}
java
// SessionTokenService.java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;

@Service
public class SessionTokenService {

    private static final String MCP_TOKEN_PREFIX = "mcp:";
    private static final SecureRandom RANDOM = new SecureRandom();

    private final McpSessionRepository sessionRepo;
    private final UserRepository userRepo;

    public SessionTokenService(McpSessionRepository sessionRepo,
                               UserRepository userRepo) {
        this.sessionRepo = sessionRepo;
        this.userRepo = userRepo;
    }

    /**
     * Create a new MCP session token for a user.
     */
    public TokenResult createSession(String userId, int ttlSec) {
        String token = generateToken();
        Instant expiresAt = Instant.now().plus(ttlSec, ChronoUnit.SECONDS);

        var session = new McpSession();
        session.setToken(token);
        session.setUserId(userId);
        session.setExpiresAt(expiresAt);
        sessionRepo.save(session);

        return new TokenResult(token, ttlSec);
    }

    /**
     * Find a valid (non-expired) session by token.
     */
    public Optional<McpSession> findSession(String token) {
        if (!token.startsWith(MCP_TOKEN_PREFIX)) {
            return Optional.empty();
        }
        return sessionRepo.findByToken(token)
            .filter(s -> s.getExpiresAt().isAfter(Instant.now()));
    }

    /**
     * Atomically rotate a session token: delete old, create new.
     */
    @Transactional
    public Optional<TokenResult> refreshSession(String oldToken,
                                                 int ttlSec) {
        if (!oldToken.startsWith(MCP_TOKEN_PREFIX)) {
            return Optional.empty();
        }

        var existing = sessionRepo.findByToken(oldToken).orElse(null);
        if (existing == null ||
                existing.getExpiresAt().isBefore(Instant.now())) {
            return Optional.empty();
        }

        // Verify user still exists
        if (!userRepo.existsById(existing.getUserId())) {
            return Optional.empty();
        }

        // Delete old token
        sessionRepo.deleteByToken(oldToken);

        // Create new token
        String newToken = generateToken();
        Instant expiresAt = Instant.now()
            .plus(ttlSec, ChronoUnit.SECONDS);

        var newSession = new McpSession();
        newSession.setToken(newToken);
        newSession.setUserId(existing.getUserId());
        newSession.setExpiresAt(expiresAt);
        sessionRepo.save(newSession);

        return Optional.of(new TokenResult(newToken, ttlSec));
    }

    /**
     * Revoke (delete) a session token.
     */
    public void revokeSession(String token) {
        if (token.startsWith(MCP_TOKEN_PREFIX)) {
            sessionRepo.deleteByToken(token);
        }
    }

    private String generateToken() {
        byte[] bytes = new byte[32];
        RANDOM.nextBytes(bytes);
        StringBuilder hex = new StringBuilder(64);
        for (byte b : bytes) {
            hex.append(String.format("%02x", b & 0xff));
        }
        return MCP_TOKEN_PREFIX + hex;
    }

    public record TokenResult(String token, int expiresIn) {}
}
typescript
// lib/mcp-contract/session-token.ts  (Node.js / Express)
import { randomBytes } from "crypto";
import { prisma } from "../prisma";

const MCP_TOKEN_PREFIX = "mcp:";

export async function createMcpSession(
  userId: string,
  ttlSec: number
): Promise<{ token: string; expiresIn: number }> {
  const token = `${MCP_TOKEN_PREFIX}${randomBytes(32).toString("hex")}`;
  const expiresAt = new Date(Date.now() + ttlSec * 1000);

  await prisma.session.create({
    data: {
      token,
      userId,
      expiresAt,
      ipAddress: "mcp-hub",
      userAgent: "MCP-Delegated-Contract",
    },
  });

  return { token, expiresIn: ttlSec };
}

export async function findMcpSession(token: string) {
  if (!token.startsWith(MCP_TOKEN_PREFIX)) return null;

  const session = await prisma.session.findUnique({ where: { token } });
  if (!session) return null;
  if (session.expiresAt <= new Date()) return null;

  return session;
}

export async function refreshMcpSession(
  oldToken: string,
  ttlSec: number
): Promise<{ token: string; expiresIn: number } | null> {
  if (!oldToken.startsWith(MCP_TOKEN_PREFIX)) return null;

  return prisma.$transaction(async (tx) => {
    const existing = await tx.session.findUnique({ where: { token: oldToken } });
    if (!existing || existing.expiresAt <= new Date()) return null;

    // Verify user still exists
    const user = await tx.user.findUnique({
      where: { id: existing.userId },
      select: { id: true },
    });
    if (!user) return null;

    // Atomic swap: delete old, create new
    await tx.session.delete({ where: { token: oldToken } });

    const newToken = `${MCP_TOKEN_PREFIX}${randomBytes(32).toString("hex")}`;
    const expiresAt = new Date(Date.now() + ttlSec * 1000);

    await tx.session.create({
      data: {
        token: newToken,
        userId: existing.userId,
        expiresAt,
        ipAddress: "mcp-hub",
        userAgent: "MCP-Delegated-Contract",
      },
    });

    return { token: newToken, expiresIn: ttlSec };
  });
}

export async function revokeMcpSession(token: string): Promise<void> {
  if (!token.startsWith(MCP_TOKEN_PREFIX)) return;
  await prisma.session.deleteMany({ where: { token } });
}
javascript
// lib/mcp-contract/session-token.js
const { randomBytes } = require("crypto");
const { prisma } = require("../prisma");

const MCP_TOKEN_PREFIX = "mcp:";

async function createMcpSession(userId, ttlSec) {
  const token = `${MCP_TOKEN_PREFIX}${randomBytes(32).toString("hex")}`;
  const expiresAt = new Date(Date.now() + ttlSec * 1000);

  await prisma.session.create({
    data: {
      token,
      userId,
      expiresAt,
      ipAddress: "mcp-hub",
      userAgent: "MCP-Delegated-Contract",
    },
  });

  return { token, expiresIn: ttlSec };
}

async function findMcpSession(token) {
  if (!token.startsWith(MCP_TOKEN_PREFIX)) return null;

  const session = await prisma.session.findUnique({ where: { token } });
  if (!session) return null;
  if (session.expiresAt <= new Date()) return null;

  return session;
}

async function refreshMcpSession(oldToken, ttlSec) {
  if (!oldToken.startsWith(MCP_TOKEN_PREFIX)) return null;

  return prisma.$transaction(async (tx) => {
    const existing = await tx.session.findUnique({ where: { token: oldToken } });
    if (!existing || existing.expiresAt <= new Date()) return null;

    // Verify user still exists
    const user = await tx.user.findUnique({
      where: { id: existing.userId },
      select: { id: true },
    });
    if (!user) return null;

    // Atomic swap: delete old, create new
    await tx.session.delete({ where: { token: oldToken } });

    const newToken = `${MCP_TOKEN_PREFIX}${randomBytes(32).toString("hex")}`;
    const expiresAt = new Date(Date.now() + ttlSec * 1000);

    await tx.session.create({
      data: {
        token: newToken,
        userId: existing.userId,
        expiresAt,
        ipAddress: "mcp-hub",
        userAgent: "MCP-Delegated-Contract",
      },
    });

    return { token: newToken, expiresIn: ttlSec };
  });
}

async function revokeMcpSession(token) {
  if (!token.startsWith(MCP_TOKEN_PREFIX)) return;
  await prisma.session.deleteMany({ where: { token } });
}

module.exports = {
  createMcpSession,
  findMcpSession,
  refreshMcpSession,
  revokeMcpSession,
};
python
# mcp_contract/session_token.py
import secrets
from datetime import datetime, timedelta, timezone

from sqlalchemy.orm import Session

MCP_TOKEN_PREFIX = "mcp:"


def create_mcp_session(
    db: Session, user_id: str, ttl_sec: int
) -> dict:
    token = MCP_TOKEN_PREFIX + secrets.token_hex(32)
    expires_at = datetime.now(timezone.utc) + timedelta(seconds=ttl_sec)
    db.add(McpSession(token=token, user_id=user_id, expires_at=expires_at))
    db.commit()
    return {"token": token, "expires_in": ttl_sec}


def find_mcp_session(db: Session, token: str):
    if not token.startswith(MCP_TOKEN_PREFIX):
        return None
    row = db.query(McpSession).filter_by(token=token).first()
    if not row or row.expires_at <= datetime.now(timezone.utc):
        return None
    return row


def refresh_mcp_session(
    db: Session, old_token: str, ttl_sec: int
) -> dict | None:
    existing = find_mcp_session(db, old_token)
    if not existing:
        return None

    # Verify user still exists
    if not db.query(User).filter_by(id=existing.user_id).first():
        return None

    # Atomic swap: delete old, create new
    db.delete(existing)
    new_token = MCP_TOKEN_PREFIX + secrets.token_hex(32)
    expires_at = datetime.now(timezone.utc) + timedelta(seconds=ttl_sec)
    db.add(McpSession(
        token=new_token, user_id=existing.user_id, expires_at=expires_at
    ))
    db.commit()
    return {"token": new_token, "expires_in": ttl_sec}


def revoke_mcp_session(db: Session, token: str) -> None:
    if token.startswith(MCP_TOKEN_PREFIX):
        db.query(McpSession).filter_by(token=token).delete()
        db.commit()