UNPKG

@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.

80 lines 9.92 kB
/** * auto-dollhouse#5: Dynamic port allocation and port file discovery. * * Extracted from server.ts. Handles finding available ports when multiple * DollhouseMCP sessions run simultaneously, and writing port discovery * files so PreToolUse hook scripts know which port to curl. */ import { createServer } from 'node:net'; import { homedir } from 'node:os'; import { join } from 'node:path'; import { mkdir, writeFile, unlink } from 'node:fs/promises'; import { ensureLatestPortFile } from '../web/portDiscovery.js'; const MAX_PORT_ATTEMPTS = 10; /** Directory for runtime state files (port discovery, PID files) */ const RUN_DIR = join(homedir(), '.dollhouse', 'run'); function pidPortFilePath(dir = RUN_DIR, pid = process.pid) { return join(dir, `permission-server-${pid}.port`); } /** Track port file path for cleanup */ let portFilePath = null; /** * Find an available port starting from the given port. * Tries sequential ports up to MAX_PORT_ATTEMPTS to avoid conflicts * when multiple DollhouseMCP sessions run simultaneously. */ function tryBindPort(port) { return new Promise((resolve, reject) => { const server = createServer(); server.once('error', (err) => reject(err)); server.once('listening', () => server.close(() => resolve(port))); server.listen(port, '127.0.0.1'); }); } export async function findAvailablePort(startPort) { for (let attempt = 0; attempt <= MAX_PORT_ATTEMPTS; attempt++) { try { return await tryBindPort(startPort + attempt); } catch (err) { if (err.code !== 'EADDRINUSE' || attempt === MAX_PORT_ATTEMPTS) { throw err; } } } throw new Error(`No available port found after ${MAX_PORT_ATTEMPTS} attempts from ${startPort}`); } /** * Write the active server port to a discoverable file. * PreToolUse hook scripts read this to know which port to curl. * Each process writes its own PID-keyed file for cleanup. */ export async function writePortFile(port) { await mkdir(RUN_DIR, { recursive: true }); const pidFile = pidPortFilePath(); await writeFile(pidFile, String(port), 'utf-8'); await ensureLatestPortFile(port, RUN_DIR); portFilePath = pidFile; return pidFile; } /** * Clean up port file on shutdown. */ export async function cleanupPortFile() { if (portFilePath) { try { await unlink(portFilePath); } catch { /* already gone */ } } } /** * Register process exit handlers to clean up port files. */ export function registerPortCleanup() { const exitCleanup = () => { cleanupPortFile().catch(() => { }); }; process.once('exit', exitCleanup); process.once('SIGTERM', exitCleanup); process.once('SIGINT', exitCleanup); } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicG9ydERpc2NvdmVyeS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hdXRvLWRvbGxob3VzZS9wb3J0RGlzY292ZXJ5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7R0FNRztBQUVILE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxVQUFVLENBQUM7QUFDeEMsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNsQyxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQ2pDLE9BQU8sRUFBRSxLQUFLLEVBQUUsU0FBUyxFQUFFLE1BQU0sRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQzVELE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBRS9ELE1BQU0saUJBQWlCLEdBQUcsRUFBRSxDQUFDO0FBRTdCLG9FQUFvRTtBQUNwRSxNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsT0FBTyxFQUFFLEVBQUUsWUFBWSxFQUFFLEtBQUssQ0FBQyxDQUFDO0FBQ3JELFNBQVMsZUFBZSxDQUFDLE1BQWMsT0FBTyxFQUFFLE1BQWMsT0FBTyxDQUFDLEdBQUc7SUFDdkUsT0FBTyxJQUFJLENBQUMsR0FBRyxFQUFFLHFCQUFxQixHQUFHLE9BQU8sQ0FBQyxDQUFDO0FBQ3BELENBQUM7QUFFRCx1Q0FBdUM7QUFDdkMsSUFBSSxZQUFZLEdBQWtCLElBQUksQ0FBQztBQUV2Qzs7OztHQUlHO0FBQ0gsU0FBUyxXQUFXLENBQUMsSUFBWTtJQUMvQixPQUFPLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxFQUFFO1FBQ3JDLE1BQU0sTUFBTSxHQUFHLFlBQVksRUFBRSxDQUFDO1FBQzlCLE1BQU0sQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUMsR0FBMEIsRUFBRSxFQUFFLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7UUFDbEUsTUFBTSxDQUFDLElBQUksQ0FBQyxXQUFXLEVBQUUsR0FBRyxFQUFFLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQ2xFLE1BQU0sQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFLFdBQVcsQ0FBQyxDQUFDO0lBQ25DLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQztBQUVELE1BQU0sQ0FBQyxLQUFLLFVBQVUsaUJBQWlCLENBQUMsU0FBaUI7SUFDdkQsS0FBSyxJQUFJLE9BQU8sR0FBRyxDQUFDLEVBQUUsT0FBTyxJQUFJLGlCQUFpQixFQUFFLE9BQU8sRUFBRSxFQUFFLENBQUM7UUFDOUQsSUFBSSxDQUFDO1lBQ0gsT0FBTyxNQUFNLFdBQVcsQ0FBQyxTQUFTLEdBQUcsT0FBTyxDQUFDLENBQUM7UUFDaEQsQ0FBQztRQUFDLE9BQU8sR0FBRyxFQUFFLENBQUM7WUFDYixJQUFLLEdBQTZCLENBQUMsSUFBSSxLQUFLLFlBQVksSUFBSSxPQUFPLEtBQUssaUJBQWlCLEVBQUUsQ0FBQztnQkFDMUYsTUFBTSxHQUFHLENBQUM7WUFDWixDQUFDO1FBQ0gsQ0FBQztJQUNILENBQUM7SUFDRCxNQUFNLElBQUksS0FBSyxDQUFDLGlDQUFpQyxpQkFBaUIsa0JBQWtCLFNBQVMsRUFBRSxDQUFDLENBQUM7QUFDbkcsQ0FBQztBQUVEOzs7O0dBSUc7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLGFBQWEsQ0FBQyxJQUFZO0lBQzlDLE1BQU0sS0FBSyxDQUFDLE9BQU8sRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQzFDLE1BQU0sT0FBTyxHQUFHLGVBQWUsRUFBRSxDQUFDO0lBQ2xDLE1BQU0sU0FBUyxDQUFDLE9BQU8sRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDaEQsTUFBTSxvQkFBb0IsQ0FBQyxJQUFJLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDMUMsWUFBWSxHQUFHLE9BQU8sQ0FBQztJQUN2QixPQUFPLE9BQU8sQ0FBQztBQUNqQixDQUFDO0FBRUQ7O0dBRUc7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLGVBQWU7SUFDbkMsSUFBSSxZQUFZLEVBQUUsQ0FBQztRQUNqQixJQUFJLENBQUM7WUFBQyxNQUFNLE1BQU0sQ0FBQyxZQUFZLENBQUMsQ0FBQztRQUFDLENBQUM7UUFBQyxNQUFNLENBQUMsQ0FBQyxrQkFBa0IsQ0FBQyxDQUFDO0lBQ2xFLENBQUM7QUFDSCxDQUFDO0FBRUQ7O0dBRUc7QUFDSCxNQUFNLFVBQVUsbUJBQW1CO0lBQ2pDLE1BQU0sV0FBVyxHQUFHLEdBQUcsRUFBRSxHQUFHLGVBQWUsRUFBRSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsR0FBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNqRSxPQUFPLENBQUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxXQUFXLENBQUMsQ0FBQztJQUNsQyxPQUFPLENBQUMsSUFBSSxDQUFDLFNBQVMsRUFBRSxXQUFXLENBQUMsQ0FBQztJQUNyQyxPQUFPLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxXQUFXLENBQUMsQ0FBQztBQUN0QyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBhdXRvLWRvbGxob3VzZSM1OiBEeW5hbWljIHBvcnQgYWxsb2NhdGlvbiBhbmQgcG9ydCBmaWxlIGRpc2NvdmVyeS5cbiAqXG4gKiBFeHRyYWN0ZWQgZnJvbSBzZXJ2ZXIudHMuIEhhbmRsZXMgZmluZGluZyBhdmFpbGFibGUgcG9ydHMgd2hlbiBtdWx0aXBsZVxuICogRG9sbGhvdXNlTUNQIHNlc3Npb25zIHJ1biBzaW11bHRhbmVvdXNseSwgYW5kIHdyaXRpbmcgcG9ydCBkaXNjb3ZlcnlcbiAqIGZpbGVzIHNvIFByZVRvb2xVc2UgaG9vayBzY3JpcHRzIGtub3cgd2hpY2ggcG9ydCB0byBjdXJsLlxuICovXG5cbmltcG9ydCB7IGNyZWF0ZVNlcnZlciB9IGZyb20gJ25vZGU6bmV0JztcbmltcG9ydCB7IGhvbWVkaXIgfSBmcm9tICdub2RlOm9zJztcbmltcG9ydCB7IGpvaW4gfSBmcm9tICdub2RlOnBhdGgnO1xuaW1wb3J0IHsgbWtkaXIsIHdyaXRlRmlsZSwgdW5saW5rIH0gZnJvbSAnbm9kZTpmcy9wcm9taXNlcyc7XG5pbXBvcnQgeyBlbnN1cmVMYXRlc3RQb3J0RmlsZSB9IGZyb20gJy4uL3dlYi9wb3J0RGlzY292ZXJ5LmpzJztcblxuY29uc3QgTUFYX1BPUlRfQVRURU1QVFMgPSAxMDtcblxuLyoqIERpcmVjdG9yeSBmb3IgcnVudGltZSBzdGF0ZSBmaWxlcyAocG9ydCBkaXNjb3ZlcnksIFBJRCBmaWxlcykgKi9cbmNvbnN0IFJVTl9ESVIgPSBqb2luKGhvbWVkaXIoKSwgJy5kb2xsaG91c2UnLCAncnVuJyk7XG5mdW5jdGlvbiBwaWRQb3J0RmlsZVBhdGgoZGlyOiBzdHJpbmcgPSBSVU5fRElSLCBwaWQ6IG51bWJlciA9IHByb2Nlc3MucGlkKTogc3RyaW5nIHtcbiAgcmV0dXJuIGpvaW4oZGlyLCBgcGVybWlzc2lvbi1zZXJ2ZXItJHtwaWR9LnBvcnRgKTtcbn1cblxuLyoqIFRyYWNrIHBvcnQgZmlsZSBwYXRoIGZvciBjbGVhbnVwICovXG5sZXQgcG9ydEZpbGVQYXRoOiBzdHJpbmcgfCBudWxsID0gbnVsbDtcblxuLyoqXG4gKiBGaW5kIGFuIGF2YWlsYWJsZSBwb3J0IHN0YXJ0aW5nIGZyb20gdGhlIGdpdmVuIHBvcnQuXG4gKiBUcmllcyBzZXF1ZW50aWFsIHBvcnRzIHVwIHRvIE1BWF9QT1JUX0FUVEVNUFRTIHRvIGF2b2lkIGNvbmZsaWN0c1xuICogd2hlbiBtdWx0aXBsZSBEb2xsaG91c2VNQ1Agc2Vzc2lvbnMgcnVuIHNpbXVsdGFuZW91c2x5LlxuICovXG5mdW5jdGlvbiB0cnlCaW5kUG9ydChwb3J0OiBudW1iZXIpOiBQcm9taXNlPG51bWJlcj4ge1xuICByZXR1cm4gbmV3IFByb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuICAgIGNvbnN0IHNlcnZlciA9IGNyZWF0ZVNlcnZlcigpO1xuICAgIHNlcnZlci5vbmNlKCdlcnJvcicsIChlcnI6IE5vZGVKUy5FcnJub0V4Y2VwdGlvbikgPT4gcmVqZWN0KGVycikpO1xuICAgIHNlcnZlci5vbmNlKCdsaXN0ZW5pbmcnLCAoKSA9PiBzZXJ2ZXIuY2xvc2UoKCkgPT4gcmVzb2x2ZShwb3J0KSkpO1xuICAgIHNlcnZlci5saXN0ZW4ocG9ydCwgJzEyNy4wLjAuMScpO1xuICB9KTtcbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGZpbmRBdmFpbGFibGVQb3J0KHN0YXJ0UG9ydDogbnVtYmVyKTogUHJvbWlzZTxudW1iZXI+IHtcbiAgZm9yIChsZXQgYXR0ZW1wdCA9IDA7IGF0dGVtcHQgPD0gTUFYX1BPUlRfQVRURU1QVFM7IGF0dGVtcHQrKykge1xuICAgIHRyeSB7XG4gICAgICByZXR1cm4gYXdhaXQgdHJ5QmluZFBvcnQoc3RhcnRQb3J0ICsgYXR0ZW1wdCk7XG4gICAgfSBjYXRjaCAoZXJyKSB7XG4gICAgICBpZiAoKGVyciBhcyBOb2RlSlMuRXJybm9FeGNlcHRpb24pLmNvZGUgIT09ICdFQUREUklOVVNFJyB8fCBhdHRlbXB0ID09PSBNQVhfUE9SVF9BVFRFTVBUUykge1xuICAgICAgICB0aHJvdyBlcnI7XG4gICAgICB9XG4gICAgfVxuICB9XG4gIHRocm93IG5ldyBFcnJvcihgTm8gYXZhaWxhYmxlIHBvcnQgZm91bmQgYWZ0ZXIgJHtNQVhfUE9SVF9BVFRFTVBUU30gYXR0ZW1wdHMgZnJvbSAke3N0YXJ0UG9ydH1gKTtcbn1cblxuLyoqXG4gKiBXcml0ZSB0aGUgYWN0aXZlIHNlcnZlciBwb3J0IHRvIGEgZGlzY292ZXJhYmxlIGZpbGUuXG4gKiBQcmVUb29sVXNlIGhvb2sgc2NyaXB0cyByZWFkIHRoaXMgdG8ga25vdyB3aGljaCBwb3J0IHRvIGN1cmwuXG4gKiBFYWNoIHByb2Nlc3Mgd3JpdGVzIGl0cyBvd24gUElELWtleWVkIGZpbGUgZm9yIGNsZWFudXAuXG4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiB3cml0ZVBvcnRGaWxlKHBvcnQ6IG51bWJlcik6IFByb21pc2U8c3RyaW5nPiB7XG4gIGF3YWl0IG1rZGlyKFJVTl9ESVIsIHsgcmVjdXJzaXZlOiB0cnVlIH0pO1xuICBjb25zdCBwaWRGaWxlID0gcGlkUG9ydEZpbGVQYXRoKCk7XG4gIGF3YWl0IHdyaXRlRmlsZShwaWRGaWxlLCBTdHJpbmcocG9ydCksICd1dGYtOCcpO1xuICBhd2FpdCBlbnN1cmVMYXRlc3RQb3J0RmlsZShwb3J0LCBSVU5fRElSKTtcbiAgcG9ydEZpbGVQYXRoID0gcGlkRmlsZTtcbiAgcmV0dXJuIHBpZEZpbGU7XG59XG5cbi8qKlxuICogQ2xlYW4gdXAgcG9ydCBmaWxlIG9uIHNodXRkb3duLlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gY2xlYW51cFBvcnRGaWxlKCk6IFByb21pc2U8dm9pZD4ge1xuICBpZiAocG9ydEZpbGVQYXRoKSB7XG4gICAgdHJ5IHsgYXdhaXQgdW5saW5rKHBvcnRGaWxlUGF0aCk7IH0gY2F0Y2ggeyAvKiBhbHJlYWR5IGdvbmUgKi8gfVxuICB9XG59XG5cbi8qKlxuICogUmVnaXN0ZXIgcHJvY2VzcyBleGl0IGhhbmRsZXJzIHRvIGNsZWFuIHVwIHBvcnQgZmlsZXMuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZWdpc3RlclBvcnRDbGVhbnVwKCk6IHZvaWQge1xuICBjb25zdCBleGl0Q2xlYW51cCA9ICgpID0+IHsgY2xlYW51cFBvcnRGaWxlKCkuY2F0Y2goKCkgPT4ge30pOyB9O1xuICBwcm9jZXNzLm9uY2UoJ2V4aXQnLCBleGl0Q2xlYW51cCk7XG4gIHByb2Nlc3Mub25jZSgnU0lHVEVSTScsIGV4aXRDbGVhbnVwKTtcbiAgcHJvY2Vzcy5vbmNlKCdTSUdJTlQnLCBleGl0Q2xlYW51cCk7XG59XG4iXX0=