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.
Token format 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 Create — after a successful code exchange, generate a new token and store it with an expiryValidate — on each hub request, look up the token and check expiryRefresh — atomically rotate: delete old token, create new token (same user, fresh expiry)Revoke — delete the token from storage (logout)Implementation Next.js (TypeScript) Next.js (JavaScript) Java Node.js (TypeScript) Node.js (JavaScript) Python
// 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 } });
}// 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 } });
}// 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) {}
}// 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 } });
}// 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,
};# 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()