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
This commit is contained in:
2026-02-13 13:46:55 -06:00
parent da6111bd1f
commit 4450d1e034
22 changed files with 2386 additions and 1832 deletions

View File

@@ -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<string>()
async function internalRegisterTool(config: ToolConfig): Promise<boolean> {
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<PageName, PageToolSet> = {
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<string, ToolConfig> | null = null
function getToolConfigs(): Map<string, ToolConfig> {
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<ToolCategory, string[]> = {
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<PageName, ToolCategory[]> = {
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<boolean> {
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)
}