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:
2026-02-13 05:42:17 -06:00
parent 3c38b75040
commit acde6b37d6
11 changed files with 759 additions and 325 deletions

View File

@@ -22,7 +22,10 @@
"Bash(C:Usersjodar.bunbinbun.exe create vite . --template vue-ts)",
"mcp__agent-ui___webmcp_get-token",
"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,

View File

@@ -1,8 +1,33 @@
<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 Toolbar from './components/Toolbar.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>
<template>

View File

@@ -1,7 +1,5 @@
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { initWebMCP } from '../services/webmcp'
import { registerCanvasTools } from '../services/canvasTools'
import { renderInlineComponent, type VueComponentDefinition } from '../services/dynamicComponents'
import { useCanvasStore } from '../stores/canvas'
@@ -34,11 +32,8 @@ function handleLoadComponent(e: Event) {
canvasStore.addToHistory({ tool: 'load_vue_component', args: detail, timestamp: Date.now() })
}
onMounted(async () => {
onMounted(() => {
window.addEventListener('load-vue-component', handleLoadComponent)
await initWebMCP()
registerCanvasTools()
})
onUnmounted(() => {

View File

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

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

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View File

@@ -1,6 +1,7 @@
import { useCanvasStore } from '../stores/canvas'
let webmcpInstance: any = null
const registeredTools = new Set<string>()
export async function initWebMCP() {
if (webmcpInstance) return webmcpInstance
@@ -46,7 +47,51 @@ export function registerTool(
) {
if (!webmcpInstance) {
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)
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.