'use strict'; const fs = require('fs'); const path = require('path'); const { clearSelection, deleteAsset, getCurrentSelection, listAssets, openAsset, queryAssetData, queryAssetInfo, queryAssetMeta, selectAsset, selectNode, } = require('./assets'); const { runScriptDiagnostics } = require('./diagnostics'); const { listWindows, sendKeyCombo, sendKeyPress, sendMouseClick, sendMouseDrag } = require('./input'); const { clearProjectLogFiles, getRecentProjectLogs, searchProjectLogs, } = require('./logs'); const { resolveProjectPath } = require('./path-safety'); const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots'); const { checkForUpdate } = require('./update-checker'); const { safeStringify } = require('./utils'); const TOOL_CATEGORY_RULES = [ ['updates', /update/], ['logs', /log/], ['diagnostics', /diagnostic|validate/], ['screenshots', /screenshot|capture/], ['input', /mouse|key|input|button_click/], ['files', /file|directory|exists|refresh_assets/], ['assets', /asset|scene$|scenes|open_scene|run_scene_asset/], ['prefabs', /prefab/], ['selection', /selection|select_/], ['components', /component/], ['ui', /canvas|label|button|sprite/], ['camera', /camera/], ['animation', /animation|clip/], ['runtime', /runtime|time_scale|node_event|invoke_component/], ['scene', /scene|hierarchy|node/], ['execution', /execute_/], ['project', /project|editor_state|tool_catalog/], ]; function createSchema(properties, required) { const schema = { type: 'object', properties, }; if (required && required.length) { schema.required = required; } return schema; } function inferToolCategory(toolName) { for (const [category, pattern] of TOOL_CATEGORY_RULES) { if (pattern.test(toolName)) { return category; } } return 'other'; } function normalizeNameSet(values) { return new Set( (Array.isArray(values) ? values : []) .map((value) => String(value || '').trim()) .filter(Boolean) ); } function normalizeCategorySet(values) { return new Set( (Array.isArray(values) ? values : []) .map((value) => String(value || '').trim().toLowerCase()) .filter(Boolean) ); } function toolCategory(tool) { return tool.category || inferToolCategory(tool.name); } function isToolExposed(config, tool) { const profile = config && config.toolProfile === 'full' ? 'full' : config && config.toolProfile === 'custom' ? 'custom' : 'core'; const category = toolCategory(tool); const enabledTools = normalizeNameSet(config && config.enabledTools); const disabledTools = normalizeNameSet(config && config.disabledTools); const enabledCategories = normalizeCategorySet(config && config.enabledToolCategories); const disabledCategories = normalizeCategorySet(config && config.disabledToolCategories); let exposed = profile === 'full' || tool.profile === 'core'; if (profile === 'custom') { exposed = tool.profile === 'core' || enabledTools.has(tool.name) || enabledCategories.has(category); } else if (enabledTools.has(tool.name) || enabledCategories.has(category)) { exposed = true; } if (disabledTools.has(tool.name) || disabledCategories.has(category)) { exposed = false; } return exposed; } function summarizeDiagnostics(result) { if (!result) { return null; } return { ok: Boolean(result.ok), tool: result.tool, summary: result.summary, diagnosticCount: Array.isArray(result.diagnostics) ? result.diagnostics.length : 0, diagnostics: Array.isArray(result.diagnostics) ? result.diagnostics.slice(0, 20) : [], }; } function toOutput(value) { if (typeof value === 'string') { return value; } return safeStringify(value); } function matchesPattern(fileName, pattern) { const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*'); return new RegExp(`^${escaped}$`, 'i').test(fileName); } function searchFiles(rootDir, pattern, limit) { const results = []; if (!fs.existsSync(rootDir)) { return results; } const stack = [rootDir]; while (stack.length && results.length < limit) { const current = stack.pop(); const entries = fs.readdirSync(current, { withFileTypes: true }); for (const entry of entries) { if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === 'temp' || entry.name === 'library') { continue; } const fullPath = path.join(current, entry.name); if (entry.isDirectory()) { stack.push(fullPath); continue; } if (matchesPattern(entry.name, pattern)) { results.push(fullPath); if (results.length >= limit) { break; } } } } return results; } function readLines(filePath) { return fs.readFileSync(filePath, 'utf8').split(/\r?\n/); } function buildSnippet(filePath, lineNumber, contextLines = 3) { const lines = readLines(filePath); const start = Math.max(1, Number(lineNumber || 1) - Math.max(0, contextLines)); const end = Math.min(lines.length, Number(lineNumber || 1) + Math.max(0, contextLines)); const snippet = []; for (let line = start; line <= end; line += 1) { const marker = line === Number(lineNumber || 1) ? '>' : ' '; snippet.push(`${marker} ${String(line).padStart(4, ' ')} | ${lines[line - 1]}`); } return snippet.join('\n'); } function replaceAllLiteral(content, search, replacement) { if (!search) { throw new Error('search text is required.'); } return content.split(search).join(replacement); } async function refreshAssets(projectPath, targetPath) { if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') { return 'Asset refresh API is unavailable; Cocos Creator should pick up file changes automatically.'; } const relative = path.relative(path.join(projectPath, 'assets'), targetPath).replace(/\\/g, '/'); if (!relative.startsWith('..')) { const dbUrl = `db://assets/${relative}`; try { await Editor.Message.request('asset-db', 'refresh-asset', dbUrl); return `Refreshed asset database for ${dbUrl}`; } catch (error) { try { await Editor.Message.request('asset-db', 'refresh-asset', 'db://assets'); return `Refreshed asset database after writing ${dbUrl}`; } catch (innerError) { return `File written, but asset refresh failed: ${innerError.message}`; } } } return 'File written outside assets directory; no asset-db refresh was needed.'; } function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runtimeLog, sceneBridge, editorExecutor }) { const tools = [ { name: 'execute_javascript', profile: 'core', description: '[primary] Execute JavaScript in either the scene or editor context. Use context=\"scene\" for live scene/runtime inspection and mutation, or context=\"editor\" for Editor APIs, asset-db workflows, MCP orchestration, local filesystem access, and higher-level automation. Prefer this as the main flexible tool when many narrow tools would be noisy.', inputSchema: createSchema( { context: { type: 'string', description: 'Execution context: scene or editor.' }, code: { type: 'string', description: 'JavaScript code to execute. May directly return a value, define run(env), or export a function.' }, args: { type: 'object', description: 'Optional JSON object passed into the script.' }, }, ['context', 'code'] ), handler: async (args) => { const context = String(args.context || '').toLowerCase(); if (context === 'scene') { return sceneBridge.call('executeCode', { code: args.code, args: args.args || {} }); } if (context === 'editor') { if (typeof editorExecutor !== 'function') { throw new Error('Editor JavaScript execution is unavailable.'); } return await editorExecutor({ code: args.code, args: args.args || {} }); } throw new Error(`Unknown execution context '${args.context}'. Expected 'scene' or 'editor'.`); }, }, { name: 'execute_scene_script', profile: 'core', description: '[compat] Execute JavaScript in the active Cocos scene context. Prefer execute_javascript with context="scene" as the main unified tool; use this when you specifically want the scene-only compatibility entrypoint.', inputSchema: createSchema( { code: { type: 'string', description: 'JavaScript code to execute inside the scene script context.' }, args: { type: 'object', description: 'Optional JSON object passed to the scene script.' }, }, ['code'] ), handler: async (args) => sceneBridge.call('executeCode', { code: args.code, args: args.args || {} }), }, { name: 'execute_editor_script', profile: 'core', description: '[compat] Execute JavaScript in the editor/browser context. Prefer execute_javascript with context="editor" as the main unified tool; use this when you specifically want the editor-only compatibility entrypoint.', inputSchema: createSchema( { code: { type: 'string', description: 'JavaScript code to execute inside the editor context.' }, args: { type: 'object', description: 'Optional JSON object passed to the editor script.' }, }, ['code'] ), handler: async (args) => { if (typeof editorExecutor !== 'function') { throw new Error('Editor JavaScript execution is unavailable.'); } return await editorExecutor({ code: args.code, args: args.args || {} }); }, }, { name: 'get_editor_state', profile: 'core', description: '[specialist] Return a structured editor-state snapshot including project info, runtime server status, current selection, and visible Electron windows. Prefer this when you want one compact editor summary.', inputSchema: createSchema({}, []), handler: async () => { const runtimeContext = getRuntimeContext(); const status = typeof getStatus === 'function' ? getStatus() : null; let scene = null; try { const sceneInfo = await sceneBridge.call('getSceneInfo', { maxDepth: 1, includeComponents: false }); scene = sceneInfo ? { sceneName: sceneInfo.sceneName, uuid: sceneInfo.uuid, childCount: sceneInfo.childCount, } : null; } catch (error) { scene = { error: error.message }; } let windows = []; try { windows = listWindows(); } catch (error) { windows = [{ error: error.message }]; } return { extensionName: runtimeContext.extensionName, version: runtimeContext.version, projectName: runtimeContext.projectName, projectPath: runtimeContext.projectPath, cocosVersion: runtimeContext.cocosVersion, toolProfile: runtimeContext.config ? runtimeContext.config.toolProfile : 'core', status, selection: getCurrentSelection(), scene, windows, }; }, }, { name: 'get_tool_catalog', profile: 'core', description: '[specialist] Return every built-in MCP tool with profile, category, and current exposure state. Use this before changing custom tool exposure.', inputSchema: createSchema({}, []), handler: async () => registry.listToolCatalog(), }, { name: 'check_for_updates', profile: 'core', description: '[specialist] Check the latest Funplay Cocos MCP GitHub release and compare it with the installed extension version.', inputSchema: createSchema( { timeoutMs: { type: 'number', description: 'Optional network timeout in milliseconds.' }, }, [] ), handler: async (args) => { const runtimeContext = getRuntimeContext(); return await checkForUpdate({ currentVersion: runtimeContext.version, timeoutMs: Number.isFinite(args.timeoutMs) ? args.timeoutMs : 5000, }); }, }, { name: 'get_selection', profile: 'core', description: '[specialist] Return the current editor selection in a compact structured form. Prefer this when selection state matters for the next action.', inputSchema: createSchema({}, []), handler: async () => getCurrentSelection(), }, { name: 'set_selection', profile: 'core', description: '[specialist] Set or clear the current editor selection for an asset or node. Use this when downstream editor workflows depend on selection state.', inputSchema: createSchema( { type: { type: 'string', description: 'Selection target type: asset, node, or clear.' }, target: { type: 'string', description: 'Asset uuid/path/db url, or node uuid when type=node.' }, clearMode: { type: 'string', description: 'When type=clear, choose asset, node, or all.' }, }, ['type'] ), handler: async (args) => { const type = String(args.type || '').trim().toLowerCase(); if (type === 'clear') { return clearSelection(args.clearMode || 'all'); } if (type === 'asset') { const info = await queryAssetInfo(args.target); return selectAsset(info.uuid || args.target); } if (type === 'node') { const target = String(args.target || '').trim(); if (!target) { throw new Error('target is required when type=node.'); } return selectNode(target); } throw new Error(`Unknown selection type '${args.type}'. Expected asset, node, or clear.`); }, }, { name: 'get_scene_info', profile: 'core', description: '[specialist] Return a structured summary of the active Cocos scene. Prefer execute_javascript for multi-step inspection or mutation; use this when you specifically want a compact scene snapshot.', inputSchema: createSchema( { maxDepth: { type: 'number', description: 'Maximum child depth to include in the scene summary.' }, includeComponents: { type: 'boolean', description: 'Include component names for nodes.' }, }, [] ), handler: async (args) => sceneBridge.call('getSceneInfo', args), }, { name: 'get_hierarchy', profile: 'core', description: '[specialist] Return a structured hierarchy tree from the active scene or a specific node path. Prefer execute_javascript for broader reasoning or repair; use this when you want a predictable hierarchy snapshot.', inputSchema: createSchema( { rootPath: { type: 'string', description: 'Optional node path to use as the traversal root.' }, maxDepth: { type: 'number', description: 'Maximum child depth to include.' }, includeComponents: { type: 'boolean', description: 'Include component names for each node.' }, includeInactive: { type: 'boolean', description: 'Include inactive nodes in the result.' }, }, [] ), handler: async (args) => sceneBridge.call('getHierarchy', args), }, { name: 'find_nodes', profile: 'full', description: '[core] Find scene nodes by exact name, partial path, or component type.', inputSchema: createSchema( { name: { type: 'string', description: 'Exact node name to match.' }, pathContains: { type: 'string', description: 'Substring that must appear in the node path.' }, component: { type: 'string', description: 'Component constructor name to match.' }, includeInactive: { type: 'boolean', description: 'Include inactive nodes.' }, }, [] ), handler: async (args) => sceneBridge.call('findNodes', args), }, { name: 'inspect_node', profile: 'full', description: '[core] Inspect a specific node by path, uuid, or name.', inputSchema: createSchema( { path: { type: 'string', description: 'Hierarchy path such as Canvas/Player.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, }, [] ), handler: async (args) => sceneBridge.call('inspectNode', args), }, { name: 'create_node', profile: 'full', description: 'Create a new node under the active scene or a specified parent path.', inputSchema: createSchema( { name: { type: 'string', description: 'Name of the node to create.' }, parentPath: { type: 'string', description: 'Optional parent node path.' }, position: { type: 'object', description: 'Optional position {x,y,z}.' }, scale: { type: 'object', description: 'Optional scale {x,y,z}.' }, eulerAngles: { type: 'object', description: 'Optional rotation {x,y,z} in degrees.' }, active: { type: 'boolean', description: 'Optional active state for the node.' }, }, ['name'] ), handler: async (args) => sceneBridge.call('createNode', args), }, { name: 'delete_node', profile: 'full', description: 'Delete a node by path, uuid, or name.', inputSchema: createSchema( { path: { type: 'string', description: 'Hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, }, [] ), handler: async (args) => sceneBridge.call('deleteNode', args), }, { name: 'set_node_transform', profile: 'full', description: 'Update node position, rotation, scale, or active state.', inputSchema: createSchema( { path: { type: 'string', description: 'Hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, position: { type: 'object', description: 'Position {x,y,z}.' }, scale: { type: 'object', description: 'Scale {x,y,z}.' }, eulerAngles: { type: 'object', description: 'Rotation {x,y,z} in degrees.' }, active: { type: 'boolean', description: 'Optional active state.' }, }, [] ), handler: async (args) => sceneBridge.call('setNodeTransform', args), }, { name: 'get_project_info', profile: 'core', description: '[specialist] Return the active Cocos project path, version, and MCP server configuration. Prefer this for a fast structured project summary; use execute_javascript when you need to inspect and act in one step.', inputSchema: createSchema({}, []), handler: async () => getRuntimeContext(), }, { name: 'list_scenes', profile: 'core', description: '[specialist] List scene assets in the project. Prefer this when you need exact scene discovery before opening one; otherwise stay in execute_javascript for broader workflows.', inputSchema: createSchema( { pattern: { type: 'string', description: 'Optional asset-db pattern. Defaults to db://assets/**.' }, }, [] ), handler: async (args) => { const assets = await listAssets({ pattern: args.pattern || 'db://assets/**', ccType: 'cc.SceneAsset' }); return { count: assets.length, scenes: assets.slice(0, 200) }; }, }, { name: 'open_scene', profile: 'core', description: '[specialist] Open a scene asset in Cocos Creator by uuid, db url, or path. Use this when scene switching is the explicit goal; otherwise keep execute_javascript as the main planning tool.', inputSchema: createSchema( { target: { type: 'string', description: 'Scene uuid, db url, or path.' }, }, ['target'] ), handler: async (args) => await openAsset(args.target), }, { name: 'list_prefabs', profile: 'full', description: '[core] List prefab assets in the project.', inputSchema: createSchema( { pattern: { type: 'string', description: 'Optional asset-db pattern. Defaults to db://assets/**.' }, }, [] ), handler: async (args) => { const assets = await listAssets({ pattern: args.pattern || 'db://assets/**', ccType: 'cc.Prefab' }); return { count: assets.length, prefabs: assets.slice(0, 200) }; }, }, { name: 'instantiate_prefab', profile: 'full', description: 'Instantiate a prefab into the active scene by prefab uuid.', inputSchema: createSchema( { prefabUuid: { type: 'string', description: 'Prefab asset uuid.' }, parentPath: { type: 'string', description: 'Optional parent node path.' }, name: { type: 'string', description: 'Optional override node name.' }, position: { type: 'object', description: 'Optional position {x,y,z}.' }, }, ['prefabUuid'] ), handler: async (args) => sceneBridge.call('instantiatePrefab', args), }, { name: 'run_scene_asset', profile: 'full', description: 'Load a scene asset by uuid directly into the current runtime scene context.', inputSchema: createSchema( { sceneUuid: { type: 'string', description: 'Scene asset uuid.' }, }, ['sceneUuid'] ), handler: async (args) => sceneBridge.call('runSceneAsset', args), }, { name: 'list_assets', profile: 'core', description: '[specialist] Query project assets from asset-db by pattern or asset type. Prefer this when you need exact asset discovery; otherwise use execute_javascript for broader automation.', inputSchema: createSchema( { pattern: { type: 'string', description: 'Optional asset-db pattern such as db://assets/** or a folder url.' }, ccType: { type: 'string', description: 'Optional Cocos asset type, such as cc.Prefab or cc.SceneAsset.' }, }, [] ), handler: async (args) => { const assets = await listAssets(args); return { count: assets.length, assets: assets.slice(0, 200), }; }, }, { name: 'inspect_asset', profile: 'core', description: '[specialist] Inspect asset-db info, metadata, and serialized asset data by uuid or path. Prefer this when you need a precise structured asset read.', inputSchema: createSchema( { target: { type: 'string', description: 'Asset uuid, db url, or path.' }, includeData: { type: 'boolean', description: 'Include serialized asset data when available.' }, }, ['target'] ), handler: async (args) => { const info = await queryAssetInfo(args.target); const meta = await queryAssetMeta(args.target).catch(() => null); const data = args.includeData ? await queryAssetData(args.target).catch(() => null) : null; return { info, meta, data }; }, }, { name: 'open_asset', profile: 'core', description: '[specialist] Open an asset inside Cocos Creator by uuid, db url, or path. Use this only when opening the asset itself is the explicit next step.', inputSchema: createSchema( { target: { type: 'string', description: 'Asset uuid, db url, or path.' }, }, ['target'] ), handler: async (args) => await openAsset(args.target), }, { name: 'delete_asset', profile: 'full', description: 'Delete an asset from asset-db by uuid, db url, or path.', inputSchema: createSchema( { target: { type: 'string', description: 'Asset uuid, db url, or path.' }, }, ['target'] ), handler: async (args) => await deleteAsset(args.target), }, { name: 'select_asset', profile: 'core', description: '[specialist] Select an asset in the Cocos editor. Use this when editor selection state matters; otherwise keep execute_javascript as the primary workflow.', inputSchema: createSchema( { target: { type: 'string', description: 'Asset uuid, db url, or path.' }, }, ['target'] ), handler: async (args) => { const info = await queryAssetInfo(args.target); return selectAsset(info.uuid || args.target); }, }, { name: 'get_editor_selection', profile: 'full', description: '[compat] Return the current node and asset selection in the Cocos editor. Prefer get_selection as the primary structured selection read tool.', inputSchema: createSchema({}, []), handler: async () => getCurrentSelection(), }, { name: 'list_components', profile: 'full', description: '[core] List components attached to a scene node.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, }, [] ), handler: async (args) => sceneBridge.call('listComponents', args), }, { name: 'inspect_component', profile: 'full', description: '[core] Inspect a component attached to a node.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, componentName: { type: 'string', description: 'Component class name.' }, index: { type: 'number', description: 'Optional component index.' }, }, [] ), handler: async (args) => sceneBridge.call('inspectComponent', args), }, { name: 'add_component', profile: 'full', description: 'Add a component to a node by component class name.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, componentName: { type: 'string', description: 'Component class name, for example Sprite or cc.UITransform.' }, }, ['componentName'] ), handler: async (args) => sceneBridge.call('addComponent', args), }, { name: 'remove_component', profile: 'full', description: 'Remove a component from a node by name or index.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, componentName: { type: 'string', description: 'Component class name.' }, index: { type: 'number', description: 'Optional component index.' }, }, [] ), handler: async (args) => sceneBridge.call('removeComponent', args), }, { name: 'set_component_property', profile: 'full', description: 'Set a component property by dot path using a JSON value.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, componentName: { type: 'string', description: 'Component class name.' }, index: { type: 'number', description: 'Optional component index.' }, propertyPath: { type: 'string', description: 'Property path such as color.r or enabled.' }, valueJson: { type: 'string', description: 'JSON encoded value to assign, for example true, 12, \"hero\", or {\"x\":1}.' }, }, ['propertyPath', 'valueJson'] ), handler: async (args) => { let value; try { value = JSON.parse(args.valueJson); } catch (error) { throw new Error(`valueJson must be valid JSON: ${error.message}`); } return sceneBridge.call('setComponentProperty', { ...args, value }); }, }, { name: 'reset_component_property', profile: 'full', description: 'Reset or clear a component property by dot path.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, componentName: { type: 'string', description: 'Component class name.' }, index: { type: 'number', description: 'Optional component index.' }, propertyPath: { type: 'string', description: 'Property path such as color.r or enabled.' }, }, ['propertyPath'] ), handler: async (args) => sceneBridge.call('resetComponentProperty', args), }, { name: 'create_canvas', profile: 'full', description: 'Create a Cocos Canvas node with UITransform.', inputSchema: createSchema( { name: { type: 'string', description: 'Canvas node name.' }, parentPath: { type: 'string', description: 'Optional parent node path.' }, width: { type: 'number', description: 'Canvas width.' }, height: { type: 'number', description: 'Canvas height.' }, position: { type: 'object', description: 'Optional position {x,y,z}.' }, }, [] ), handler: async (args) => sceneBridge.call('createCanvas', args), }, { name: 'create_label', profile: 'full', description: 'Create a UI Label node under a parent.', inputSchema: createSchema( { name: { type: 'string', description: 'Label node name.' }, parentPath: { type: 'string', description: 'Optional parent node path.' }, text: { type: 'string', description: 'Label text.' }, fontSize: { type: 'number', description: 'Font size.' }, width: { type: 'number', description: 'UI width.' }, height: { type: 'number', description: 'UI height.' }, color: { type: 'string', description: 'Text color as #RRGGBB or #RRGGBBAA.' }, position: { type: 'object', description: 'Optional position {x,y,z}.' }, }, [] ), handler: async (args) => sceneBridge.call('createLabel', args), }, { name: 'create_button', profile: 'full', description: 'Create a UI Button node with child Label.', inputSchema: createSchema( { name: { type: 'string', description: 'Button node name.' }, parentPath: { type: 'string', description: 'Optional parent node path.' }, text: { type: 'string', description: 'Button text.' }, width: { type: 'number', description: 'Button width.' }, height: { type: 'number', description: 'Button height.' }, fontSize: { type: 'number', description: 'Text font size.' }, backgroundColor: { type: 'string', description: 'Background color as #RRGGBB or #RRGGBBAA.' }, textColor: { type: 'string', description: 'Text color as #RRGGBB or #RRGGBBAA.' }, position: { type: 'object', description: 'Optional position {x,y,z}.' }, }, [] ), handler: async (args) => sceneBridge.call('createButton', args), }, { name: 'create_sprite', profile: 'full', description: 'Create a UI Sprite node, optionally assigning a SpriteFrame asset uuid.', inputSchema: createSchema( { name: { type: 'string', description: 'Sprite node name.' }, parentPath: { type: 'string', description: 'Optional parent node path.' }, spriteFrameUuid: { type: 'string', description: 'Optional SpriteFrame asset uuid.' }, width: { type: 'number', description: 'UI width.' }, height: { type: 'number', description: 'UI height.' }, color: { type: 'string', description: 'Sprite color as #RRGGBB or #RRGGBBAA.' }, position: { type: 'object', description: 'Optional position {x,y,z}.' }, }, [] ), handler: async (args) => sceneBridge.call('createSprite', args), }, { name: 'list_cameras', profile: 'full', description: '[core] List Camera components in the active scene.', inputSchema: createSchema({}, []), handler: async (args) => sceneBridge.call('listCameras', args), }, { name: 'create_camera', profile: 'full', description: 'Create a Camera node in the active scene.', inputSchema: createSchema( { name: { type: 'string', description: 'Camera node name.' }, parentPath: { type: 'string', description: 'Optional parent node path.' }, priority: { type: 'number', description: 'Camera priority.' }, visibility: { type: 'number', description: 'Camera visibility mask.' }, clearFlags: { type: 'number', description: 'Camera clear flags.' }, position: { type: 'object', description: 'Optional position {x,y,z}.' }, eulerAngles: { type: 'object', description: 'Optional rotation {x,y,z}.' }, }, [] ), handler: async (args) => sceneBridge.call('createCamera', args), }, { name: 'set_camera_properties', profile: 'full', description: 'Set selected Camera component properties.', inputSchema: createSchema( { path: { type: 'string', description: 'Camera node path.' }, uuid: { type: 'string', description: 'Camera node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, priority: { type: 'number', description: 'Camera priority.' }, visibility: { type: 'number', description: 'Camera visibility mask.' }, clearFlags: { type: 'number', description: 'Camera clear flags.' }, projection: { type: 'number', description: 'Projection enum value.' }, orthoHeight: { type: 'number', description: 'Ortho height.' }, fov: { type: 'number', description: 'Field of view.' }, near: { type: 'number', description: 'Near clip.' }, far: { type: 'number', description: 'Far clip.' }, }, [] ), handler: async (args) => sceneBridge.call('setCameraProperties', args), }, { name: 'list_animations', profile: 'full', description: '[core] List Animation components in the active scene or under one node.', inputSchema: createSchema( { path: { type: 'string', description: 'Optional node path.' }, uuid: { type: 'string', description: 'Optional node uuid.' }, name: { type: 'string', description: 'Optional exact node name.' }, }, [] ), handler: async (args) => sceneBridge.call('listAnimations', args), }, { name: 'add_animation_clip', profile: 'full', description: 'Add an AnimationClip asset to a node Animation component.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, clipUuid: { type: 'string', description: 'AnimationClip asset uuid.' }, makeDefault: { type: 'boolean', description: 'Set this clip as defaultClip.' }, }, ['clipUuid'] ), handler: async (args) => sceneBridge.call('addAnimationClip', args), }, { name: 'play_animation', profile: 'full', description: '[core] Play an Animation component clip on a node.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, clipName: { type: 'string', description: 'Optional clip name.' }, }, [] ), handler: async (args) => sceneBridge.call('playAnimation', args), }, { name: 'stop_animation', profile: 'full', description: '[core] Stop an Animation component clip on a node.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, clipName: { type: 'string', description: 'Optional clip name.' }, }, [] ), handler: async (args) => sceneBridge.call('stopAnimation', args), }, { name: 'read_file', profile: 'full', description: '[core] Read a file from the Cocos project.', inputSchema: createSchema( { path: { type: 'string', description: 'Project-relative or absolute file path.' }, }, ['path'] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const fullPath = resolveProjectPath(projectPath, args.path); if (!fs.existsSync(fullPath)) { throw new Error(`File not found: ${args.path}`); } const content = fs.readFileSync(fullPath, 'utf8'); return content.length > 12000 ? `${content.slice(0, 12000)}\n... (truncated)` : content; }, }, { name: 'get_file_snippet', profile: 'full', description: '[core] Read a focused snippet around a file line number.', inputSchema: createSchema( { path: { type: 'string', description: 'Project-relative or absolute file path.' }, line: { type: 'number', description: 'Target line number, starting at 1.' }, contextLines: { type: 'number', description: 'Number of surrounding context lines.' }, }, ['path', 'line'] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const fullPath = resolveProjectPath(projectPath, args.path); if (!fs.existsSync(fullPath)) { throw new Error(`File not found: ${args.path}`); } return buildSnippet(fullPath, args.line, Number.isFinite(args.contextLines) ? args.contextLines : 3); }, }, { name: 'write_file', profile: 'full', description: '[core] Write or overwrite a file in the Cocos project.', inputSchema: createSchema( { path: { type: 'string', description: 'Project-relative or absolute file path.' }, content: { type: 'string', description: 'File content to write.' }, }, ['path', 'content'] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const fullPath = resolveProjectPath(projectPath, args.path); fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.writeFileSync(fullPath, args.content, 'utf8'); return `Wrote ${args.content.length} chars to ${args.path}\n${await refreshAssets(projectPath, fullPath)}`; }, }, { name: 'replace_in_file', profile: 'full', description: '[core] Replace text in a file, useful for script auto-fix loops.', inputSchema: createSchema( { path: { type: 'string', description: 'Project-relative or absolute file path.' }, search: { type: 'string', description: 'Literal text to search for.' }, replace: { type: 'string', description: 'Replacement text.' }, replaceAll: { type: 'boolean', description: 'Replace every occurrence instead of only the first.' }, }, ['path', 'search', 'replace'] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const fullPath = resolveProjectPath(projectPath, args.path); if (!fs.existsSync(fullPath)) { throw new Error(`File not found: ${args.path}`); } const original = fs.readFileSync(fullPath, 'utf8'); if (!original.includes(args.search)) { throw new Error(`Search text was not found in ${args.path}`); } const updated = args.replaceAll ? replaceAllLiteral(original, args.search, args.replace) : original.replace(args.search, args.replace); fs.writeFileSync(fullPath, updated, 'utf8'); return `Updated ${args.path} (${args.replaceAll ? 'all matches' : 'first match'})\n${await refreshAssets(projectPath, fullPath)}`; }, }, { name: 'search_files', profile: 'full', description: '[core] Search project files by simple wildcard pattern.', inputSchema: createSchema( { pattern: { type: 'string', description: "Wildcard file pattern such as '*.ts' or 'Player*'." }, directory: { type: 'string', description: 'Project-relative search root. Defaults to assets.' }, limit: { type: 'number', description: 'Maximum number of results to return.' }, }, ['pattern'] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const searchRoot = resolveProjectPath(projectPath, args.directory || 'assets'); if (!fs.existsSync(searchRoot)) { throw new Error(`Directory not found: ${args.directory || 'assets'}`); } const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(500, args.limit)) : 100; const results = searchFiles(searchRoot, args.pattern, limit).map((fullPath) => path.relative(projectPath, fullPath).replace(/\\/g, '/') ); return { count: results.length, files: results, }; }, }, { name: 'list_directory', profile: 'full', description: '[core] List files and directories inside a project directory.', inputSchema: createSchema( { path: { type: 'string', description: 'Project-relative or absolute directory path.' }, }, ['path'] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const targetPath = resolveProjectPath(projectPath, args.path); if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) { throw new Error(`Directory not found: ${args.path}`); } const entries = fs .readdirSync(targetPath, { withFileTypes: true }) .filter((entry) => !entry.name.startsWith('.')) .map((entry) => ({ name: entry.name, type: entry.isDirectory() ? 'directory' : 'file', })); return { path: args.path, entries, }; }, }, { name: 'exists', profile: 'full', description: '[core] Check whether a project file or directory exists.', inputSchema: createSchema( { path: { type: 'string', description: 'Project-relative or absolute path.' }, }, ['path'] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const targetPath = resolveProjectPath(projectPath, args.path); return { path: args.path, exists: fs.existsSync(targetPath), isFile: fs.existsSync(targetPath) ? fs.statSync(targetPath).isFile() : false, isDirectory: fs.existsSync(targetPath) ? fs.statSync(targetPath).isDirectory() : false, }; }, }, { name: 'refresh_assets', profile: 'full', description: '[core] Best-effort asset database refresh for a file or the assets root.', inputSchema: createSchema( { path: { type: 'string', description: 'Optional project-relative file path to refresh.' }, }, [] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const targetPath = resolveProjectPath(projectPath, args.path || 'assets'); return await refreshAssets(projectPath, targetPath); }, }, { name: 'run_script_diagnostics', profile: 'core', description: '[specialist] Run a TypeScript no-emit check for the current Cocos project and return parsed diagnostics. This is a preferred specialist tool for script errors when diagnostics are needed.', inputSchema: createSchema( { tsconfigPath: { type: 'string', description: 'Optional path to the tsconfig file to use.' }, }, [] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); return await runScriptDiagnostics(projectPath, args); }, }, { name: 'get_recent_logs', profile: 'core', description: '[specialist] Return recent MCP runtime logs, recent tool interactions, and tails of common project log files.', inputSchema: createSchema( { limit: { type: 'number', description: 'Maximum in-memory runtime/interactions to return.' }, includeProjectLogs: { type: 'boolean', description: 'Include tails from common project log files.' }, projectLogLines: { type: 'number', description: 'Tail lines to read per project log file.' }, }, [] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(200, args.limit)) : 50; return { runtimeLogs: runtimeLog && typeof runtimeLog.list === 'function' ? runtimeLog.list(limit) : [], interactions: interactionLog && typeof interactionLog.list === 'function' ? interactionLog.list(limit) : [], projectLogs: args.includeProjectLogs === false ? [] : getRecentProjectLogs(projectPath, { limit: 10, lines: Number.isFinite(args.projectLogLines) ? args.projectLogLines : 80, }), }; }, }, { name: 'search_project_logs', profile: 'core', description: '[specialist] Search common Cocos project log files for a string or regular expression.', inputSchema: createSchema( { query: { type: 'string', description: 'Text or regex pattern to search for.' }, regex: { type: 'boolean', description: 'Treat query as a JavaScript regular expression.' }, caseSensitive: { type: 'boolean', description: 'Use case-sensitive matching.' }, limit: { type: 'number', description: 'Maximum matches to return.' }, directory: { type: 'string', description: 'Optional project-relative log directory to search.' }, }, ['query'] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); return searchProjectLogs(projectPath, args); }, }, { name: 'clear_logs', profile: 'core', description: '[specialist] Clear in-memory MCP logs and, only with explicit confirmation, truncate common project log files.', inputSchema: createSchema( { scope: { type: 'string', description: 'mcp, project, or all. Defaults to mcp.' }, confirmProjectLogs: { type: 'boolean', description: 'Required when scope includes project log files.' }, directory: { type: 'string', description: 'Optional project-relative log directory to clear.' }, }, [] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const scope = String(args.scope || 'mcp').toLowerCase(); const clearMcp = scope === 'mcp' || scope === 'all'; const clearProject = scope === 'project' || scope === 'all'; const result = { runtimeLogEntriesCleared: 0, interactionEntriesCleared: 0, projectLogFilesCleared: [], }; if (clearMcp) { result.runtimeLogEntriesCleared = runtimeLog && typeof runtimeLog.clear === 'function' ? runtimeLog.clear() : 0; result.interactionEntriesCleared = interactionLog && typeof interactionLog.clear === 'function' ? interactionLog.clear() : 0; } if (clearProject) { if (!args.confirmProjectLogs) { throw new Error('confirmProjectLogs=true is required before truncating project log files.'); } result.projectLogFilesCleared = clearProjectLogFiles(projectPath, { directory: args.directory, limit: 50 }); } if (!clearMcp && !clearProject) { throw new Error("scope must be 'mcp', 'project', or 'all'."); } return result; }, }, { name: 'validate_scene', profile: 'core', description: '[specialist] Run a compact validation pass over the active scene, runtime state, TypeScript diagnostics, and recent project log errors.', inputSchema: createSchema( { maxDepth: { type: 'number', description: 'Scene hierarchy depth for the scene snapshot.' }, includeScriptDiagnostics: { type: 'boolean', description: 'Run TypeScript diagnostics as part of validation.' }, includeLogErrors: { type: 'boolean', description: 'Search project logs for error lines.' }, }, [] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const scene = await sceneBridge.call('getSceneInfo', { maxDepth: Number.isFinite(args.maxDepth) ? args.maxDepth : 2, includeComponents: true, }).catch((error) => ({ ok: false, error: error.message })); const runtime = await sceneBridge.call('getRuntimeState', {}).catch((error) => ({ ok: false, error: error.message })); const diagnostics = args.includeScriptDiagnostics === false ? null : summarizeDiagnostics(await runScriptDiagnostics(projectPath, args).catch((error) => ({ ok: false, summary: error.message, diagnostics: [] }))); const logErrors = args.includeLogErrors === false ? null : searchProjectLogs(projectPath, { query: 'error', limit: 20 }).matches; return { ok: !scene.error && !runtime.error && (!diagnostics || diagnostics.ok) && (!logErrors || logErrors.length === 0), scene, runtime, diagnostics, logErrors, }; }, }, { name: 'get_runtime_state', profile: 'core', description: '[specialist] Return structured Cocos runtime state including pause state, frame count, and scheduler time scale. Prefer this when you want a compact validation snapshot.', inputSchema: createSchema({}, []), handler: async (args) => sceneBridge.call('getRuntimeState', args), }, { name: 'pause_runtime', profile: 'full', description: '[core] Pause Cocos director game logic execution.', inputSchema: createSchema({}, []), handler: async (args) => sceneBridge.call('pauseRuntime', args), }, { name: 'resume_runtime', profile: 'full', description: '[core] Resume Cocos director game logic execution.', inputSchema: createSchema({}, []), handler: async (args) => sceneBridge.call('resumeRuntime', args), }, { name: 'set_time_scale', profile: 'full', description: '[core] Set Cocos scheduler time scale for runtime validation.', inputSchema: createSchema( { scale: { type: 'number', description: 'Time scale from 0 to 100.' }, }, ['scale'] ), handler: async (args) => sceneBridge.call('setTimeScale', args), }, { name: 'emit_node_event', profile: 'full', description: '[core] Emit a custom event on a target scene node with an optional JSON payload.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, eventName: { type: 'string', description: 'Event name to emit.' }, payload: { type: 'object', description: 'Optional event payload object.' }, }, ['eventName'] ), handler: async (args) => sceneBridge.call('emitNodeEvent', args), }, { name: 'simulate_button_click', profile: 'full', description: '[core] Simulate a Cocos Button click by emitting click events on the target button node.', inputSchema: createSchema( { path: { type: 'string', description: 'Button node hierarchy path.' }, uuid: { type: 'string', description: 'Button node uuid.' }, name: { type: 'string', description: 'Fallback exact button node name.' }, }, [] ), handler: async (args) => sceneBridge.call('simulateButtonClick', args), }, { name: 'invoke_component_method', profile: 'full', description: '[core] Invoke a method on a component for runtime validation and test hooks.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node uuid.' }, name: { type: 'string', description: 'Fallback exact node name.' }, componentName: { type: 'string', description: 'Component class name.' }, index: { type: 'number', description: 'Optional component index.' }, methodName: { type: 'string', description: 'Method name to invoke.' }, args: { type: 'array', description: 'Optional argument array.' }, }, ['methodName'] ), handler: async (args) => sceneBridge.call('invokeComponentMethod', args), }, { name: 'get_script_diagnostic_context', profile: 'core', description: '[specialist] Run TypeScript diagnostics and attach source snippets for each error. This is a preferred specialist tool for compile-error triage before repair.', inputSchema: createSchema( { tsconfigPath: { type: 'string', description: 'Optional path to the tsconfig file to use.' }, contextLines: { type: 'number', description: 'Number of surrounding source lines per diagnostic.' }, limit: { type: 'number', description: 'Maximum diagnostics to include.' }, }, [] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const result = await runScriptDiagnostics(projectPath, args); const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(50, args.limit)) : 10; const contextLines = Number.isFinite(args.contextLines) ? Math.max(0, Math.min(20, args.contextLines)) : 3; const diagnostics = result.diagnostics.slice(0, limit).map((diagnostic) => ({ ...diagnostic, snippet: fs.existsSync(diagnostic.file) ? buildSnippet(diagnostic.file, diagnostic.line, contextLines) : 'Source file not found.', })); return { ...result, diagnostics, }; }, }, { name: 'capture_desktop_screenshot', profile: 'full', description: '[core] Capture a screenshot from the local desktop and return it as an MCP image payload.', inputSchema: createSchema( { fileName: { type: 'string', description: 'Optional output file name under temp/mcp-captures.' }, }, [] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const result = await captureDesktopScreenshot(projectPath, args); return result.dataUri; }, }, { name: 'capture_editor_screenshot', profile: 'core', description: '[specialist] Capture the focused Cocos Creator editor window and return it as an MCP image payload. Prefer screenshot tools only when visual verification is explicitly needed.', inputSchema: createSchema( { fileName: { type: 'string', description: 'Optional output file name under temp/mcp-captures.' }, titleContains: { type: 'string', description: 'Optional window title substring fallback if no window is focused.' }, }, [] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const result = await captureEditorWindowScreenshot(projectPath, args); return result.dataUri; }, }, { name: 'capture_scene_screenshot', profile: 'core', description: '[specialist] Capture the Scene panel region from the editor window with panel-level cropping when available. Prefer this only for visual validation of scene-side results.', inputSchema: createSchema( { fileName: { type: 'string', description: 'Optional output file name under temp/mcp-captures.' }, windowKind: { type: 'string', description: 'Window target kind: focused, editor, simulator, or preview.' }, titleContains: { type: 'string', description: 'Optional window title substring fallback if no window is focused.' }, }, [] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const result = await capturePanelScreenshot(projectPath, { ...args, panel: 'scene', windowKind: args.windowKind || 'editor' }); return result.dataUri; }, }, { name: 'capture_game_screenshot', profile: 'full', description: '[core] Capture the Game/Preview panel region from the editor window with panel-level cropping when available.', inputSchema: createSchema( { fileName: { type: 'string', description: 'Optional output file name under temp/mcp-captures.' }, windowKind: { type: 'string', description: 'Window target kind: focused, editor, simulator, or preview.' }, titleContains: { type: 'string', description: 'Optional window title substring fallback if no window is focused.' }, }, [] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const result = await capturePanelScreenshot(projectPath, { ...args, panel: 'game', windowKind: args.windowKind || 'editor' }); return result.dataUri; }, }, { name: 'list_editor_windows', profile: 'core', description: '[specialist] List available Electron windows so screenshots or input-targeting can choose the correct window. Use this when window targeting is the explicit problem.', inputSchema: createSchema({}, []), handler: async () => listWindows(), }, { name: 'simulate_mouse_click', profile: 'full', description: '[core] Send a low-level Electron mouse click to the editor, preview, or simulator window.', inputSchema: createSchema( { windowKind: { type: 'string', description: 'Window target kind: focused, editor, simulator, or preview.' }, panel: { type: 'string', description: 'Optional panel hint such as scene or game.' }, titleContains: { type: 'string', description: 'Optional window title substring.' }, x: { type: 'number', description: 'Panel-relative or window-relative x offset from center/focus target.' }, y: { type: 'number', description: 'Panel-relative or window-relative y offset from center/focus target.' }, button: { type: 'string', description: 'Mouse button: left, right, or middle.' }, clickCount: { type: 'number', description: 'Click count.' }, modifiers: { type: 'array', description: 'Optional key modifiers array.' }, }, [] ), handler: async (args) => await sendMouseClick(args), }, { name: 'simulate_mouse_drag', profile: 'full', description: '[core] Send a low-level Electron mouse drag to the editor, preview, or simulator window.', inputSchema: createSchema( { windowKind: { type: 'string', description: 'Window target kind: focused, editor, simulator, or preview.' }, panel: { type: 'string', description: 'Optional panel hint such as scene or game.' }, titleContains: { type: 'string', description: 'Optional window title substring.' }, startX: { type: 'number', description: 'Start x offset.' }, startY: { type: 'number', description: 'Start y offset.' }, endX: { type: 'number', description: 'End x offset.' }, endY: { type: 'number', description: 'End y offset.' }, button: { type: 'string', description: 'Mouse button: left, right, or middle.' }, steps: { type: 'number', description: 'How many intermediate move steps to send.' }, stepDelayMs: { type: 'number', description: 'Optional delay between drag steps.' }, modifiers: { type: 'array', description: 'Optional key modifiers array.' }, }, [] ), handler: async (args) => await sendMouseDrag(args), }, { name: 'simulate_key_press', profile: 'full', description: '[core] Send a low-level Electron key press to the editor, preview, or simulator window.', inputSchema: createSchema( { windowKind: { type: 'string', description: 'Window target kind: focused, editor, simulator, or preview.' }, panel: { type: 'string', description: 'Optional panel hint such as scene or game.' }, titleContains: { type: 'string', description: 'Optional window title substring.' }, keyCode: { type: 'string', description: 'Electron keyCode such as A, Space, Enter, ArrowLeft.' }, text: { type: 'string', description: 'Optional text payload for char events.' }, modifiers: { type: 'array', description: 'Optional key modifiers array.' }, }, ['keyCode'] ), handler: async (args) => await sendKeyPress(args), }, { name: 'simulate_key_combo', profile: 'full', description: '[core] Send a low-level Electron modified key press such as Ctrl+S or Cmd+P.', inputSchema: createSchema( { windowKind: { type: 'string', description: 'Window target kind: focused, editor, simulator, or preview.' }, panel: { type: 'string', description: 'Optional panel hint such as scene or game.' }, titleContains: { type: 'string', description: 'Optional window title substring.' }, keyCode: { type: 'string', description: 'Electron keyCode such as S, P, Enter.' }, modifiers: { type: 'array', description: 'Modifier array such as [\"command\"] or [\"control\",\"shift\"].' }, }, ['keyCode', 'modifiers'] ), handler: async (args) => await sendKeyCombo(args), }, { name: 'simulate_preview_input', profile: 'full', description: '[core] Convenience wrapper for low-level preview/simulator input. Uses mouse click by default or key press when keyCode is provided.', inputSchema: createSchema( { windowKind: { type: 'string', description: 'Window target kind, usually preview or simulator.' }, panel: { type: 'string', description: 'Optional panel hint such as game.' }, titleContains: { type: 'string', description: 'Optional window title substring.' }, mode: { type: 'string', description: 'click, drag, key, or combo.' }, x: { type: 'number', description: 'Mouse x offset.' }, y: { type: 'number', description: 'Mouse y offset.' }, startX: { type: 'number', description: 'Drag start x offset.' }, startY: { type: 'number', description: 'Drag start y offset.' }, endX: { type: 'number', description: 'Drag end x offset.' }, endY: { type: 'number', description: 'Drag end y offset.' }, keyCode: { type: 'string', description: 'Electron keyCode for key or combo mode.' }, text: { type: 'string', description: 'Optional char payload.' }, button: { type: 'string', description: 'Mouse button.' }, modifiers: { type: 'array', description: 'Modifier array.' }, }, [] ), handler: async (args) => { const mode = String(args.mode || (args.keyCode ? 'key' : 'click')).toLowerCase(); const base = { ...args, windowKind: args.windowKind || 'preview', panel: args.panel || 'game' }; if (mode === 'drag') return await sendMouseDrag(base); if (mode === 'combo') return await sendKeyCombo(base); if (mode === 'key') return await sendKeyPress(base); return await sendMouseClick(base); }, }, { name: 'capture_preview_screenshot', profile: 'core', description: '[specialist] Capture the preview or simulator window as an MCP image payload. Prefer this only when you need visual proof of game or preview output.', inputSchema: createSchema( { fileName: { type: 'string', description: 'Optional output file name under temp/mcp-captures.' }, windowKind: { type: 'string', description: 'Window target kind, usually preview or simulator.' }, titleContains: { type: 'string', description: 'Optional window title substring.' }, }, [] ), handler: async (args) => { const { projectPath } = getRuntimeContext(); const result = await capturePanelScreenshot(projectPath, { ...args, panel: 'game', windowKind: args.windowKind || 'preview' }); return result.dataUri; }, }, ]; const registry = { listTools() { const { config } = getRuntimeContext(); return tools .filter((tool) => isToolExposed(config || {}, tool)) .map((tool) => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, })); }, listToolCatalog() { const { config } = getRuntimeContext(); return tools.map((tool) => ({ name: tool.name, description: tool.description, profile: tool.profile, category: toolCategory(tool), enabled: isToolExposed(config || {}, tool), })); }, async callToolDetailed(name, args) { const { config } = getRuntimeContext(); const tool = tools.find((item) => item.name === name); if (!tool) { throw new Error(`Unknown tool '${name}'`); } if (!isToolExposed(config || {}, tool)) { throw new Error(`Tool '${name}' is not exposed by the current MCP tool profile '${config.toolProfile}'.`); } try { const result = await tool.handler(args || {}); const output = toOutput(result); interactionLog.add(name, 'success', output.slice(0, 500)); return { value: result, text: output, }; } catch (error) { interactionLog.add(name, 'error', error.message); throw error; } }, async callTool(name, args) { const result = await registry.callToolDetailed(name, args); return result.text; }, }; return registry; } module.exports = { createToolRegistry, };