feat: Add dynamic MCP tool registration per page
- webmcp.ts: Add tool tracking, unregisterTool(), clearAllTools() - tools/canvasTools.ts: render_html, render_vue_component - tools/componentTools.ts: save/load/list/delete_vue_component - tools/themeTools.ts: get_design_tokens, get_active_theme, set_theme_variable, save_theme - tools/globalTools.ts: get_current_page, navigate_to, list_available_tools - toolRegistry.ts: Orchestrates tool registration per page - App.vue: Initializes WebMCP once, watches route for tool changes - Canvas.vue: Removed tool registration (now handled by registry) Tools are now registered when entering a page and unregistered when leaving. Refresh initializes tools correctly for the current page. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,10 @@
|
|||||||
"Bash(C:Usersjodar.bunbinbun.exe create vite . --template vue-ts)",
|
"Bash(C:Usersjodar.bunbinbun.exe create vite . --template vue-ts)",
|
||||||
"mcp__agent-ui___webmcp_get-token",
|
"mcp__agent-ui___webmcp_get-token",
|
||||||
"mcp__agent-ui___webmcp_quitar-tool",
|
"mcp__agent-ui___webmcp_quitar-tool",
|
||||||
"mcp__agent-ui__localhost_3000-render_html"
|
"mcp__agent-ui__localhost_3000-render_html",
|
||||||
|
"mcp__agent-ui__localhost_4100-navigate_to",
|
||||||
|
"mcp__agent-ui__localhost_4100-get_design_tokens",
|
||||||
|
"mcp__agent-ui__localhost_4100-set_theme_variable"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"enableAllProjectMcpServers": true,
|
"enableAllProjectMcpServers": true,
|
||||||
|
|||||||
@@ -1,8 +1,33 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterView } from 'vue-router'
|
import { onMounted, watch } from 'vue'
|
||||||
|
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||||
import StatusBar from './components/StatusBar.vue'
|
import StatusBar from './components/StatusBar.vue'
|
||||||
import Toolbar from './components/Toolbar.vue'
|
import Toolbar from './components/Toolbar.vue'
|
||||||
import ComponentsDropdown from './components/ComponentsDropdown.vue'
|
import ComponentsDropdown from './components/ComponentsDropdown.vue'
|
||||||
|
import { initWebMCP } from './services/webmcp'
|
||||||
|
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Initialize WebMCP connection
|
||||||
|
await initWebMCP()
|
||||||
|
|
||||||
|
// Initialize tool registry with router
|
||||||
|
initToolRegistry(router)
|
||||||
|
|
||||||
|
// Initialize tools for current page (handles refresh)
|
||||||
|
const currentPage = (route.name as string) || 'canvas'
|
||||||
|
initToolsOnRefresh(currentPage as 'canvas' | 'components' | 'themes')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for route changes and update tools
|
||||||
|
watch(() => route.name, (newPage) => {
|
||||||
|
if (newPage) {
|
||||||
|
activatePageTools(newPage as 'canvas' | 'components' | 'themes')
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
import { initWebMCP } from '../services/webmcp'
|
|
||||||
import { registerCanvasTools } from '../services/canvasTools'
|
|
||||||
import { renderInlineComponent, type VueComponentDefinition } from '../services/dynamicComponents'
|
import { renderInlineComponent, type VueComponentDefinition } from '../services/dynamicComponents'
|
||||||
import { useCanvasStore } from '../stores/canvas'
|
import { useCanvasStore } from '../stores/canvas'
|
||||||
|
|
||||||
@@ -34,11 +32,8 @@ function handleLoadComponent(e: Event) {
|
|||||||
canvasStore.addToHistory({ tool: 'load_vue_component', args: detail, timestamp: Date.now() })
|
canvasStore.addToHistory({ tool: 'load_vue_component', args: detail, timestamp: Date.now() })
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(() => {
|
||||||
window.addEventListener('load-vue-component', handleLoadComponent)
|
window.addEventListener('load-vue-component', handleLoadComponent)
|
||||||
|
|
||||||
await initWebMCP()
|
|
||||||
registerCanvasTools()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@@ -1,316 +0,0 @@
|
|||||||
import { useCanvasStore } from '../stores/canvas'
|
|
||||||
import { useThemeStore } from '../stores/theme'
|
|
||||||
import { registerTool } from './webmcp'
|
|
||||||
import {
|
|
||||||
renderInlineComponent,
|
|
||||||
componentsApi,
|
|
||||||
type VueComponentDefinition
|
|
||||||
} from './dynamicComponents'
|
|
||||||
|
|
||||||
function getCanvasContainer() {
|
|
||||||
return document.getElementById('canvas-content')
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePlaceholder(container: HTMLElement) {
|
|
||||||
const placeholder = container.querySelector('.canvas-placeholder')
|
|
||||||
if (placeholder) placeholder.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitComponentRendered(args: any) {
|
|
||||||
window.dispatchEvent(new CustomEvent('vue-component-rendered', {
|
|
||||||
detail: {
|
|
||||||
id: args.id,
|
|
||||||
name: args.name,
|
|
||||||
template: args.template,
|
|
||||||
setup: args.setup,
|
|
||||||
style: args.style,
|
|
||||||
props: args.props,
|
|
||||||
imports: args.imports
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerCanvasTools() {
|
|
||||||
const canvasStore = useCanvasStore()
|
|
||||||
|
|
||||||
// render_html
|
|
||||||
registerTool(
|
|
||||||
'render_html',
|
|
||||||
'Renderiza HTML en el canvas. Soporta <script> tags que se ejecutan automáticamente y <style> tags.',
|
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
html: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'El código HTML a renderizar (puede incluir <script> y <style> tags)'
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: 'string',
|
|
||||||
enum: ['replace', 'append', 'prepend'],
|
|
||||||
description: 'Modo: replace (reemplaza), append (agrega al final), prepend (al inicio)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ['html']
|
|
||||||
},
|
|
||||||
(args: { html: string; mode?: string }) => {
|
|
||||||
const container = getCanvasContainer()
|
|
||||||
if (!container) return 'Error: canvas no encontrado'
|
|
||||||
|
|
||||||
removePlaceholder(container)
|
|
||||||
|
|
||||||
const mode = args.mode || 'replace'
|
|
||||||
if (mode === 'replace') {
|
|
||||||
container.innerHTML = args.html
|
|
||||||
} else if (mode === 'append') {
|
|
||||||
container.insertAdjacentHTML('beforeend', args.html)
|
|
||||||
} else if (mode === 'prepend') {
|
|
||||||
container.insertAdjacentHTML('afterbegin', args.html)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ejecutar scripts inline
|
|
||||||
const scripts = container.querySelectorAll('script')
|
|
||||||
scripts.forEach((oldScript) => {
|
|
||||||
const newScript = document.createElement('script')
|
|
||||||
Array.from(oldScript.attributes).forEach(attr => {
|
|
||||||
newScript.setAttribute(attr.name, attr.value)
|
|
||||||
})
|
|
||||||
newScript.textContent = oldScript.textContent
|
|
||||||
oldScript.parentNode?.replaceChild(newScript, oldScript)
|
|
||||||
})
|
|
||||||
|
|
||||||
canvasStore.addToHistory({ tool: 'render_html', args, timestamp: Date.now() })
|
|
||||||
return 'HTML renderizado'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// render_vue_component
|
|
||||||
registerTool(
|
|
||||||
'render_vue_component',
|
|
||||||
'Renderiza un componente Vue 3 completo con acceso a ref, reactive, computed, watch, Pinia stores, etc.',
|
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
id: { type: 'string', description: 'ID único del componente' },
|
|
||||||
name: { type: 'string', description: 'Nombre del componente (ej: MyCounter)' },
|
|
||||||
template: { type: 'string', description: 'Template HTML del componente con sintaxis Vue' },
|
|
||||||
setup: { type: 'string', description: 'Código de la función setup (debe retornar un objeto con las propiedades reactivas)' },
|
|
||||||
style: { type: 'string', description: 'CSS del componente (opcional)' },
|
|
||||||
props: { type: 'array', items: { type: 'string' }, description: 'Lista de props que acepta el componente' },
|
|
||||||
imports: { type: 'array', items: { type: 'string' }, description: 'Funciones de Vue a importar: ref, reactive, computed, watch, watchEffect, onMounted, onUnmounted, nextTick, h' },
|
|
||||||
componentProps: { type: 'object', description: 'Valores para las props del componente' },
|
|
||||||
mode: { type: 'string', enum: ['replace', 'append'], description: 'replace: limpia el canvas, append: agrega al final' }
|
|
||||||
},
|
|
||||||
required: ['id', 'name', 'template']
|
|
||||||
},
|
|
||||||
(args: {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
template: string
|
|
||||||
setup?: string
|
|
||||||
style?: string
|
|
||||||
props?: string[]
|
|
||||||
imports?: string[]
|
|
||||||
componentProps?: Record<string, any>
|
|
||||||
mode?: string
|
|
||||||
}) => {
|
|
||||||
const container = getCanvasContainer()
|
|
||||||
if (!container) return 'Error: canvas no encontrado'
|
|
||||||
|
|
||||||
removePlaceholder(container)
|
|
||||||
|
|
||||||
const definition: VueComponentDefinition = {
|
|
||||||
id: args.id,
|
|
||||||
name: args.name,
|
|
||||||
template: args.template,
|
|
||||||
setup: args.setup,
|
|
||||||
style: args.style,
|
|
||||||
props: args.props,
|
|
||||||
imports: args.imports || ['ref', 'reactive', 'computed']
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAppend = args.mode === 'append'
|
|
||||||
const result = renderInlineComponent(definition, container, args.componentProps || {}, isAppend)
|
|
||||||
|
|
||||||
;(window as any).__vueComponentUnmount = result.unmount
|
|
||||||
|
|
||||||
emitComponentRendered(args)
|
|
||||||
|
|
||||||
canvasStore.addToHistory({ tool: 'render_vue_component', args, timestamp: Date.now() })
|
|
||||||
return `Componente Vue "${args.name}" renderizado correctamente`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// save_vue_component
|
|
||||||
registerTool(
|
|
||||||
'save_vue_component',
|
|
||||||
'Guarda un componente Vue en la base de datos para reutilizarlo después',
|
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
id: { type: 'string', description: 'ID único del componente (se genera automáticamente si no se proporciona)' },
|
|
||||||
name: { type: 'string', description: 'Nombre del componente' },
|
|
||||||
template: { type: 'string', description: 'Template HTML del componente' },
|
|
||||||
setup: { type: 'string', description: 'Código de la función setup' },
|
|
||||||
style: { type: 'string', description: 'CSS del componente' },
|
|
||||||
props: { type: 'array', items: { type: 'string' }, description: 'Lista de props' },
|
|
||||||
imports: { type: 'array', items: { type: 'string' }, description: 'Funciones de Vue necesarias' }
|
|
||||||
},
|
|
||||||
required: ['name', 'template']
|
|
||||||
},
|
|
||||||
async (args: Omit<VueComponentDefinition, 'id'> & { id?: string }) => {
|
|
||||||
try {
|
|
||||||
const result = await componentsApi.save({
|
|
||||||
id: args.id || `comp-${Date.now()}`,
|
|
||||||
name: args.name,
|
|
||||||
template: args.template,
|
|
||||||
setup: args.setup,
|
|
||||||
style: args.style,
|
|
||||||
props: args.props,
|
|
||||||
imports: args.imports
|
|
||||||
})
|
|
||||||
canvasStore.addToHistory({ tool: 'save_vue_component', args, timestamp: Date.now() })
|
|
||||||
return `Componente "${args.name}" guardado con ID: ${result.id}`
|
|
||||||
} catch (e: any) {
|
|
||||||
return `Error al guardar: ${e.message}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// load_vue_component
|
|
||||||
registerTool(
|
|
||||||
'load_vue_component',
|
|
||||||
'Carga un componente Vue guardado desde la base de datos y lo renderiza',
|
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
id: { type: 'string', description: 'ID del componente a cargar' },
|
|
||||||
componentProps: { type: 'object', description: 'Props para pasar al componente' },
|
|
||||||
mode: { type: 'string', enum: ['replace', 'append'], description: 'replace: limpia el canvas, append: agrega al final' }
|
|
||||||
},
|
|
||||||
required: ['id']
|
|
||||||
},
|
|
||||||
async (args: { id: string; componentProps?: Record<string, any>; mode?: string }) => {
|
|
||||||
try {
|
|
||||||
const definition = await componentsApi.getById(args.id)
|
|
||||||
if (!definition) {
|
|
||||||
return `Error: Componente con ID "${args.id}" no encontrado`
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = getCanvasContainer()
|
|
||||||
if (!container) return 'Error: canvas no encontrado'
|
|
||||||
|
|
||||||
removePlaceholder(container)
|
|
||||||
|
|
||||||
const isAppend = args.mode === 'append'
|
|
||||||
const result = renderInlineComponent(definition, container, args.componentProps || {}, isAppend)
|
|
||||||
;(window as any).__vueComponentUnmount = result.unmount
|
|
||||||
|
|
||||||
canvasStore.addToHistory({ tool: 'load_vue_component', args, timestamp: Date.now() })
|
|
||||||
return `Componente "${definition.name}" cargado y renderizado`
|
|
||||||
} catch (e: any) {
|
|
||||||
return `Error: ${e.message}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// list_vue_components
|
|
||||||
registerTool(
|
|
||||||
'list_vue_components',
|
|
||||||
'Lista todos los componentes Vue guardados en la base de datos',
|
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
properties: {}
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const components = await componentsApi.getAll()
|
|
||||||
if (components.length === 0) {
|
|
||||||
return 'No hay componentes guardados'
|
|
||||||
}
|
|
||||||
const list = components.map(c => `- ${c.id}: ${c.name}`).join('\n')
|
|
||||||
return `Componentes guardados:\n${list}`
|
|
||||||
} catch (e: any) {
|
|
||||||
return `Error: ${e.message}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// delete_vue_component
|
|
||||||
registerTool(
|
|
||||||
'delete_vue_component',
|
|
||||||
'Elimina un componente Vue de la base de datos',
|
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
id: { type: 'string', description: 'ID del componente a eliminar' }
|
|
||||||
},
|
|
||||||
required: ['id']
|
|
||||||
},
|
|
||||||
async (args: { id: string }) => {
|
|
||||||
try {
|
|
||||||
await componentsApi.delete(args.id)
|
|
||||||
return `Componente "${args.id}" eliminado`
|
|
||||||
} catch (e: any) {
|
|
||||||
return `Error: ${e.message}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// get_design_tokens
|
|
||||||
registerTool(
|
|
||||||
'get_design_tokens',
|
|
||||||
'Obtiene los design tokens y guía de estilos del tema activo. Usa esto para crear componentes con estilos consistentes.',
|
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
category: {
|
|
||||||
type: 'string',
|
|
||||||
enum: ['all', 'colors', 'text', 'accent', 'semantic', 'spacing', 'typography', 'effects'],
|
|
||||||
description: 'Categoría específica de tokens. Por defecto "all" retorna todos.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async (args: { category?: string }) => {
|
|
||||||
try {
|
|
||||||
const themeStore = useThemeStore()
|
|
||||||
const theme = themeStore.activeTheme
|
|
||||||
|
|
||||||
if (!theme) {
|
|
||||||
return 'No hay tema activo. Usa las variables CSS por defecto.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const category = args.category || 'all'
|
|
||||||
const variables = theme.variables
|
|
||||||
|
|
||||||
if (category !== 'all' && variables[category as keyof typeof variables]) {
|
|
||||||
const categoryVars = variables[category as keyof typeof variables]
|
|
||||||
const tokenList = Object.entries(categoryVars)
|
|
||||||
.map(([name, value]) => `--${name}: ${value}`)
|
|
||||||
.join('\n')
|
|
||||||
|
|
||||||
return `Design Tokens - ${category.toUpperCase()}:\n\n${tokenList}\n\nUsa estas variables CSS en tus estilos para mantener consistencia con el tema.`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return all tokens organized by category
|
|
||||||
const allTokens = Object.entries(variables)
|
|
||||||
.map(([cat, vars]) => {
|
|
||||||
const tokenList = Object.entries(vars as Record<string, string>)
|
|
||||||
.map(([name, value]) => ` --${name}: ${value}`)
|
|
||||||
.join('\n')
|
|
||||||
return `[${cat.toUpperCase()}]\n${tokenList}`
|
|
||||||
})
|
|
||||||
.join('\n\n')
|
|
||||||
|
|
||||||
return `Design Tokens del tema "${theme.name}":\n\n${allTokens}\n\n` +
|
|
||||||
`GUÍA DE USO:\n` +
|
|
||||||
`- Usa var(--nombre-variable) en CSS\n` +
|
|
||||||
`- Los componentes dinámicos tienen acceso a $theme.getVariable('nombre')\n` +
|
|
||||||
`- Puedes modificar temporalmente con $theme.setVariable('nombre', 'valor')\n` +
|
|
||||||
`- Colores semánticos: success, warning, error, info (con -bg para fondos)\n` +
|
|
||||||
`- Radius: radius-sm (4px), radius-md (8px), radius-lg (12px), radius-full (9999px)`
|
|
||||||
} catch (e: any) {
|
|
||||||
return `Error: ${e.message}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
143
frontend/src/services/toolRegistry.ts
Normal file
143
frontend/src/services/toolRegistry.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { clearAllTools } from './webmcp'
|
||||||
|
import {
|
||||||
|
registerCanvasTools,
|
||||||
|
unregisterCanvasTools,
|
||||||
|
CANVAS_TOOLS
|
||||||
|
} from './tools/canvasTools'
|
||||||
|
import {
|
||||||
|
registerComponentTools,
|
||||||
|
unregisterComponentTools,
|
||||||
|
COMPONENT_TOOLS
|
||||||
|
} from './tools/componentTools'
|
||||||
|
import {
|
||||||
|
registerThemeTools,
|
||||||
|
unregisterThemeTools,
|
||||||
|
THEME_TOOLS
|
||||||
|
} from './tools/themeTools'
|
||||||
|
import {
|
||||||
|
registerGlobalTools,
|
||||||
|
setRouter,
|
||||||
|
GLOBAL_TOOLS
|
||||||
|
} from './tools/globalTools'
|
||||||
|
|
||||||
|
type PageName = 'canvas' | 'components' | 'themes'
|
||||||
|
|
||||||
|
interface PageToolSet {
|
||||||
|
register: () => void
|
||||||
|
unregister: () => void
|
||||||
|
toolNames: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageTools: Record<PageName, PageToolSet> = {
|
||||||
|
canvas: {
|
||||||
|
register: () => {
|
||||||
|
registerCanvasTools()
|
||||||
|
registerComponentTools() // Canvas también puede guardar/cargar
|
||||||
|
},
|
||||||
|
unregister: () => {
|
||||||
|
unregisterCanvasTools()
|
||||||
|
unregisterComponentTools()
|
||||||
|
},
|
||||||
|
toolNames: [...CANVAS_TOOLS, ...COMPONENT_TOOLS]
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
register: registerComponentTools,
|
||||||
|
unregister: unregisterComponentTools,
|
||||||
|
toolNames: COMPONENT_TOOLS
|
||||||
|
},
|
||||||
|
themes: {
|
||||||
|
register: registerThemeTools,
|
||||||
|
unregister: unregisterThemeTools,
|
||||||
|
toolNames: THEME_TOOLS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentPage: PageName | null = null
|
||||||
|
let isInitialized = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicializa el registry con el router de Vue
|
||||||
|
*/
|
||||||
|
export function initToolRegistry(router: any) {
|
||||||
|
setRouter(router)
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activa las tools para una página específica.
|
||||||
|
* Desregistra las tools de otras páginas primero.
|
||||||
|
*/
|
||||||
|
export function activatePageTools(pageName: PageName) {
|
||||||
|
if (!isInitialized) {
|
||||||
|
console.warn('[ToolRegistry] Not initialized. Call initToolRegistry first.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si ya estamos en esta página, no hacer nada
|
||||||
|
if (currentPage === pageName) {
|
||||||
|
console.log(`[ToolRegistry] Already on page "${pageName}", skipping`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[ToolRegistry] Switching from "${currentPage}" to "${pageName}"`)
|
||||||
|
|
||||||
|
// Desregistrar tools de la página anterior
|
||||||
|
if (currentPage && pageTools[currentPage]) {
|
||||||
|
pageTools[currentPage].unregister()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registrar tools de la nueva página
|
||||||
|
if (pageTools[pageName]) {
|
||||||
|
pageTools[pageName].register()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asegurar que las tools globales estén registradas
|
||||||
|
registerGlobalTools()
|
||||||
|
|
||||||
|
currentPage = pageName
|
||||||
|
|
||||||
|
console.log(`[ToolRegistry] Page "${pageName}" tools activated`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicializa las tools para un refresh de página.
|
||||||
|
* Limpia todo y registra las tools correctas.
|
||||||
|
*/
|
||||||
|
export function initToolsOnRefresh(pageName: PageName) {
|
||||||
|
if (!isInitialized) {
|
||||||
|
console.warn('[ToolRegistry] Not initialized. Call initToolRegistry first.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[ToolRegistry] Initializing on refresh for page "${pageName}"`)
|
||||||
|
|
||||||
|
// Limpiar todas las tools existentes
|
||||||
|
clearAllTools()
|
||||||
|
|
||||||
|
// Reset current page tracking
|
||||||
|
currentPage = null
|
||||||
|
|
||||||
|
// Activar tools de la página actual
|
||||||
|
activatePageTools(pageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene el nombre de la página actual
|
||||||
|
*/
|
||||||
|
export function getCurrentPage(): PageName | null {
|
||||||
|
return currentPage
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene los nombres de las tools para una página
|
||||||
|
*/
|
||||||
|
export function getPageToolNames(pageName: PageName): string[] {
|
||||||
|
return [...(pageTools[pageName]?.toolNames || []), ...GLOBAL_TOOLS]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si el registry está inicializado
|
||||||
|
*/
|
||||||
|
export function isRegistryInitialized(): boolean {
|
||||||
|
return isInitialized
|
||||||
|
}
|
||||||
146
frontend/src/services/tools/canvasTools.ts
Normal file
146
frontend/src/services/tools/canvasTools.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { useCanvasStore } from '../../stores/canvas'
|
||||||
|
import { registerTool, unregisterTools } from '../webmcp'
|
||||||
|
import {
|
||||||
|
renderInlineComponent,
|
||||||
|
type VueComponentDefinition
|
||||||
|
} from '../dynamicComponents'
|
||||||
|
|
||||||
|
export const CANVAS_TOOLS = ['render_html', 'render_vue_component']
|
||||||
|
|
||||||
|
function getCanvasContainer() {
|
||||||
|
return document.getElementById('canvas-content')
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePlaceholder(container: HTMLElement) {
|
||||||
|
const placeholder = container.querySelector('.canvas-placeholder')
|
||||||
|
if (placeholder) placeholder.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitComponentRendered(args: any) {
|
||||||
|
window.dispatchEvent(new CustomEvent('vue-component-rendered', {
|
||||||
|
detail: {
|
||||||
|
id: args.id,
|
||||||
|
name: args.name,
|
||||||
|
template: args.template,
|
||||||
|
setup: args.setup,
|
||||||
|
style: args.style,
|
||||||
|
props: args.props,
|
||||||
|
imports: args.imports
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerCanvasTools() {
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
|
// render_html
|
||||||
|
registerTool(
|
||||||
|
'render_html',
|
||||||
|
'Renderiza HTML en el canvas. Soporta <script> tags que se ejecutan automáticamente y <style> tags.',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
html: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'El código HTML a renderizar (puede incluir <script> y <style> tags)'
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['replace', 'append', 'prepend'],
|
||||||
|
description: 'Modo: replace (reemplaza), append (agrega al final), prepend (al inicio)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['html']
|
||||||
|
},
|
||||||
|
(args: { html: string; mode?: string }) => {
|
||||||
|
const container = getCanvasContainer()
|
||||||
|
if (!container) return 'Error: canvas no encontrado'
|
||||||
|
|
||||||
|
removePlaceholder(container)
|
||||||
|
|
||||||
|
const mode = args.mode || 'replace'
|
||||||
|
if (mode === 'replace') {
|
||||||
|
container.innerHTML = args.html
|
||||||
|
} else if (mode === 'append') {
|
||||||
|
container.insertAdjacentHTML('beforeend', args.html)
|
||||||
|
} else if (mode === 'prepend') {
|
||||||
|
container.insertAdjacentHTML('afterbegin', args.html)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ejecutar scripts inline
|
||||||
|
const scripts = container.querySelectorAll('script')
|
||||||
|
scripts.forEach((oldScript) => {
|
||||||
|
const newScript = document.createElement('script')
|
||||||
|
Array.from(oldScript.attributes).forEach(attr => {
|
||||||
|
newScript.setAttribute(attr.name, attr.value)
|
||||||
|
})
|
||||||
|
newScript.textContent = oldScript.textContent
|
||||||
|
oldScript.parentNode?.replaceChild(newScript, oldScript)
|
||||||
|
})
|
||||||
|
|
||||||
|
canvasStore.addToHistory({ tool: 'render_html', args, timestamp: Date.now() })
|
||||||
|
return 'HTML renderizado'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// render_vue_component
|
||||||
|
registerTool(
|
||||||
|
'render_vue_component',
|
||||||
|
'Renderiza un componente Vue 3 completo con acceso a ref, reactive, computed, watch, Pinia stores, etc.',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'ID único del componente' },
|
||||||
|
name: { type: 'string', description: 'Nombre del componente (ej: MyCounter)' },
|
||||||
|
template: { type: 'string', description: 'Template HTML del componente con sintaxis Vue' },
|
||||||
|
setup: { type: 'string', description: 'Código de la función setup (debe retornar un objeto con las propiedades reactivas)' },
|
||||||
|
style: { type: 'string', description: 'CSS del componente (opcional)' },
|
||||||
|
props: { type: 'array', items: { type: 'string' }, description: 'Lista de props que acepta el componente' },
|
||||||
|
imports: { type: 'array', items: { type: 'string' }, description: 'Funciones de Vue a importar: ref, reactive, computed, watch, watchEffect, onMounted, onUnmounted, nextTick, h' },
|
||||||
|
componentProps: { type: 'object', description: 'Valores para las props del componente' },
|
||||||
|
mode: { type: 'string', enum: ['replace', 'append'], description: 'replace: limpia el canvas, append: agrega al final' }
|
||||||
|
},
|
||||||
|
required: ['id', 'name', 'template']
|
||||||
|
},
|
||||||
|
(args: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
template: string
|
||||||
|
setup?: string
|
||||||
|
style?: string
|
||||||
|
props?: string[]
|
||||||
|
imports?: string[]
|
||||||
|
componentProps?: Record<string, any>
|
||||||
|
mode?: string
|
||||||
|
}) => {
|
||||||
|
const container = getCanvasContainer()
|
||||||
|
if (!container) return 'Error: canvas no encontrado'
|
||||||
|
|
||||||
|
removePlaceholder(container)
|
||||||
|
|
||||||
|
const definition: VueComponentDefinition = {
|
||||||
|
id: args.id,
|
||||||
|
name: args.name,
|
||||||
|
template: args.template,
|
||||||
|
setup: args.setup,
|
||||||
|
style: args.style,
|
||||||
|
props: args.props,
|
||||||
|
imports: args.imports || ['ref', 'reactive', 'computed']
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAppend = args.mode === 'append'
|
||||||
|
const result = renderInlineComponent(definition, container, args.componentProps || {}, isAppend)
|
||||||
|
|
||||||
|
;(window as any).__vueComponentUnmount = result.unmount
|
||||||
|
|
||||||
|
emitComponentRendered(args)
|
||||||
|
|
||||||
|
canvasStore.addToHistory({ tool: 'render_vue_component', args, timestamp: Date.now() })
|
||||||
|
return `Componente Vue "${args.name}" renderizado correctamente`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterCanvasTools() {
|
||||||
|
unregisterTools(CANVAS_TOOLS)
|
||||||
|
}
|
||||||
147
frontend/src/services/tools/componentTools.ts
Normal file
147
frontend/src/services/tools/componentTools.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { useCanvasStore } from '../../stores/canvas'
|
||||||
|
import { registerTool, unregisterTools } from '../webmcp'
|
||||||
|
import {
|
||||||
|
renderInlineComponent,
|
||||||
|
componentsApi,
|
||||||
|
type VueComponentDefinition
|
||||||
|
} from '../dynamicComponents'
|
||||||
|
|
||||||
|
export const COMPONENT_TOOLS = [
|
||||||
|
'save_vue_component',
|
||||||
|
'load_vue_component',
|
||||||
|
'list_vue_components',
|
||||||
|
'delete_vue_component'
|
||||||
|
]
|
||||||
|
|
||||||
|
function getCanvasContainer() {
|
||||||
|
return document.getElementById('canvas-content')
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePlaceholder(container: HTMLElement) {
|
||||||
|
const placeholder = container.querySelector('.canvas-placeholder')
|
||||||
|
if (placeholder) placeholder.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerComponentTools() {
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
|
// save_vue_component
|
||||||
|
registerTool(
|
||||||
|
'save_vue_component',
|
||||||
|
'Guarda un componente Vue en la base de datos para reutilizarlo después',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'ID único del componente (se genera automáticamente si no se proporciona)' },
|
||||||
|
name: { type: 'string', description: 'Nombre del componente' },
|
||||||
|
template: { type: 'string', description: 'Template HTML del componente' },
|
||||||
|
setup: { type: 'string', description: 'Código de la función setup' },
|
||||||
|
style: { type: 'string', description: 'CSS del componente' },
|
||||||
|
props: { type: 'array', items: { type: 'string' }, description: 'Lista de props' },
|
||||||
|
imports: { type: 'array', items: { type: 'string' }, description: 'Funciones de Vue necesarias' }
|
||||||
|
},
|
||||||
|
required: ['name', 'template']
|
||||||
|
},
|
||||||
|
async (args: Omit<VueComponentDefinition, 'id'> & { id?: string }) => {
|
||||||
|
try {
|
||||||
|
const result = await componentsApi.save({
|
||||||
|
id: args.id || `comp-${Date.now()}`,
|
||||||
|
name: args.name,
|
||||||
|
template: args.template,
|
||||||
|
setup: args.setup,
|
||||||
|
style: args.style,
|
||||||
|
props: args.props,
|
||||||
|
imports: args.imports
|
||||||
|
})
|
||||||
|
canvasStore.addToHistory({ tool: 'save_vue_component', args, timestamp: Date.now() })
|
||||||
|
return `Componente "${args.name}" guardado con ID: ${result.id}`
|
||||||
|
} catch (e: any) {
|
||||||
|
return `Error al guardar: ${e.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// load_vue_component
|
||||||
|
registerTool(
|
||||||
|
'load_vue_component',
|
||||||
|
'Carga un componente Vue guardado desde la base de datos y lo renderiza',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'ID del componente a cargar' },
|
||||||
|
componentProps: { type: 'object', description: 'Props para pasar al componente' },
|
||||||
|
mode: { type: 'string', enum: ['replace', 'append'], description: 'replace: limpia el canvas, append: agrega al final' }
|
||||||
|
},
|
||||||
|
required: ['id']
|
||||||
|
},
|
||||||
|
async (args: { id: string; componentProps?: Record<string, any>; mode?: string }) => {
|
||||||
|
try {
|
||||||
|
const definition = await componentsApi.getById(args.id)
|
||||||
|
if (!definition) {
|
||||||
|
return `Error: Componente con ID "${args.id}" no encontrado`
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = getCanvasContainer()
|
||||||
|
if (!container) return 'Error: canvas no encontrado'
|
||||||
|
|
||||||
|
removePlaceholder(container)
|
||||||
|
|
||||||
|
const isAppend = args.mode === 'append'
|
||||||
|
const result = renderInlineComponent(definition, container, args.componentProps || {}, isAppend)
|
||||||
|
;(window as any).__vueComponentUnmount = result.unmount
|
||||||
|
|
||||||
|
canvasStore.addToHistory({ tool: 'load_vue_component', args, timestamp: Date.now() })
|
||||||
|
return `Componente "${definition.name}" cargado y renderizado`
|
||||||
|
} catch (e: any) {
|
||||||
|
return `Error: ${e.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// list_vue_components
|
||||||
|
registerTool(
|
||||||
|
'list_vue_components',
|
||||||
|
'Lista todos los componentes Vue guardados en la base de datos',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const components = await componentsApi.getAll()
|
||||||
|
if (components.length === 0) {
|
||||||
|
return 'No hay componentes guardados'
|
||||||
|
}
|
||||||
|
const list = components.map(c => `- ${c.id}: ${c.name}`).join('\n')
|
||||||
|
return `Componentes guardados:\n${list}`
|
||||||
|
} catch (e: any) {
|
||||||
|
return `Error: ${e.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// delete_vue_component
|
||||||
|
registerTool(
|
||||||
|
'delete_vue_component',
|
||||||
|
'Elimina un componente Vue de la base de datos',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'ID del componente a eliminar' }
|
||||||
|
},
|
||||||
|
required: ['id']
|
||||||
|
},
|
||||||
|
async (args: { id: string }) => {
|
||||||
|
try {
|
||||||
|
await componentsApi.delete(args.id)
|
||||||
|
return `Componente "${args.id}" eliminado`
|
||||||
|
} catch (e: any) {
|
||||||
|
return `Error: ${e.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterComponentTools() {
|
||||||
|
unregisterTools(COMPONENT_TOOLS)
|
||||||
|
}
|
||||||
107
frontend/src/services/tools/globalTools.ts
Normal file
107
frontend/src/services/tools/globalTools.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { registerTool, unregisterTools, getRegisteredTools } from '../webmcp'
|
||||||
|
|
||||||
|
export const GLOBAL_TOOLS = [
|
||||||
|
'get_current_page',
|
||||||
|
'navigate_to',
|
||||||
|
'list_available_tools'
|
||||||
|
]
|
||||||
|
|
||||||
|
let routerInstance: any = null
|
||||||
|
|
||||||
|
export function setRouter(router: any) {
|
||||||
|
routerInstance = router
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerGlobalTools() {
|
||||||
|
// get_current_page
|
||||||
|
registerTool(
|
||||||
|
'get_current_page',
|
||||||
|
'Obtiene la página actualmente activa en Agent UI',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (!routerInstance) {
|
||||||
|
return 'Error: Router no disponible'
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = routerInstance.currentRoute.value
|
||||||
|
const pageInfo: Record<string, string> = {
|
||||||
|
canvas: 'Canvas - Renderiza componentes Vue y HTML dinámicamente',
|
||||||
|
components: 'Components - Gestiona componentes guardados en la base de datos',
|
||||||
|
themes: 'Themes - Editor visual de temas y design tokens'
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageName = route.name as string || 'unknown'
|
||||||
|
const description = pageInfo[pageName] || 'Página desconocida'
|
||||||
|
|
||||||
|
return `Página actual: ${pageName}\n` +
|
||||||
|
`Ruta: ${route.path}\n` +
|
||||||
|
`Descripción: ${description}\n\n` +
|
||||||
|
`Herramientas disponibles en esta página:\n${getRegisteredTools().map(t => ` - ${t}`).join('\n')}`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// navigate_to
|
||||||
|
registerTool(
|
||||||
|
'navigate_to',
|
||||||
|
'Navega a una página específica de Agent UI',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
page: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['canvas', 'components', 'themes'],
|
||||||
|
description: 'Página a la que navegar'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['page']
|
||||||
|
},
|
||||||
|
async (args: { page: string }) => {
|
||||||
|
if (!routerInstance) {
|
||||||
|
return 'Error: Router no disponible'
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes: Record<string, string> = {
|
||||||
|
canvas: '/',
|
||||||
|
components: '/components',
|
||||||
|
themes: '/themes'
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = routes[args.page]
|
||||||
|
if (!path) {
|
||||||
|
return `Error: Página "${args.page}" no válida. Opciones: canvas, components, themes`
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await routerInstance.push(path)
|
||||||
|
return `Navegando a ${args.page} (${path})\n\n` +
|
||||||
|
`Nota: Las herramientas MCP se actualizarán automáticamente para esta página.`
|
||||||
|
} catch (e: any) {
|
||||||
|
return `Error al navegar: ${e.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// list_available_tools
|
||||||
|
registerTool(
|
||||||
|
'list_available_tools',
|
||||||
|
'Lista todas las herramientas MCP actualmente disponibles',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
const tools = getRegisteredTools()
|
||||||
|
if (tools.length === 0) {
|
||||||
|
return 'No hay herramientas registradas'
|
||||||
|
}
|
||||||
|
return `Herramientas MCP disponibles (${tools.length}):\n${tools.map(t => ` - ${t}`).join('\n')}`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterGlobalTools() {
|
||||||
|
unregisterTools(GLOBAL_TOOLS)
|
||||||
|
}
|
||||||
139
frontend/src/services/tools/themeTools.ts
Normal file
139
frontend/src/services/tools/themeTools.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { useThemeStore } from '../../stores/theme'
|
||||||
|
import { registerTool, unregisterTools } from '../webmcp'
|
||||||
|
|
||||||
|
export const THEME_TOOLS = [
|
||||||
|
'get_design_tokens',
|
||||||
|
'get_active_theme',
|
||||||
|
'set_theme_variable'
|
||||||
|
]
|
||||||
|
|
||||||
|
export function registerThemeTools() {
|
||||||
|
// get_design_tokens
|
||||||
|
registerTool(
|
||||||
|
'get_design_tokens',
|
||||||
|
'Obtiene los design tokens y guía de estilos del tema activo. Usa esto para crear componentes con estilos consistentes.',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
category: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['all', 'colors', 'text', 'accent', 'semantic', 'spacing', 'typography', 'effects'],
|
||||||
|
description: 'Categoría específica de tokens. Por defecto "all" retorna todos.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async (args: { category?: string }) => {
|
||||||
|
try {
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
const theme = themeStore.activeTheme
|
||||||
|
|
||||||
|
if (!theme) {
|
||||||
|
return 'No hay tema activo. Usa las variables CSS por defecto.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = args.category || 'all'
|
||||||
|
const variables = theme.variables
|
||||||
|
|
||||||
|
if (category !== 'all' && variables[category as keyof typeof variables]) {
|
||||||
|
const categoryVars = variables[category as keyof typeof variables]
|
||||||
|
const tokenList = Object.entries(categoryVars)
|
||||||
|
.map(([name, value]) => `--${name}: ${value}`)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
return `Design Tokens - ${category.toUpperCase()}:\n\n${tokenList}\n\nUsa estas variables CSS en tus estilos para mantener consistencia con el tema.`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all tokens organized by category
|
||||||
|
const allTokens = Object.entries(variables)
|
||||||
|
.map(([cat, vars]) => {
|
||||||
|
const tokenList = Object.entries(vars as Record<string, string>)
|
||||||
|
.map(([name, value]) => ` --${name}: ${value}`)
|
||||||
|
.join('\n')
|
||||||
|
return `[${cat.toUpperCase()}]\n${tokenList}`
|
||||||
|
})
|
||||||
|
.join('\n\n')
|
||||||
|
|
||||||
|
return `Design Tokens del tema "${theme.name}":\n\n${allTokens}\n\n` +
|
||||||
|
`GUÍA DE USO:\n` +
|
||||||
|
`- Usa var(--nombre-variable) en CSS\n` +
|
||||||
|
`- Los componentes dinámicos tienen acceso a $theme.getVariable('nombre')\n` +
|
||||||
|
`- Puedes modificar temporalmente con $theme.setVariable('nombre', 'valor')\n` +
|
||||||
|
`- Colores semánticos: success, warning, error, info (con -bg para fondos)\n` +
|
||||||
|
`- Radius: radius-sm (4px), radius-md (8px), radius-lg (12px), radius-full (9999px)`
|
||||||
|
} catch (e: any) {
|
||||||
|
return `Error: ${e.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// get_active_theme
|
||||||
|
registerTool(
|
||||||
|
'get_active_theme',
|
||||||
|
'Obtiene información del tema actualmente activo',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
try {
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
const theme = themeStore.activeTheme
|
||||||
|
|
||||||
|
if (!theme) {
|
||||||
|
return 'No hay tema activo'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Tema activo: "${theme.name}"\n` +
|
||||||
|
`ID: ${theme.id}\n` +
|
||||||
|
`Sistema: ${theme.is_system ? 'Sí' : 'No'}\n` +
|
||||||
|
`Default: ${theme.is_default ? 'Sí' : 'No'}\n` +
|
||||||
|
`Descripción: ${theme.description || 'Sin descripción'}`
|
||||||
|
} catch (e: any) {
|
||||||
|
return `Error: ${e.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// set_theme_variable
|
||||||
|
registerTool(
|
||||||
|
'set_theme_variable',
|
||||||
|
'Modifica una variable CSS del tema en tiempo real (cambio temporal, no se guarda)',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Nombre de la variable sin el prefijo -- (ej: "accent", "bg-primary")'
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Nuevo valor para la variable (ej: "#ff0000", "12px")'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['name', 'value']
|
||||||
|
},
|
||||||
|
(args: { name: string; value: string }) => {
|
||||||
|
try {
|
||||||
|
const root = document.documentElement
|
||||||
|
const varName = args.name.startsWith('--') ? args.name : `--${args.name}`
|
||||||
|
|
||||||
|
// Get current value for feedback
|
||||||
|
const currentValue = getComputedStyle(root).getPropertyValue(varName).trim()
|
||||||
|
|
||||||
|
// Set new value
|
||||||
|
root.style.setProperty(varName, args.value)
|
||||||
|
|
||||||
|
return `Variable ${varName} cambiada:\n` +
|
||||||
|
` Anterior: ${currentValue || '(no definida)'}\n` +
|
||||||
|
` Nuevo: ${args.value}\n\n` +
|
||||||
|
`Nota: Este cambio es temporal. Para hacerlo permanente, usa el editor de temas en /themes`
|
||||||
|
} catch (e: any) {
|
||||||
|
return `Error: ${e.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterThemeTools() {
|
||||||
|
unregisterTools(THEME_TOOLS)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCanvasStore } from '../stores/canvas'
|
import { useCanvasStore } from '../stores/canvas'
|
||||||
|
|
||||||
let webmcpInstance: any = null
|
let webmcpInstance: any = null
|
||||||
|
const registeredTools = new Set<string>()
|
||||||
|
|
||||||
export async function initWebMCP() {
|
export async function initWebMCP() {
|
||||||
if (webmcpInstance) return webmcpInstance
|
if (webmcpInstance) return webmcpInstance
|
||||||
@@ -46,7 +47,51 @@ export function registerTool(
|
|||||||
) {
|
) {
|
||||||
if (!webmcpInstance) {
|
if (!webmcpInstance) {
|
||||||
console.warn('[WebMCP] Instance not initialized')
|
console.warn('[WebMCP] Instance not initialized')
|
||||||
return
|
return false
|
||||||
|
}
|
||||||
|
if (registeredTools.has(name)) {
|
||||||
|
console.warn(`[WebMCP] Tool "${name}" already registered, skipping`)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
webmcpInstance.registerTool(name, description, schema, handler)
|
webmcpInstance.registerTool(name, description, schema, handler)
|
||||||
|
registeredTools.add(name)
|
||||||
|
console.log(`[WebMCP] Tool registered: ${name}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterTool(name: string) {
|
||||||
|
if (!webmcpInstance) {
|
||||||
|
console.warn('[WebMCP] Instance not initialized')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!registeredTools.has(name)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
webmcpInstance.unregisterTool(name)
|
||||||
|
registeredTools.delete(name)
|
||||||
|
console.log(`[WebMCP] Tool unregistered: ${name}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterTools(names: string[]) {
|
||||||
|
for (const name of names) {
|
||||||
|
unregisterTool(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAllTools() {
|
||||||
|
if (!webmcpInstance) return
|
||||||
|
for (const name of registeredTools) {
|
||||||
|
webmcpInstance.unregisterTool(name)
|
||||||
|
}
|
||||||
|
console.log(`[WebMCP] Cleared ${registeredTools.size} tools`)
|
||||||
|
registeredTools.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRegisteredTools(): string[] {
|
||||||
|
return [...registeredTools]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isToolRegistered(name: string): boolean {
|
||||||
|
return registeredTools.has(name)
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user