diff --git a/frontend/src/components/ConnectionDropdown.vue b/frontend/src/components/ConnectionDropdown.vue new file mode 100644 index 0000000..b162524 --- /dev/null +++ b/frontend/src/components/ConnectionDropdown.vue @@ -0,0 +1,363 @@ + + + + + diff --git a/frontend/src/components/ToolsDropdown.vue b/frontend/src/components/ToolsDropdown.vue new file mode 100644 index 0000000..3700d16 --- /dev/null +++ b/frontend/src/components/ToolsDropdown.vue @@ -0,0 +1,391 @@ + + + + + diff --git a/frontend/src/services/toolRegistry.ts b/frontend/src/services/toolRegistry.ts index 022c9f6..5c14dc8 100644 --- a/frontend/src/services/toolRegistry.ts +++ b/frontend/src/services/toolRegistry.ts @@ -23,7 +23,7 @@ import { } from './tools/handlers' import { setRouter } from './tools/handlers/globalHandlers' import { setGiteaCredentials, clearGiteaCredentials } from './tools/handlers/sourceCodeHandlers' -import { ALL_TOOL_METAS, type ToolCategory } from './tools/toolDefinitions' +import { ALL_TOOL_METAS, getAllToolNames, type ToolCategory } from './tools/toolDefinitions' export type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' @@ -75,9 +75,37 @@ function getToolConfigs(): Map { toolConfigsCache = new Map() + // Create callbacks for global handlers + const toolManagementCallbacks = { + getRegisteredTools: () => Array.from(registeredToolsSet), + getAllToolNames: () => getAllToolNames(), + activateTool: async (name: string) => { + const config = toolConfigsCache?.get(name) + if (!config) return false + const result = await internalRegisterTool(config) + syncStoreWithActiveTools() + return result + }, + deactivateTool: (name: string) => { + const toolsStore = useToolsStore() + if (toolsStore.isToolPinned(name)) return false + const result = internalUnregisterTool(name) + syncStoreWithActiveTools() + return result + }, + togglePin: (name: string) => { + const toolsStore = useToolsStore() + toolsStore.togglePin(name) + }, + isToolPinned: (name: string) => { + const toolsStore = useToolsStore() + return toolsStore.isToolPinned(name) + } + } + // Create all handlers const allHandlers = [ - ...createGlobalHandlers(() => Array.from(registeredToolsSet)), + ...createGlobalHandlers(toolManagementCallbacks), ...createCanvasHandlers(), ...createComponentHandlers(), ...createThemeHandlers(), @@ -95,7 +123,7 @@ function getToolConfigs(): Map { // Category to tool names mapping const categoryTools: Record = { - global: ['get_current_page', 'navigate_to', 'list_available_tools'], + global: ['get_current_page', 'navigate_to', 'list_available_tools', 'activate_tool', 'deactivate_tool', 'pin_tool'], canvas: ['render_html', 'render_vue_component'], component: ['save_vue_component', 'load_vue_component', 'list_vue_components', 'delete_vue_component'], theme: ['get_design_tokens', 'get_active_theme', 'set_theme_variable', 'save_theme', 'list_themes', 'switch_theme', 'reset_theme'], diff --git a/frontend/src/services/tools/handlers/globalHandlers.ts b/frontend/src/services/tools/handlers/globalHandlers.ts index 0c27d99..2274c6c 100644 --- a/frontend/src/services/tools/handlers/globalHandlers.ts +++ b/frontend/src/services/tools/handlers/globalHandlers.ts @@ -6,7 +6,18 @@ export function setRouter(router: any) { routerInstance = router } -export function createGlobalHandlers(getRegisteredTools: () => string[]): ToolConfig[] { +export interface ToolManagementCallbacks { + getRegisteredTools: () => string[] + getAllToolNames: () => string[] + activateTool: (name: string) => Promise + deactivateTool: (name: string) => boolean + togglePin: (name: string) => void + isToolPinned: (name: string) => boolean +} + +export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolConfig[] { + const { getRegisteredTools, getAllToolNames, activateTool, deactivateTool, togglePin, isToolPinned } = callbacks + const allToolNames = getAllToolNames() return [ { name: 'get_current_page', @@ -103,6 +114,100 @@ export function createGlobalHandlers(getRegisteredTools: () => string[]): ToolCo } return `Herramientas MCP disponibles (${tools.length}):\n${tools.map(t => ` - ${t}`).join('\n')}` } + }, + { + name: 'activate_tool', + description: 'Activa una herramienta MCP para que este disponible', + category: 'global', + schema: { + type: 'object', + properties: { + tool_name: { + type: 'string', + enum: allToolNames, + description: 'Nombre de la herramienta a activar' + } + }, + required: ['tool_name'] + }, + handler: async (args: { tool_name: string }) => { + const activeTools = getRegisteredTools() + if (activeTools.includes(args.tool_name)) { + return `La herramienta "${args.tool_name}" ya esta activa` + } + + const success = await activateTool(args.tool_name) + if (success) { + return `Herramienta "${args.tool_name}" activada correctamente` + } else { + return `Error: No se pudo activar "${args.tool_name}". Verifica que el nombre sea correcto.` + } + } + }, + { + name: 'deactivate_tool', + description: 'Desactiva una herramienta MCP. Si esta pinneada, la despinea primero.', + category: 'global', + schema: { + type: 'object', + properties: { + tool_name: { + type: 'string', + enum: allToolNames, + description: 'Nombre de la herramienta a desactivar' + } + }, + required: ['tool_name'] + }, + handler: (args: { tool_name: string }) => { + const activeTools = getRegisteredTools() + if (!activeTools.includes(args.tool_name)) { + return `La herramienta "${args.tool_name}" no esta activa` + } + + // Si esta pinneada, despinear primero + if (isToolPinned(args.tool_name)) { + togglePin(args.tool_name) + } + + const success = deactivateTool(args.tool_name) + if (success) { + return `Herramienta "${args.tool_name}" desactivada correctamente` + } else { + return `Error: No se pudo desactivar "${args.tool_name}"` + } + } + }, + { + name: 'pin_tool', + description: 'Pinnea una herramienta MCP. Las herramientas pinneadas permanecen activas al cambiar de pagina.', + category: 'global', + schema: { + type: 'object', + properties: { + tool_name: { + type: 'string', + enum: allToolNames, + description: 'Nombre de la herramienta a pinnear' + } + }, + required: ['tool_name'] + }, + handler: async (args: { tool_name: string }) => { + if (isToolPinned(args.tool_name)) { + return `La herramienta "${args.tool_name}" ya esta pinneada` + } + + togglePin(args.tool_name) + + // Asegurar que este activa + const activeTools = getRegisteredTools() + if (!activeTools.includes(args.tool_name)) { + await activateTool(args.tool_name) + } + + return `Herramienta "${args.tool_name}" pinneada. Permanecera activa al cambiar de pagina.` + } } ] } diff --git a/frontend/src/services/tools/toolDefinitions.ts b/frontend/src/services/tools/toolDefinitions.ts index 949bcd2..9c5fbe0 100644 --- a/frontend/src/services/tools/toolDefinitions.ts +++ b/frontend/src/services/tools/toolDefinitions.ts @@ -12,6 +12,9 @@ export const ALL_TOOL_METAS: ToolMeta[] = [ { name: 'get_current_page', description: 'Obtiene la pagina actualmente activa', category: 'global' }, { name: 'navigate_to', description: 'Navega a una pagina especifica', category: 'global' }, { name: 'list_available_tools', description: 'Lista todas las herramientas MCP disponibles', category: 'global' }, + { name: 'activate_tool', description: 'Activa una herramienta MCP', category: 'global' }, + { name: 'deactivate_tool', description: 'Desactiva una herramienta MCP', category: 'global' }, + { name: 'pin_tool', description: 'Pinnea una herramienta', category: 'global' }, // Canvas tools { name: 'render_html', description: 'Renderiza HTML en el canvas', category: 'canvas' }, diff --git a/frontend/src/services/webmcp.ts b/frontend/src/services/webmcp.ts index a279282..ad6ee20 100644 --- a/frontend/src/services/webmcp.ts +++ b/frontend/src/services/webmcp.ts @@ -2,6 +2,7 @@ import { useCanvasStore } from '../stores/canvas' let webmcpInstance: any = null const registeredTools = new Set() +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 { - 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 } diff --git a/frontend/src/stores/canvas.ts b/frontend/src/stores/canvas.ts index 7fca411..3a49cff 100644 --- a/frontend/src/stores/canvas.ts +++ b/frontend/src/stores/canvas.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia' -import { ref } from 'vue' +import { ref, computed } from 'vue' interface HistoryEntry { tool: string @@ -14,16 +14,60 @@ interface Notification { duration: number } +interface ConnectionInfo { + isConnected: boolean + channel: string | null + server: string | null + status: string + tools: string[] + prompts: string[] + resources: string[] +} + export const useCanvasStore = defineStore('canvas', () => { + // Connection state const isConnected = ref(false) + const isReconnecting = ref(false) + const connectionStatus = ref('disconnected') + const connectionError = ref(null) + const connectionInfo = ref(null) + const history = ref([]) const notifications = ref([]) const showHistoryPanel = ref(false) let notificationId = 0 + // Computed + const statusColor = computed(() => { + if (isReconnecting.value) return 'warning' + if (isConnected.value) return 'success' + if (connectionError.value) return 'error' + return 'muted' + }) + function setConnected(connected: boolean) { isConnected.value = connected + if (connected) { + isReconnecting.value = false + connectionError.value = null + } + } + + function setReconnecting(reconnecting: boolean) { + isReconnecting.value = reconnecting + } + + function setConnectionStatus(status: string) { + connectionStatus.value = status + } + + function setConnectionError(error: string | null) { + connectionError.value = error + } + + function setConnectionInfo(info: ConnectionInfo | null) { + connectionInfo.value = info } function addToHistory(entry: HistoryEntry) { @@ -60,11 +104,23 @@ export const useCanvasStore = defineStore('canvas', () => { } return { + // Connection state isConnected, + isReconnecting, + connectionStatus, + connectionError, + connectionInfo, + statusColor, + // History & UI history, notifications, showHistoryPanel, + // Actions setConnected, + setReconnecting, + setConnectionStatus, + setConnectionError, + setConnectionInfo, addToHistory, clearHistory, showNotification,