feat: Improve WebMCP connection handling and tools management

WebMCP service:
- Add headless mode configuration
- Implement proper event handlers with unsubscribe support
- Add connection info tracking (channel, server, status, tools)
- Add destroyWebMCP for cleanup
- Improve connectWithToken to pass token directly

Canvas store:
- Add connection state (reconnecting, status, error, info)
- Add computed statusColor for UI feedback

Components:
- Add ConnectionDropdown for connection status display
- Add ToolsDropdown for tools management UI

Tool registry:
- Improve tool activation/deactivation logic
- Better error handling and logging
This commit is contained in:
2026-02-13 18:06:45 -06:00
parent 3c57f95b90
commit 424afa060c
7 changed files with 1099 additions and 33 deletions

View File

@@ -2,6 +2,7 @@ import { useCanvasStore } from '../stores/canvas'
let webmcpInstance: any = null
const registeredTools = new Set<string>()
const eventUnsubscribers: Array<() => void> = []
const API_BASE = 'http://localhost:4101'
let tokenPollingInterval: number | null = null
@@ -13,31 +14,130 @@ export async function initWebMCP() {
const WebMCP = WebMCPModule.default || WebMCPModule
webmcpInstance = new WebMCP({
color: '#6366f1',
position: 'bottom-right',
headless: true,
inactivityTimeout: 60 * 60 * 1000 // 1 hora
})
const canvasStore = useCanvasStore()
webmcpInstance.on?.('connected', () => {
canvasStore.setConnected(true)
})
webmcpInstance.on?.('disconnected', () => {
canvasStore.setConnected(false)
})
setupEventHandlers()
// Check initial connection state
if (webmcpInstance.isConnected) {
const canvasStore = useCanvasStore()
canvasStore.setConnected(true)
updateConnectionInfo()
}
// Exponer globalmente para debug
// 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
}
@@ -91,6 +191,26 @@ export function clearAllTools() {
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]
}
@@ -151,27 +271,27 @@ export function parseToken(token: string): { server: string; token: string } | n
}
export async function connectWithToken(token: string): Promise<boolean> {
const parsed = parseToken(token)
if (!parsed) return false
if (!webmcpInstance) {
console.error('[WebMCP] Instance not initialized')
return false
}
console.log('[WebMCP] Connecting with token to:', parsed.server)
if (typeof webmcpInstance.connect !== 'function') {
console.error('[WebMCP] connect method not available')
return false
}
// Store token for webmcp to use
localStorage.setItem('webmcp_token', token)
console.log('[WebMCP] Connecting with token...')
// Clear the pending token from server
await clearToken()
// If webmcp is already initialized, try to reconnect
if (webmcpInstance && typeof webmcpInstance.connect === 'function') {
try {
await webmcpInstance.connect()
return true
} catch (e) {
console.error('[WebMCP] Failed to connect:', e)
return false
}
// Connect passing the token directly
try {
await webmcpInstance.connect(token)
return true
} catch (e) {
console.error('[WebMCP] Failed to connect:', e)
return false
}
return true
}