- Add page_refresh tool to reload the page via MCP - Change push-to-talk shortcut from Ctrl+S to Ctrl+Space - Use capture phase for keyboard events to intercept before terminal
360 lines
10 KiB
TypeScript
360 lines
10 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
|
|
} from './webmcp'
|
|
import { useToolsStore } from '../stores/tools'
|
|
import {
|
|
createGlobalHandlers,
|
|
createCanvasHandlers,
|
|
createComponentHandlers,
|
|
createThemeHandlers,
|
|
createDatabaseHandlers,
|
|
createProjectCanvasHandlers,
|
|
createSourceCodeHandlers,
|
|
createTerminalHandlers,
|
|
createResponseHandlers,
|
|
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'
|
|
|
|
// 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
|
|
}
|
|
|
|
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()
|
|
]
|
|
|
|
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'],
|
|
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']
|
|
}
|
|
|
|
// Page to categories mapping
|
|
const pageCategories: Record<PageName, ToolCategory[]> = {
|
|
home: ['global', 'canvas', 'component', 'project', 'terminal'],
|
|
canvas: ['global', 'canvas', 'component', 'terminal'],
|
|
'project-canvas': ['global', 'canvas', 'component', 'project', 'terminal'],
|
|
projects: ['global', 'project', 'terminal'],
|
|
components: ['global', 'component', 'terminal'],
|
|
themes: ['global', 'theme', 'terminal'],
|
|
database: ['global', 'database', 'terminal'],
|
|
source: ['global', 'source', 'terminal'],
|
|
terminal: ['global', 'terminal'],
|
|
tools: ['global', 'terminal']
|
|
}
|
|
|
|
let currentPage: PageName | null = null
|
|
let isInitialized = false
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
export async function activatePageTools(pageName: PageName) {
|
|
if (!isInitialized) {
|
|
console.warn('[ToolRegistry] Not initialized')
|
|
return
|
|
}
|
|
|
|
if (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
|
|
*/
|
|
export async function initToolsOnRefresh(pageName: PageName) {
|
|
if (!isInitialized) {
|
|
console.warn('[ToolRegistry] Not initialized')
|
|
return
|
|
}
|
|
|
|
console.log(`[ToolRegistry] Initializing on refresh for "${pageName}"`)
|
|
|
|
internalClearAllTools()
|
|
currentPage = null
|
|
await activatePageTools(pageName)
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
}
|