@dollhousemcp/mcp-server
Version:
DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.
172 lines • 6.99 kB
TypeScript
/**
* Leader election for the unified web console.
*
* When multiple MCP server instances run concurrently, only one should host
* the web console (the "leader"). Others become "followers" that forward
* events to the leader. This module handles:
*
* 1. Reading/writing a leader lock file at ~/.dollhouse/run/console-leader.lock
* 2. Atomic claim via temp+rename to prevent TOCTOU races
* 3. PID-based stale detection (signal-0 liveness check)
* 4. Heartbeat updates (10s interval) so followers can detect hung leaders
* 5. Cleanup on process exit
*
* The configured port binding is the ultimate tiebreaker: even if two
* processes both write the lock file, only one can bind the port (see
* `DOLLHOUSE_WEB_CONSOLE_PORT` in `src/config/env.ts`).
*
* @since v2.1.0 — Issue #1700
*/
/** Current lock file schema version */
export declare const LOCK_VERSION = 1;
/**
* Version of the leader-election/session metadata contract used by the
* authenticated web console. Older leaders will not have this field.
*/
export declare const CONSOLE_PROTOCOL_VERSION = 1;
/** Missing protocol metadata means the leader predates version-aware election. */
export declare const LEGACY_CONSOLE_PROTOCOL_VERSION = 0;
/** Old lock files do not carry package version metadata. Treat them as oldest. */
export declare const LEGACY_SERVER_VERSION = "0.0.0";
/**
* Information stored in the leader lock file.
*/
export interface ConsoleLeaderInfo {
version: number;
pid: number;
port: number;
sessionId: string;
startedAt: string;
heartbeat: string;
serverVersion?: string;
consoleProtocolVersion?: number;
}
/**
* Result of a leader election attempt.
*/
export interface ElectionResult {
role: 'leader' | 'follower';
/** Leader info — for followers, this is the existing leader's info */
leaderInfo: ConsoleLeaderInfo;
}
export interface LeaderPreferenceDecision {
shouldReplace: boolean;
reason: 'newer-compatible-version' | 'same-version' | 'older-version' | 'incompatible-protocol';
candidateVersion: string;
existingVersion: string;
candidateProtocolVersion: number;
existingProtocolVersion: number;
}
/**
* Check whether a process with the given PID is alive.
* Uses signal 0 which checks existence without sending a signal.
*/
export declare function isProcessAlive(pid: number): boolean;
/**
* Normalize the server version present in the leader lock.
* Missing metadata means "legacy leader" for election purposes.
*/
export declare function getLeaderServerVersion(info: ConsoleLeaderInfo): string;
/**
* Normalize the console protocol version present in the leader lock.
* Missing metadata means a leader from before version-aware election.
*/
export declare function getLeaderConsoleProtocolVersion(info: ConsoleLeaderInfo): number;
/**
* Create this process's leader metadata in one place so all leadership paths
* publish the same version and protocol information.
*/
export declare function createLeaderInfo(sessionId: string, port: number): ConsoleLeaderInfo;
/**
* Decide whether this process should replace the current live leader based on
* compatibility first, then package version.
*/
export declare function evaluateLeaderPreference(candidate: ConsoleLeaderInfo, existing: ConsoleLeaderInfo): LeaderPreferenceDecision;
/**
* Result of a legacy-leader detection scan.
* `legacyRunning === true` means a pre-authentication DollhouseMCP console
* is currently running on this machine (its lock file exists and its pid
* is alive). Callers can surface this to the user as a warning.
*/
export interface LegacyLeaderInfo {
legacyRunning: boolean;
pid?: number;
port?: number;
lockPath: string;
}
/**
* Detect whether a legacy (pre-authentication) DollhouseMCP console is
* currently running on this machine (#1794).
*
* The pre-authentication console writes its lock to
* `~/.dollhouse/run/console-leader.lock` (no `.auth` suffix). An
* authenticated console on a different port will not interfere with
* it — they have fully independent ports, lock files, and token files —
* but the user probably wants to know the two exist simultaneously
* because the security posture of each console is different.
*
* Returns info about the legacy leader if one is detected, or
* `{ legacyRunning: false }` otherwise.
*
* @param lockPath - Optional override for the legacy lock file path.
* Defaults to the built-in legacy location. Primarily
* used by tests to point at a temp directory.
*/
export declare function detectLegacyLeader(lockPath?: string): Promise<LegacyLeaderInfo>;
/**
* Read and parse the leader lock file.
* Returns null if the file doesn't exist, is unreadable, or has invalid content.
*/
export declare function readLeaderLock(): Promise<ConsoleLeaderInfo | null>;
/**
* Check if a leader lock is stale (dead process or expired heartbeat).
*/
export declare function isLockStale(info: ConsoleLeaderInfo): boolean;
/**
* Attempt to atomically claim leadership.
*
* Writes to a temp file then renames to the lock path. On POSIX systems
* rename is atomic, so only one writer wins. After renaming, re-reads the
* lock to verify our PID won.
*
* @returns true if this process successfully claimed leadership
*/
export declare function claimLeadership(info: ConsoleLeaderInfo): Promise<boolean>;
/**
* Delete the leader lock file (for cleanup or takeover).
*/
export declare function deleteLeaderLock(): Promise<void>;
/**
* Run the leader election protocol.
*
* 1. If no lock exists or lock is stale → claim leadership
* 2. If lock exists with a live, responsive leader → become follower
*
* @param sessionId - This process's unique session identifier
* @param port - The port this process would use as leader (see `DOLLHOUSE_WEB_CONSOLE_PORT`)
* @returns Election result with role and leader info
*/
export declare function electLeader(sessionId: string, port: number): Promise<ElectionResult>;
/**
* Probe whether the leader's web console is reachable.
* Returns true if the leader's ingest endpoint responds, false otherwise.
*/
export declare function isLeaderWebConsoleReachable(leaderInfo: ConsoleLeaderInfo): Promise<boolean>;
/**
* Force claim leadership by deleting the existing lock and claiming.
* Used when the existing leader is alive but not running a web console.
*/
export declare function forceClaimLeadership(sessionId: string, port: number): Promise<ElectionResult>;
/**
* Start the leader heartbeat loop.
* Updates the lock file every HEARTBEAT_INTERVAL_MS so followers know the leader is alive.
*
* @returns A stop function to clear the interval
*/
export declare function startHeartbeat(info: ConsoleLeaderInfo): () => void;
/**
* Register cleanup handlers to remove the leader lock on process exit.
* Should only be called by the leader.
*/
export declare function registerLeaderCleanup(): void;
//# sourceMappingURL=LeaderElection.d.ts.map