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:
@@ -24,6 +24,7 @@ import {
|
|||||||
createTerminalHandlers,
|
createTerminalHandlers,
|
||||||
createResponseHandlers,
|
createResponseHandlers,
|
||||||
createGitHandlers,
|
createGitHandlers,
|
||||||
|
createTorchHandlers,
|
||||||
type ToolConfig
|
type ToolConfig
|
||||||
} from './tools/handlers'
|
} from './tools/handlers'
|
||||||
import { setRouter } from './tools/handlers/globalHandlers'
|
import { setRouter } from './tools/handlers/globalHandlers'
|
||||||
@@ -125,7 +126,8 @@ function getToolConfigs(): Map<string, ToolConfig> {
|
|||||||
...createSourceCodeHandlers(),
|
...createSourceCodeHandlers(),
|
||||||
...createTerminalHandlers(),
|
...createTerminalHandlers(),
|
||||||
...createResponseHandlers(),
|
...createResponseHandlers(),
|
||||||
...createGitHandlers()
|
...createGitHandlers(),
|
||||||
|
...createTorchHandlers()
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const config of allHandlers) {
|
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'],
|
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'],
|
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'],
|
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
|
// Page to categories mapping
|
||||||
const pageCategories: Record<PageName, ToolCategory[]> = {
|
const pageCategories: Record<PageName, ToolCategory[]> = {
|
||||||
home: ['global', 'canvas', 'component', 'project', 'terminal'],
|
home: ['global', 'torch', 'canvas', 'component', 'project', 'terminal'],
|
||||||
canvas: ['global', 'canvas', 'component', 'terminal'],
|
canvas: ['global', 'torch', 'canvas', 'component', 'terminal'],
|
||||||
'project-canvas': ['global', 'canvas', 'component', 'project', 'terminal'],
|
'project-canvas': ['global', 'torch', 'canvas', 'component', 'project', 'terminal'],
|
||||||
projects: ['global', 'project', 'terminal'],
|
projects: ['global', 'torch', 'project', 'terminal'],
|
||||||
components: ['global', 'component', 'terminal'],
|
components: ['global', 'torch', 'component', 'terminal'],
|
||||||
themes: ['global', 'theme', 'terminal'],
|
themes: ['global', 'torch', 'theme', 'terminal'],
|
||||||
database: ['global', 'database', 'terminal'],
|
database: ['global', 'torch', 'database', 'terminal'],
|
||||||
source: ['global', 'source', 'terminal'],
|
source: ['global', 'torch', 'source', 'terminal'],
|
||||||
terminal: ['global', 'terminal'],
|
terminal: ['global', 'torch', 'terminal'],
|
||||||
tools: ['global', 'terminal'],
|
tools: ['global', 'torch', 'terminal'],
|
||||||
git: ['global', 'git', 'terminal']
|
git: ['global', 'torch', 'git', 'terminal']
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentPage: PageName | null = null
|
let currentPage: PageName | null = null
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ export type { TerminalControls } from './terminalHandlers'
|
|||||||
export { createResponseHandlers, setResponseControls } from './responseHandlers'
|
export { createResponseHandlers, setResponseControls } from './responseHandlers'
|
||||||
export type { ResponseControls } from './responseHandlers'
|
export type { ResponseControls } from './responseHandlers'
|
||||||
export { createGitHandlers } from './gitHandlers'
|
export { createGitHandlers } from './gitHandlers'
|
||||||
|
export { createTorchHandlers } from './torchHandlers'
|
||||||
|
|
||||||
export type ToolHandler = (args: any) => string | Promise<string>
|
export type ToolHandler = (args: any) => string | Promise<string>
|
||||||
|
|
||||||
export interface ToolConfig {
|
export interface ToolConfig {
|
||||||
name: string
|
name: string
|
||||||
description: 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
|
schema: object
|
||||||
handler: ToolHandler
|
handler: ToolHandler
|
||||||
}
|
}
|
||||||
|
|||||||
154
frontend/src/services/tools/handlers/torchHandlers.ts
Normal file
154
frontend/src/services/tools/handlers/torchHandlers.ts
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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 {
|
export interface ToolMeta {
|
||||||
name: string
|
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: '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: 'compare_commits', description: 'Compara dos commits o ramas', category: 'git' },
|
||||||
{ name: 'git_log', description: 'Obtiene el historial de commits', 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
|
// Get all tool names
|
||||||
|
|||||||
@@ -144,6 +144,40 @@ export async function releaseTorch(): Promise<boolean> {
|
|||||||
return true
|
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)
|
* Connect to MCP (when we have the torch)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -102,6 +102,43 @@ export function handleTorchMessage(ws: any, data: any, broadcast: (message: stri
|
|||||||
}
|
}
|
||||||
break
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user