Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | /**
* Hook for communicating with the Laputa MCP WebSocket bridge.
*
* Provides typed tool invocations for vault operations:
* - readNote, createNote, searchNotes, appendToNote
*
* Connection is lazy — only opens when first tool is called.
*/
import { useCallback, useRef, useState } from 'react'
const DEFAULT_WS_URL = 'ws://localhost:9710'
interface PendingRequest {
resolve: (value: any) => void
reject: (reason: any) => void
}
interface SearchResult {
path: string
title: string
snippet: string
}
export function useMcpBridge(wsUrl = DEFAULT_WS_URL) {
const wsRef = useRef<WebSocket | null>(null)
const pendingRef = useRef<Map<string, PendingRequest>>(new Map())
const idCounterRef = useRef(0)
const [connected, setConnected] = useState(false)
const ensureConnection = useCallback((): Promise<WebSocket> => {
const ws = wsRef.current
if (ws && ws.readyState === WebSocket.OPEN) return Promise.resolve(ws)
return new Promise((resolve, reject) => {
const newWs = new WebSocket(wsUrl)
newWs.onopen = () => {
wsRef.current = newWs
setConnected(true)
resolve(newWs)
}
newWs.onmessage = (event) => {
try {
const msg = JSON.parse(event.data)
const pending = pendingRef.current.get(msg.id)
if (pending) {
pendingRef.current.delete(msg.id)
if (msg.error) {
pending.reject(new Error(msg.error))
} else {
pending.resolve(msg.result)
}
}
} catch {
// ignore malformed messages
}
}
newWs.onclose = () => {
wsRef.current = null
setConnected(false)
}
newWs.onerror = () => {
reject(new Error('WebSocket connection failed'))
}
})
}, [wsUrl])
const callTool = useCallback(async <T>(tool: string, args: Record<string, any>): Promise<T> => {
const ws = await ensureConnection()
const id = `mcp-${++idCounterRef.current}`
return new Promise((resolve, reject) => {
pendingRef.current.set(id, { resolve, reject })
ws.send(JSON.stringify({ id, tool, args }))
// Timeout after 30 seconds
setTimeout(() => {
if (pendingRef.current.has(id)) {
pendingRef.current.delete(id)
reject(new Error('MCP tool call timed out'))
}
}, 30_000)
})
}, [ensureConnection])
const readNote = useCallback(
(path: string) => callTool<{ content: string }>('read_note', { path }),
[callTool],
)
const createNote = useCallback(
(path: string, title: string, isA?: string) =>
callTool<string>('create_note', { path, title, is_a: isA }),
[callTool],
)
const searchNotes = useCallback(
(query: string, limit?: number) =>
callTool<SearchResult[]>('search_notes', { query, limit }),
[callTool],
)
const appendToNote = useCallback(
(path: string, text: string) =>
callTool<{ ok: boolean }>('append_to_note', { path, text }),
[callTool],
)
return { connected, readNote, createNote, searchNotes, appendToNote, callTool }
}
|