Files
agent-ui/frontend/src/services/toolRegistry.ts
josedario87 5fd57ba70f feat: Add DOM inspection and manipulation tools for canvas
- inspect_window: Inspect HTML content of a window with selector filter
- get_canvas: Read canvas elements using CSS selectors (like Read tool)
- edit_canvas: Edit canvas elements with old/new value replacement (like Edit tool)
- canvas_css: Inject/update/remove CSS blocks with ID tracking
- canvas_js: Execute JavaScript in canvas context with helper functions
- get_canvas_css: List or get specific injected CSS blocks
2026-02-14 20:07:25 -06:00

463 lines
13 KiB
TypeScript

/**
* 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.
*/
import {
initWebMCP,
getRegisteredTools as getWebMCPTools,
clearAllTools as clearWebMCPTools,
getWebMCP
} from './webmcp'
import { useToolsStore } from '../stores/tools'
import { useCanvasStore } from '../stores/canvas'
import {
createGlobalHandlers,
createCanvasHandlers,
createComponentHandlers,
createThemeHandlers,
createDatabaseHandlers,
createProjectCanvasHandlers,
createSourceCodeHandlers,
createTerminalHandlers,
createResponseHandlers,
createGitHandlers,
createTorchHandlers,
type ToolConfig
} from './tools/handlers'
import { setRouter } from './tools/handlers/globalHandlers'
import { setGiteaCredentials, clearGiteaCredentials } from './tools/handlers/sourceCodeHandlers'
import { ALL_TOOL_METAS, getAllToolNames, type ToolCategory } from './tools/toolDefinitions'
export type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' | 'git'
// Internal webmcp functions (not exported for external use)
let webmcpInstance: any = null
const registeredToolsSet = new Set<string>()
async function internalRegisterTool(config: ToolConfig, force: boolean = false): Promise<boolean> {
if (!webmcpInstance) {
webmcpInstance = await initWebMCP()
}
// Always re-register if force is true (needed after reconnection)
if (!force && registeredToolsSet.has(config.name)) {
return false // Already registered
}
// Unregister first if exists (to clear any stale handlers from storage)
if (registeredToolsSet.has(config.name)) {
webmcpInstance.unregisterTool(config.name)
}
webmcpInstance.registerTool(config.name, config.description, config.schema, config.handler)
registeredToolsSet.add(config.name)
console.log(`[ToolRegistry] Registered: ${config.name}`)
return true
}
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 callbacks for global handlers
const toolManagementCallbacks = {
getRegisteredTools: () => Array.from(registeredToolsSet),
getAllToolNames: () => getAllToolNames(),
activateTool: async (name: string) => {
const config = toolConfigsCache?.get(name)
if (!config) return false
const result = await internalRegisterTool(config)
syncStoreWithActiveTools()
return result
},
deactivateTool: (name: string) => {
const toolsStore = useToolsStore()
if (toolsStore.isToolPinned(name)) return false
const result = internalUnregisterTool(name)
syncStoreWithActiveTools()
return result
},
togglePin: (name: string) => {
const toolsStore = useToolsStore()
toolsStore.togglePin(name)
},
isToolPinned: (name: string) => {
const toolsStore = useToolsStore()
return toolsStore.isToolPinned(name)
}
}
// Create all handlers
const allHandlers = [
...createGlobalHandlers(toolManagementCallbacks),
...createCanvasHandlers(),
...createComponentHandlers(),
...createThemeHandlers(),
...createDatabaseHandlers(),
...createProjectCanvasHandlers(),
...createSourceCodeHandlers(),
...createTerminalHandlers(),
...createResponseHandlers(),
...createGitHandlers(),
...createTorchHandlers()
]
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', 'activate_tool', 'deactivate_tool', 'pin_tool', 'page_refresh'],
canvas: ['render_html', 'render_vue_component', 'move_window', 'resize_window', 'close_window', 'list_windows', 'inspect_window', 'get_canvas', 'edit_canvas', 'canvas_css', 'canvas_js', 'get_canvas_css'],
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'],
terminal: ['terminal_open', 'terminal_close', 'terminal_toggle', 'terminal_move', 'terminal_resize', 'bubbleResponse'],
git: ['get_git_status', 'get_git_diff', 'compare_commits', 'git_log', 'get_git_branches'],
torch: ['list_torch_clients', 'get_torch_status', 'transfer_torch', 'request_torch', 'release_torch']
}
// Page to categories mapping
const pageCategories: Record<PageName, ToolCategory[]> = {
home: ['global', 'torch', 'canvas', 'component', 'project', 'terminal'],
canvas: ['global', 'torch', 'canvas', 'component', 'terminal'],
'project-canvas': ['global', 'torch', 'canvas', 'component', 'project', 'terminal'],
projects: ['global', 'torch', 'project', 'terminal'],
components: ['global', 'torch', 'component', 'terminal'],
themes: ['global', 'torch', 'theme', 'terminal'],
database: ['global', 'torch', 'database', 'terminal'],
source: ['global', 'torch', 'source', 'terminal'],
terminal: ['global', 'torch', 'terminal'],
tools: ['global', 'torch', 'terminal'],
git: ['global', 'torch', 'git', 'terminal']
}
let currentPage: PageName | null = null
let isInitialized = false
/**
* Check if connected to MCP
*/
function isConnectedToMCP(): boolean {
const canvasStore = useCanvasStore()
return canvasStore.isConnected
}
/**
* Initialize the tool registry with Vue router
*/
export function initToolRegistry(router: any) {
setRouter(router)
isInitialized = true
}
/**
* Set Gitea credentials for source code tools
*/
export function setSourceCodeCredentials(creds: any) {
setGiteaCredentials(creds)
}
/**
* Clear Gitea credentials
*/
export function clearSourceCodeCredentials() {
clearGiteaCredentials()
}
/**
* Activate tools for a specific page
* Only works when connected to MCP
*/
export async function activatePageTools(pageName: PageName, force: boolean = false) {
if (!isInitialized) {
console.warn('[ToolRegistry] Not initialized')
return
}
// Only register tools if connected to MCP
if (!isConnectedToMCP()) {
console.log(`[ToolRegistry] Not connected to MCP, storing page "${pageName}" for later`)
currentPage = pageName
return
}
if (!force && currentPage === pageName) {
console.log(`[ToolRegistry] Already on "${pageName}", skipping`)
return
}
console.log(`[ToolRegistry] Switching from "${currentPage}" to "${pageName}"`)
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)
}
}
// Register new tools
for (const toolName of newTools) {
const config = configs.get(toolName)
if (config) {
await internalRegisterTool(config)
}
}
// 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`)
}
/**
* Initialize tools on page refresh
* Just stores the current page, actual registration happens when connected
*/
export async function initToolsOnRefresh(pageName: PageName) {
if (!isInitialized) {
console.warn('[ToolRegistry] Not initialized')
return
}
console.log(`[ToolRegistry] Storing current page "${pageName}" for when connected`)
currentPage = pageName
// Only activate if already connected
if (isConnectedToMCP()) {
internalClearAllTools()
await activatePageTools(pageName, true)
}
}
/**
* Called when torch is obtained and MCP is connected
* Activates tools for the current page
*/
export async function onTorchConnected() {
if (!isInitialized) {
console.warn('[ToolRegistry] Not initialized')
return
}
if (!currentPage) {
console.warn('[ToolRegistry] No current page set')
return
}
console.log(`[ToolRegistry] Torch connected, activating tools for "${currentPage}"`)
// Clear any stale tools from WebMCP storage first
clearWebMCPTools()
registeredToolsSet.clear()
// Force re-register all tools for current page
await activatePageToolsForced(currentPage)
}
/**
* Force activate tools for a page (used after torch connection)
*/
async function activatePageToolsForced(pageName: PageName) {
const toolsStore = useToolsStore()
const pinnedTools = toolsStore.getPinnedToolNames()
const configs = getToolConfigs()
const categories = pageCategories[pageName]
const tools = new Set(categories.flatMap(cat => categoryTools[cat]))
// Register all tools with force flag
for (const toolName of tools) {
const config = configs.get(toolName)
if (config) {
await internalRegisterTool(config, true)
}
}
// Also register pinned tools
for (const toolName of pinnedTools) {
if (!tools.has(toolName)) {
const config = configs.get(toolName)
if (config) {
await internalRegisterTool(config, true)
}
}
}
currentPage = pageName
syncStoreWithActiveTools()
console.log(`[ToolRegistry] Force activated ${registeredToolsSet.size} tools for "${pageName}"`)
}
/**
* Called when torch is lost and MCP is disconnected
* Clears all tools
*/
export function onTorchDisconnected() {
console.log('[ToolRegistry] Torch disconnected, clearing tools')
internalClearAllTools()
syncStoreWithActiveTools()
}
/**
* 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
}
/**
* Get tool names for a page
*/
export function getPageToolNames(pageName: PageName): string[] {
const categories = pageCategories[pageName] || []
return categories.flatMap(cat => categoryTools[cat])
}
/**
* 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)
}