From 4244ba8169aaf43265e0008e3260980824904d65 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 13:06:36 +0000 Subject: [PATCH 1/5] Make showServerDetails best-effort after session restart The `restartSession` function calls `showServerDetails` after successfully starting a new bridge. Under CI load, this health check can fail transiently (bridge socket not yet ready), causing the entire restart command to fail even though the restart itself succeeded. Make `showServerDetails` non-fatal in the restart path: catch errors, reset session status to 'active' (since the bridge was started), and show a warning instead of failing. This fixes the flaky sessions/mcp-session e2e test. https://claude.ai/code/session_0138WV4ffyNZzBFuzoTDzuBn --- src/cli/commands/sessions.ts | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/cli/commands/sessions.ts b/src/cli/commands/sessions.ts index 742059b..bd5af0b 100644 --- a/src/cli/commands/sessions.ts +++ b/src/cli/commands/sessions.ts @@ -851,10 +851,32 @@ export async function restartSession( } // Show server details (like when creating a session) - await showServerDetails(name, { - ...options, - hideTarget: false, - }); + // Best-effort: the restart itself succeeded (bridge started, session updated). + // A transient failure displaying details should not fail the restart command. + try { + await showServerDetails(name, { + ...options, + hideTarget: false, + }); + } catch (detailsError) { + logger.debug('Failed to show server details after restart:', detailsError); + // ensureBridgeReady may have changed status during internal retries; reset it + // since the restart itself succeeded (bridge was started and is running). + await updateSession(name, { status: 'active' }).catch(() => {}); + if (options.outputMode === 'human') { + console.error(formatWarning('Session restarted but could not display server details yet.')); + } else { + console.log( + formatOutput( + { + _mcpc: { sessionName: name }, + restarted: true, + }, + 'json' + ) + ); + } + } } catch (error) { if (options.outputMode === 'human') { console.error(formatError((error as Error).message)); From 39dc9853e159cf9f00b6e9ab03f110471aaabb45 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 15:04:05 +0000 Subject: [PATCH 2/5] Don't delete socket file during bridge cleanup to fix flaky e2e test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When tests run in parallel with a shared home directory, a background bridge reconnect (triggered by consolidateSessions in a parallel CLI process) can race with an explicit restart. The exiting bridge's cleanup() would delete the socket file at this.socketPath — but a NEW bridge for the same session may have already created its socket at that same path. This causes the new bridge's socket to vanish, producing the ENOENT error seen in the flaky sessions/mcp-session test. Fix: stop deleting the socket file in the bridge's cleanup(). Stale sockets are already cleaned up in three other places: - startBridge() removes old sockets before spawning a new bridge - createSocketServer() removes old sockets before listening - consolidateSessions() removes sockets for expired/unauthorized sessions https://claude.ai/code/session_0138WV4ffyNZzBFuzoTDzuBn --- src/bridge/index.ts | 19 +++++++------------ src/cli/commands/sessions.ts | 30 ++++-------------------------- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/src/bridge/index.ts b/src/bridge/index.ts index bfdbcb7..64fab1d 100644 --- a/src/bridge/index.ts +++ b/src/bridge/index.ts @@ -1506,6 +1506,13 @@ class BridgeProcess { this.connections.clear(); // Close socket server + // NOTE: We intentionally do NOT delete the socket file here. + // When multiple bridges for the same session overlap (e.g. background + // reconnect from a parallel CLI process racing with an explicit restart), + // the exiting bridge would delete the NEW bridge's socket, causing ENOENT. + // Stale sockets are cleaned up by startBridge() and createSocketServer() + // before each new bridge starts, and by consolidateSessions() for + // expired/unauthorized sessions. if (this.server) { const server = this.server; await new Promise((resolve) => { @@ -1514,18 +1521,6 @@ class BridgeProcess { resolve(); }); }); - - // Remove socket file (Unix only - Windows named pipes don't leave files) - if (process.platform !== 'win32') { - try { - if (await fileExists(this.socketPath)) { - await unlink(this.socketPath); - logger.debug('Socket file removed'); - } - } catch (error) { - logger.warn('Failed to remove socket file:', error); - } - } } // Close MCP client diff --git a/src/cli/commands/sessions.ts b/src/cli/commands/sessions.ts index bd5af0b..742059b 100644 --- a/src/cli/commands/sessions.ts +++ b/src/cli/commands/sessions.ts @@ -851,32 +851,10 @@ export async function restartSession( } // Show server details (like when creating a session) - // Best-effort: the restart itself succeeded (bridge started, session updated). - // A transient failure displaying details should not fail the restart command. - try { - await showServerDetails(name, { - ...options, - hideTarget: false, - }); - } catch (detailsError) { - logger.debug('Failed to show server details after restart:', detailsError); - // ensureBridgeReady may have changed status during internal retries; reset it - // since the restart itself succeeded (bridge was started and is running). - await updateSession(name, { status: 'active' }).catch(() => {}); - if (options.outputMode === 'human') { - console.error(formatWarning('Session restarted but could not display server details yet.')); - } else { - console.log( - formatOutput( - { - _mcpc: { sessionName: name }, - restarted: true, - }, - 'json' - ) - ); - } - } + await showServerDetails(name, { + ...options, + hideTarget: false, + }); } catch (error) { if (options.outputMode === 'human') { console.error(formatError((error as Error).message)); From 5f72bfd153448b5bd8ce0e60767bb5827d560344 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 10:09:29 +0000 Subject: [PATCH 3/5] Use PID-based unique socket paths to eliminate bridge socket races When tests run in parallel with a shared home directory, a background reconnect (triggered by consolidateSessions in a parallel CLI process) can race with an explicit restart. Both start bridges for the same session, and the exiting bridge's cleanup deletes the new bridge's socket at the shared path, causing ENOENT. Fix by giving each bridge instance a unique socket path based on its PID: `@session-name..sock`. Since each bridge owns its own path, cleanup never conflicts with other bridges. Changes: - getSocketPath() accepts optional `pid` parameter for unique paths - Bridge uses process.pid to compute its socket path - startBridge computes socket path after spawn (when PID is known) - ensureBridgeReady reads session PID to find the right socket - ensureBridgeReady sets lastConnectionAttemptAt before restarting, preventing parallel consolidateSessions from triggering concurrent background restarts for the same session - consolidateSessions cleans up PID-based sockets when clearing dead bridge PIDs and removing expired sessions - SessionClient reconnect logic re-reads session after restartBridge to get the new PID-based socket path https://claude.ai/code/session_0138WV4ffyNZzBFuzoTDzuBn --- src/bridge/index.ts | 26 ++++++++++++++++--------- src/lib/bridge-manager.ts | 41 ++++++++++++++++++++------------------- src/lib/session-client.ts | 10 ++++++---- src/lib/sessions.ts | 12 ++++++++++-- src/lib/utils.ts | 7 ++++--- 5 files changed, 58 insertions(+), 38 deletions(-) diff --git a/src/bridge/index.ts b/src/bridge/index.ts index 64fab1d..a64f197 100644 --- a/src/bridge/index.ts +++ b/src/bridge/index.ts @@ -125,8 +125,10 @@ class BridgeProcess { constructor(options: BridgeOptions) { this.options = options; - // Compute socket path from session name (platform-aware: Unix socket or Windows named pipe) - this.socketPath = getSocketPath(options.sessionName); + // Each bridge instance gets a unique socket path based on its PID, so that + // overlapping bridges (e.g. background reconnect racing with explicit restart) + // never delete each other's sockets during cleanup. + this.socketPath = getSocketPath(options.sessionName, process.pid); // Create promise that resolves when MCP client connects this.mcpClientReady = new Promise((resolve, reject) => { @@ -1506,13 +1508,6 @@ class BridgeProcess { this.connections.clear(); // Close socket server - // NOTE: We intentionally do NOT delete the socket file here. - // When multiple bridges for the same session overlap (e.g. background - // reconnect from a parallel CLI process racing with an explicit restart), - // the exiting bridge would delete the NEW bridge's socket, causing ENOENT. - // Stale sockets are cleaned up by startBridge() and createSocketServer() - // before each new bridge starts, and by consolidateSessions() for - // expired/unauthorized sessions. if (this.server) { const server = this.server; await new Promise((resolve) => { @@ -1521,6 +1516,19 @@ class BridgeProcess { resolve(); }); }); + + // Remove socket file (Unix only - Windows named pipes don't leave files) + // Safe because each bridge has a unique PID-based socket path. + if (process.platform !== 'win32') { + try { + if (await fileExists(this.socketPath)) { + await unlink(this.socketPath); + logger.debug('Socket file removed'); + } + } catch (error) { + logger.warn('Failed to remove socket file:', error); + } + } } // Close MCP client diff --git a/src/lib/bridge-manager.ts b/src/lib/bridge-manager.ts index 637d423..244839c 100644 --- a/src/lib/bridge-manager.ts +++ b/src/lib/bridge-manager.ts @@ -13,7 +13,6 @@ */ import { spawn, type ChildProcess } from 'child_process'; -import { unlink } from 'fs/promises'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import type { ServerConfig, AuthCredentials, ProxyConfig, X402WalletCredentials } from './types.js'; @@ -21,7 +20,6 @@ import { getSocketPath, waitForFile, isProcessAlive, - fileExists, getLogsDir, isSessionExpiredError, enrichErrorMessage, @@ -137,17 +135,6 @@ export async function startBridge(options: StartBridgeOptions): Promise { ); } - // Socket path is computed from session name (platform-aware) - const socketPath = getSocketPath(sessionName); + // Socket path is PID-based: each bridge instance gets its own unique path + const socketPath = session.pid ? getSocketPath(sessionName, session.pid) : null; // Quick check: is the process alive? const processAlive = session.pid ? isProcessAlive(session.pid) : false; - if (processAlive) { + if (processAlive && socketPath) { // Process alive, try getServerDetails (blocks until MCP connected) const result = await checkBridgeHealth(socketPath); if (result.healthy) { @@ -642,16 +634,25 @@ export async function ensureBridgeReady(sessionName: string): Promise { // Bridge not healthy - restart it // Use 'connecting' if the session has never successfully connected (no lastSeenAt), // 'reconnecting' if it was previously active. + // Set lastConnectionAttemptAt to prevent parallel CLI processes from + // also triggering a restart via consolidateSessions/reconnectCrashedSessions. const restartStatus = session.lastSeenAt ? 'reconnecting' : 'connecting'; - await updateSession(sessionName, { status: restartStatus }); + await updateSession(sessionName, { + status: restartStatus, + lastConnectionAttemptAt: new Date().toISOString(), + }); await restartBridge(sessionName); + // Re-read session to get the new PID from restartBridge and compute socket path + const updatedSession = await getSession(sessionName); + const newSocketPath = getSocketPath(sessionName, updatedSession?.pid); + // Try getServerDetails on restarted bridge (blocks until MCP connected) - const result = await checkBridgeHealth(socketPath); + const result = await checkBridgeHealth(newSocketPath); if (result.healthy) { await updateSession(sessionName, { status: 'active' }); logger.debug(`Bridge for ${sessionName} passed health check`); - return socketPath; + return newSocketPath; } // Not healthy after restart - classify the error diff --git a/src/lib/session-client.ts b/src/lib/session-client.ts index 49822d0..053aa02 100644 --- a/src/lib/session-client.ts +++ b/src/lib/session-client.ts @@ -33,7 +33,7 @@ import type { import type { ListResourceTemplatesResult } from '@modelcontextprotocol/sdk/types.js'; import { BridgeClient } from './bridge-client.js'; import { ensureBridgeReady, restartBridge } from './bridge-manager.js'; -import { updateSession } from './sessions.js'; +import { updateSession, getSession } from './sessions.js'; import { NetworkError } from './errors.js'; import { getSocketPath, getLogsDir, generateRequestId } from './utils.js'; import { createLogger } from './logger.js'; @@ -106,8 +106,9 @@ export class SessionClient extends EventEmitter implements IMcpClient { await updateSession(this.sessionName, { status: 'reconnecting' }); await restartBridge(this.sessionName); - // Reconnect using computed socket path - const socketPath = getSocketPath(this.sessionName); + // Reconnect using the new bridge's PID-based socket path + const updatedSession = await getSession(this.sessionName); + const socketPath = getSocketPath(this.sessionName, updatedSession?.pid); this.bridgeClient = new BridgeClient(socketPath); this.setupNotificationForwarding(); await this.bridgeClient.connect(); @@ -342,7 +343,8 @@ export class SessionClient extends EventEmitter implements IMcpClient { await this.bridgeClient.close(); await restartBridge(this.sessionName); - const socketPath = getSocketPath(this.sessionName); + const updatedSession = await getSession(this.sessionName); + const socketPath = getSocketPath(this.sessionName, updatedSession?.pid); this.bridgeClient = new BridgeClient(socketPath); this.setupNotificationForwarding(); await this.bridgeClient.connect(); diff --git a/src/lib/sessions.ts b/src/lib/sessions.ts index b5ef459..34b9bdb 100644 --- a/src/lib/sessions.ts +++ b/src/lib/sessions.ts @@ -305,8 +305,8 @@ export async function consolidateSessions( } // Delete socket file (Unix only - Windows named pipes don't leave files) - if (process.platform !== 'win32') { - const socketPath = getSocketPath(name); + if (process.platform !== 'win32' && session.pid) { + const socketPath = getSocketPath(name, session.pid); try { await unlink(socketPath); logger.debug(`Removed stale socket: ${socketPath}`); @@ -321,6 +321,14 @@ export async function consolidateSessions( // Check bridge status - always remove pid if process is not alive if (session.pid && !isProcessAlive(session.pid)) { logger.debug(`Clearing crashed bridge PID for session: ${name} (PID: ${session.pid})`); + // Clean up the PID-based socket file for this dead bridge + if (process.platform !== 'win32') { + try { + await unlink(getSocketPath(name, session.pid)); + } catch { + // Ignore - file may already be deleted by bridge cleanup + } + } delete session.pid; hasChanges = true; // Don't overwrite terminal/transient statuses diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6e71a8d..c69abc4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -67,16 +67,17 @@ export function getBridgesDir(): string { * @param sessionName - The session name (e.g., "@my-session") * @returns The platform-appropriate socket/pipe path */ -export function getSocketPath(sessionName: string): string { +export function getSocketPath(sessionName: string, pid?: number): string { + const suffix = pid ? `.${pid}` : ''; if (process.platform === 'win32') { // Windows named pipes are global, so include a hash of the home directory // to avoid conflicts between different mcpc instances const homeHash = createHash('sha256').update(getMcpcHome()).digest('hex').slice(0, 8); - return `\\\\.\\pipe\\mcpc-${homeHash}-${sessionName}`; + return `\\\\.\\pipe\\mcpc-${homeHash}-${sessionName}${suffix}`; } // Unix/macOS: use socket file in bridges directory (naturally isolated per home dir) - return join(getBridgesDir(), `${sessionName}.sock`); + return join(getBridgesDir(), `${sessionName}${suffix}.sock`); } /** From 829c37a1ca0f61cc23a77b67cc0d983be3548de5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 11:58:31 +0000 Subject: [PATCH 4/5] Use restartBridge return value and add orphaned socket cleanup - Use the PID returned by restartBridge() directly instead of re-reading sessions.json via getSession() - Add cleanupOrphanedSockets() to remove stale PID-based socket files from the bridges directory during `mcpc clean`. Only deletes sockets older than 5 minutes (configurable) to avoid racing with a bridge that was just spawned. - Wire orphaned socket cleanup into cleanStale/cleanAll and display https://claude.ai/code/session_0138WV4ffyNZzBFuzoTDzuBn --- src/cli/commands/clean.ts | 17 +++++++++- src/lib/bridge-manager.ts | 6 ++-- src/lib/cleanup.ts | 65 ++++++++++++++++++++++++++++++++++++++- src/lib/session-client.ts | 12 +++----- 4 files changed, 87 insertions(+), 13 deletions(-) diff --git a/src/cli/commands/clean.ts b/src/cli/commands/clean.ts index ee5ab03..a43f52b 100644 --- a/src/cli/commands/clean.ts +++ b/src/cli/commands/clean.ts @@ -12,6 +12,7 @@ import { getLogsDir, fileExists, cleanupOrphanedLogFiles, + cleanupOrphanedSockets, } from '../../lib/index.js'; import { formatOutput, formatSuccess, formatWarning } from '../output.js'; import { loadSessions, deleteSession, consolidateSessions } from '../../lib/sessions.js'; @@ -33,6 +34,7 @@ interface CleanResult { crashedBridges: number; expiredSessions: number; orphanedBridgeLogs: number; + orphanedSockets: number; sessions: number; profiles: number; logs: number; @@ -47,6 +49,7 @@ async function cleanStale(): Promise<{ crashedBridges: number; expiredSessions: number; orphanedBridgeLogs: number; + orphanedSockets: number; }> { // Consolidate sessions, removes expired ones const consolidateResult = await consolidateSessions(true); @@ -54,10 +57,14 @@ async function cleanStale(): Promise<{ // Clean up orphaned log files (for sessions that no longer exist, older than 7 days) const orphanedBridgeLogs = await cleanupOrphanedLogFiles(consolidateResult.sessions); + // Clean up orphaned socket files (PID-based sockets from dead bridges, older than 5 min) + const orphanedSockets = await cleanupOrphanedSockets(consolidateResult.sessions); + return { crashedBridges: consolidateResult.crashedBridges, expiredSessions: consolidateResult.expiredSessions, orphanedBridgeLogs, + orphanedSockets, }; } @@ -127,6 +134,7 @@ async function cleanAll(): Promise { crashedBridges: 0, expiredSessions: 0, orphanedBridgeLogs: 0, + orphanedSockets: 0, sessions: 0, profiles: 0, logs: 0, @@ -148,6 +156,7 @@ async function cleanAll(): Promise { result.crashedBridges = staleResult.crashedBridges; result.expiredSessions = staleResult.expiredSessions; result.orphanedBridgeLogs = staleResult.orphanedBridgeLogs; + result.orphanedSockets = staleResult.orphanedSockets; // Remove any remaining empty directories const mcpcHome = getMcpcHome(); @@ -188,6 +197,7 @@ export async function clean(options: CleanOptions): Promise { crashedBridges: 0, expiredSessions: 0, orphanedBridgeLogs: 0, + orphanedSockets: 0, sessions: 0, profiles: 0, logs: 0, @@ -223,6 +233,7 @@ export async function clean(options: CleanOptions): Promise { result.crashedBridges = staleResult.crashedBridges; result.expiredSessions = staleResult.expiredSessions; result.orphanedBridgeLogs = staleResult.orphanedBridgeLogs; + result.orphanedSockets = staleResult.orphanedSockets; } // Clean specific resources if requested @@ -246,7 +257,10 @@ export async function clean(options: CleanOptions): Promise { if (!cleaningSpecific) { const hasCleanups = - result.crashedBridges > 0 || result.expiredSessions > 0 || result.orphanedBridgeLogs > 0; + result.crashedBridges > 0 || + result.expiredSessions > 0 || + result.orphanedBridgeLogs > 0 || + result.orphanedSockets > 0; if (hasCleanups) { const parts: string[] = []; @@ -254,6 +268,7 @@ export async function clean(options: CleanOptions): Promise { if (result.expiredSessions > 0) parts.push(`${result.expiredSessions} expired session(s)`); if (result.orphanedBridgeLogs > 0) parts.push(`${result.orphanedBridgeLogs} orphaned log(s)`); + if (result.orphanedSockets > 0) parts.push(`${result.orphanedSockets} orphaned socket(s)`); messages.push(`Cleaned ${parts.join(', ')}`); } else { messages.push('No stale resources found'); diff --git a/src/lib/bridge-manager.ts b/src/lib/bridge-manager.ts index 244839c..a9ce07d 100644 --- a/src/lib/bridge-manager.ts +++ b/src/lib/bridge-manager.ts @@ -641,11 +641,9 @@ export async function ensureBridgeReady(sessionName: string): Promise { status: restartStatus, lastConnectionAttemptAt: new Date().toISOString(), }); - await restartBridge(sessionName); + const { pid: newPid } = await restartBridge(sessionName); - // Re-read session to get the new PID from restartBridge and compute socket path - const updatedSession = await getSession(sessionName); - const newSocketPath = getSocketPath(sessionName, updatedSession?.pid); + const newSocketPath = getSocketPath(sessionName, newPid); // Try getServerDetails on restarted bridge (blocks until MCP connected) const result = await checkBridgeHealth(newSocketPath); diff --git a/src/lib/cleanup.ts b/src/lib/cleanup.ts index c77f553..2034f50 100644 --- a/src/lib/cleanup.ts +++ b/src/lib/cleanup.ts @@ -4,7 +4,7 @@ import { readdir, unlink, stat } from 'fs/promises'; import { join } from 'path'; -import { getLogsDir, fileExists } from './utils.js'; +import { getLogsDir, getBridgesDir, getSocketPath, fileExists } from './utils.js'; import { createLogger } from './logger.js'; const logger = createLogger('cleanup'); @@ -80,3 +80,66 @@ export async function cleanupOrphanedLogFiles( return deletedCount; } + +/** + * Clean up orphaned socket files in the bridges directory. + * With PID-based socket paths (@session.1234.sock), stale sockets can accumulate + * when a bridge exits without cleanup (e.g. SIGKILL, crash, or orphaned background restart). + * + * A socket is considered orphaned if it doesn't match any active session's current PID. + * Only sockets older than `minAgeSeconds` are removed to avoid racing with a bridge + * that was just spawned but hasn't updated sessions.json yet. + * + * @param activeSessions - Map of session names to session data (with optional pid) + * @param options.minAgeSeconds - Only delete sockets older than this (default: 300 = 5 min) + * @returns Number of socket files deleted + */ +export async function cleanupOrphanedSockets( + activeSessions: Record, + options: { minAgeSeconds?: number } = {} +): Promise { + const { minAgeSeconds = 300 } = options; + + if (process.platform === 'win32') { + return 0; // Windows named pipes don't leave files + } + + const bridgesDir = getBridgesDir(); + if (!(await fileExists(bridgesDir))) { + return 0; + } + + // Build set of active socket paths for fast lookup + const activeSocketPaths = new Set(); + for (const [name, session] of Object.entries(activeSessions)) { + if (session?.pid) { + activeSocketPaths.add(getSocketPath(name, session.pid)); + } + } + + const cutoffTime = Date.now() - minAgeSeconds * 1000; + let deletedCount = 0; + + const files = await readdir(bridgesDir); + for (const file of files) { + if (!file.endsWith('.sock')) continue; + + const filePath = join(bridgesDir, file); + + // Skip sockets that belong to a known active session+PID + if (activeSocketPaths.has(filePath)) continue; + + try { + const fileStats = await stat(filePath); + if (fileStats.mtime.getTime() < cutoffTime) { + await unlink(filePath); + deletedCount++; + logger.debug(`Removed orphaned socket: ${file}`); + } + } catch { + // Ignore stat/unlink errors + } + } + + return deletedCount; +} diff --git a/src/lib/session-client.ts b/src/lib/session-client.ts index 053aa02..dc053bc 100644 --- a/src/lib/session-client.ts +++ b/src/lib/session-client.ts @@ -33,7 +33,7 @@ import type { import type { ListResourceTemplatesResult } from '@modelcontextprotocol/sdk/types.js'; import { BridgeClient } from './bridge-client.js'; import { ensureBridgeReady, restartBridge } from './bridge-manager.js'; -import { updateSession, getSession } from './sessions.js'; +import { updateSession } from './sessions.js'; import { NetworkError } from './errors.js'; import { getSocketPath, getLogsDir, generateRequestId } from './utils.js'; import { createLogger } from './logger.js'; @@ -104,11 +104,10 @@ export class SessionClient extends EventEmitter implements IMcpClient { // Restart bridge await updateSession(this.sessionName, { status: 'reconnecting' }); - await restartBridge(this.sessionName); + const { pid: newPid } = await restartBridge(this.sessionName); // Reconnect using the new bridge's PID-based socket path - const updatedSession = await getSession(this.sessionName); - const socketPath = getSocketPath(this.sessionName, updatedSession?.pid); + const socketPath = getSocketPath(this.sessionName, newPid); this.bridgeClient = new BridgeClient(socketPath); this.setupNotificationForwarding(); await this.bridgeClient.connect(); @@ -341,10 +340,9 @@ export class SessionClient extends EventEmitter implements IMcpClient { logger.debug(`Socket error during callToolWithTask, will restart bridge...`); await this.bridgeClient.close(); - await restartBridge(this.sessionName); + const { pid: newPid } = await restartBridge(this.sessionName); - const updatedSession = await getSession(this.sessionName); - const socketPath = getSocketPath(this.sessionName, updatedSession?.pid); + const socketPath = getSocketPath(this.sessionName, newPid); this.bridgeClient = new BridgeClient(socketPath); this.setupNotificationForwarding(); await this.bridgeClient.connect(); From c56bd59e0f499508f24e2fb63d3392aea1e6f9de Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 14:24:42 +0000 Subject: [PATCH 5/5] Make pid parameter required in getSocketPath Every call site always passes a PID. Making it optional would silently produce a path without the PID suffix that no bridge ever listens on. https://claude.ai/code/session_0138WV4ffyNZzBFuzoTDzuBn --- src/lib/utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c69abc4..6928a7f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -65,10 +65,11 @@ export function getBridgesDir(): string { * to avoid conflicts between different mcpc instances. * * @param sessionName - The session name (e.g., "@my-session") + * @param pid - The bridge process PID (each bridge gets a unique socket path) * @returns The platform-appropriate socket/pipe path */ -export function getSocketPath(sessionName: string, pid?: number): string { - const suffix = pid ? `.${pid}` : ''; +export function getSocketPath(sessionName: string, pid: number): string { + const suffix = `.${pid}`; if (process.platform === 'win32') { // Windows named pipes are global, so include a hash of the home directory // to avoid conflicts between different mcpc instances