From 1a51b34228010d209de8c688c255ecd0cf8998ec Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sat, 14 Feb 2026 17:52:16 -0600 Subject: [PATCH] feat: Add torch MCP tools for multi-browser control - Add torchHandlers.ts with 5 tools: - list_torch_clients: list connected browsers with metadata - get_torch_status: show current torch holder - transfer_torch: transfer control to another browser - request_torch: request MCP control - release_torch: release MCP control - Add 'transfer' message type to server torch-handler - Add torch category to all pages - Export helper functions from torch.ts --- frontend/src/services/toolRegistry.ts | 29 ++-- frontend/src/services/tools/handlers/index.ts | 3 +- .../services/tools/handlers/torchHandlers.ts | 154 ++++++++++++++++++ .../src/services/tools/toolDefinitions.ts | 11 +- frontend/src/services/torch.ts | 34 ++++ server/services/handlers/torch-handler.ts | 37 +++++ 6 files changed, 252 insertions(+), 16 deletions(-) create mode 100644 frontend/src/services/tools/handlers/torchHandlers.ts diff --git a/frontend/src/services/toolRegistry.ts b/frontend/src/services/toolRegistry.ts index 955f70c..1f1514c 100644 --- a/frontend/src/services/toolRegistry.ts +++ b/frontend/src/services/toolRegistry.ts @@ -24,6 +24,7 @@ import { createTerminalHandlers, createResponseHandlers, createGitHandlers, + createTorchHandlers, type ToolConfig } from './tools/handlers' import { setRouter } from './tools/handlers/globalHandlers' @@ -125,7 +126,8 @@ function getToolConfigs(): Map { ...createSourceCodeHandlers(), ...createTerminalHandlers(), ...createResponseHandlers(), - ...createGitHandlers() + ...createGitHandlers(), + ...createTorchHandlers() ] for (const config of allHandlers) { @@ -145,22 +147,23 @@ const categoryTools: Record = { source: ['get_repo_info', 'list_repo_files', 'read_repo_file', 'search_repo_code'], project: ['list_canvases', 'create_canvas', 'get_canvas', 'update_canvas', 'delete_canvas', 'clone_canvas', 'add_component_to_canvas', 'remove_component_from_canvas', 'get_canvas_components'], terminal: ['terminal_open', 'terminal_close', 'terminal_toggle', 'terminal_move', 'terminal_resize', 'bubbleResponse'], - git: ['get_git_status', 'get_git_diff', 'compare_commits', 'git_log', 'get_git_branches'] + git: ['get_git_status', 'get_git_diff', 'compare_commits', 'git_log', 'get_git_branches'], + torch: ['list_torch_clients', 'get_torch_status', 'transfer_torch', 'request_torch', 'release_torch'] } // Page to categories mapping const pageCategories: Record = { - home: ['global', 'canvas', 'component', 'project', 'terminal'], - canvas: ['global', 'canvas', 'component', 'terminal'], - 'project-canvas': ['global', 'canvas', 'component', 'project', 'terminal'], - projects: ['global', 'project', 'terminal'], - components: ['global', 'component', 'terminal'], - themes: ['global', 'theme', 'terminal'], - database: ['global', 'database', 'terminal'], - source: ['global', 'source', 'terminal'], - terminal: ['global', 'terminal'], - tools: ['global', 'terminal'], - git: ['global', 'git', 'terminal'] + home: ['global', 'torch', 'canvas', 'component', 'project', 'terminal'], + canvas: ['global', 'torch', 'canvas', 'component', 'terminal'], + 'project-canvas': ['global', 'torch', 'canvas', 'component', 'project', 'terminal'], + projects: ['global', 'torch', 'project', 'terminal'], + components: ['global', 'torch', 'component', 'terminal'], + themes: ['global', 'torch', 'theme', 'terminal'], + database: ['global', 'torch', 'database', 'terminal'], + source: ['global', 'torch', 'source', 'terminal'], + terminal: ['global', 'torch', 'terminal'], + tools: ['global', 'torch', 'terminal'], + git: ['global', 'torch', 'git', 'terminal'] } let currentPage: PageName | null = null diff --git a/frontend/src/services/tools/handlers/index.ts b/frontend/src/services/tools/handlers/index.ts index a3419b9..e029416 100644 --- a/frontend/src/services/tools/handlers/index.ts +++ b/frontend/src/services/tools/handlers/index.ts @@ -15,13 +15,14 @@ export type { TerminalControls } from './terminalHandlers' export { createResponseHandlers, setResponseControls } from './responseHandlers' export type { ResponseControls } from './responseHandlers' export { createGitHandlers } from './gitHandlers' +export { createTorchHandlers } from './torchHandlers' export type ToolHandler = (args: any) => string | Promise export interface ToolConfig { name: string description: string - category: 'global' | 'canvas' | 'component' | 'theme' | 'database' | 'source' | 'project' | 'terminal' | 'git' + category: 'global' | 'canvas' | 'component' | 'theme' | 'database' | 'source' | 'project' | 'terminal' | 'git' | 'torch' schema: object handler: ToolHandler } diff --git a/frontend/src/services/tools/handlers/torchHandlers.ts b/frontend/src/services/tools/handlers/torchHandlers.ts new file mode 100644 index 0000000..8d57142 --- /dev/null +++ b/frontend/src/services/tools/handlers/torchHandlers.ts @@ -0,0 +1,154 @@ +import type { ToolConfig } from './index' +import { getTorchClients, getTorchStatus, transferTorch, requestTorch, releaseTorch } from '../../torch' + +export function createTorchHandlers(): ToolConfig[] { + return [ + { + name: 'list_torch_clients', + description: 'Lista todos los browsers/clientes conectados al sistema de antorcha con sus metadatos', + category: 'global', + schema: { + type: 'object', + properties: {} + }, + handler: () => { + const clients = getTorchClients() + const status = getTorchStatus() + + if (clients.length === 0) { + return 'No hay clientes conectados al sistema de antorcha' + } + + let result = `Clientes conectados (${clients.length}):\n\n` + + for (const client of clients) { + const isMe = client.id === status.clientId + const hasTorch = client.hasTorch + + result += `${hasTorch ? '🔥' : '⚪'} ${client.id}${isMe ? ' (este browser)' : ''}\n` + result += ` Hostname: ${client.hostname}\n` + result += ` User Agent: ${client.userAgent.substring(0, 80)}...\n` + result += ` Conectado: ${client.connectedAt}\n\n` + } + + return result + } + }, + { + name: 'get_torch_status', + description: 'Obtiene el estado actual del sistema de antorcha (quien tiene el control del MCP)', + category: 'global', + schema: { + type: 'object', + properties: {} + }, + handler: () => { + const status = getTorchStatus() + const clients = getTorchClients() + + let result = '=== Estado de la Antorcha ===\n\n' + result += `Mi ID: ${status.clientId || 'No registrado'}\n` + result += `Tengo la antorcha: ${status.hasTorch ? 'Si' : 'No'}\n` + result += `Holder actual: ${status.torchHolderId || 'Nadie'}\n` + result += `Clientes conectados: ${status.clientCount}\n\n` + + if (status.torchHolderId) { + const holder = clients.find(c => c.id === status.torchHolderId) + if (holder) { + result += `Detalles del holder:\n` + result += ` Hostname: ${holder.hostname}\n` + result += ` Conectado desde: ${holder.connectedAt}\n` + } + } + + return result + } + }, + { + name: 'transfer_torch', + description: 'Transfiere la antorcha (control del MCP) a otro cliente especifico', + category: 'global', + schema: { + type: 'object', + properties: { + target_id: { + type: 'string', + description: 'ID del cliente al que transferir la antorcha (usar list_torch_clients para ver IDs)' + } + }, + required: ['target_id'] + }, + handler: async (args: { target_id: string }) => { + const status = getTorchStatus() + + if (!status.hasTorch && status.torchHolderId !== null) { + return 'Error: No tienes la antorcha. Solo el holder actual puede transferirla.' + } + + const clients = getTorchClients() + const targetExists = clients.some(c => c.id === args.target_id) + + if (!targetExists) { + return `Error: Cliente "${args.target_id}" no encontrado.\n\nClientes disponibles:\n${clients.map(c => ` - ${c.id}`).join('\n')}` + } + + if (args.target_id === status.clientId) { + return 'Error: No puedes transferir la antorcha a ti mismo' + } + + const success = await transferTorch(args.target_id) + if (success) { + return `Antorcha transferida a ${args.target_id}. El otro browser ahora tiene control del MCP.` + } else { + return 'Error al transferir la antorcha' + } + } + }, + { + name: 'request_torch', + description: 'Solicita la antorcha (control del MCP) para este browser', + category: 'global', + schema: { + type: 'object', + properties: {} + }, + handler: async () => { + const status = getTorchStatus() + + if (status.hasTorch) { + return 'Ya tienes la antorcha' + } + + const success = await requestTorch() + if (success) { + return 'Antorcha solicitada. Conectando al MCP...' + } else { + return 'Error al solicitar la antorcha' + } + } + }, + { + name: 'release_torch', + description: 'Libera la antorcha (desconecta del MCP y permite que otro browser la tome)', + category: 'global', + schema: { + type: 'object', + properties: {} + }, + handler: async () => { + const status = getTorchStatus() + + if (!status.hasTorch) { + return 'No tienes la antorcha para liberar' + } + + const success = await releaseTorch() + if (success) { + return 'Antorcha liberada. Desconectando del MCP...' + } else { + return 'Error al liberar la antorcha' + } + } + } + ] +} diff --git a/frontend/src/services/tools/toolDefinitions.ts b/frontend/src/services/tools/toolDefinitions.ts index b06cd57..c981310 100644 --- a/frontend/src/services/tools/toolDefinitions.ts +++ b/frontend/src/services/tools/toolDefinitions.ts @@ -1,4 +1,4 @@ -export type ToolCategory = 'global' | 'canvas' | 'component' | 'theme' | 'database' | 'source' | 'project' | 'terminal' | 'git' +export type ToolCategory = 'global' | 'canvas' | 'component' | 'theme' | 'database' | 'source' | 'project' | 'terminal' | 'git' | 'torch' export interface ToolMeta { name: string @@ -75,7 +75,14 @@ export const ALL_TOOL_METAS: ToolMeta[] = [ { name: 'get_git_diff', description: 'Obtiene el diff de cambios no commiteados', category: 'git' }, { name: 'compare_commits', description: 'Compara dos commits o ramas', category: 'git' }, { name: 'git_log', description: 'Obtiene el historial de commits', category: 'git' }, - { name: 'get_git_branches', description: 'Lista todas las ramas del repositorio', category: 'git' } + { name: 'get_git_branches', description: 'Lista todas las ramas del repositorio', category: 'git' }, + + // Torch tools (multi-browser MCP control) + { name: 'list_torch_clients', description: 'Lista browsers conectados al sistema de antorcha', category: 'torch' }, + { name: 'get_torch_status', description: 'Obtiene quien tiene el control del MCP', category: 'torch' }, + { name: 'transfer_torch', description: 'Transfiere el control a otro browser', category: 'torch' }, + { name: 'request_torch', description: 'Solicita el control del MCP', category: 'torch' }, + { name: 'release_torch', description: 'Libera el control del MCP', category: 'torch' } ] // Get all tool names diff --git a/frontend/src/services/torch.ts b/frontend/src/services/torch.ts index e9ee046..fc03742 100644 --- a/frontend/src/services/torch.ts +++ b/frontend/src/services/torch.ts @@ -144,6 +144,40 @@ export async function releaseTorch(): Promise { return true } +/** + * Transfer the torch to a specific client + */ +export async function transferTorch(targetId: string): Promise { + if (!torchWs || torchWs.readyState !== WebSocket.OPEN) { + console.error('[Torch] Not connected to server') + return false + } + + torchWs.send(JSON.stringify({ type: 'transfer', targetId })) + return true +} + +/** + * Get list of connected clients + */ +export function getTorchClients() { + const torchStore = useTorchStore() + return torchStore.clients +} + +/** + * Get current torch status + */ +export function getTorchStatus() { + const torchStore = useTorchStore() + return { + clientId: torchStore.clientId, + hasTorch: torchStore.hasTorch, + torchHolderId: torchStore.torchHolderId, + clientCount: torchStore.clients.length + } +} + /** * Connect to MCP (when we have the torch) */ diff --git a/server/services/handlers/torch-handler.ts b/server/services/handlers/torch-handler.ts index 50152dd..3ce7924 100644 --- a/server/services/handlers/torch-handler.ts +++ b/server/services/handlers/torch-handler.ts @@ -102,6 +102,43 @@ export function handleTorchMessage(ws: any, data: any, broadcast: (message: stri } break } + + case 'transfer': { + // Transfer torch to a specific client (only if sender has torch or no one has it) + const targetId = data.targetId + if (!targetId) { + ws.send(JSON.stringify({ type: 'error', message: 'targetId required' })) + break + } + + // Check if target exists + let targetExists = false + for (const [, c] of torchClients) { + if (c.id === targetId) { + targetExists = true + break + } + } + + if (!targetExists) { + ws.send(JSON.stringify({ type: 'error', message: 'Target client not found' })) + break + } + + // Only allow transfer if sender has torch or no one has it + if (torchHolderId !== null && torchHolderId !== client.id) { + ws.send(JSON.stringify({ type: 'error', message: 'Cannot transfer - you do not have the torch' })) + break + } + + const previousHolder = torchHolderId + torchHolderId = targetId + + ws.send(JSON.stringify({ type: 'transferred', targetId })) + console.log(`[Torch] Transferred by ${client.id}: ${previousHolder} → ${targetId}`) + broadcastTorchState(broadcast) + break + } } }