From 4450d1e03426605c882f3ac30befc80987523dcf Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 13 Feb 2026 13:46:55 -0600 Subject: [PATCH] feat: Add /tools page with centralized tool registry management - Add ToolsPage for managing MCP tools activation and persistence - Centralize all tool handlers in services/tools/handlers/ - toolRegistry.ts is now the single source of truth for tool state - Add tools store for pinned tools (persisted in localStorage) - Tools can be pinned to stay active across page navigation - Remove old individual tool files, replaced by centralized handlers --- frontend/src/App.vue | 2 +- frontend/src/components/Toolbar.vue | 6 + frontend/src/pages/ToolsPage.vue | 595 ++++++++++++++++++ frontend/src/router/index.ts | 5 + frontend/src/services/toolRegistry.ts | 400 ++++++++---- frontend/src/services/tools/canvasTools.ts | 146 ----- frontend/src/services/tools/componentTools.ts | 147 ----- frontend/src/services/tools/databaseTools.ts | 231 ------- frontend/src/services/tools/globalTools.ts | 108 ---- .../services/tools/handlers/canvasHandlers.ts | 141 +++++ .../tools/handlers/componentHandlers.ts | 135 ++++ .../tools/handlers/databaseHandlers.ts | 186 ++++++ .../services/tools/handlers/globalHandlers.ts | 108 ++++ frontend/src/services/tools/handlers/index.ts | 22 + .../tools/handlers/projectCanvasHandlers.ts | 223 +++++++ .../tools/handlers/sourceCodeHandlers.ts | 238 +++++++ .../services/tools/handlers/themeHandlers.ts | 211 +++++++ .../src/services/tools/projectCanvasTools.ts | 246 -------- .../src/services/tools/sourceCodeTools.ts | 290 --------- frontend/src/services/tools/themeTools.ts | 524 --------------- .../src/services/tools/toolDefinitions.ts | 84 +++ frontend/src/stores/tools.ts | 170 +++++ 22 files changed, 2386 insertions(+), 1832 deletions(-) create mode 100644 frontend/src/pages/ToolsPage.vue delete mode 100644 frontend/src/services/tools/canvasTools.ts delete mode 100644 frontend/src/services/tools/componentTools.ts delete mode 100644 frontend/src/services/tools/databaseTools.ts delete mode 100644 frontend/src/services/tools/globalTools.ts create mode 100644 frontend/src/services/tools/handlers/canvasHandlers.ts create mode 100644 frontend/src/services/tools/handlers/componentHandlers.ts create mode 100644 frontend/src/services/tools/handlers/databaseHandlers.ts create mode 100644 frontend/src/services/tools/handlers/globalHandlers.ts create mode 100644 frontend/src/services/tools/handlers/index.ts create mode 100644 frontend/src/services/tools/handlers/projectCanvasHandlers.ts create mode 100644 frontend/src/services/tools/handlers/sourceCodeHandlers.ts create mode 100644 frontend/src/services/tools/handlers/themeHandlers.ts delete mode 100644 frontend/src/services/tools/projectCanvasTools.ts delete mode 100644 frontend/src/services/tools/sourceCodeTools.ts delete mode 100644 frontend/src/services/tools/themeTools.ts create mode 100644 frontend/src/services/tools/toolDefinitions.ts create mode 100644 frontend/src/stores/tools.ts diff --git a/frontend/src/App.vue b/frontend/src/App.vue index fcffbfa..a4c5459 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -14,7 +14,7 @@ const router = useRouter() const showTerminal = ref(false) const canvasStore = useCanvasStore() -type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal' +type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' onMounted(async () => { // Initialize WebMCP connection diff --git a/frontend/src/components/Toolbar.vue b/frontend/src/components/Toolbar.vue index 178ab9c..ced8141 100644 --- a/frontend/src/components/Toolbar.vue +++ b/frontend/src/components/Toolbar.vue @@ -125,6 +125,12 @@ onMounted(() => { + + + + + +
diff --git a/frontend/src/pages/ToolsPage.vue b/frontend/src/pages/ToolsPage.vue new file mode 100644 index 0000000..825380a --- /dev/null +++ b/frontend/src/pages/ToolsPage.vue @@ -0,0 +1,595 @@ + + + + + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 46d1ca9..767fc82 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -48,6 +48,11 @@ const router = createRouter({ path: '/terminal', name: 'terminal', component: () => import('../pages/TerminalPage.vue') + }, + { + path: '/tools', + name: 'tools', + component: () => import('../pages/ToolsPage.vue') } ] }) diff --git a/frontend/src/services/toolRegistry.ts b/frontend/src/services/toolRegistry.ts index 859336e..022c9f6 100644 --- a/frontend/src/services/toolRegistry.ts +++ b/frontend/src/services/toolRegistry.ts @@ -1,123 +1,128 @@ -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' -import { - registerProjectCanvasTools, - unregisterProjectCanvasTools, - PROJECT_CANVAS_TOOLS -} from './tools/projectCanvasTools' -import { - registerDatabaseTools, - unregisterDatabaseTools, - DATABASE_TOOLS -} from './tools/databaseTools' -import { - registerSourceCodeTools, - unregisterSourceCodeTools, - SOURCE_CODE_TOOLS -} from './tools/sourceCodeTools' +/** + * Tool Registry - Single source of truth for MCP tool management + * + * All tool registration/unregistration MUST go through this module. + * Other modules should NOT directly use webmcp registration functions. + */ -type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal' +import { + initWebMCP, + getRegisteredTools as getWebMCPTools, + clearAllTools as clearWebMCPTools +} from './webmcp' +import { useToolsStore } from '../stores/tools' +import { + createGlobalHandlers, + createCanvasHandlers, + createComponentHandlers, + createThemeHandlers, + createDatabaseHandlers, + createProjectCanvasHandlers, + createSourceCodeHandlers, + type ToolConfig +} from './tools/handlers' +import { setRouter } from './tools/handlers/globalHandlers' +import { setGiteaCredentials, clearGiteaCredentials } from './tools/handlers/sourceCodeHandlers' +import { ALL_TOOL_METAS, type ToolCategory } from './tools/toolDefinitions' -interface PageToolSet { - register: () => void - unregister: () => void - toolNames: string[] +export type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' + +// Internal webmcp functions (not exported for external use) +let webmcpInstance: any = null +const registeredToolsSet = new Set() + +async function internalRegisterTool(config: ToolConfig): Promise { + if (!webmcpInstance) { + webmcpInstance = await initWebMCP() + } + + if (registeredToolsSet.has(config.name)) { + return false // Already registered + } + + webmcpInstance.registerTool(config.name, config.description, config.schema, config.handler) + registeredToolsSet.add(config.name) + console.log(`[ToolRegistry] Registered: ${config.name}`) + return true } -const pageTools: Record = { - home: { - register: () => { - registerCanvasTools() - registerComponentTools() - registerProjectCanvasTools() - }, - unregister: () => { - unregisterCanvasTools() - unregisterComponentTools() - unregisterProjectCanvasTools() - }, - toolNames: [...CANVAS_TOOLS, ...COMPONENT_TOOLS, ...PROJECT_CANVAS_TOOLS] - }, - canvas: { - register: () => { - registerCanvasTools() - registerComponentTools() - }, - unregister: () => { - unregisterCanvasTools() - unregisterComponentTools() - }, - toolNames: [...CANVAS_TOOLS, ...COMPONENT_TOOLS] - }, - 'project-canvas': { - register: () => { - registerCanvasTools() - registerComponentTools() - registerProjectCanvasTools() - }, - unregister: () => { - unregisterCanvasTools() - unregisterComponentTools() - unregisterProjectCanvasTools() - }, - toolNames: [...CANVAS_TOOLS, ...COMPONENT_TOOLS, ...PROJECT_CANVAS_TOOLS] - }, - projects: { - register: registerProjectCanvasTools, - unregister: unregisterProjectCanvasTools, - toolNames: PROJECT_CANVAS_TOOLS - }, - components: { - register: registerComponentTools, - unregister: unregisterComponentTools, - toolNames: COMPONENT_TOOLS - }, - themes: { - register: registerThemeTools, - unregister: unregisterThemeTools, - toolNames: THEME_TOOLS - }, - database: { - register: registerDatabaseTools, - unregister: unregisterDatabaseTools, - toolNames: DATABASE_TOOLS - }, - source: { - register: registerSourceCodeTools, - unregister: unregisterSourceCodeTools, - toolNames: SOURCE_CODE_TOOLS - }, - terminal: { - register: () => {}, - unregister: () => {}, - toolNames: [] +function internalUnregisterTool(name: string): boolean { + if (!webmcpInstance || !registeredToolsSet.has(name)) { + return false } + + webmcpInstance.unregisterTool(name) + registeredToolsSet.delete(name) + console.log(`[ToolRegistry] Unregistered: ${name}`) + return true +} + +function internalClearAllTools() { + if (!webmcpInstance) return + + for (const name of registeredToolsSet) { + webmcpInstance.unregisterTool(name) + } + console.log(`[ToolRegistry] Cleared ${registeredToolsSet.size} tools`) + registeredToolsSet.clear() +} + +// Tool configurations cache +let toolConfigsCache: Map | null = null + +function getToolConfigs(): Map { + if (toolConfigsCache) return toolConfigsCache + + toolConfigsCache = new Map() + + // Create all handlers + const allHandlers = [ + ...createGlobalHandlers(() => Array.from(registeredToolsSet)), + ...createCanvasHandlers(), + ...createComponentHandlers(), + ...createThemeHandlers(), + ...createDatabaseHandlers(), + ...createProjectCanvasHandlers(), + ...createSourceCodeHandlers() + ] + + for (const config of allHandlers) { + toolConfigsCache.set(config.name, config) + } + + return toolConfigsCache +} + +// Category to tool names mapping +const categoryTools: Record = { + global: ['get_current_page', 'navigate_to', 'list_available_tools'], + canvas: ['render_html', 'render_vue_component'], + component: ['save_vue_component', 'load_vue_component', 'list_vue_components', 'delete_vue_component'], + theme: ['get_design_tokens', 'get_active_theme', 'set_theme_variable', 'save_theme', 'list_themes', 'switch_theme', 'reset_theme'], + database: ['list_tables', 'get_table_schema', 'get_table_data', 'get_database_stats', 'execute_query'], + 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'] +} + +// Page to categories mapping +const pageCategories: Record = { + home: ['global', 'canvas', 'component', 'project'], + canvas: ['global', 'canvas', 'component'], + 'project-canvas': ['global', 'canvas', 'component', 'project'], + projects: ['global', 'project'], + components: ['global', 'component'], + themes: ['global', 'theme'], + database: ['global', 'database'], + source: ['global', 'source'], + terminal: ['global'], + tools: ['global'] } let currentPage: PageName | null = null let isInitialized = false /** - * Inicializa el registry con el router de Vue + * Initialize the tool registry with Vue router */ export function initToolRegistry(router: any) { setRouter(router) @@ -125,80 +130,197 @@ export function initToolRegistry(router: any) { } /** - * Activa las tools para una página específica. - * Desregistra las tools de otras páginas primero. + * Set Gitea credentials for source code tools */ -export function activatePageTools(pageName: PageName) { +export function setSourceCodeCredentials(creds: any) { + setGiteaCredentials(creds) +} + +/** + * Clear Gitea credentials + */ +export function clearSourceCodeCredentials() { + clearGiteaCredentials() +} + +/** + * Activate tools for a specific page + */ +export async function activatePageTools(pageName: PageName) { if (!isInitialized) { - console.warn('[ToolRegistry] Not initialized. Call initToolRegistry first.') + console.warn('[ToolRegistry] Not initialized') return } - // Si ya estamos en esta página, no hacer nada if (currentPage === pageName) { - console.log(`[ToolRegistry] Already on page "${pageName}", skipping`) + console.log(`[ToolRegistry] Already on "${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() + const toolsStore = useToolsStore() + const pinnedTools = toolsStore.getPinnedToolNames() + const configs = getToolConfigs() + + // Get tools for old and new page + const oldCategories = currentPage ? pageCategories[currentPage] : [] + const newCategories = pageCategories[pageName] + + const oldTools = new Set(oldCategories.flatMap(cat => categoryTools[cat])) + const newTools = new Set(newCategories.flatMap(cat => categoryTools[cat])) + + // Unregister old tools (except pinned) + for (const tool of oldTools) { + if (!newTools.has(tool) && !pinnedTools.includes(tool)) { + internalUnregisterTool(tool) + } } - // Registrar tools de la nueva página - if (pageTools[pageName]) { - pageTools[pageName].register() + // Register new tools + for (const toolName of newTools) { + const config = configs.get(toolName) + if (config) { + await internalRegisterTool(config) + } } - // Asegurar que las tools globales estén registradas - registerGlobalTools() + // Ensure pinned tools are registered + for (const toolName of pinnedTools) { + const config = configs.get(toolName) + if (config && !registeredToolsSet.has(toolName)) { + await internalRegisterTool(config) + } + } currentPage = pageName + syncStoreWithActiveTools() console.log(`[ToolRegistry] Page "${pageName}" tools activated`) } /** - * Inicializa las tools para un refresh de página. - * Limpia todo y registra las tools correctas. + * Initialize tools on page refresh */ -export function initToolsOnRefresh(pageName: PageName) { +export async function initToolsOnRefresh(pageName: PageName) { if (!isInitialized) { - console.warn('[ToolRegistry] Not initialized. Call initToolRegistry first.') + console.warn('[ToolRegistry] Not initialized') return } - console.log(`[ToolRegistry] Initializing on refresh for page "${pageName}"`) + console.log(`[ToolRegistry] Initializing on refresh for "${pageName}"`) - // Limpiar todas las tools existentes - clearAllTools() - - // Reset current page tracking + internalClearAllTools() currentPage = null - - // Activar tools de la página actual - activatePageTools(pageName) + await activatePageTools(pageName) } /** - * Obtiene el nombre de la página actual + * Activate a single tool by name + */ +export async function activateTool(toolName: string): Promise { + const configs = getToolConfigs() + const config = configs.get(toolName) + + if (!config) { + console.warn(`[ToolRegistry] Tool "${toolName}" not found`) + return false + } + + const result = await internalRegisterTool(config) + syncStoreWithActiveTools() + return result +} + +/** + * Deactivate a single tool by name + */ +export function deactivateTool(toolName: string): boolean { + const toolsStore = useToolsStore() + + if (toolsStore.isToolPinned(toolName)) { + console.warn(`[ToolRegistry] Cannot deactivate pinned tool "${toolName}"`) + return false + } + + const result = internalUnregisterTool(toolName) + syncStoreWithActiveTools() + return result +} + +/** + * Activate all tools in a category + */ +export async function activateCategory(category: ToolCategory) { + const configs = getToolConfigs() + const tools = categoryTools[category] || [] + + for (const toolName of tools) { + const config = configs.get(toolName) + if (config) { + await internalRegisterTool(config) + } + } + + syncStoreWithActiveTools() +} + +/** + * Deactivate all tools in a category (respecting pinned) + */ +export function deactivateCategory(category: ToolCategory) { + const toolsStore = useToolsStore() + const tools = categoryTools[category] || [] + + for (const toolName of tools) { + if (!toolsStore.isToolPinned(toolName)) { + internalUnregisterTool(toolName) + } + } + + syncStoreWithActiveTools() +} + +/** + * Sync the store with currently active tools + */ +export function syncStoreWithActiveTools() { + const toolsStore = useToolsStore() + toolsStore.setActiveTools(Array.from(registeredToolsSet)) +} + +/** + * Get current page name */ export function getCurrentPage(): PageName | null { return currentPage } /** - * Obtiene los nombres de las tools para una página + * Get tool names for a page */ export function getPageToolNames(pageName: PageName): string[] { - return [...(pageTools[pageName]?.toolNames || []), ...GLOBAL_TOOLS] + const categories = pageCategories[pageName] || [] + return categories.flatMap(cat => categoryTools[cat]) } /** - * Verifica si el registry está inicializado + * Check if registry is initialized */ export function isRegistryInitialized(): boolean { return isInitialized } + +/** + * Get all tool metadata + */ +export function getAllToolMetas() { + return ALL_TOOL_METAS +} + +/** + * Get registered tools (for internal use) + */ +export function getRegisteredTools(): string[] { + return Array.from(registeredToolsSet) +} diff --git a/frontend/src/services/tools/canvasTools.ts b/frontend/src/services/tools/canvasTools.ts deleted file mode 100644 index 8972c63..0000000 --- a/frontend/src/services/tools/canvasTools.ts +++ /dev/null @@ -1,146 +0,0 @@ -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