2056 lines
82 KiB
JavaScript
2056 lines
82 KiB
JavaScript
'use strict';
|
|
|
|
const crypto = require('crypto');
|
|
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 {
|
|
createProjectSkill,
|
|
listProjectInstructions,
|
|
readProjectInstruction,
|
|
writeProjectInstruction,
|
|
} = require('./project-instructions');
|
|
const {
|
|
applyPrefabInstance,
|
|
duplicatePrefab,
|
|
editPrefabJson,
|
|
inspectPrefab,
|
|
revertPrefabInstance,
|
|
validatePrefabReferences,
|
|
} = require('./prefabs');
|
|
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
|
|
const { checkForUpdate } = require('./update-checker');
|
|
const { safeStringify } = require('./utils');
|
|
const IMAGE_DATA_URI_PREFIX = 'data:image/png;base64,';
|
|
|
|
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/],
|
|
['instructions', /instruction|skill/],
|
|
['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 createOutputSchema(dataSchema = {}) {
|
|
return {
|
|
type: 'object',
|
|
properties: {
|
|
ok: { type: 'boolean', description: 'Whether the tool call completed successfully.' },
|
|
tool: { type: 'string', description: 'Tool name that produced this result.' },
|
|
callId: { type: 'string', description: 'Stable identifier for this tool call result.' },
|
|
timestamp: { type: 'string', description: 'ISO timestamp when the result envelope was produced.' },
|
|
summary: { type: 'string', description: 'Short human-readable result summary.' },
|
|
data: dataSchema,
|
|
refs: {
|
|
type: 'array',
|
|
description: 'Stable references discovered in the result for follow-up tool calls.',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
type: { type: 'string' },
|
|
id: { type: 'string' },
|
|
path: { type: 'string' },
|
|
name: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
required: ['ok', 'tool', 'callId', 'timestamp', 'data'],
|
|
};
|
|
}
|
|
|
|
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 inferToolAnnotations(tool) {
|
|
const name = tool.name;
|
|
const category = toolCategory(tool);
|
|
const readOnly = /^(get|list|inspect|find|read|search|check|validate|exists|capture)/.test(name);
|
|
const destructive = /(delete|remove|clear|replace|write|reset|set_|execute|run_scene|invoke|emit|simulate)/.test(name);
|
|
const idempotent = readOnly || /^(set|select|open|pause|resume|stop|refresh)/.test(name);
|
|
|
|
return {
|
|
title: name
|
|
.split('_')
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join(' '),
|
|
readOnlyHint: readOnly,
|
|
destructiveHint: readOnly ? false : destructive,
|
|
idempotentHint: idempotent,
|
|
openWorldHint: category === 'updates',
|
|
...(tool.annotations || {}),
|
|
};
|
|
}
|
|
|
|
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 hashObject(value) {
|
|
return crypto
|
|
.createHash('sha256')
|
|
.update(safeStringify(value))
|
|
.digest('hex')
|
|
.slice(0, 16);
|
|
}
|
|
|
|
function summarizeResult(result) {
|
|
if (typeof result === 'string') {
|
|
if (result.startsWith(IMAGE_DATA_URI_PREFIX)) {
|
|
return 'Image payload returned.';
|
|
}
|
|
return result.length > 160 ? `${result.slice(0, 160)}...` : result;
|
|
}
|
|
if (!result || typeof result !== 'object') {
|
|
return String(result);
|
|
}
|
|
if (typeof result.summary === 'string') {
|
|
return result.summary;
|
|
}
|
|
for (const key of ['message', 'path', 'url', 'sceneName', 'projectName']) {
|
|
if (typeof result[key] === 'string' && result[key]) {
|
|
return `${key}: ${result[key]}`;
|
|
}
|
|
}
|
|
if (Number.isFinite(result.count)) {
|
|
return `count: ${result.count}`;
|
|
}
|
|
return 'Structured result returned.';
|
|
}
|
|
|
|
function normalizeEnvelopeData(result) {
|
|
if (typeof result === 'string' && result.startsWith(IMAGE_DATA_URI_PREFIX)) {
|
|
return {
|
|
image: true,
|
|
mimeType: 'image/png',
|
|
byteLength: Buffer.byteLength(result.slice(IMAGE_DATA_URI_PREFIX.length), 'base64'),
|
|
};
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function addRef(refs, type, id, extra = {}) {
|
|
if (!id) {
|
|
return;
|
|
}
|
|
const key = `${type}:${id}`;
|
|
if (refs.some((ref) => ref.key === key)) {
|
|
return;
|
|
}
|
|
refs.push({ key, type, id: String(id), ...extra });
|
|
}
|
|
|
|
function collectRefs(value, refs = [], depth = 0, seen = new WeakSet()) {
|
|
if (!value || depth > 5) {
|
|
return refs;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
for (const item of value) {
|
|
collectRefs(item, refs, depth + 1, seen);
|
|
}
|
|
return refs;
|
|
}
|
|
if (typeof value !== 'object') {
|
|
return refs;
|
|
}
|
|
if (seen.has(value)) {
|
|
return refs;
|
|
}
|
|
seen.add(value);
|
|
|
|
const uuid = value.uuid || value.prefabUuid || value.sceneUuid || value.assetUuid;
|
|
const pathValue = value.path || value.node || value.url;
|
|
if (uuid) {
|
|
addRef(refs, pathValue && String(pathValue).startsWith('db://') ? 'asset' : 'uuid', uuid, {
|
|
path: pathValue ? String(pathValue) : undefined,
|
|
name: value.name ? String(value.name) : undefined,
|
|
});
|
|
}
|
|
if (typeof pathValue === 'string' && pathValue) {
|
|
addRef(refs, pathValue.startsWith('db://') ? 'asset' : 'path', pathValue, {
|
|
name: value.name ? String(value.name) : undefined,
|
|
});
|
|
}
|
|
|
|
for (const item of Object.values(value)) {
|
|
collectRefs(item, refs, depth + 1, seen);
|
|
}
|
|
return refs;
|
|
}
|
|
|
|
function createResultEnvelope(tool, args, result, options = {}) {
|
|
const data = normalizeEnvelopeData(result);
|
|
const refs = collectRefs(data).map(({ key, ...ref }) => ref);
|
|
const timestamp = new Date().toISOString();
|
|
const summary = options.summary || summarizeResult(result);
|
|
const callId = `fp_${hashObject({ tool: tool.name, args: args || {}, result: data })}`;
|
|
return {
|
|
ok: options.ok !== false,
|
|
tool: tool.name,
|
|
callId,
|
|
timestamp,
|
|
summary,
|
|
data,
|
|
refs,
|
|
};
|
|
}
|
|
|
|
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.';
|
|
}
|
|
|
|
async function resolveNodeUuid(sceneBridge, args) {
|
|
if (args && args.uuid) {
|
|
return String(args.uuid);
|
|
}
|
|
const inspected = await sceneBridge.call('inspectNode', args || {});
|
|
if (!inspected || !inspected.uuid) {
|
|
throw new Error('Target node uuid could not be resolved.');
|
|
}
|
|
return inspected.uuid;
|
|
}
|
|
|
|
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: 'list_project_instructions',
|
|
profile: 'core',
|
|
description: '[specialist] List project AI instruction files and local Codex project skills.',
|
|
inputSchema: createSchema({}, []),
|
|
handler: async () => {
|
|
const { projectPath } = getRuntimeContext();
|
|
return listProjectInstructions(projectPath);
|
|
},
|
|
},
|
|
{
|
|
name: 'read_project_instruction',
|
|
profile: 'core',
|
|
description: '[specialist] Read a project AI instruction file such as AGENTS.md, CLAUDE.md, or a .codex skill SKILL.md.',
|
|
inputSchema: createSchema(
|
|
{
|
|
target: { type: 'string', description: 'Project-relative instruction path.' },
|
|
},
|
|
['target']
|
|
),
|
|
handler: async (args) => {
|
|
const { projectPath } = getRuntimeContext();
|
|
return readProjectInstruction(projectPath, args.target);
|
|
},
|
|
},
|
|
{
|
|
name: 'write_project_instruction',
|
|
profile: 'full',
|
|
description: '[core] Create or update a project AI instruction file inside the Cocos project.',
|
|
inputSchema: createSchema(
|
|
{
|
|
target: { type: 'string', description: 'Project-relative instruction path.' },
|
|
content: { type: 'string', description: 'Instruction file content.' },
|
|
overwrite: { type: 'boolean', description: 'Allow overwriting an existing file. Defaults to true.' },
|
|
},
|
|
['target', 'content']
|
|
),
|
|
handler: async (args) => {
|
|
const { projectPath } = getRuntimeContext();
|
|
return writeProjectInstruction(projectPath, args);
|
|
},
|
|
},
|
|
{
|
|
name: 'create_project_skill',
|
|
profile: 'full',
|
|
description: '[core] Create a local Codex project skill under .codex/skills/{skillName}/SKILL.md.',
|
|
inputSchema: createSchema(
|
|
{
|
|
skillName: { type: 'string', description: 'Filesystem-safe project skill name.' },
|
|
title: { type: 'string', description: 'Human-readable skill title.' },
|
|
description: { type: 'string', description: 'Skill trigger description.' },
|
|
instructions: { type: 'string', description: 'Skill instructions body.' },
|
|
overwrite: { type: 'boolean', description: 'Allow overwriting an existing skill. Defaults to true.' },
|
|
},
|
|
['skillName']
|
|
),
|
|
handler: async (args) => {
|
|
const { projectPath } = getRuntimeContext();
|
|
return createProjectSkill(projectPath, args);
|
|
},
|
|
},
|
|
{
|
|
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: 'inspect_prefab',
|
|
profile: 'core',
|
|
description: '[specialist] Inspect a prefab asset, its metadata, serialized file path, and UUID-like asset references.',
|
|
inputSchema: createSchema(
|
|
{
|
|
target: { type: 'string', description: 'Prefab uuid, db url, or project path.' },
|
|
},
|
|
['target']
|
|
),
|
|
handler: async (args) => {
|
|
const { projectPath } = getRuntimeContext();
|
|
return await inspectPrefab(projectPath, args.target);
|
|
},
|
|
},
|
|
{
|
|
name: 'validate_prefab_references',
|
|
profile: 'core',
|
|
description: '[specialist] Validate prefab asset references by checking serialized UUID references against asset-db.',
|
|
inputSchema: createSchema(
|
|
{
|
|
target: { type: 'string', description: 'Optional prefab uuid, db url, or path. When omitted, scans prefab assets.' },
|
|
pattern: { type: 'string', description: 'Optional asset-db pattern used when scanning prefabs.' },
|
|
limit: { type: 'number', description: 'Maximum prefab assets to scan when target is omitted.' },
|
|
},
|
|
[]
|
|
),
|
|
handler: async (args) => {
|
|
const { projectPath } = getRuntimeContext();
|
|
return await validatePrefabReferences(projectPath, args);
|
|
},
|
|
},
|
|
{
|
|
name: 'duplicate_prefab',
|
|
profile: 'full',
|
|
description: '[core] Create a new prefab asset by duplicating an existing prefab file without copying its .meta UUID.',
|
|
inputSchema: createSchema(
|
|
{
|
|
source: { type: 'string', description: 'Source prefab uuid, db url, or project path.' },
|
|
target: { type: 'string', description: 'Project-relative target path under assets, with or without .prefab.' },
|
|
overwrite: { type: 'boolean', description: 'Overwrite target prefab if it already exists.' },
|
|
},
|
|
['source', 'target']
|
|
),
|
|
handler: async (args) => {
|
|
const { projectPath } = getRuntimeContext();
|
|
const result = await duplicatePrefab(projectPath, args);
|
|
return { ...result, refresh: await refreshAssets(projectPath, resolveProjectPath(projectPath, result.target)) };
|
|
},
|
|
},
|
|
{
|
|
name: 'edit_prefab_json',
|
|
profile: 'full',
|
|
description: '[core] Edit a prefab JSON file by JSON path assignment or literal search/replace, then validate references.',
|
|
inputSchema: createSchema(
|
|
{
|
|
target: { type: 'string', description: 'Prefab uuid, db url, or project path.' },
|
|
jsonPath: { type: 'string', description: 'JSON path such as /0/_name or 0._name when assigning valueJson.' },
|
|
valueJson: { type: 'string', description: 'JSON encoded value to assign at jsonPath.' },
|
|
search: { type: 'string', description: 'Literal text to search for instead of jsonPath assignment.' },
|
|
replace: { type: 'string', description: 'Replacement text for literal search.' },
|
|
replaceAll: { type: 'boolean', description: 'Replace all literal matches.' },
|
|
createBackup: { type: 'boolean', description: 'Create a .bak file before writing.' },
|
|
},
|
|
['target']
|
|
),
|
|
handler: async (args) => {
|
|
const { projectPath } = getRuntimeContext();
|
|
const result = await editPrefabJson(projectPath, args);
|
|
return { ...result, refresh: await refreshAssets(projectPath, resolveProjectPath(projectPath, result.path)) };
|
|
},
|
|
},
|
|
{
|
|
name: 'create_prefab_instance',
|
|
profile: 'full',
|
|
description: '[core] Create a linked prefab instance in the editor hierarchy using Cocos scene create-node when available.',
|
|
inputSchema: createSchema(
|
|
{
|
|
prefabUuid: { type: 'string', description: 'Prefab asset uuid, db url, or path.' },
|
|
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}; fallback runtime path only.' },
|
|
},
|
|
['prefabUuid']
|
|
),
|
|
handler: async (args) => {
|
|
const info = await queryAssetInfo(args.prefabUuid);
|
|
const payload = {
|
|
assetUuid: info.uuid || args.prefabUuid,
|
|
unlinkPrefab: false,
|
|
};
|
|
if (args.parentPath) {
|
|
payload.parent = await resolveNodeUuid(sceneBridge, { path: args.parentPath });
|
|
}
|
|
if (args.name) {
|
|
payload.name = args.name;
|
|
}
|
|
|
|
if (global.Editor && Editor.Message && typeof Editor.Message.request === 'function') {
|
|
try {
|
|
const createdUuid = await Editor.Message.request('scene', 'create-node', payload);
|
|
return {
|
|
created: true,
|
|
linkedPrefab: true,
|
|
prefabUuid: payload.assetUuid,
|
|
uuid: createdUuid,
|
|
};
|
|
} catch (error) {
|
|
runtimeLog && runtimeLog.add('warn', `Linked prefab create-node failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
return await sceneBridge.call('instantiatePrefab', {
|
|
...args,
|
|
prefabUuid: info.uuid || args.prefabUuid,
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'inspect_prefab_instance',
|
|
profile: 'core',
|
|
description: '[specialist] Inspect whether a scene node is linked to a prefab instance and return prefab metadata when available.',
|
|
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('getPrefabInstanceInfo', args),
|
|
},
|
|
{
|
|
name: 'apply_prefab_instance',
|
|
profile: 'full',
|
|
description: '[core] Apply a scene prefab instance back to its associated prefab asset using the Cocos editor scene apply-prefab message.',
|
|
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) => await applyPrefabInstance(await resolveNodeUuid(sceneBridge, args)),
|
|
},
|
|
{
|
|
name: 'revert_prefab_instance',
|
|
profile: 'full',
|
|
description: '[core] Revert a scene prefab instance from its associated prefab asset using available Cocos editor prefab revert messages.',
|
|
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) => await revertPrefabInstance(await resolveNodeUuid(sceneBridge, args)),
|
|
},
|
|
{
|
|
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 performance = await sceneBridge.call('getPerformanceSnapshot', {}).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 && !performance.error && (!diagnostics || diagnostics.ok) && (!logErrors || logErrors.length === 0),
|
|
scene,
|
|
runtime,
|
|
performance,
|
|
diagnostics,
|
|
logErrors,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
name: 'get_performance_snapshot',
|
|
profile: 'core',
|
|
description: '[specialist] Return scene scale and runtime performance-oriented counters such as node/component counts, UI counts, depth, memory, and warnings.',
|
|
inputSchema: createSchema({}, []),
|
|
handler: async (args) => sceneBridge.call('getPerformanceSnapshot', args),
|
|
},
|
|
{
|
|
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,
|
|
outputSchema: tool.outputSchema || createOutputSchema(tool.dataSchema),
|
|
annotations: inferToolAnnotations(tool),
|
|
}));
|
|
},
|
|
listToolCatalog() {
|
|
const { config } = getRuntimeContext();
|
|
return tools.map((tool) => ({
|
|
name: tool.name,
|
|
description: tool.description,
|
|
profile: tool.profile,
|
|
category: toolCategory(tool),
|
|
annotations: inferToolAnnotations(tool),
|
|
outputSchema: tool.outputSchema || createOutputSchema(tool.dataSchema),
|
|
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 envelope = createResultEnvelope(tool, args || {}, result);
|
|
const output = typeof result === 'string' && result.startsWith(IMAGE_DATA_URI_PREFIX)
|
|
? result
|
|
: toOutput(envelope);
|
|
interactionLog.add(name, 'success', envelope.summary.slice(0, 500));
|
|
return {
|
|
value: envelope,
|
|
text: output,
|
|
};
|
|
} catch (error) {
|
|
interactionLog.add(name, 'error', error.message);
|
|
error.toolEnvelope = createResultEnvelope(tool, args || {}, { message: error.message }, {
|
|
ok: false,
|
|
summary: error.message,
|
|
});
|
|
throw error;
|
|
}
|
|
},
|
|
async callTool(name, args) {
|
|
const result = await registry.callToolDetailed(name, args);
|
|
return result.text;
|
|
},
|
|
};
|
|
|
|
return registry;
|
|
}
|
|
|
|
module.exports = {
|
|
createToolRegistry,
|
|
};
|