feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
241
product-source/hololake-platform/mcp-server/ws-bridge.js
Normal file
241
product-source/hololake-platform/mcp-server/ws-bridge.js
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* WebSocket bridge for Tolaria MCP tools.
|
||||
*
|
||||
* Exposes vault operations over WebSocket so the Tolaria app frontend
|
||||
* can invoke MCP tools in real-time without going through stdio.
|
||||
*
|
||||
* Port 9710: Tool bridge — Claude/AI clients call vault tools here.
|
||||
* Port 9711: UI bridge — Frontend listens for UI action broadcasts.
|
||||
*
|
||||
* Usage:
|
||||
* VAULT_PATH=/path/to/vault WS_PORT=9710 WS_UI_PORT=9711 node ws-bridge.js
|
||||
*
|
||||
* Protocol (tool bridge):
|
||||
* Client sends: { "id": "req-1", "tool": "search_notes", "args": { "query": "test" } }
|
||||
* Server sends: { "id": "req-1", "result": { ... } }
|
||||
* On error: { "id": "req-1", "error": "message" }
|
||||
*
|
||||
* Protocol (UI bridge):
|
||||
* Server broadcasts: { "type": "ui_action", "action": "open_note", "path": "..." }
|
||||
*/
|
||||
import { createServer } from 'node:http'
|
||||
import { WebSocketServer } from 'ws'
|
||||
import { createMcpToolService } from './tool-service.js'
|
||||
|
||||
const WS_PORT = parseInt(process.env.WS_PORT || '9710', 10)
|
||||
const WS_UI_PORT = parseInt(process.env.WS_UI_PORT || '9711', 10)
|
||||
const LOOPBACK_HOST = 'localhost'
|
||||
const TRUSTED_UI_ORIGINS = new Set([
|
||||
'tauri://localhost',
|
||||
'http://tauri.localhost',
|
||||
'https://tauri.localhost',
|
||||
])
|
||||
|
||||
/** @type {WebSocketServer | null} */
|
||||
let uiBridge = null
|
||||
const UNKNOWN_TOOL = Symbol('unknown tool')
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
const toolService = createMcpToolService({ emitUiAction: broadcastUiAction })
|
||||
|
||||
async function readNoteTool(args) {
|
||||
const note = await toolService.readNote(args)
|
||||
return { content: note.content, frontmatter: note.frontmatter }
|
||||
}
|
||||
|
||||
function uiOpenNoteTool(args) {
|
||||
toolService.openNoteInEditor(args)
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
function uiOpenTabTool(args) {
|
||||
toolService.openNoteAsTab(args)
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
async function createNoteTool(args = {}) {
|
||||
return { ok: true, ...(await toolService.createNote(args)) }
|
||||
}
|
||||
|
||||
function highlightTool(args) {
|
||||
toolService.highlightEditor(args)
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
function uiSetFilterTool(args) {
|
||||
toolService.setFilter(args)
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
function refreshVaultTool(args) {
|
||||
toolService.refreshVault(args)
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
const TOOL_EXECUTORS = [
|
||||
['open_note', readNoteTool],
|
||||
['read_note', readNoteTool],
|
||||
['create_note', createNoteTool],
|
||||
['search_notes', (args) => toolService.searchNotes(args)],
|
||||
['vault_context', (args) => toolService.vaultContext(args)],
|
||||
['list_vaults', () => toolService.listVaults()],
|
||||
['ui_open_note', uiOpenNoteTool],
|
||||
['ui_open_tab', uiOpenTabTool],
|
||||
['ui_highlight', highlightTool],
|
||||
['highlight_editor', highlightTool],
|
||||
['ui_set_filter', uiSetFilterTool],
|
||||
['refresh_vault', refreshVaultTool],
|
||||
]
|
||||
|
||||
function callToolHandler(tool, args) {
|
||||
const executor = TOOL_EXECUTORS.find(([name]) => name === tool)?.[1]
|
||||
return executor ? executor(args) : UNKNOWN_TOOL
|
||||
}
|
||||
|
||||
async function handleMessage(data) {
|
||||
const msg = JSON.parse(data)
|
||||
const { id, tool, args } = msg
|
||||
|
||||
try {
|
||||
const result = await callToolHandler(tool, args || {})
|
||||
if (result === UNKNOWN_TOOL) {
|
||||
return { id, error: `Unknown tool: ${tool}` }
|
||||
}
|
||||
return { id, result }
|
||||
} catch (err) {
|
||||
return { id, error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
export function isLoopbackAddress(remoteAddress) {
|
||||
return remoteAddress === '127.0.0.1'
|
||||
|| remoteAddress === '::1'
|
||||
|| remoteAddress === '::ffff:127.0.0.1'
|
||||
}
|
||||
|
||||
export function isTrustedUiOrigin(origin) {
|
||||
if (!origin) return true
|
||||
if (TRUSTED_UI_ORIGINS.has(origin)) return true
|
||||
return /^http:\/\/(?:localhost|127\.0\.0\.1):\d+$/u.test(origin)
|
||||
}
|
||||
|
||||
export function evaluateBridgeRequest({ bridgeType, origin, remoteAddress }) {
|
||||
if (!isLoopbackAddress(remoteAddress)) {
|
||||
return { ok: false, reason: 'non-local client' }
|
||||
}
|
||||
|
||||
if (bridgeType === 'tool' && origin) {
|
||||
return { ok: false, reason: 'browser origins are not allowed on the tool bridge' }
|
||||
}
|
||||
|
||||
if (bridgeType === 'ui' && !isTrustedUiOrigin(origin)) {
|
||||
return { ok: false, reason: 'untrusted UI origin' }
|
||||
}
|
||||
|
||||
return { ok: true, reason: null }
|
||||
}
|
||||
|
||||
function verifyBridgeRequest(bridgeType) {
|
||||
return (info, done) => {
|
||||
const verdict = evaluateBridgeRequest({
|
||||
bridgeType,
|
||||
origin: info.origin,
|
||||
remoteAddress: info.req.socket.remoteAddress,
|
||||
})
|
||||
|
||||
if (!verdict.ok) {
|
||||
console.error(`[ws-bridge] Rejected ${bridgeType} bridge client: ${verdict.reason}`)
|
||||
done(false, 403, 'Forbidden')
|
||||
return
|
||||
}
|
||||
|
||||
done(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Tolaria instance owns it).
|
||||
*/
|
||||
export function startUiBridge(port = WS_UI_PORT) {
|
||||
return new Promise((resolve) => {
|
||||
const httpServer = createServer()
|
||||
|
||||
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, LOOPBACK_HOST, () => {
|
||||
const wss = new WebSocketServer({
|
||||
server: httpServer,
|
||||
verifyClient: verifyBridgeRequest('ui'),
|
||||
})
|
||||
wss.on('connection', (ws) => {
|
||||
console.error(`[ws-bridge] UI client connected on port ${port}`)
|
||||
// Relay: when a client sends a message, broadcast to all OTHER clients.
|
||||
// This allows the MCP stdio server (connected as a client) to reach the frontend.
|
||||
ws.on('message', (raw) => {
|
||||
for (const client of wss.clients) {
|
||||
if (client !== ws && client.readyState === 1) client.send(raw.toString())
|
||||
}
|
||||
})
|
||||
})
|
||||
uiBridge = wss
|
||||
console.error(`[ws-bridge] UI bridge listening on ws://localhost:${port}`)
|
||||
resolve(wss)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function startBridge(port = WS_PORT) {
|
||||
const currentVaultPaths = toolService.activeVaultPaths()
|
||||
const wss = new WebSocketServer({
|
||||
port,
|
||||
host: LOOPBACK_HOST,
|
||||
verifyClient: verifyBridgeRequest('tool'),
|
||||
})
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.error(`[ws-bridge] Client connected (vaults: ${currentVaultPaths.join(', ')})`)
|
||||
|
||||
ws.on('message', async (raw) => {
|
||||
try {
|
||||
const response = await handleMessage(raw.toString())
|
||||
ws.send(JSON.stringify(response))
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ error: `Parse error: ${err.message}` }))
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('close', () => console.error('[ws-bridge] Client disconnected'))
|
||||
})
|
||||
|
||||
console.error(`[ws-bridge] Listening on ws://${LOOPBACK_HOST}:${port}`)
|
||||
return wss
|
||||
}
|
||||
|
||||
// Run directly if invoked as main module
|
||||
const isMain = process.argv[1]?.endsWith('ws-bridge.js')
|
||||
if (isMain) {
|
||||
try {
|
||||
toolService.activeVaultPaths()
|
||||
startUiBridge().then(() => startBridge())
|
||||
} catch (err) {
|
||||
console.error(`[ws-bridge] ${err.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue