Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ function track(
identify: options.identify,
redactSensitiveInformation: options.redactSensitiveInformation,
},
sessionSource: "mcpcat", // Initially MCPCat-generated, will change to "mcp" if MCP sessionId is provided
sessionSource: "mcpcat", // Initially MCPCat-generated, will change to "mcp" if MCP sessionId is provided in requests
};

setServerTrackingData(lowLevelServer, mcpcatData);
Expand Down
122 changes: 0 additions & 122 deletions src/modules/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { PublishEventRequestEventTypeEnum } from "mcpcat-api";
import { publishEvent } from "./eventQueue.js";
import { getMCPCompatibleErrorMessage } from "./compatibility.js";
import { writeToLog } from "./logging.js";
import { INACTIVITY_TIMEOUT_IN_MINUTES } from "./constants.js";

/**
* Simple LRU cache for session identities.
Expand Down Expand Up @@ -66,66 +65,6 @@ class IdentityCache {
// This prevents duplicate identify events when server objects are recreated
const _globalIdentityCache = new IdentityCache(1000);

/**
* Maps userId to recent session IDs for reconnection support.
* When a user reconnects (new initialize without MCP sessionId),
* we can reuse their previous session if it's recent enough.
*/
class UserSessionCache {
private cache: Map<string, { sessionId: string; lastSeen: number }>;
private maxSize: number;

constructor(maxSize: number = 1000) {
this.cache = new Map();
this.maxSize = maxSize;
}

getRecentSession(userId: string, timeoutMs: number): string | undefined {
const entry = this.cache.get(userId);
if (!entry) return undefined;

// Check if session has expired
if (Date.now() - entry.lastSeen > timeoutMs) {
this.cache.delete(userId);
return undefined;
}

return entry.sessionId;
}

set(userId: string, sessionId: string): void {
// Remove if already exists (to re-add at end for LRU)
this.cache.delete(userId);

// Evict oldest if at capacity
if (this.cache.size >= this.maxSize) {
const oldestKey = this.cache.keys().next().value;
if (oldestKey !== undefined) {
this.cache.delete(oldestKey);
}
}

this.cache.set(userId, { sessionId, lastSeen: Date.now() });
}
}

// Global user session cache for reconnection support
const _globalUserSessionCache = new UserSessionCache(1000);

/**
* FOR TESTING ONLY: Manually set a user session cache entry with custom lastSeen timestamp
*/
export function _testSetUserSession(
userId: string,
sessionId: string,
lastSeenMs: number,
): void {
(_globalUserSessionCache as any).cache.set(userId, {
sessionId,
lastSeen: lastSeenMs,
});
}

// Internal tracking storage
const _serverTracking = new WeakMap<MCPServerLike, MCPCatData>();

Expand Down Expand Up @@ -224,64 +163,6 @@ export async function handleIdentify(
try {
const identityResult = await data.options.identify(request, extra);
if (identityResult) {
// Check for session reconnection (if no MCP sessionId provided in extra)
// If this user had a recent session, switch to it instead of creating new one
if (!extra?.sessionId && identityResult.userId) {
const timeoutMs = INACTIVITY_TIMEOUT_IN_MINUTES * 60 * 1000;
const previousSessionId = _globalUserSessionCache.getRecentSession(
identityResult.userId,
timeoutMs,
);

if (previousSessionId && previousSessionId !== data.sessionId) {
// User has a previous session - reconnect to it
const currentSessionIdentity = _globalIdentityCache.get(
data.sessionId,
);

if (!currentSessionIdentity) {
// Current session is brand new (no identity) - reconnect to previous session
data.sessionId = previousSessionId;
data.lastActivity = new Date();
setServerTrackingData(server, data);

writeToLog(
`Reconnected user ${identityResult.userId} to previous session ${previousSessionId} (current session was new)`,
);
} else if (currentSessionIdentity.userId !== identityResult.userId) {
// Current session belongs to different user - reconnect to user's previous session
data.sessionId = previousSessionId;
data.lastActivity = new Date();
setServerTrackingData(server, data);

writeToLog(
`Reconnected user ${identityResult.userId} to previous session ${previousSessionId}`,
);
}
// If current session already belongs to this user, no need to do anything
} else if (!previousSessionId) {
// User has NO previous session - check if current session belongs to someone else
const currentSessionIdentity = _globalIdentityCache.get(
data.sessionId,
);
if (
currentSessionIdentity &&
currentSessionIdentity.userId !== identityResult.userId
) {
// Current session belongs to different user - create new session
const { newSessionId } = await import("./session.js");
data.sessionId = newSessionId();
data.sessionSource = "mcpcat";
data.lastActivity = new Date();
setServerTrackingData(server, data);

writeToLog(
`Created new session ${data.sessionId} for user ${identityResult.userId} (previous session belonged to ${currentSessionIdentity.userId})`,
);
}
}
}

// Now use the (possibly updated) sessionId for all subsequent operations
const currentSessionId = data.sessionId;

Expand All @@ -302,9 +183,6 @@ export async function handleIdentify(
// Per-server cache: used by getSessionInfo() for fast local access
data.identifiedSessions.set(data.sessionId, mergedIdentity);

// Track userId → sessionId mapping for reconnection support
_globalUserSessionCache.set(mergedIdentity.userId, currentSessionId);

if (hasChanged) {
writeToLog(
`Identified session ${currentSessionId} with identity: ${JSON.stringify(mergedIdentity)}`,
Expand Down
19 changes: 8 additions & 11 deletions src/modules/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,14 @@ export function getServerSessionId(

// If MCP sessionId is provided
if (mcpSessionId) {
// Check if it's a new or changed MCP sessionId
if (mcpSessionId !== data.lastMcpSessionId) {
// Derive deterministic KSUID from MCP sessionId
data.sessionId = deriveSessionIdFromMCPSession(
mcpSessionId,
data.projectId || undefined,
);
data.lastMcpSessionId = mcpSessionId;
data.sessionSource = "mcp";
setServerTrackingData(server, data);
}
// Derive deterministic KSUID from MCP sessionId
data.sessionId = deriveSessionIdFromMCPSession(
mcpSessionId,
data.projectId || undefined,
);
data.lastMcpSessionId = mcpSessionId;
data.sessionSource = "mcp";
setServerTrackingData(server, data);
// If MCP sessionId hasn't changed, continue using the existing derived KSUID
setLastActivity(server);
return data.sessionId;
Expand Down
Loading