- Add /api/torch endpoints for torch state management - Torch system uses HTTP polling instead of WebSocket - Only browser with torch connects to MCP - Other browsers disconnect and poll for torch state - Auto-assign torch to first registered client - Auto-reassign torch when holder disconnects This approach requires no changes to WebMCP library.
385 lines
9.9 KiB
TypeScript
385 lines
9.9 KiB
TypeScript
import { useCanvasStore } from '../stores/canvas'
|
|
import { endpoints, isSecure, wsProtocol, hostname } from '../config/endpoints'
|
|
|
|
// WebMCP HTTP API base for direct token requests
|
|
const WEBMCP_HTTP = endpoints.webmcpHttp
|
|
|
|
let webmcpInstance: any = null
|
|
const registeredTools = new Set<string>()
|
|
const eventUnsubscribers: Array<() => void> = []
|
|
|
|
const API_BASE = endpoints.api
|
|
let tokenPollingInterval: number | null = null
|
|
|
|
export async function initWebMCP() {
|
|
if (webmcpInstance) return webmcpInstance
|
|
|
|
const WebMCPModule = await import('@nucleoriofrio/webmcp/src/webmcp.js')
|
|
const WebMCP = WebMCPModule.default || WebMCPModule
|
|
|
|
webmcpInstance = new WebMCP({
|
|
headless: true,
|
|
inactivityTimeout: 60 * 60 * 1000 // 1 hora
|
|
})
|
|
|
|
setupEventHandlers()
|
|
|
|
// Check initial connection state
|
|
if (webmcpInstance.isConnected) {
|
|
const canvasStore = useCanvasStore()
|
|
canvasStore.setConnected(true)
|
|
updateConnectionInfo()
|
|
}
|
|
|
|
// Expose globally for debug
|
|
;(window as any).webmcp = webmcpInstance
|
|
|
|
return webmcpInstance
|
|
}
|
|
|
|
function setupEventHandlers() {
|
|
// Skip if instance doesn't support events
|
|
if (typeof webmcpInstance.on !== 'function') {
|
|
console.warn('[WebMCP] Event emitter not available')
|
|
return
|
|
}
|
|
|
|
const canvasStore = useCanvasStore()
|
|
|
|
// Connection events
|
|
eventUnsubscribers.push(
|
|
webmcpInstance.on('connected', () => {
|
|
console.log('[WebMCP] Connected')
|
|
canvasStore.setConnected(true)
|
|
canvasStore.setReconnecting(false)
|
|
canvasStore.setConnectionError(null)
|
|
updateConnectionInfo()
|
|
})
|
|
)
|
|
|
|
eventUnsubscribers.push(
|
|
webmcpInstance.on('disconnected', () => {
|
|
console.log('[WebMCP] Disconnected')
|
|
canvasStore.setConnected(false)
|
|
canvasStore.setReconnecting(false)
|
|
canvasStore.setConnectionInfo(null)
|
|
})
|
|
)
|
|
|
|
eventUnsubscribers.push(
|
|
webmcpInstance.on('reconnecting', () => {
|
|
console.log('[WebMCP] Reconnecting...')
|
|
canvasStore.setReconnecting(true)
|
|
})
|
|
)
|
|
|
|
// Status changes
|
|
eventUnsubscribers.push(
|
|
webmcpInstance.on('statusChange', (data: { status: string }) => {
|
|
canvasStore.setConnectionStatus(data.status)
|
|
})
|
|
)
|
|
|
|
// Error handling
|
|
eventUnsubscribers.push(
|
|
webmcpInstance.on('error', (data: { message: string }) => {
|
|
console.error('[WebMCP] Error:', data.message)
|
|
canvasStore.setConnectionError(data.message)
|
|
canvasStore.showNotification(data.message, 'error')
|
|
})
|
|
)
|
|
|
|
// Tool events
|
|
eventUnsubscribers.push(
|
|
webmcpInstance.on('toolRegistered', (data: { name: string }) => {
|
|
console.log('[WebMCP] Tool registered by server:', data.name)
|
|
updateConnectionInfo()
|
|
})
|
|
)
|
|
|
|
eventUnsubscribers.push(
|
|
webmcpInstance.on('toolCreated', (data: { name: string }) => {
|
|
console.log('[WebMCP] Tool created:', data.name)
|
|
registeredTools.add(data.name)
|
|
updateConnectionInfo()
|
|
})
|
|
)
|
|
|
|
eventUnsubscribers.push(
|
|
webmcpInstance.on('toolRemoved', (data: { name: string }) => {
|
|
if (data.name === '*') {
|
|
console.log('[WebMCP] All tools removed')
|
|
registeredTools.clear()
|
|
} else {
|
|
console.log('[WebMCP] Tool removed:', data.name)
|
|
registeredTools.delete(data.name)
|
|
}
|
|
updateConnectionInfo()
|
|
})
|
|
)
|
|
}
|
|
|
|
function updateConnectionInfo() {
|
|
if (!webmcpInstance) return
|
|
|
|
const canvasStore = useCanvasStore()
|
|
const info = webmcpInstance.getConnectionInfo?.()
|
|
|
|
if (info) {
|
|
canvasStore.setConnectionInfo({
|
|
isConnected: info.isConnected,
|
|
channel: info.channel,
|
|
server: info.server,
|
|
status: info.status,
|
|
tools: info.tools || [],
|
|
prompts: info.prompts || [],
|
|
resources: info.resources || []
|
|
})
|
|
}
|
|
}
|
|
|
|
export function getConnectionInfo() {
|
|
return webmcpInstance?.getConnectionInfo?.() || null
|
|
}
|
|
|
|
export function getWebMCP() {
|
|
return webmcpInstance
|
|
}
|
|
|
|
export function registerTool(
|
|
name: string,
|
|
description: string,
|
|
schema: object,
|
|
handler: Function
|
|
) {
|
|
if (!webmcpInstance) {
|
|
console.warn('[WebMCP] Instance not initialized')
|
|
return false
|
|
}
|
|
if (registeredTools.has(name)) {
|
|
console.warn(`[WebMCP] Tool "${name}" already registered, skipping`)
|
|
return false
|
|
}
|
|
webmcpInstance.registerTool(name, description, schema, handler)
|
|
registeredTools.add(name)
|
|
console.log(`[WebMCP] Tool registered: ${name}`)
|
|
return true
|
|
}
|
|
|
|
export function unregisterTool(name: string) {
|
|
if (!webmcpInstance) {
|
|
console.warn('[WebMCP] Instance not initialized')
|
|
return false
|
|
}
|
|
if (!registeredTools.has(name)) {
|
|
return false
|
|
}
|
|
webmcpInstance.unregisterTool(name)
|
|
registeredTools.delete(name)
|
|
console.log(`[WebMCP] Tool unregistered: ${name}`)
|
|
return true
|
|
}
|
|
|
|
export function unregisterTools(names: string[]) {
|
|
for (const name of names) {
|
|
unregisterTool(name)
|
|
}
|
|
}
|
|
|
|
export function clearAllTools() {
|
|
if (!webmcpInstance) return
|
|
for (const name of registeredTools) {
|
|
webmcpInstance.unregisterTool(name)
|
|
}
|
|
console.log(`[WebMCP] Cleared ${registeredTools.size} tools`)
|
|
registeredTools.clear()
|
|
}
|
|
|
|
export function destroyWebMCP() {
|
|
// Unsubscribe all event handlers
|
|
for (const unsub of eventUnsubscribers) {
|
|
unsub()
|
|
}
|
|
eventUnsubscribers.length = 0
|
|
|
|
// Clear tools
|
|
clearAllTools()
|
|
|
|
// Disconnect if connected
|
|
if (webmcpInstance?.disconnect) {
|
|
webmcpInstance.disconnect()
|
|
}
|
|
|
|
webmcpInstance = null
|
|
;(window as any).webmcp = null
|
|
console.log('[WebMCP] Instance destroyed')
|
|
}
|
|
|
|
export function getRegisteredTools(): string[] {
|
|
return [...registeredTools]
|
|
}
|
|
|
|
export function isToolRegistered(name: string): boolean {
|
|
return registeredTools.has(name)
|
|
}
|
|
|
|
// Token polling functions
|
|
export async function checkForToken(): Promise<string | null> {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/webmcp-token`)
|
|
const data = await res.json()
|
|
return data.token || null
|
|
} catch (e) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function clearToken(): Promise<void> {
|
|
try {
|
|
await fetch(`${API_BASE}/webmcp-token`, { method: 'DELETE' })
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
export function startTokenPolling(onToken: (token: string) => void, intervalMs: number = 2000) {
|
|
if (tokenPollingInterval) return
|
|
|
|
console.log('[WebMCP] Starting token polling...')
|
|
tokenPollingInterval = window.setInterval(async () => {
|
|
const token = await checkForToken()
|
|
if (token) {
|
|
console.log('[WebMCP] Token detected!')
|
|
stopTokenPolling()
|
|
onToken(token)
|
|
}
|
|
}, intervalMs)
|
|
}
|
|
|
|
export function stopTokenPolling() {
|
|
if (tokenPollingInterval) {
|
|
window.clearInterval(tokenPollingInterval)
|
|
tokenPollingInterval = null
|
|
console.log('[WebMCP] Token polling stopped')
|
|
}
|
|
}
|
|
|
|
export function parseToken(token: string): { server: string; token: string } | null {
|
|
try {
|
|
const decoded = atob(token)
|
|
return JSON.parse(decoded)
|
|
} catch (e) {
|
|
console.error('[WebMCP] Failed to parse token:', e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request a new token directly from WebMCP server via HTTP API
|
|
* In development: calls WebMCP directly at port 4102
|
|
* In production (HTTPS): calls Agent UI API which proxies to WebMCP
|
|
*/
|
|
export async function requestToken(): Promise<string | null> {
|
|
try {
|
|
console.log('[WebMCP] Requesting token from server...')
|
|
|
|
// In HTTPS mode, use Agent UI API as proxy (Traefik can't reach WebMCP directly)
|
|
// In development, call WebMCP directly
|
|
const url = isSecure ? `${API_BASE}/webmcp-request-token` : `${WEBMCP_HTTP}/token`
|
|
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
|
|
if (!res.ok) {
|
|
console.error('[WebMCP] Failed to request token:', res.status)
|
|
return null
|
|
}
|
|
|
|
// Check if response is JSON
|
|
const contentType = res.headers.get('content-type')
|
|
if (!contentType?.includes('application/json')) {
|
|
console.warn('[WebMCP] /token endpoint not available (server returned non-JSON)')
|
|
return null
|
|
}
|
|
|
|
const data = await res.json()
|
|
if (data.success && data.token) {
|
|
console.log('[WebMCP] Token received from server')
|
|
return data.token
|
|
}
|
|
return null
|
|
} catch (e) {
|
|
// Network error or endpoint not available
|
|
console.warn('[WebMCP] /token endpoint not available:', (e as Error).message)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Auto-connect to WebMCP by requesting a token and connecting
|
|
* Returns true if connection was successful
|
|
*/
|
|
export async function autoConnect(): Promise<boolean> {
|
|
if (!webmcpInstance) {
|
|
console.error('[WebMCP] Instance not initialized, call initWebMCP() first')
|
|
return false
|
|
}
|
|
|
|
// Check if already connected
|
|
if (webmcpInstance.isConnected) {
|
|
console.log('[WebMCP] Already connected')
|
|
return true
|
|
}
|
|
|
|
// Request token from server
|
|
const token = await requestToken()
|
|
if (!token) {
|
|
console.error('[WebMCP] Failed to get token')
|
|
return false
|
|
}
|
|
|
|
// Connect with the token
|
|
return connectWithToken(token)
|
|
}
|
|
|
|
export async function connectWithToken(token: string): Promise<boolean> {
|
|
if (!webmcpInstance) {
|
|
console.error('[WebMCP] Instance not initialized')
|
|
return false
|
|
}
|
|
|
|
if (typeof webmcpInstance.connect !== 'function') {
|
|
console.error('[WebMCP] connect method not available')
|
|
return false
|
|
}
|
|
|
|
console.log('[WebMCP] Connecting with token...')
|
|
|
|
// Clear the pending token from server
|
|
await clearToken()
|
|
|
|
// If behind HTTPS/Traefik, modify token to use secure WebSocket
|
|
let finalToken = token
|
|
if (isSecure) {
|
|
const parsed = parseToken(token)
|
|
if (parsed) {
|
|
// Replace ws://localhost:4102 with wss://hostname/ws/mcp
|
|
const secureServer = `${wsProtocol}//${hostname}/ws/mcp`
|
|
const modifiedToken = { server: secureServer, token: parsed.token }
|
|
finalToken = btoa(JSON.stringify(modifiedToken))
|
|
console.log('[WebMCP] Modified token for HTTPS:', secureServer)
|
|
}
|
|
}
|
|
|
|
// Connect passing the token directly
|
|
try {
|
|
await webmcpInstance.connect(finalToken)
|
|
return true
|
|
} catch (e) {
|
|
console.error('[WebMCP] Failed to connect:', e)
|
|
return false
|
|
}
|
|
}
|