From 10e6d7b366efd681b08728fe00e824a0210d4022 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 3 Mar 2026 13:20:56 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20handle=20EADDRINUSE=20in=20MCP=20server?= =?UTF-8?q?=20ws-bridge=20=E2=80=94=20allow=20Claude=20Code=20to=20start?= =?UTF-8?q?=20when=20port=20is=20taken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Claude CLI starts the Laputa MCP server, it crashed immediately because startUiBridge() tried to bind port 9711 which is already held by the running Laputa app. The unhandled EADDRINUSE error killed the process, making all Laputa MCP tools unavailable to Claude Code in AI Chat. Fix: - Make startUiBridge() async, return Promise - Handle 'error' event on HTTP server: EADDRINUSE resolves to null instead of crashing - Guard broadcastUiAction() with 'if (!uiBridge) return' for graceful no-op - In ws-bridge.js main: chain startUiBridge().then(() => startBridge()) All vault tools (read/write/search) now work via stdio MCP when port is busy. --- mcp-server/index.js | 10 ++++++++-- mcp-server/ws-bridge.js | 35 +++++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/mcp-server/index.js b/mcp-server/index.js index 08b4e323..5dea659a 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -32,10 +32,16 @@ import { startUiBridge } from './ws-bridge.js' const VAULT_PATH = process.env.VAULT_PATH || process.env.HOME + '/Laputa' const WS_UI_PORT = parseInt(process.env.WS_UI_PORT || '9711', 10) -// Start the UI bridge so stdio-based MCP tools can broadcast UI actions -const uiBridge = startUiBridge(WS_UI_PORT) +// Start the UI bridge so stdio-based MCP tools can broadcast UI actions. +// If the port is already in use (e.g. by the running Laputa app), continue +// without the bridge — vault tools still work via stdio MCP. +let uiBridge = null +startUiBridge(WS_UI_PORT).then((bridge) => { + uiBridge = bridge +}) function broadcastUiAction(action, payload) { + if (!uiBridge) return const msg = JSON.stringify({ type: 'ui_action', action, ...payload }) for (const client of uiBridge.clients) { if (client.readyState === 1) client.send(msg) diff --git a/mcp-server/ws-bridge.js b/mcp-server/ws-bridge.js index 9db7efc7..a2bbca77 100644 --- a/mcp-server/ws-bridge.js +++ b/mcp-server/ws-bridge.js @@ -19,6 +19,7 @@ * Protocol (UI bridge): * Server broadcasts: { "type": "ui_action", "action": "open_note", "path": "..." } */ +import { createServer } from 'node:http' import { WebSocketServer } from 'ws' import { readNote, createNote, searchNotes, appendToNote, @@ -80,15 +81,34 @@ async function handleMessage(data) { } } +/** + * Attempt to start the UI bridge WebSocket server. + * Returns a Promise that resolves to the WebSocketServer or null if the port + * is unavailable (e.g. another Laputa instance owns it). + */ export function startUiBridge(port = WS_UI_PORT) { - uiBridge = new WebSocketServer({ port }) + return new Promise((resolve) => { + const httpServer = createServer() - uiBridge.on('connection', () => { - console.error(`[ws-bridge] UI client connected on port ${port}`) + httpServer.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.error(`[ws-bridge] UI bridge port ${port} already in use, disabling bridge`) + } else { + console.error(`[ws-bridge] UI bridge error: ${err.message}`) + } + resolve(null) + }) + + httpServer.listen(port, () => { + const wss = new WebSocketServer({ server: httpServer }) + wss.on('connection', () => { + console.error(`[ws-bridge] UI client connected on port ${port}`) + }) + uiBridge = wss + console.error(`[ws-bridge] UI bridge listening on ws://localhost:${port}`) + resolve(wss) + }) }) - - console.error(`[ws-bridge] UI bridge listening on ws://localhost:${port}`) - return uiBridge } export function startBridge(port = WS_PORT) { @@ -116,6 +136,5 @@ export function startBridge(port = WS_PORT) { // Run directly if invoked as main module const isMain = process.argv[1]?.endsWith('ws-bridge.js') if (isMain) { - startUiBridge() - startBridge() + startUiBridge().then(() => startBridge()) }