From 2446be33a12b5cb69537a1a5dbc8d4408e89edcc Mon Sep 17 00:00:00 2001 From: Naseem AlNaji Date: Tue, 18 Nov 2025 20:44:00 -0500 Subject: [PATCH] fix: revert actor corellation changes for sessions identification --- src/index.ts | 2 +- src/modules/internal.ts | 122 -------- src/modules/session.ts | 19 +- src/tests/identify.test.ts | 583 ----------------------------------- src/tests/session-id.test.ts | 331 -------------------- 5 files changed, 9 insertions(+), 1048 deletions(-) diff --git a/src/index.ts b/src/index.ts index 50b6c9f..95fc87f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); diff --git a/src/modules/internal.ts b/src/modules/internal.ts index eaa8f9b..ba08e0f 100644 --- a/src/modules/internal.ts +++ b/src/modules/internal.ts @@ -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. @@ -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; - 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(); @@ -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; @@ -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)}`, diff --git a/src/modules/session.ts b/src/modules/session.ts index 3a915cf..b7e18cd 100644 --- a/src/modules/session.ts +++ b/src/modules/session.ts @@ -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; diff --git a/src/tests/identify.test.ts b/src/tests/identify.test.ts index dcd273e..3f74afd 100644 --- a/src/tests/identify.test.ts +++ b/src/tests/identify.test.ts @@ -751,587 +751,4 @@ describe("Identify Feature", () => { await eventCapture.stop(); }); }); - - describe("Identity Merging Behavior", () => { - it("should create separate sessions for different users", async () => { - const eventCapture = new EventCapture(); - await eventCapture.start(); - - let callCount = 0; - const firstUserId = `user-${randomUUID()}`; - const secondUserId = `user-${randomUUID()}`; - - // Enable tracking with identify function that returns different data on each call - track(server, "test-project", { - enableTracing: true, - identify: async () => { - callCount++; - if (callCount === 1) { - return { - userId: firstUserId, - userName: "Alice", - userData: { - role: "admin", - department: "Engineering", - }, - }; - } else { - return { - userId: secondUserId, - userName: "Bob", - userData: { - department: "Sales", - location: "NYC", - }, - }; - } - }, - }); - - // First tool call - sets initial identity for Alice - await client.request( - { - method: "tools/call", - params: { - name: "add_todo", - arguments: { - text: "First todo", - context: "Testing identity separation", - }, - }, - }, - CallToolResultSchema, - ); - - // Wait for first identify to complete - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Verify Alice's identity was stored - const data = getServerTrackingData(server.server); - const aliceSessionId = data?.sessionId; - const aliceIdentity = data?.identifiedSessions.get(aliceSessionId!); - - expect(aliceIdentity).toEqual({ - userId: firstUserId, - userName: "Alice", - userData: { - role: "admin", - department: "Engineering", - }, - }); - - // Second tool call - Bob should get his own NEW session (not take over Alice's) - await client.request( - { - method: "tools/call", - params: { - name: "list_todos", - arguments: { - context: "Testing identity separation again", - }, - }, - }, - CallToolResultSchema, - ); - - // Wait for second identify to complete - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Verify Bob got his own session (different from Alice's) - const bobSessionId = data?.sessionId; - expect(bobSessionId).not.toEqual(aliceSessionId); - - // Verify Bob's identity is stored in his own session - const bobIdentity = data?.identifiedSessions.get(bobSessionId!); - expect(bobIdentity).toEqual({ - userId: secondUserId, - userName: "Bob", - userData: { - department: "Sales", - location: "NYC", - }, - }); - - // Verify Alice's session still has her identity (unchanged) - const aliceIdentityStillThere = data?.identifiedSessions.get( - aliceSessionId!, - ); - expect(aliceIdentityStillThere).toEqual({ - userId: firstUserId, - userName: "Alice", - userData: { - role: "admin", - department: "Engineering", - }, - }); - - // Verify two identify events were published (one for each user) - const events = eventCapture.getEvents(); - const identifyEvents = events.filter( - (e) => e.eventType === PublishEventRequestEventTypeEnum.mcpcatIdentify, - ); - - expect(identifyEvents.length).toBe(2); - - await eventCapture.stop(); - }); - - it("should merge complex nested userData fields correctly", async () => { - const eventCapture = new EventCapture(); - await eventCapture.start(); - - let callCount = 0; - const userId = `user-${randomUUID()}`; - - track(server, "test-project", { - enableTracing: true, - identify: async () => { - callCount++; - if (callCount === 1) { - return { - userId: userId, - userData: { - name: "Alice", - role: "admin", - preferences: { - theme: "dark", - language: "en", - }, - permissions: ["read", "write"], - }, - }; - } else { - return { - userId: userId, - userData: { - role: "user", // Overwrite - location: "NYC", // Add new field - preferences: { - // This will replace the entire preferences object - theme: "light", - notifications: true, - }, - }, - }; - } - }, - }); - - // First tool call - await client.request( - { - method: "tools/call", - params: { - name: "add_todo", - arguments: { - text: "Test", - context: "Testing complex merge", - }, - }, - }, - CallToolResultSchema, - ); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Second tool call - await client.request( - { - method: "tools/call", - params: { - name: "list_todos", - arguments: { - context: "Testing complex merge again", - }, - }, - }, - CallToolResultSchema, - ); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Verify final merged identity - const data = getServerTrackingData(server.server); - const sessionId = data?.sessionId; - const storedIdentity = data?.identifiedSessions.get(sessionId!); - - expect(storedIdentity).toEqual({ - userId: userId, - userData: { - name: "Alice", // Preserved - role: "user", // Overwritten - location: "NYC", // Added - permissions: ["read", "write"], // Preserved - preferences: { - // Completely replaced (not deep merged) - theme: "light", - notifications: true, - }, - }, - }); - - await eventCapture.stop(); - }); - - it("should overwrite userData field when same key provided in subsequent call", async () => { - const eventCapture = new EventCapture(); - await eventCapture.start(); - - let callCount = 0; - const userId = `user-${randomUUID()}`; - - track(server, "test-project", { - enableTracing: true, - identify: async () => { - callCount++; - if (callCount === 1) { - return { - userId: userId, - userData: { - setting: "value1", - counter: 1, - }, - }; - } else { - return { - userId: userId, - userData: { - setting: "value2", // Overwrite - counter: 2, // Overwrite - }, - }; - } - }, - }); - - // First call - await client.request( - { - method: "tools/call", - params: { - name: "add_todo", - arguments: { - text: "Test", - context: "Testing overwrite", - }, - }, - }, - CallToolResultSchema, - ); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Second call - await client.request( - { - method: "tools/call", - params: { - name: "list_todos", - arguments: { - context: "Testing overwrite again", - }, - }, - }, - CallToolResultSchema, - ); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - const data = getServerTrackingData(server.server); - const sessionId = data?.sessionId; - const storedIdentity = data?.identifiedSessions.get(sessionId!); - - expect(storedIdentity?.userData?.setting).toBe("value2"); - expect(storedIdentity?.userData?.counter).toBe(2); - - await eventCapture.stop(); - }); - - it("should only publish identify events when identity actually changes", async () => { - const eventCapture = new EventCapture(); - await eventCapture.start(); - - let callCount = 0; - const userId = `user-${randomUUID()}`; - - track(server, "test-project", { - enableTracing: true, - identify: async () => { - callCount++; - if (callCount === 1) { - // First call - new identity - return { - userId: userId, - userData: { field1: "value1" }, - }; - } else if (callCount === 2) { - // Second call - same identity (no change) - return { - userId: userId, - userData: { field1: "value1" }, - }; - } else if (callCount === 3) { - // Third call - change in userData - return { - userId: userId, - userData: { field1: "value1", field2: "value2" }, - }; - } else { - // Fourth call - same as third (no change) - return { - userId: userId, - userData: { field1: "value1", field2: "value2" }, - }; - } - }, - }); - - // Call 1 - should publish (new identity) - await client.request( - { - method: "tools/call", - params: { - name: "add_todo", - arguments: { text: "1", context: "Test" }, - }, - }, - CallToolResultSchema, - ); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - let events = eventCapture.getEvents(); - let identifyEvents = events.filter( - (e) => e.eventType === PublishEventRequestEventTypeEnum.mcpcatIdentify, - ); - expect(identifyEvents.length).toBe(1); - - // Call 2 - should NOT publish (same identity) - await client.request( - { - method: "tools/call", - params: { - name: "list_todos", - arguments: { context: "Test" }, - }, - }, - CallToolResultSchema, - ); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - events = eventCapture.getEvents(); - identifyEvents = events.filter( - (e) => e.eventType === PublishEventRequestEventTypeEnum.mcpcatIdentify, - ); - expect(identifyEvents.length).toBe(1); // Still only 1 - - // Call 3 - should publish (userData changed) - await client.request( - { - method: "tools/call", - params: { - name: "add_todo", - arguments: { text: "2", context: "Test" }, - }, - }, - CallToolResultSchema, - ); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - events = eventCapture.getEvents(); - identifyEvents = events.filter( - (e) => e.eventType === PublishEventRequestEventTypeEnum.mcpcatIdentify, - ); - expect(identifyEvents.length).toBe(2); // Now 2 - - // Call 4 - should NOT publish (same as call 3) - await client.request( - { - method: "tools/call", - params: { - name: "list_todos", - arguments: { context: "Test" }, - }, - }, - CallToolResultSchema, - ); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - events = eventCapture.getEvents(); - identifyEvents = events.filter( - (e) => e.eventType === PublishEventRequestEventTypeEnum.mcpcatIdentify, - ); - expect(identifyEvents.length).toBe(2); // Still only 2 - - await eventCapture.stop(); - }); - - it("should preserve userData when subsequent identify call has no userData", async () => { - const eventCapture = new EventCapture(); - await eventCapture.start(); - - let callCount = 0; - const userId = `user-${randomUUID()}`; - - track(server, "test-project", { - enableTracing: true, - identify: async () => { - callCount++; - if (callCount === 1) { - return { - userId: userId, - userData: { - importantField: "importantValue", - metadata: { created: "2025-01-01" }, - }, - }; - } else { - // Second call doesn't include userData - return { - userId: userId, - }; - } - }, - }); - - // First call - sets userData - await client.request( - { - method: "tools/call", - params: { - name: "add_todo", - arguments: { - text: "Test", - context: "Testing userData preservation", - }, - }, - }, - CallToolResultSchema, - ); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Verify initial userData - const data = getServerTrackingData(server.server); - const sessionId = data?.sessionId; - let storedIdentity = data?.identifiedSessions.get(sessionId!); - - expect(storedIdentity?.userData).toEqual({ - importantField: "importantValue", - metadata: { created: "2025-01-01" }, - }); - - // Second call - no userData in return value - await client.request( - { - method: "tools/call", - params: { - name: "list_todos", - arguments: { - context: "Testing userData preservation again", - }, - }, - }, - CallToolResultSchema, - ); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Verify userData is still preserved - storedIdentity = data?.identifiedSessions.get(sessionId!); - - expect(storedIdentity?.userData).toEqual({ - importantField: "importantValue", - metadata: { created: "2025-01-01" }, - }); - - await eventCapture.stop(); - }); - - it("should handle userName being added in subsequent identify call", async () => { - const eventCapture = new EventCapture(); - await eventCapture.start(); - - let callCount = 0; - const userId = `user-${randomUUID()}`; - - track(server, "test-project", { - enableTracing: true, - identify: async () => { - callCount++; - if (callCount === 1) { - // First call - no userName - return { - userId: userId, - userData: { role: "admin" }, - }; - } else { - // Second call - adds userName - return { - userId: userId, - userName: "Alice Smith", - userData: { department: "Engineering" }, - }; - } - }, - }); - - // First call - await client.request( - { - method: "tools/call", - params: { - name: "add_todo", - arguments: { text: "Test", context: "Testing userName addition" }, - }, - }, - CallToolResultSchema, - ); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - const data = getServerTrackingData(server.server); - const sessionId = data?.sessionId; - let storedIdentity = data?.identifiedSessions.get(sessionId!); - - expect(storedIdentity?.userName).toBeUndefined(); - - // Second call - await client.request( - { - method: "tools/call", - params: { - name: "list_todos", - arguments: { context: "Testing userName addition again" }, - }, - }, - CallToolResultSchema, - ); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - storedIdentity = data?.identifiedSessions.get(sessionId!); - - expect(storedIdentity).toEqual({ - userId: userId, - userName: "Alice Smith", - userData: { - role: "admin", // Preserved - department: "Engineering", // Added - }, - }); - - // Should publish 2 identify events (both represent changes) - const events = eventCapture.getEvents(); - const identifyEvents = events.filter( - (e) => e.eventType === PublishEventRequestEventTypeEnum.mcpcatIdentify, - ); - expect(identifyEvents.length).toBe(2); - - await eventCapture.stop(); - }); - }); }); diff --git a/src/tests/session-id.test.ts b/src/tests/session-id.test.ts index f9c8be0..810739c 100644 --- a/src/tests/session-id.test.ts +++ b/src/tests/session-id.test.ts @@ -4,7 +4,6 @@ import { resetTodos, } from "./test-utils/client-server-factory"; import { track } from "../index"; -import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types"; import { EventCapture } from "./test-utils"; import { getServerTrackingData } from "../modules/internal"; import { HighLevelMCPServerLike } from "../types"; @@ -361,334 +360,4 @@ describe("Session ID Management", () => { await eventCapture.stop(); }); }); - - describe("Session Reconnection", () => { - it("should reconnect user to previous session when reinitializing without MCP sessionId", async () => { - const eventCapture = new EventCapture(); - await eventCapture.start(); - - const projectId = "test-project-reconnect"; - let identifyCallCount = 0; - - track(server, projectId, { - enableTracing: true, - identify: async (request: any, extra?: any) => { - identifyCallCount++; - return { - userId: "user-123", - userName: "Test User", - }; - }, - }); - - const lowLevelServer = server.server; - - // First initialize - creates initial session - const request1 = { - method: "initialize", - params: { - protocolVersion: "1.0", - capabilities: {}, - clientInfo: { name: "test-client", version: "1.0" }, - }, - }; - await lowLevelServer._requestHandlers.get("initialize")?.(request1, {}); - - const data1 = getServerTrackingData(lowLevelServer); - const firstSessionId = data1?.sessionId; - expect(firstSessionId).toMatch(/^ses_/); - expect(identifyCallCount).toBe(1); - - // Second initialize WITHOUT MCP sessionId - should reconnect to first session - const request2 = { - method: "initialize", - params: { - protocolVersion: "1.0", - capabilities: {}, - clientInfo: { name: "test-client", version: "1.0" }, - }, - }; - await lowLevelServer._requestHandlers.get("initialize")?.(request2, {}); - - const data2 = getServerTrackingData(lowLevelServer); - const secondSessionId = data2?.sessionId; - - // Should reuse the first session - expect(secondSessionId).toBe(firstSessionId); - expect(identifyCallCount).toBe(2); - - await eventCapture.stop(); - }); - - it("should create new session if previous session expired (>30 min)", async () => { - const eventCapture = new EventCapture(); - await eventCapture.start(); - - const projectId = "test-project-timeout-reconnect"; - let identifyCallCount = 0; - - track(server, projectId, { - enableTracing: true, - identify: async (request: any, extra?: any) => { - identifyCallCount++; - return { - userId: "user-456", - userName: "Test User", - }; - }, - }); - - const lowLevelServer = server.server; - - // First initialize - const request1 = { - method: "initialize", - params: { - protocolVersion: "1.0", - capabilities: {}, - clientInfo: { name: "test-client", version: "1.0" }, - }, - }; - await lowLevelServer._requestHandlers.get("initialize")?.(request1, {}); - - const data1 = getServerTrackingData(lowLevelServer); - const firstSessionId = data1?.sessionId; - - // Manually expire the session by setting lastActivity to 31 minutes ago - // Also update the cache entry to reflect this expiration - if (data1) { - data1.lastActivity = new Date(Date.now() - 31 * 60 * 1000); - // Update cache entry to 31 minutes ago - const { _testSetUserSession } = await import("../modules/internal.js"); - _testSetUserSession( - "user-456", - firstSessionId!, - Date.now() - 31 * 60 * 1000, - ); - } - - // Second initialize - session expired, should get new session - const request2 = { - method: "initialize", - params: { - protocolVersion: "1.0", - capabilities: {}, - clientInfo: { name: "test-client", version: "1.0" }, - }, - }; - await lowLevelServer._requestHandlers.get("initialize")?.(request2, {}); - - const data2 = getServerTrackingData(lowLevelServer); - const secondSessionId = data2?.sessionId; - - // Should have a different session (expired) - expect(secondSessionId).not.toBe(firstSessionId); - expect(secondSessionId).toMatch(/^ses_/); - expect(identifyCallCount).toBe(2); - - await eventCapture.stop(); - }); - - it("should NOT reconnect when MCP sessionId is provided (MCP takes priority)", async () => { - const eventCapture = new EventCapture(); - await eventCapture.start(); - - const projectId = "test-project-mcp-priority"; - const mcpSessionId1 = "mcp-session-aaa"; - const mcpSessionId2 = "mcp-session-bbb"; - - track(server, projectId, { - enableTracing: true, - identify: async (request: any, extra?: any) => { - return { - userId: "user-789", - userName: "Test User", - }; - }, - }); - - const lowLevelServer = server.server; - - // First initialize with MCP sessionId - const request1 = { - method: "initialize", - params: { - protocolVersion: "1.0", - capabilities: {}, - clientInfo: { name: "test-client", version: "1.0" }, - }, - }; - await lowLevelServer._requestHandlers.get("initialize")?.(request1, { - sessionId: mcpSessionId1, - }); - - const data1 = getServerTrackingData(lowLevelServer); - const firstSessionId = data1?.sessionId; - const expectedSessionId1 = deriveSessionIdFromMCPSession( - mcpSessionId1, - projectId, - ); - expect(firstSessionId).toBe(expectedSessionId1); - - // Second initialize with DIFFERENT MCP sessionId - // Should use the new MCP sessionId, NOT reconnect to previous - const request2 = { - method: "initialize", - params: { - protocolVersion: "1.0", - capabilities: {}, - clientInfo: { name: "test-client", version: "1.0" }, - }, - }; - await lowLevelServer._requestHandlers.get("initialize")?.(request2, { - sessionId: mcpSessionId2, - }); - - const data2 = getServerTrackingData(lowLevelServer); - const secondSessionId = data2?.sessionId; - const expectedSessionId2 = deriveSessionIdFromMCPSession( - mcpSessionId2, - projectId, - ); - - // Should use new MCP-derived session, not reconnect - expect(secondSessionId).toBe(expectedSessionId2); - expect(secondSessionId).not.toBe(firstSessionId); - - await eventCapture.stop(); - }); - - it("should create new session when no identify function configured", async () => { - const eventCapture = new EventCapture(); - await eventCapture.start(); - - const projectId = "test-project-no-identify"; - - track(server, projectId, { - enableTracing: true, - // No identify function - }); - - const lowLevelServer = server.server; - - // First initialize - const request1 = { - method: "initialize", - params: { - protocolVersion: "1.0", - capabilities: {}, - clientInfo: { name: "test-client", version: "1.0" }, - }, - }; - await lowLevelServer._requestHandlers.get("initialize")?.(request1, {}); - - const data1 = getServerTrackingData(lowLevelServer); - const firstSessionId = data1?.sessionId; - - // Second initialize - no identify, should timeout and create new session - // Simulate timeout - if (data1) { - data1.lastActivity = new Date(Date.now() - 31 * 60 * 1000); - } - - const request2 = { - method: "initialize", - params: { - protocolVersion: "1.0", - capabilities: {}, - clientInfo: { name: "test-client", version: "1.0" }, - }, - }; - await lowLevelServer._requestHandlers.get("initialize")?.(request2, {}); - - const data2 = getServerTrackingData(lowLevelServer); - const secondSessionId = data2?.sessionId; - - // Should have different session (no reconnection without identify) - expect(secondSessionId).not.toBe(firstSessionId); - - await eventCapture.stop(); - }); - - it("should handle different users reconnecting to their own sessions", async () => { - const eventCapture = new EventCapture(); - await eventCapture.start(); - - const projectId = "test-project-multi-user"; - let currentUserId = "user-alice"; - - track(server, projectId, { - enableTracing: true, - identify: async (request: any, extra?: any) => { - return { - userId: currentUserId, - userName: currentUserId === "user-alice" ? "Alice" : "Bob", - }; - }, - }); - - const lowLevelServer = server.server; - - // Alice's first session - currentUserId = "user-alice"; - const request1 = { - method: "initialize", - params: { - protocolVersion: "1.0", - capabilities: {}, - clientInfo: { name: "test-client", version: "1.0" }, - }, - }; - await lowLevelServer._requestHandlers.get("initialize")?.(request1, {}); - const aliceSession1 = getServerTrackingData(lowLevelServer)?.sessionId; - - // Bob's first session - currentUserId = "user-bob"; - const request2 = { - method: "initialize", - params: { - protocolVersion: "1.0", - capabilities: {}, - clientInfo: { name: "test-client", version: "1.0" }, - }, - }; - await lowLevelServer._requestHandlers.get("initialize")?.(request2, {}); - const bobSession1 = getServerTrackingData(lowLevelServer)?.sessionId; - - // Different users should have different sessions - expect(aliceSession1).not.toBe(bobSession1); - - // Alice reconnects - should get her original session back - currentUserId = "user-alice"; - const request3 = { - method: "initialize", - params: { - protocolVersion: "1.0", - capabilities: {}, - clientInfo: { name: "test-client", version: "1.0" }, - }, - }; - await lowLevelServer._requestHandlers.get("initialize")?.(request3, {}); - const aliceSession2 = getServerTrackingData(lowLevelServer)?.sessionId; - - expect(aliceSession2).toBe(aliceSession1); - - // Bob reconnects - should get his original session back - currentUserId = "user-bob"; - const request4 = { - method: "initialize", - params: { - protocolVersion: "1.0", - capabilities: {}, - clientInfo: { name: "test-client", version: "1.0" }, - }, - }; - await lowLevelServer._requestHandlers.get("initialize")?.(request4, {}); - const bobSession2 = getServerTrackingData(lowLevelServer)?.sessionId; - - expect(bobSession2).toBe(bobSession1); - - await eventCapture.stop(); - }); - }); });