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
This commit is contained in:
2026-02-14 17:52:16 -06:00
parent 210e15d8d1
commit 1a51b34228
6 changed files with 252 additions and 16 deletions

View File

@@ -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<string, ToolConfig> {
...createSourceCodeHandlers(),
...createTerminalHandlers(),
...createResponseHandlers(),
...createGitHandlers()
...createGitHandlers(),
...createTorchHandlers()
]
for (const config of allHandlers) {
@@ -145,22 +147,23 @@ const categoryTools: Record<ToolCategory, string[]> = {
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<PageName, ToolCategory[]> = {
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

View File

@@ -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<string>
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
}

View File

@@ -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'
}
}
}
]
}

View File

@@ -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

View File

@@ -144,6 +144,40 @@ export async function releaseTorch(): Promise<boolean> {
return true
}
/**
* Transfer the torch to a specific client
*/
export async function transferTorch(targetId: string): Promise<boolean> {
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)
*/

View File

@@ -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
}
}
}