The MCP server implements multi-user session isolation to ensure users can only access their own MCP sessions. This is accomplished using user identifiers and session ownership tracking.
Note: While this document describes Redis-backed storage, the server also supports in-memory storage for development. When Redis is not configured, session data is stored in memory and lost on restart.
Each MCP session is owned by a specific user. Session ownership is stored in Redis and validated on each request.
Production: OAuth providers assign unique user identifiers via the sub (subject) claim - a standard OAuth/OpenID Connect field.
User ID sources:
- Provider's user database: Users created in Auth0/Okta (example:
auth0|507f1f77bcf86cd799439011) - Social/federated identities: Provider links to Google, Facebook (example:
google-oauth2|112233445566778899) - Enterprise connections: SAML, LDAP, Active Directory (example:
samlp|ad|[email protected])
Demo mode: The demo auth server simulates user authentication by generating unique user IDs. This allows testing multi-user scenarios without a real identity provider.
When the MCP server validates an access token:
- MCP server calls the auth server's
/introspectendpoint with the token - Auth server validates the token and returns user information including the
subfield (user ID) - MCP server populates
AuthInfo.extra.userIdwith this user identifier
This separation allows the auth server to be replaced with any OAuth provider that supports token introspection.
const userId = req.auth?.extra?.userId;
const sessionId = randomUUID();
await redisClient.set(`session:${sessionId}:owner`, userId);const owner = await redisClient.get(`session:${sessionId}:owner`);
if (owner !== userId) {
throw new Error('Session not owned by user');
}session:{sessionId}:owner → userId
Example: session:550e8400-...:owner → auth0|507f1f77bcf86cd799439011
Sessions are considered "live" when an MCP server process is actively handling them. This is tracked via Redis pub/sub subscription counts:
mcp:shttp:toserver:{sessionId} → pub/sub channel
Check if live: PUBSUB NUMSUB mcp:shttp:toserver:{sessionId} returns > 0
When an MCP server starts handling a session, it subscribes to the channel. When it shuts down (gracefully or via crash), Redis automatically removes the subscription.
- Session isolation: Users can only access sessions they own
- Ownership persistence: Survives across requests and server restarts
- Token-based validation: User ID extracted from validated OAuth token
- Access control: All operations (GET, POST, DELETE) validate ownership
export async function setSessionOwner(sessionId: string, userId: string): Promise<void>
export async function isSessionOwnedBy(sessionId: string, userId: string): Promise<boolean>
export async function isLive(sessionId: string): Promise<boolean>
export async function shutdownSession(sessionId: string): Promise<void>sequenceDiagram
participant Client
participant MCP as MCP Server
participant Redis
Client->>MCP: POST /mcp (initialize)
MCP->>MCP: Extract userId from OAuth token
MCP->>MCP: Generate sessionId
MCP->>Redis: SET session:{sessionId}:owner = userId
MCP->>Client: Return sessionId
Client->>MCP: POST /mcp (with sessionId)
MCP->>MCP: Extract userId from OAuth token
MCP->>Redis: GET session:{sessionId}:owner
Redis-->>MCP: userId
MCP->>MCP: Verify userId matches token
MCP->>Client: Process request
REDIS_URL=redis://localhost:6379
BASE_URI=http://localhost:3232# List all session owners
redis-cli KEYS "session:*:owner"
# Check specific session ownership
redis-cli GET "session:{sessionId}:owner"
# Check if session is live (actively being handled)
redis-cli PUBSUB NUMSUB "mcp:shttp:toserver:{sessionId}"
# Monitor session operations
redis-cli MONITOR | grep "session:"# Test session isolation
npm test -- --testNamePattern="User Session Isolation"
# Test session ownership
npm test -- --testNamePattern="Session Ownership"- RFC 7662: Token Introspection - Standard for
subclaim - MCP Authorization Spec