fix: handle EADDRINUSE in MCP server ws-bridge — allow Claude Code to start when port is taken
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<WebSocketServer|null> - 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.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user