LeanMCP provides built-in multi-tenancy support through session management and authentication. Each user or organization can have isolated data and state.
Session Management
LeanMCPSessionProvider
The LeanMCPSessionProvider is a drop-in replacement for Map<string, StreamableHTTPServerTransport> with automatic persistence:
import { LeanMCPSessionProvider } from '@leanmcp/core' ;
// Before: const transports = new Map<string, StreamableHTTPServerTransport>();
// After:
const sessions = new LeanMCPSessionProvider ();
Environment Auto-Detection:
Local Development : Uses in-memory session store
LeanMCP Lambda (LEANMCP_LAMBDA=true): Uses DynamoDB session store
Explicit Override : Pass custom sessionStore option
Source: packages/core/src/session-provider.ts:34
Basic Usage
import { Server } from '@modelcontextprotocol/sdk/server/index.js' ;
import { LeanMCPSessionProvider } from '@leanmcp/core' ;
const sessions = new LeanMCPSessionProvider ();
const handleRequest = async ( request : Request ) => {
const sessionId = extractSessionId ( request );
// Get or recreate transport for this session
let transport = await sessions . getOrRecreate (
sessionId ,
() => createServer (), // Fresh server instance
{
onsessioninitialized : ( sid ) => {
console . log ( 'Session initialized:' , sid );
},
onclose : () => {
console . log ( 'Session closed' );
}
}
);
if ( ! transport ) {
// Session doesn't exist, create new one
transport = new StreamableHTTPServerTransport ({
sessionIdGenerator : () => sessionId ,
onsessioninitialized : async ( sid ) => {
await sessions . set ( sid , transport );
}
});
const server = createServer ();
await server . connect ( transport );
}
return transport . handleRequest ( request );
};
Session Isolation
Per-Session Data Storage
Store custom data per session in DynamoDB:
// Store user preferences per session
await sessions . updateSessionData ( sessionId , {
userId: authUser . sub ,
preferences: {
theme: 'dark' ,
notifications: true
}
});
// Retrieve session data
const data = await sessions . getSessionData ( sessionId );
console . log ( 'User ID:' , data ?. userId );
Source: packages/core/src/session-provider.ts:165
Session Lifecycle
// Check if session exists
const exists = await sessions . has ( sessionId );
// Delete session
await sessions . delete ( sessionId );
// Get in-memory session count
console . log ( 'Active sessions:' , sessions . size );
Multi-Tenant Architecture
User-Scoped Authentication
Combine authentication with session management for true multi-tenancy:
import { Authenticated , AuthProvider } from '@leanmcp/auth' ;
import { Tool } from '@leanmcp/core' ;
const authProvider = new AuthProvider ( 'auth0' , {
domain: 'your-tenant.auth0.com' ,
clientId: 'your-client-id' ,
audience: 'your-api'
});
@ Authenticated ( authProvider )
export class MultiTenantService {
@ Tool ({ description: 'Get user-specific data' })
async getUserData ( args : { query : string }) {
// authUser.sub is the unique user ID
const userId = authUser . sub ;
// Fetch data scoped to this user
const data = await database . query ({
userId: userId ,
query: args . query
});
return data ;
}
}
Organization-Level Isolation
@ Tool ({ description: 'List team members' })
@ Authenticated ( authProvider )
async listTeamMembers ( args : any ) {
// Get organization ID from user claims
const orgId = authUser . attributes [ 'org_id' ];
// Query members for this organization only
const members = await database . getOrgMembers ( orgId );
return { members };
}
Session Store Implementations
In-Memory Store (Development)
Default for local development - no external dependencies:
import { InMemorySessionStore } from '@leanmcp/core' ;
const sessions = new LeanMCPSessionProvider ({
sessionStore: new InMemorySessionStore ()
});
Source: packages/core/src/inmemory-session-store.ts:9
In-memory sessions are lost when the process restarts. Use DynamoDB for production.
DynamoDB Store (Production)
Automatic for Lambda deployments, or force it for local testing:
import { DynamoDBSessionStore , LeanMCPSessionProvider } from '@leanmcp/core' ;
const sessions = new LeanMCPSessionProvider ({
forceDynamoDB: true ,
tableName: 'mcp-sessions' ,
region: 'us-east-1' ,
ttlSeconds: 86400 , // 24 hours
logging: true
});
DynamoDB Table Schema:
interface SessionData {
sessionId : string ; // Partition key
createdAt : Date ;
updatedAt : Date ;
ttl ?: number ; // TTL attribute for auto-cleanup
data ?: Record < string , any >; // Custom session data
}
Source: packages/core/src/session-store.ts:4
Custom Store Implementation
Implement your own session store (Redis, PostgreSQL, etc.):
import { ISessionStore , SessionData } from '@leanmcp/core' ;
class RedisSessionStore implements ISessionStore {
async sessionExists ( sessionId : string ) : Promise < boolean > {
return await redis . exists ( `session: ${ sessionId } ` );
}
async createSession ( sessionId : string , data ?: Record < string , any >) : Promise < void > {
await redis . setex ( `session: ${ sessionId } ` , 86400 , JSON . stringify ({
sessionId ,
createdAt: new Date (),
updatedAt: new Date (),
data
}));
}
async getSession ( sessionId : string ) : Promise < SessionData | null > {
const raw = await redis . get ( `session: ${ sessionId } ` );
return raw ? JSON . parse ( raw ) : null ;
}
async updateSession ( sessionId : string , updates : Partial < SessionData >) : Promise < void > {
const session = await this . getSession ( sessionId );
if ( ! session ) return ;
await redis . setex ( `session: ${ sessionId } ` , 86400 , JSON . stringify ({
... session ,
... updates ,
updatedAt: new Date ()
}));
}
async deleteSession ( sessionId : string ) : Promise < void > {
await redis . del ( `session: ${ sessionId } ` );
}
}
const sessions = new LeanMCPSessionProvider ({
sessionStore: new RedisSessionStore ()
});
Lambda Cold Start Handling
The session provider automatically handles Lambda container recycling:
// getOrRecreate handles cold starts transparently
const transport = await sessions . getOrRecreate (
sessionId ,
() => createServer () // Fresh server for recycled container
);
What happens:
Check in-memory transports (fast path)
Check DynamoDB if not in memory
Recreate transport with existing session ID
Set _initialized flag to bypass MCP SDK checks
Connect to fresh server instance
Source: packages/core/src/session-provider.ts:113
This is critical for serverless deployments where Lambda containers are recycled between invocations.
Best Practices
1. Always Use Session-Scoped Data
// Good: Session-scoped
@ Tool ({ description: 'Add to cart' })
@ Authenticated ( authProvider )
async addToCart ( args : { itemId: string }) {
const userId = authUser . sub ;
await database . addToCart ( userId , args . itemId );
}
// Bad: Global state
const globalCart = [];
@ Tool ({ description: 'Add to cart' })
async addToCart ( args : { itemId: string }) {
globalCart . push ( args . itemId ); // Shared across all users!
}
2. Store Tenant Context Early
const handleRequest = async ( request : Request ) => {
const sessionId = extractSessionId ( request );
// Store tenant context on session creation
await sessions . updateSessionData ( sessionId , {
tenantId: extractTenantId ( request ),
createdAt: new Date (). toISOString ()
});
// ... handle request
};
3. Cleanup Expired Sessions
DynamoDB TTL handles automatic cleanup:
const sessions = new LeanMCPSessionProvider ({
ttlSeconds: 86400 // Auto-delete after 24 hours
});
Next Steps
Authentication Secure your multi-tenant server
Env Injection Per-user environment variables