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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user