Skip to content

feat: add session expiration tracking and reconnection support#140

Merged
paulbreuler merged 2 commits intomainfrom
feature/session-reconnection
Feb 11, 2026
Merged

feat: add session expiration tracking and reconnection support#140
paulbreuler merged 2 commits intomainfrom
feature/session-reconnection

Conversation

@paulbreuler
Copy link
Owner

Summary

This PR adds session expiration tracking and reconnection support to help MCP clients automatically recover from idle timeouts.

Problem

The limps HTTP server has a default 30-minute session idle timeout. When a session expires, MCP clients receive a generic "Session not found" 404 error without any indication of whether the session expired or was invalid. This makes it difficult for clients to implement automatic reconnection logic.

Solution

1. Session Expiration Tracking

  • Track expired sessions with their reason (timeout, closed, deleted) for 24 hours
  • Distinguish between SESSION_EXPIRED (previously valid) and SESSION_NOT_FOUND (never existed)
  • Return specific error codes and headers to help clients implement reconnection

2. Enhanced Error Responses

Session Expired:

{
  "error": "Session expired",
  "code": "SESSION_EXPIRED",
  "message": "Session expired due to timeout. Please reconnect without session ID.",
  "expiredAt": "2026-02-11T10:30:00.000Z"
}

Headers:

  • X-Session-Expired: true
  • X-Session-Expired-Reason: timeout (or closed, deleted)

Session Not Found (never existed):

{
  "error": "Session not found",
  "code": "SESSION_NOT_FOUND",
  "message": "Session ID not recognized. It may have been invalid or the server was restarted."
}

3. Client Reconnection Flow

When receiving SESSION_EXPIRED, clients should:

  1. Clear the stored mcp-session-id
  2. Send a new POST to /mcp without the session ID header
  3. Store the new session ID from response headers
  4. Retry the original request

4. Documentation

Added comprehensive session management documentation to the README covering:

  • Session expiration responses
  • Reconnection flow for MCP clients
  • Configuration options (sessionTimeoutMs)

Changes

  • packages/limps/src/server-http.ts: Add expired session tracking, enhanced error responses
  • README.md: Add Session Management & Reconnection section

Testing

All 1714 tests pass:

  • 1397 tests in packages/limps
  • 317 tests in packages/limps-headless

Configuration

Users can adjust the session timeout in their config:

{
  "server": {
    "sessionTimeoutMs": 3600000  // 1 hour (default: 30 min)
  }
}

Set to 0 to disable timeout entirely (sessions persist until server restart).

- Track expired sessions with reason (timeout/closed/deleted) for 24h
- Return specific error codes: SESSION_EXPIRED vs SESSION_NOT_FOUND
- Add X-Session-Expired headers for MCP client reconnection
- Document reconnection flow in README

This allows MCP clients to detect expired sessions and automatically reconnect by creating a new session without the old session ID.
Copilot AI review requested due to automatic review settings February 11, 2026 15:20
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the HTTP MCP server with session expiration tracking and reconnection support to help clients automatically recover from idle timeouts. The implementation distinguishes between expired sessions (previously valid but timed out) and non-existent sessions (never existed or server restarted) through distinct error codes and HTTP headers.

Changes:

  • Added expired session tracking with a 24-hour retention period to distinguish between SESSION_EXPIRED and SESSION_NOT_FOUND errors
  • Enhanced error responses with specific error codes, descriptive messages, and custom headers (X-Session-Expired, X-Session-Expired-Reason) to guide client reconnection logic
  • Updated documentation in README.md with session management guidance, reconnection flow, and configuration examples

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
packages/limps/src/server-http.ts Added ExpiredSessionInfo interface and expiredSessions Map for tracking expired sessions; modified cleanupSession to accept a reason parameter and track expiration; enhanced error handling to distinguish SESSION_EXPIRED from SESSION_NOT_FOUND with appropriate headers; updated transport.onclose and stopHttpServer to use new cleanup mechanism
README.md Added Session Management & Reconnection section documenting session expiration behavior, error responses, client reconnection flow, and configuration options
Comments suppressed due to low confidence (3)

packages/limps/src/server-http.ts:255

  • When a DELETE request is made with an unknown session ID (line 252-255), it returns a generic "Session not found" error without distinguishing between SESSION_EXPIRED and SESSION_NOT_FOUND. This is inconsistent with the enhanced error handling added for GET/POST requests. For consistency and to help clients understand the session state, DELETE should also check expiredSessions and return the appropriate error code and headers.
        } else {
          res.writeHead(404, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({ error: 'Session not found' }));
        }

packages/limps/src/server-http.ts:505

  • The stopHttpServer function does not clear the expiredSessions map. When the server stops and restarts, the expiredSessions map will still contain old session IDs, which could lead to incorrect SESSION_EXPIRED responses for sessions from a previous server instance. Consider adding expiredSessions.clear() after line 477 or at the end of the function to ensure a clean state on server shutdown.
export async function stopHttpServer(): Promise<void> {
  // Stop session cleanup interval
  if (cleanupInterval) {
    clearInterval(cleanupInterval);
    cleanupInterval = null;
  }

  // Close all active sessions
  for (const [sessionId, session] of sessions) {
    try {
      if (session.server.loadedExtensions) {
        await shutdownExtensions(session.server.loadedExtensions);
      }
      await session.transport.close();
      // Track as closed for reconnection support
      expiredSessions.set(sessionId, {
        expiredAt: new Date(),
        reason: 'closed',
      });
    } catch (error) {
      logRedactedError(`Error closing session ${maskSessionId(sessionId)}`, error);
    }
  }
  sessions.clear();

  // Close HTTP server
  if (httpServer) {
    const server = httpServer;
    await new Promise<void>((resolve, reject) => {
      server.close((err) => {
        if (err) reject(err);
        else resolve();
      });
    });
    httpServer = null;
    console.error('HTTP server stopped');
  }

  // Clean up PID file
  if (resources) {
    const pidFilePath =
      activePidFilePath ?? getPidFilePath(getHttpServerConfig(resources.config).port);
    removePidFile(pidFilePath);

    // Shut down shared resources
    await shutdownServerResources(resources);
    resources = null;
  }

  startTime = null;
  activePidFilePath = null;
}

README.md:673

  • The text "Use this endpoint for:" on line 670 appears to be misplaced. It refers to the /health endpoint but is located at the end of the Session Management & Reconnection section. This text should either be moved up to the Health Endpoint section (after line 618) or removed if it was intended to be replaced by the Session Management section.
Use this endpoint for:
- Monitoring daemon health in scripts or dashboards
- Verifying daemon is running before connecting MCP clients
- Automated health checks in orchestration tools (Kubernetes, Docker Compose)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Allow sessionTimeoutMs: 0 to disable timeout (config validation)
- Fix race condition in cleanupSession by removing from sessions first
- Add comprehensive tests for SESSION_EXPIRED and SESSION_NOT_FOUND
- Add test for sessionTimeoutMs: 0 boundary value

Addresses review feedback on PR #140
@paulbreuler paulbreuler merged commit 86de71c into main Feb 11, 2026
4 checks passed
@github-actions github-actions bot mentioned this pull request Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants