Skip to content

feat: add file-based session persistence#2

Merged
marcusquinn merged 1 commit intomainfrom
feature/session-persistence
Jan 15, 2026
Merged

feat: add file-based session persistence#2
marcusquinn merged 1 commit intomainfrom
feature/session-persistence

Conversation

@marcusquinn
Copy link
Owner

@marcusquinn marcusquinn commented Jan 15, 2026

Summary

Sessions now persist across MCP server restarts by storing mappings in ~/.config/claude-code-mcp/sessions.json.

Problem

When the MCP is invoked via npx (as in OpenCode's config), each call spawns a fresh server process. The in-memory session map was lost between calls, breaking session continuity.

Solution

  • Store session mappings to ~/.config/claude-code-mcp/sessions.json
  • Load sessions from file on server startup
  • Save to file whenever a session is created/updated
  • Auto-expire sessions after 24 hours
  • Clean expired sessions on load

File Format

{
  "parent-session-123": {
    "claudeSessionId": "eff82f41-a021-4098-80a0-95117aa3f3e4",
    "updatedAt": "2026-01-14T23:15:00Z"
  }
}

Testing

Tested with Claude CLI --resume flag - sessions correctly persist and resume across server restarts.

Summary by CodeRabbit

  • New Features
    • Sessions are now persistent and survive server restarts without data loss.
    • Sessions automatically expire after 24 hours of inactivity for enhanced security.
    • Session capacity management with automatic optimization.

✏️ Tip: You can customize this high-level summary in your review settings.

- Store session mappings in ~/.config/claude-code-mcp/sessions.json
- Sessions persist across MCP server restarts
- Auto-expire sessions after 24 hours
- Clean expired sessions on load
- Maintain LRU eviction (max 1000 sessions)

This enables session continuity even when the MCP server is spawned
fresh for each call (e.g., via npx).
@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

The PR introduces a persistent, TTL-based session management system that replaces simple in-memory session storage. Sessions are now stored on disk in sessions.json with automatic expiration handling and LRU capacity eviction. Startup loads previous sessions while cleaning expired entries.

Changes

Cohort / File(s) Summary
Session Persistence & TTL Management
src/server.ts
Introduces persistent session storage with 24-hour TTL, disk-based sessions.json file, automatic expiration cleanup, LRU capacity eviction, and replaces direct map access with getSessionMapping/setSessionMapping functions. Loads and validates sessions on startup.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 Sessions now persist through the night,
With TTL and LRU in sight,
Files on disk keep memories strong,
Expired whiskers swept along,
Hop-hop-hopping to lasting delight! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add file-based session persistence' directly matches the main change: implementing persistent session storage with TTL-based expiration and file I/O.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
src/server.ts (4)

87-90: Consider respecting XDG_CONFIG_HOME environment variable.

The session directory is hardcoded to ~/.config/claude-code-mcp. On Linux systems, users may have XDG_CONFIG_HOME set to a different location. This is a minor consideration and can be deferred.


107-125: Add validation for parsed JSON structure.

The parsed JSON is directly converted to a Map without validating its structure. If sessions.json is corrupted or contains unexpected data (e.g., an array, null, or malformed entries), this could cause runtime errors when accessing entry.updatedAt or entry.claudeSessionId later.

♻️ Suggested validation
 function loadSessions(): void {
   try {
     if (existsSync(SESSION_FILE)) {
       const data = readFileSync(SESSION_FILE, 'utf-8');
       const parsed = JSON.parse(data);
-      sessionMap = new Map(Object.entries(parsed));
+      
+      // Validate parsed data is a plain object
+      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+        sessionMap = new Map();
+        for (const [key, value] of Object.entries(parsed)) {
+          if (
+            value &&
+            typeof value === 'object' &&
+            typeof (value as SessionEntry).claudeSessionId === 'string' &&
+            typeof (value as SessionEntry).updatedAt === 'string'
+          ) {
+            sessionMap.set(key, value as SessionEntry);
+          } else {
+            debugLog(`[Debug] Skipping invalid session entry: ${key}`);
+          }
+        }
+      } else {
+        debugLog('[Debug] Sessions file has invalid format, starting fresh');
+        sessionMap = new Map();
+      }
       debugLog(`[Debug] Loaded ${sessionMap.size} sessions from ${SESSION_FILE}`);
       
       // Clean up expired sessions on load
       cleanExpiredSessions();
     }
   } catch (e) {

127-139: Consider atomic writes for crash safety.

The current implementation writes directly to sessions.json. If the process crashes or is killed during the write, the file could be corrupted. A safer approach is to write to a temporary file and atomically rename it.

This is a low-risk issue for this use case and can be deferred.

♻️ Atomic write pattern
+import { renameSync } from 'node:fs';
+
 function saveSessions(): void {
   try {
     ensureSessionDir();
     const data = Object.fromEntries(sessionMap);
-    writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2));
+    const tempFile = `${SESSION_FILE}.tmp`;
+    writeFileSync(tempFile, JSON.stringify(data, null, 2));
+    renameSync(tempFile, SESSION_FILE);
     debugLog(`[Debug] Saved ${sessionMap.size} sessions to ${SESSION_FILE}`);
   } catch (e) {
     debugLog('[Debug] Failed to save sessions file:', e);
   }
 }

180-198: Eviction is FIFO, not LRU.

The comment mentions "LRU-style eviction," but the implementation is FIFO (first-in-first-out). True LRU would require moving accessed entries to the end of the Map on read, or tracking last access time separately.

Additionally, when updating an existing session, the entry is modified in place without moving to the end, so re-used sessions don't get their position refreshed.

This is functionally acceptable but the comment could be clarified to say "FIFO eviction" or enhanced to implement true LRU.

♻️ True LRU implementation
 function setSessionMapping(parentId: string, claudeId: string): void {
-  // LRU eviction if at capacity
+  // FIFO eviction if at capacity (oldest entry removed first)
   if (sessionMap.size >= MAX_SESSIONS) {
     // Remove oldest entry (first inserted)
     const firstKey = sessionMap.keys().next().value;
     if (firstKey) sessionMap.delete(firstKey);
   }
   
+  // Delete and re-insert to move to end (maintains insertion order for FIFO)
+  sessionMap.delete(parentId);
   sessionMap.set(parentId, {
     claudeSessionId: claudeId,
     updatedAt: new Date().toISOString()
   });

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e97cb6f and 167e29c.

📒 Files selected for processing (1)
  • src/server.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/agent.mdc)

Use TypeScript for type safety and improved development experience

Files:

  • src/server.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/agent.mdc)

**/*.{js,jsx,ts,tsx}: Follow ESLint configuration for code style and quality
Use meaningful variable and function names that reflect their purpose
Document public APIs with JSDoc comments
Keep functions small and focused on a single responsibility
Use const and let instead of var for variable declarations
Avoid deeply nested callbacks; prefer async/await or promises

Files:

  • src/server.ts
src/server.ts

📄 CodeRabbit inference engine (AGENT.md)

src/server.ts: When asked to edit the Claude Code tool description or behavior, update it in src/server.ts
The tool description can be found in the description field in the setupToolHandlers method in src/server.ts

Files:

  • src/server.ts
🧠 Learnings (1)
📚 Learning: 2026-01-14T23:01:49.563Z
Learnt from: CR
Repo: marcusquinn/claude-code-mcp PR: 0
File: AGENT.md:0-0
Timestamp: 2026-01-14T23:01:49.563Z
Learning: Applies to src/server.ts : When asked to edit the Claude Code tool description or behavior, update it in `src/server.ts`

Applied to files:

  • src/server.ts
🔇 Additional comments (6)
src/server.ts (6)

77-82: LGTM!

The SessionEntry interface is well-defined with clear fields for session tracking and expiration.


97-105: LGTM!

The function correctly ensures the session directory exists with recursive: true, which handles concurrent creation gracefully.


141-160: LGTM!

The cleanup logic correctly identifies and removes expired sessions, with file persistence only when changes occur.


162-178: LGTM!

The lazy expiration check on read is an efficient pattern that avoids unnecessary cleanup operations.


200-201: LGTM!

Loading sessions synchronously at module initialization ensures session data is available before handling any requests.


541-542: LGTM!

The integration correctly uses getSessionMapping() which handles TTL expiration checks.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @marcusquinn, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust file-based session persistence mechanism for the Claude CLI server. By storing session mappings in a dedicated JSON file, it resolves the problem of session data being lost across server restarts, particularly when the server is spawned as a fresh process via npx. This enhancement ensures seamless session continuity for users, improving the overall reliability and user experience of the Claude CLI integration.

Highlights

  • Session Persistence: Implemented file-based session persistence, storing session mappings in ~/.config/claude-code-mcp/sessions.json to ensure continuity across server restarts.
  • Automatic Session Management: Sessions are automatically loaded from the file system on server startup and saved whenever a session is created or updated.
  • Session Expiration: Introduced a 24-hour time-to-live (TTL) for sessions, with expired sessions being automatically cleaned during loading and retrieval.
  • Improved Continuity for npx Invocations: This change resolves the problem of session data being lost when the server is invoked via npx, ensuring a seamless and continuous session experience.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces file-based session persistence, which is a valuable addition for maintaining session continuity across server restarts. The implementation correctly handles loading, saving, and expiring sessions. However, I've identified a critical issue regarding potential race conditions if multiple server processes run concurrently, which could lead to data loss. There's also a high-severity performance concern with the use of synchronous file I/O that could block the event loop in a long-running server. I've provided detailed comments and suggestions to address these issues, along with some medium-severity recommendations for improving error logging and code clarity.

Comment on lines +110 to +139
function loadSessions(): void {
try {
if (existsSync(SESSION_FILE)) {
const data = readFileSync(SESSION_FILE, 'utf-8');
const parsed = JSON.parse(data);
sessionMap = new Map(Object.entries(parsed));
debugLog(`[Debug] Loaded ${sessionMap.size} sessions from ${SESSION_FILE}`);

// Clean up expired sessions on load
cleanExpiredSessions();
}
} catch (e) {
debugLog('[Debug] Failed to load sessions file, starting fresh:', e);
sessionMap = new Map();
}
}

/**
* Save sessions from memory to file
*/
function saveSessions(): void {
try {
ensureSessionDir();
const data = Object.fromEntries(sessionMap);
writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2));
debugLog(`[Debug] Saved ${sessionMap.size} sessions to ${SESSION_FILE}`);
} catch (e) {
debugLog('[Debug] Failed to save sessions file:', e);
}
}

Choose a reason for hiding this comment

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

critical

The current implementation of loading from and saving to sessions.json is not safe for concurrent execution. The pull request description mentions that each npx call spawns a new process. If two such processes run concurrently, they can create a race condition:

  1. Process A reads sessions.json.
  2. Process B reads sessions.json.
  3. Process A modifies its in-memory session map and writes the result to sessions.json.
  4. Process B modifies its in-memory session map and writes its result to sessions.json, overwriting the changes from Process A.

This will lead to session data loss. To prevent this, you should implement a file locking mechanism around the read-modify-write cycle to ensure atomic updates to the session file. Libraries like proper-lockfile are well-suited for this purpose.

Comment on lines +130 to +139
function saveSessions(): void {
try {
ensureSessionDir();
const data = Object.fromEntries(sessionMap);
writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2));
debugLog(`[Debug] Saved ${sessionMap.size} sessions to ${SESSION_FILE}`);
} catch (e) {
debugLog('[Debug] Failed to save sessions file:', e);
}
}

Choose a reason for hiding this comment

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

high

The use of writeFileSync makes this a synchronous, blocking I/O operation. This function is called frequently (on every session creation/update via setSessionMapping, on expiration checks in getSessionMapping, and during cleanup). In a long-running server context, blocking the Node.js event loop for file I/O will cause significant performance degradation.

