Session Expiration Notification
Status: ✅ Implemented (2026-01-06) Priority: Medium Effort: 1-2 hours Category: UX Enhancement
Overview
The Session Expiration Notification feature adds an X-Session-Expires-At response header to all MCP requests, informing clients when their session will expire. This enables clients to proactively manage session lifecycles, warn users before disconnection, and provide a better user experience.
Problem Statement
Prior to this enhancement:
- Sessions expired silently after 24 hours of inactivity
- Next request after expiration returned 404 (session not found)
- No advance warning to clients
- Users experienced sudden disconnections without notice
- Clients had no way to determine when to refresh sessions
Solution
Add an X-Session-Expires-At header to all MCP POST responses containing:
- ISO 8601 formatted timestamp
- Calculated as:
current_time + session_ttl - Updated on every request due to sliding window TTL
Implementation
Response Header
Header Format:
X-Session-Expires-At: 2026-01-07T10:30:00.000ZExample Response:
POST /mcp
mcp-session-id: 123e4567-e89b-12d3-a456-426614174000
HTTP/1.1 200 OK
X-Session-Expires-At: 2026-01-07T10:30:00.000Z
Content-Type: application/json
{
"jsonrpc": "2.0",
"result": {...},
"id": 1
}Code Changes
File: src/routes/mcp.ts
For existing sessions:
if (sessionId) {
const transport = await getTransport(sessionId);
if (transport) {
// Update last accessed time and refresh TTL in Redis
const sessionStore = getSessionStore();
await sessionStore.touch(sessionId);
// Calculate expiration time from current time + TTL
// (after touch, the TTL is refreshed from now)
const expiresAtMs = Date.now() + config.session.ttlSeconds * 1000;
const expiresAt = new Date(expiresAtMs).toISOString();
res.setHeader("X-Session-Expires-At", expiresAt);
await transport.handleRequest(req, res, req.body);
return;
}
}For new sessions (initialize):
if (!sessionId && isInitializeRequest(req.body)) {
const user = (req as Request & { user?: { sub: string } }).user;
const transport = await createTransport(user?.sub);
// For new sessions, set expiration header based on current time + TTL
const expiresAtMs = Date.now() + config.session.ttlSeconds * 1000;
const expiresAt = new Date(expiresAtMs).toISOString();
res.setHeader("X-Session-Expires-At", expiresAt);
await transport.handleRequest(req, res, req.body);
return;
}Test Coverage
File: src/routes/mcp.test.ts
Two new tests added:
- Existing session expiration header: Verifies header is present and timestamp is approximately current time + 24 hours
- New session expiration header: Verifies header is included in initialize response
it("should include X-Session-Expires-At header for existing session", async () => {
const beforeRequest = Date.now();
const response = await request(app)
.post("/mcp")
.set("mcp-session-id", "existing-session-123")
.send({ jsonrpc: "2.0", method: "tools/list", id: 1 });
const afterRequest = Date.now();
expect(response.status).toBe(200);
const expiresAtHeader = response.headers["x-session-expires-at"];
expect(expiresAtHeader).toBeDefined();
expect(typeof expiresAtHeader).toBe("string");
// Type guard ensures expiresAtHeader is string at this point
if (typeof expiresAtHeader === "string") {
const expiresAt = new Date(expiresAtHeader);
const expiresAtMs = expiresAt.getTime();
const expectedMinMs = beforeRequest + 86400 * 1000;
const expectedMaxMs = afterRequest + 86400 * 1000;
expect(expiresAtMs).toBeGreaterThanOrEqual(expectedMinMs);
expect(expiresAtMs).toBeLessThanOrEqual(expectedMaxMs);
}
});Benefits
For Clients
- Predictable expiration: Clients know exactly when their session will expire
- Proactive management: Can send requests before expiration to refresh session
- Better UX: Can warn users before disconnection
- No surprise disconnects: Users are notified in advance
Client Implementation Example
class MCPClient {
private sessionExpiresAt?: Date;
async sendRequest(request: any): Promise<any> {
const response = await fetch('/mcp', {
method: 'POST',
headers: {
'mcp-session-id': this.sessionId,
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
// Update expiration time from response header
const expiresAtHeader = response.headers.get('x-session-expires-at');
if (expiresAtHeader) {
this.sessionExpiresAt = new Date(expiresAtHeader);
this.scheduleExpirationWarning();
}
return response.json();
}
private scheduleExpirationWarning(): void {
if (!this.sessionExpiresAt) return;
const timeUntilExpiry = this.sessionExpiresAt.getTime() - Date.now();
const warningTime = 5 * 60 * 1000; // 5 minutes
if (timeUntilExpiry > warningTime) {
setTimeout(() => {
this.emit('session-expiring-soon', {
expiresAt: this.sessionExpiresAt,
minutesRemaining: 5,
});
}, timeUntilExpiry - warningTime);
}
}
}For Operations
- Consistent behavior: All sessions have expiration information
- Client debugging: Clients can log expiration times for troubleshooting
- Monitoring: Can track session lifetimes from client perspective
Sliding Window Behavior
Because Seed uses a sliding window TTL (refreshed on each access), the expiration time updates with every request:
Example Timeline:
10:00 AM - Initialize session
X-Session-Expires-At: 10:00 AM + 24h = 10:00 AM (next day)
10:30 AM - Send request
TTL refreshed (session touched)
X-Session-Expires-At: 10:30 AM + 24h = 10:30 AM (next day)
2:15 PM - Send request
TTL refreshed again
X-Session-Expires-At: 2:15 PM + 24h = 2:15 PM (next day)Clients should update their local expiration tracking on every response to reflect the sliding window.
Configuration
No new configuration required. The expiration time is calculated using existing session TTL:
// src/config/session.ts
export const sessionConfig = {
ttlSeconds: parseInt(process.env.MCP_SESSION_TTL_SECONDS ?? "86400", 10), // 24 hours
};Limitations
- Header only approach: No MCP notification protocol extension (would require spec change)
- Approximation: Expiration time is calculated at response time, not session creation
- Server clock: Assumes client and server clocks are synchronized (ISO 8601 absolute time)
Future Enhancements
- MCP Notification: Add MCP protocol notification 5 minutes before expiration (requires spec change)
- Session metadata endpoint:
GET /sessions/:idto query expiration time independently - Warning threshold config: Configurable threshold for client warnings
Related Work
- Gap Analysis: Section 3.3 "No Session Expiration Notification"
- Session Architecture: sessions.md
- Related Enhancements:
- Session Management API - List and revoke sessions
- Session Persistence - Persist sessions across restarts
Implementation Date
Completed: 2026-01-06 Actual Effort: 1.5 hours Test Coverage: 2 additional tests (100% coverage for new code)
References
src/routes/mcp.ts- MCP Routes Implementationsrc/services/session-store.ts- Session Store Servicesrc/config/session.ts- Session Configuration