Session Management
Seed implements two types of sessions: MCP Sessions for protocol communication and OAuth Client Sessions for registered clients. This page focuses on the session lifecycle and storage mechanisms.
Overview
Session management in Seed consists of:
- MCP Sessions: UUID-based transport tracking for MCP protocol
- OAuth Client Sessions: Redis-backed storage for dynamically registered clients
- User Context: JWT-derived user information attached to requests
MCP Session Management
Session Lifecycle
MCP sessions track individual client connections and their associated transports.
stateDiagram-v2
[*] --> Created: Client sends initialize
Created --> Active: Transport stored with UUID
Active --> Active: Subsequent requests
Active --> Expired: Connection closes
Expired --> [*]
note right of Created
UUID generated
Transport created
Server metadata sent
end note
note right of Active
Session ID in header
Transport looked up
Requests processed
end noteSession Creation
When a client sends an initialize request:
Request Received (no session ID):
json{ "jsonrpc": "2.0", "method": "initialize", "params": { "protocolVersion": "0.1.0", "capabilities": {} }, "id": 1 }UUID Generation:
typescriptconst sessionId = randomUUID(); // Example: "123e4567-e89b-12d3-a456-426614174000"Transport Creation:
typescriptconst transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => sessionId, enableJsonResponse: true, onsessioninitialized: (id) => { transports[id] = transport; }, });Response with Session ID:
httpHTTP/1.1 200 OK mcp-session-id: 123e4567-e89b-12d3-a456-426614174000 Content-Type: application/json { "jsonrpc": "2.0", "result": { "protocolVersion": "0.1.0", "capabilities": { "tools": {} }, "serverInfo": { "name": "seed", "version": "0.1.3" } }, "id": 1 }
Session Storage
Storage Mechanism: In-memory JavaScript object
interface TransportStore {
[sessionId: string]: StreamableHTTPServerTransport;
}
const transports: TransportStore = {};Characteristics:
- Ephemeral: Sessions lost on server restart
- Fast: O(1) lookup by session ID
- Automatic cleanup: Transports removed when connections close
- No persistence: Not stored in Redis or database
Session Usage
After initialization, clients include the session ID in every request:
POST /mcp
Content-Type: application/json
mcp-session-id: 123e4567-e89b-12d3-a456-426614174000
Authorization: Bearer eyJhbGc...
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": { "name": "seed_ping", "arguments": {} },
"id": 2
}Lookup Process:
const sessionId = req.headers["mcp-session-id"];
const transport = transports[sessionId];
if (!transport) {
return res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Invalid session ID"
},
id: null
});
}
await transport.handleRequest(req, res, req.body);Session Expiration
Sessions expire when:
- Connection closes: HTTP connection terminated
- Server restarts: In-memory storage cleared
- Transport cleanup: MCP SDK garbage collection
No explicit timeout: Sessions persist as long as the connection is active.
OAuth Client Sessions
Client Registration Storage
Dynamically registered OAuth clients are stored in Redis with TTL.
Storage Format:
Key: dcr:client:{client_id}
Value: JSON.stringify(RegisteredClient)
TTL: 2592000 seconds (30 days)Example:
Key: dcr:client:seed-a1b2c3d4e5f6
Value: {
"client_id": "seed-a1b2c3d4e5f6",
"client_name": "My Application",
"redirect_uris": ["https://app.example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"client_id_issued_at": 1702857600,
"client_secret_expires_at": 0
}
TTL: 2592000Client Lifecycle
stateDiagram-v2
[*] --> Registered: POST /oauth/register
Registered --> Active: Used for OAuth flows
Active --> Active: Token exchanges
Active --> Expired: 30 days TTL
Expired --> [*]
note right of Registered
Client ID generated
Stored in Redis
TTL: 30 days
end note
note right of Active
Validated on authorize
Validated on token
Mapped to static IdP client
end noteClient Lookup
Clients are retrieved from Redis during OAuth flows:
// Authorization endpoint
const client = await clientStore.get(clientId);
if (!client) {
return res.redirect(`${redirectUri}?error=invalid_client`);
}
// Token endpoint
const client = await clientStore.get(params.client_id);
if (!client) {
return res.status(401).json({
error: "invalid_client",
error_description: "Client not found or expired"
});
}Client Expiration
TTL-Based Expiration:
- Default: 30 days (2,592,000 seconds)
- Configurable via
DCR_CLIENT_TTLenvironment variable - Redis automatically deletes expired keys
- No renewal mechanism (clients must re-register)
Expiration Handling:
- Client not found in Redis
- Return
invalid_clienterror - Client must re-register via
POST /oauth/register
User Context Sessions
JWT-Derived Context
Every authenticated request includes user context extracted from the JWT:
interface UserContext {
sub: string; // Subject (user ID) - REQUIRED
email?: string; // User email - OPTIONAL
name?: string; // Display name - OPTIONAL
groups?: string[]; // Group memberships - OPTIONAL
token: string; // Original JWT token
}Attachment to Request:
// src/middleware/auth.ts
const user: UserContext = {
sub: payload.sub as string,
token,
};
if (payload.email) user.email = payload.email as string;
if (payload.name) user.name = payload.name as string;
if (payload.groups) user.groups = payload.groups as string[];
req.user = user;Context Availability
User context is available in:
- Route handlers: via
req.user - MCP tools: via request object (if passed to tool)
- Middleware: for authorization checks
Example Usage in Tool:
export function registerAuthenticatedTool(server: McpServer): void {
server.tool("get_user_info", "Get current user information", {}, async (params, req) => {
const user = req.user; // Access user context
return {
content: [
{
type: "text",
text: `User: ${user.name} (${user.email})\nID: ${user.sub}`,
},
],
};
});
}Redis Connection Management
Connection Configuration
File: src/services/redis.ts
const redisClient = new Redis(config.dcr.redisUrl, {
maxRetriesPerRequest: 3,
retryStrategy(times: number) {
// Exponential backoff: 100ms, 200ms, 300ms, ..., max 30s
return Math.min(times * 100, 30000);
},
lazyConnect: true,
});Configuration Details:
- URL:
redis://redis:6379(default, configurable viaREDIS_URL) - Max Retries: 3 attempts per request
- Retry Strategy: Exponential backoff up to 30 seconds
- Lazy Connect: Connection established on first command
Health Checking
export async function isRedisHealthy(): Promise<boolean> {
try {
const result = await redisClient.ping();
return result.toUpperCase() === "PONG";
} catch {
return false;
}
}Used by: Health check endpoint (/health)
Error Handling
- Connection errors logged but don't crash service
- Exponential backoff prevents connection storms
- Client operations fail gracefully if Redis unavailable
- OAuth flows return
server_errorif storage unavailable
Session Security
MCP Sessions
- Not Authenticated Separately: Rely on JWT middleware
- UUID Unpredictability: 128-bit random IDs
- Transport Isolation: Each session has its own transport
- No Persistence: Can't be hijacked after server restart
OAuth Client Sessions
- TTL-Based Expiration: Automatic cleanup after 30 days
- Validation on Use: Clients validated on every OAuth flow
- Redirect URI Matching: Strict validation of callback URLs
- Public Clients: No client secrets (PKCE-protected)
User Context Security
- JWT Validation: Every request validates JWT signature
- Claim Verification: Issuer, audience, expiration checked
- Stateless: No server-side user sessions
- Token Refresh: Handled via OAuth refresh token flow
Monitoring and Observability
Session Metrics
MCP Sessions:
- Count:
Object.keys(transports).length - Active connections: Number of stored transports
- Session creation rate: Monitor initialize requests
OAuth Clients:
- Registered clients: Redis key count matching
dcr:client:* - Active registrations: Clients used in last 30 days
- Registration rate: Monitor
/oauth/registerrequests
Health Checks
Redis Health:
GET /health
Response:
{
"status": "healthy",
"redis": "connected"
}MCP Session Count:
export function getSessionCount(): number {
return Object.keys(transports).length;
}Configuration
Environment Variables
# Redis Configuration
REDIS_URL=redis://redis:6379
# OAuth Client TTL
DCR_CLIENT_TTL=2592000 # 30 days in seconds
# Rate Limiting (for client registration)
DCR_RATE_LIMIT_WINDOW_MS=3600000 # 1 hour
DCR_RATE_LIMIT_MAX=10 # 10 registrations per hourCode Configuration
// src/config/dcr.ts
export const dcrConfig = {
redisUrl: process.env.REDIS_URL ?? "redis://redis:6379",
clientTtlSeconds: parseInt(process.env.DCR_CLIENT_TTL ?? "2592000", 10),
keyPrefix: "dcr:client:",
clientIdPrefix: "seed-",
maxRedirectUris: 10,
rateLimit: {
windowMs: parseInt(process.env.DCR_RATE_LIMIT_WINDOW_MS ?? "3600000", 10),
maxRequests: parseInt(process.env.DCR_RATE_LIMIT_MAX ?? "10", 10),
},
};Best Practices
MCP Sessions
- Generate session ID once: Don't regenerate on every request
- Store transport reference: Keep in session map for reuse
- Clean up on close: Remove from map when connection terminates
- Handle missing sessions: Return proper error for invalid session IDs
OAuth Clients
- Set appropriate TTL: Balance security and user experience
- Validate on every use: Don't trust cached client data
- Monitor Redis health: Implement alerting for connection issues
- Rate limit registration: Prevent abuse of registration endpoint
User Context
- Validate JWT on every request: Don't cache user context
- Extract only needed claims: Minimize data exposure
- Pass context to tools: Make user info available where needed
- Log access patterns: Monitor for suspicious activity
Implementation Files
- MCP Sessions:
src/mcp/mcp.ts,src/routes/mcp.ts - OAuth Clients:
src/services/client-store.ts - Redis Client:
src/services/redis.ts - User Context:
src/middleware/auth.ts - Config:
src/config/dcr.ts
Related Documentation
- MCP Server Design - Transport and session tracking details
- OAuth 2.1 Implementation - Client registration and flows
- Authentication Flow - JWT validation and user context
- Configuration System - Environment-based configuration