All file I/O should be performed asynchronously using fs.promises. This would require this function and its callers to become async and use await. Additionally, you might consider "debouncing" calls to saveSessions to avoid inefficiently writing to disk on every single modification.

Comment on lines +121 to +124
} catch (e) {
debugLog('[Debug] Failed to load sessions file, starting fresh:', e);
sessionMap = new Map();
}

Choose a reason for hiding this comment

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

medium

Failures in loading the session file are logged using debugLog, which is conditional on the MCP_CLAUDE_DEBUG environment variable. A failure to load sessions is a significant event that leads to the loss of all session continuity. This should be logged as a standard error to ensure it's always visible, regardless of debug settings.

Suggested change
} catch (e) {
debugLog('[Debug] Failed to load sessions file, starting fresh:', e);
sessionMap = new Map();
}
} catch (e) {
console.error('[Error] Failed to load sessions file, starting fresh:', e);
sessionMap = new Map();
}

Comment on lines +136 to +138
} catch (e) {
debugLog('[Debug] Failed to save sessions file:', e);
}

Choose a reason for hiding this comment

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

medium

Failures in saving the session file are logged using debugLog, which is conditional. A failure to save sessions can lead to data inconsistency between memory and disk and should be logged as a standard error to ensure the issue is always visible.

Suggested change
} catch (e) {
debugLog('[Debug] Failed to save sessions file:', e);
}
} catch (e) {
console.error('[Error] Failed to save sessions file:', e);
}

Comment on lines +181 to 182
* Store a session mapping with LRU-style eviction and file persistence
*/

Choose a reason for hiding this comment

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

medium

The comment describes "LRU-style eviction", but the implementation is actually FIFO (First-In, First-Out), as it removes the oldest entry by insertion order (sessionMap.keys().next().value). A true LRU cache evicts the least recently used item. To avoid confusion, please update the comment to accurately describe the FIFO behavior.

Suggested change
* Store a session mapping with LRU-style eviction and file persistence
*/
* Store a session mapping with FIFO-style eviction and file persistence

@augmentcode
Copy link

augmentcode bot commented Jan 15, 2026

🤖 Augment PR Summary

Summary: Sessions now persist across MCP server restarts by storing parent-session → Claude CLI session mappings on disk.

Changes:

  • Introduce a persisted SessionEntry (with updatedAt) and a 24h TTL-based expiry/cleanup
  • Load/save mappings in ~/.config/claude-code-mcp/sessions.json (creating the directory as needed)
  • Update resume behavior to look up existing mappings via getSessionMapping() and persist updates on writes

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

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

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

if (existsSync(SESSION_FILE)) {
const data = readFileSync(SESSION_FILE, 'utf-8');
const parsed = JSON.parse(data);
sessionMap = new Map(Object.entries(parsed));
Copy link

Choose a reason for hiding this comment

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

Because sessions.json is effectively external input, Object.entries(parsed) can populate sessionMap with values missing a valid updatedAt/claudeSessionId; then new Date(entry.updatedAt).getTime() becomes NaN and TTL expiry won’t work (or resumes can silently fail). Consider validating/coercing loaded entries before assigning to sessionMap.

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

try {
ensureSessionDir();
const data = Object.fromEntries(sessionMap);
writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2));
Copy link

Choose a reason for hiding this comment

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

Since saveSessions() overwrites sessions.json directly, concurrent MCP processes (or a crash mid-write) can leave a partially-written file that resets sessions on the next startup. Consider making the persistence write atomic to reduce the chance of JSON corruption.

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

@marcusquinn marcusquinn merged commit 4dacb3a into main Jan 15, 2026
2 checks passed
marcusquinn added a commit that referenced this pull request Jan 15, 2026
- Store session mappings in ~/.config/claude-code-mcp/sessions.json
- Sessions persist across MCP server restarts
- Auto-expire sessions after 24 hours
- Clean expired sessions on load
- Maintain LRU eviction (max 1000 sessions)

This enables session continuity even when the MCP server is spawned
fresh for each call (e.g., via npx).
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.

1 participant