Bug fixes (verified against Cocos Creator 3.8.8): - diagnostics: shell:true for .cmd/.bat on Windows (T125/T126) - prefabs: duplicatePrefab target resolves under assets/ (T420) - cocos-project: add preview.open candidate for 3.8.8 (T444) - assets-advanced: detect directory assets in inspectAssetDependencies (T110) - scene: improve component-not-found errors with compilation hint (T429) Documentation improvements: - tool-registry: add enum and injected vars to execute_javascript, path format examples, asset ref limitation note - resources: add cocos://mcp/execute-context resource with variables, patterns, pitfalls New tools (core 37->38, full 101->110): - scene-management: create_scene, query_scene_state (core), copy_paste_node, rename_node, reparent_node - scripts: create_script with component/plain templates - prefabs: create_prefab - assets-advanced: batch_asset_ops, find_unused_assets
20 KiB
Implementation Plan: High-Frequency Editor Operations
Goal: Fill the 9 highest-impact tool gaps identified in the Cocos Creator business-development workflow analysis.
Current state: 101 tools (core=37, full=101), version 0.4.0 Target state: 110 tools (core=38, full=110)
Tool Summary
| # | Tool | Profile | Side | Phase | New file? |
|---|---|---|---|---|---|
| 1 | create_scene |
full |
editor | 1 | lib/tools/scene-management.js |
| 2 | query_scene_state |
core |
editor | 1 | same |
| 3 | copy_paste_node |
full |
editor | 1 | same |
| 4 | rename_node |
full |
scene | 2 | scene.js + scene-management.js |
| 5 | reparent_node |
full |
scene | 2 | same |
| 6 | create_prefab |
full |
editor | 3 | prefabs.js + tool-registry.js |
| 7 | create_script |
full |
editor | 4 | lib/tools/scripts.js |
| 8 | batch_asset_ops |
full |
editor | 5 | assets-advanced.js (append) |
| 9 | find_unused_assets |
full |
editor | 5 | same |
Files Changed
| File | Action | Phase |
|---|---|---|
lib/tools/scene-management.js |
New — factory for tools 1-5 | 1, 2 |
scene.js |
Modify — add renameNode, reparentNode methods |
2 |
lib/prefabs.js |
Modify — add createPrefab helper |
3 |
lib/tool-registry.js |
Modify — import + spread new factories, add create_prefab inline |
1-5 |
lib/tools/scripts.js |
New — factory for tool 7 | 4 |
lib/tools/assets-advanced.js |
Modify — append tools 8-9 | 5 |
docs/TOOLS.md |
Regenerate via npm run docs:generate |
final |
README.md / README_CN.md |
Modify — add new tool categories | final |
CHANGELOG.md |
Modify — add ## [0.5.0] entry |
final |
test/*.test.js |
New — unit tests for pure-logic helpers | each phase |
Phase 1: Scene Management Tools
File: lib/tools/scene-management.js (new)
Dependencies: { createSchema, getRuntimeContext } — uses Editor.Message.request directly (editor-side), no scene bridge needed for tools 1-3.
Imports: Reuse tryEditorRequests from ./cocos-project.js.
Tool 1: create_scene
{
name: 'create_scene',
profile: 'full',
description: '[core] Create a new scene asset in the Cocos project and open it.',
inputSchema: createSchema(
{
sceneName: { type: 'string', description: 'Name of the new scene (without extension).' },
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/scenes/NewScene.scene.' },
open: { type: 'boolean', description: 'Whether to open the scene after creation. Default true.' },
},
['sceneName', 'savePath']
),
handler: async (args) => { ... }
}
Implementation approach:
Use Editor.Message.request('asset-db', 'create-asset', savePath, '') to create an empty .scene file. Cocos Creator's asset-db will generate a valid scene template automatically when the file extension is .scene. Then optionally openAsset(savePath).
Fallback: if create-asset doesn't auto-generate scene content, write a minimal scene JSON template to disk and let asset-db import it.
// Pseudocode
const result = await Editor.Message.request('asset-db', 'create-asset', args.savePath, '');
// result contains { uuid, url, ... }
if (args.open !== false) {
await openAsset(result.url || args.savePath);
}
return { created: true, sceneName: args.sceneName, url: result.url, uuid: result.uuid };
IPC mapping:
asset-db: create-asset(primary)asset-db: import-asset(fallback if create-asset doesn't work for scenes)
Error handling: Throw if savePath doesn't end with .scene. Throw if asset already exists.
Tool 2: query_scene_state
{
name: 'query_scene_state',
profile: 'core',
description: '[specialist] Query scene state: dirty (unsaved changes), ready, or soft-reload.',
inputSchema: createSchema(
{
action: { type: 'string', enum: ['is_dirty', 'is_ready', 'soft_reload'], description: 'State query or action.' },
},
['action']
),
handler: async (args) => { ... }
}
Why core: is_dirty is a critical signal for AI to decide whether to save before making changes. This is the only new tool promoted to core.
IPC mapping:
is_dirty→Editor.Message.request('scene', 'query-is-dirty')→ returns booleanis_ready→Editor.Message.request('scene', 'query-ready')→ returns booleansoft_reload→Editor.Message.request('scene', 'soft-reload')→ reloads scene from disk
Implementation:
const IPC_MAP = {
is_dirty: { channel: 'scene', method: 'query-is-dirty' },
is_ready: { channel: 'scene', method: 'query-ready' },
soft_reload: { channel: 'scene', method: 'soft-reload' },
};
const candidate = IPC_MAP[args.action];
const result = await tryEditorRequests([candidate]);
return { action: args.action, result: result.result };
Tool 3: copy_paste_node
{
name: 'copy_paste_node',
profile: 'full',
description: '[core] Copy, cut, or paste scene nodes via the Cocos editor clipboard.',
inputSchema: createSchema(
{
action: { type: 'string', enum: ['copy', 'cut', 'paste'], description: 'Clipboard action.' },
uuids: { type: 'array', items: { type: 'string' }, description: 'Node UUIDs to copy or cut.' },
target: { type: 'string', description: 'Target parent node UUID for paste.' },
keepWorldTransform: { type: 'boolean', description: 'Keep world transform on paste. Default false.' },
},
['action']
),
handler: async (args) => { ... }
}
IPC mapping:
copy→Editor.Message.request('scene', 'copy-node', args.uuids)cut→Editor.Message.request('scene', 'cut-node', args.uuids)paste→Editor.Message.request('scene', 'paste-node', { target: args.target, keepWorldTransform: args.keepWorldTransform })
Validation:
copy/cutrequiresuuidsnon-emptypasterequirestarget
Phase 2: Node Operations
Files: scene.js (add methods) + lib/tools/scene-management.js (add tool definitions)
Tool 4: rename_node (scene-side)
scene.js method — add to exports.methods:
async renameNode(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const oldName = node.name;
const oldPath = getNodePath(node);
const newName = String(options.newName || '').trim();
if (!newName) {
throw new Error('newName is required.');
}
node.name = newName;
return {
renamed: true,
oldName,
newName,
oldPath,
newPath: getNodePath(node),
uuid: node.uuid,
};
},
Tool definition (in scene-management.js):
{
name: 'rename_node',
profile: 'full',
description: '[core] Rename 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.' },
newName: { type: 'string', description: 'New name for the node.' },
},
['newName']
),
handler: async (args) => sceneBridge.call('renameNode', args),
}
Dependencies: { createSchema, sceneBridge } — add sceneBridge to the factory's dependency bag.
Tool 5: reparent_node (scene-side)
scene.js method — add to exports.methods:
async reparentNode(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const oldPath = getNodePath(node);
const oldParent = node.parent;
let newParent;
if (options.targetUuid) {
newParent = findNodeByUuid(options.targetUuid);
} else if (options.targetPath) {
newParent = findNodeByPath(options.targetPath);
} else if (options.targetName) {
newParent = findNodeByName(options.targetName);
}
if (!newParent) {
throw new Error('Target parent node was not found.');
}
const siblingIndex = Number.isFinite(options.siblingIndex) ? options.siblingIndex : -1;
if (siblingIndex >= 0 && siblingIndex <= newParent.children.length) {
newParent.insertChild(node, siblingIndex);
} else {
node.parent = newParent;
}
return {
reparented: true,
uuid: node.uuid,
oldPath,
newPath: getNodePath(node),
oldParent: oldParent ? oldParent.name : null,
newParent: newParent.name,
siblingIndex: node.getSiblingIndex(),
};
},
Tool definition (in scene-management.js):
{
name: 'reparent_node',
profile: 'full',
description: '[core] Move a node to a new parent in the scene hierarchy.',
inputSchema: createSchema(
{
path: { type: 'string', description: 'Node hierarchy path to move.' },
uuid: { type: 'string', description: 'Node UUID to move.' },
name: { type: 'string', description: 'Fallback exact node name.' },
targetPath: { type: 'string', description: 'New parent node path.' },
targetUuid: { type: 'string', description: 'New parent node UUID.' },
targetName: { type: 'string', description: 'Fallback exact new parent name.' },
siblingIndex: { type: 'number', description: 'Optional insert position among siblings. Default: append.' },
},
[]
),
handler: async (args) => sceneBridge.call('reparentNode', args),
}
Phase 3: Prefab Creation
Files: lib/prefabs.js (add helper) + lib/tool-registry.js (add inline tool)
Tool 6: create_prefab
lib/prefabs.js — add createPrefab helper:
async function createPrefab(options = {}) {
const nodeUuid = String(options.nodeUuid || '').trim();
const savePath = String(options.savePath || '').trim();
if (!nodeUuid) throw new Error('nodeUuid is required.');
if (!savePath) throw new Error('savePath is required.');
const result = await Editor.Message.request('scene', 'create-prefab', nodeUuid, savePath);
return {
created: true,
nodeUuid,
savePath,
result,
};
}
Tool definition (inline in tool-registry.js, near existing prefab tools ~line 760):
{
name: 'create_prefab',
profile: 'full',
description: '[core] Create a prefab asset from a scene node.',
inputSchema: createSchema(
{
nodeUuid: { type: 'string', description: 'UUID of the scene node to save as prefab.' },
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/prefabs/Enemy.prefab.' },
},
['nodeUuid', 'savePath']
),
handler: async (args) => createPrefab(args),
},
IPC mapping: Editor.Message.request('scene', 'create-prefab', nodeUuid, savePath)
Notes: The create-prefab scene message is well-documented in Cocos Creator 3.8+. It takes a node UUID and a target .prefab path, creates the prefab asset, and links the scene node as a prefab instance.
Phase 4: Script Creation
File: lib/tools/scripts.js (new)
Dependencies: { createSchema, getRuntimeContext } — uses fs for file writing + Editor.Message.request('asset-db', 'refresh-asset') for refresh.
Tool 7: create_script
{
name: 'create_script',
profile: 'full',
description: '[core] Create a new TypeScript component script with a standard Cocos template.',
inputSchema: createSchema(
{
scriptName: { type: 'string', description: 'Class name in PascalCase, e.g. PlayerController.' },
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/scripts/PlayerController.ts.' },
template: { type: 'string', enum: ['component', 'plain'], description: 'Script template. Default: component.' },
properties: { type: 'array', items: { type: 'object' }, description: 'Optional @property declarations.' },
},
['scriptName', 'savePath']
),
handler: async (args) => { ... }
}
Template generation (pure logic, unit-testable):
function generateComponentTemplate(className, properties = []) {
const props = properties.map(p => {
const type = p.type || 'number';
const name = p.name || 'property';
return ` @property({ type: ${type} })\n ${name}: ${type} = ${p.default || '0'};`;
}).join('\n\n');
return `import { _decorator, Component, ${properties.map(p => p.type).filter(t => t && t !== 'number' && t !== 'string' && t !== 'boolean').join(', ')} } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('${className}')
export class ${className} extends Component {
${props || ' // Add properties here'}
}`;
Implementation:
- Generate template string from
scriptName+template+properties - Write to project filesystem via
resolveProjectPath+fs.writeFileSync - Refresh asset-db:
Editor.Message.request('asset-db', 'refresh-asset', savePath) - Return
{ created: true, scriptName, savePath, className }
Unit tests (test/scripts.test.js):
generateComponentTemplate('PlayerController')→ contains@ccclass,export class PlayerController extends ComponentgenerateComponentTemplate('Enemy', [{name:'speed',type:'number',default:'5'}])→ contains@property({ type: number })andspeed: number = 5generatePlainTemplate('Utils')→ no@ccclass, justexport class Utils
Phase 5: Asset Batch Operations
File: lib/tools/assets-advanced.js (append to existing)
Dependencies: { createSchema, getRuntimeContext } — uses Editor.Message.request('asset-db', ...) and listAssets.
Tool 8: batch_asset_ops
{
name: 'batch_asset_ops',
profile: 'full',
description: '[core] Batch import or delete assets in the Cocos asset database.',
inputSchema: createSchema(
{
action: { type: 'string', enum: ['import', 'delete'], description: 'Batch operation.' },
sourceDirectory: { type: 'string', description: 'Local filesystem source (import only).' },
targetDirectory: { type: 'string', description: 'Asset-db target URL, e.g. db://assets/textures (import only).' },
urls: { type: 'array', items: { type: 'string' }, description: 'Asset URLs to delete (delete only).' },
fileFilter: { type: 'array', items: { type: 'string' }, description: 'File extensions, e.g. [".png", ".jpg"].' },
recursive: { type: 'boolean', description: 'Import recursively. Default false.' },
overwrite: { type: 'boolean', description: 'Overwrite existing assets. Default false.' },
},
['action']
),
handler: async (args) => { ... }
}
IPC mapping:
import→Editor.Message.request('asset-db', 'import', targetDirectory, sourceDirectory)delete→Editor.Message.request('asset-db', 'delete-assets', urls)
Validation:
importrequiressourceDirectory+targetDirectorydeleterequiresurlsnon-empty
Tool 9: find_unused_assets
{
name: 'find_unused_assets',
profile: 'full',
description: '[core] Find assets not referenced by any scene, prefab, or script in the project.',
inputSchema: createSchema(
{
directory: { type: 'string', description: 'Asset-db directory to scan. Default: db://assets.' },
excludeDirectories: { type: 'array', items: { type: 'string' }, description: 'Directories to exclude.' },
assetTypes: { type: 'array', items: { type: 'string' }, description: 'Filter by asset type, e.g. ["cc.Texture2D", "cc.AudioClip"].' },
limit: { type: 'number', description: 'Max results. Default 200.' },
},
[]
),
handler: async (args) => { ... }
}
Implementation (pure-logic core, unit-testable):
async function findUnusedAssets(projectPath, options = {}) {
// 1. Query all assets in the directory
const allAssets = await listAssets({
pattern: options.directory || 'db://assets/**',
ccType: options.assetTypes,
});
// 2. Build a set of all referenced UUIDs by scanning:
// - All .scene files
// - All .prefab files
// - All .ts/.js files (for dynamic asset references)
// - All .anim files
// - All .meta files (for sub-asset references)
const referencedUuids = new Set();
// ... scan files, collect UUIDs using collectUuidReferences from assets-advanced.js
// 3. Filter: assets whose UUID is NOT in referencedUuids
const unused = allAssets.filter(asset => !referencedUuids.has(asset.uuid));
return {
totalScanned: allAssets.length,
unusedCount: unused.length,
unused: unused.slice(0, options.limit || 200),
};
}
Unit tests (test/find-unused-assets.test.js):
- Mock
listAssets+ file scanning → verify correct filtering - Test exclude directories
- Test asset type filtering
Registration
lib/tool-registry.js changes
1. Add imports (top of file):
const { createSceneManagementTools } = require('./tools/scene-management');
const { createScriptTools } = require('./tools/scripts');
const { createPrefab } = require('./prefabs'); // add to existing destructure
2. Spread factories into the tools array:
// After existing cocos-project tools (~line 717)
...createSceneManagementTools({ createSchema, sceneBridge, getRuntimeContext }),
// After existing prefab tools (~line 780)
{
name: 'create_prefab',
// ... inline definition
handler: async (args) => createPrefab(args),
},
// After existing file tools (~line 1315)
...createScriptTools({ createSchema, getRuntimeContext }),
// After existing assets-advanced tools (~line 1023)
// batch_asset_ops and find_unused_assets are appended inside
// createAssetsAdvancedTools via the existing factory
3. Pass sceneBridge to createSceneManagementTools — currently only createSceneEventTools receives sceneBridge. Add it to the scene-management factory call.
lib/tools/assets-advanced.js changes
Append batch_asset_ops and find_unused_assets tool definitions to the createAssetsAdvancedTools return array. Add getRuntimeContext to the factory's dependency bag (already received).
lib/tools/scene-management.js factory signature
function createSceneManagementTools({ createSchema, sceneBridge, getRuntimeContext }) {
return [
// Tool 1: create_scene (editor-side, uses getRuntimeContext for projectPath)
// Tool 2: query_scene_state (editor-side, uses tryEditorRequests)
// Tool 3: copy_paste_node (editor-side, uses tryEditorRequests)
// Tool 4: rename_node (scene-side, uses sceneBridge.call)
// Tool 5: reparent_node (scene-side, uses sceneBridge.call)
];
}
module.exports = { createSceneManagementTools };
Implementation Order
Execute phases sequentially. Each phase ends with npm run check && npm test before proceeding.
Phase 1 (tools 1-3) ──▶ npm run check && npm test
│
Phase 2 (tools 4-5) ──▶ npm run check && npm test
│
Phase 3 (tool 6) ──▶ npm run check && npm test
│
Phase 4 (tool 7) ──▶ npm run check && npm test
│
Phase 5 (tools 8-9) ──▶ npm run check && npm test
│
Final: docs:generate ──▶ docs:check ──▶ release:check ──▶ pack:dry-run
Verification Checklist (per phase)
npm run check— syntax check all filesnpm test— unit tests passnpm run docs:generate— regenerate TOOLS.md (final phase only)npm run docs:check— docs in sync (final phase only)npm run release:check— release metadata valid (final phase only)npm run pack:dry-run— package builds (final phase only)
Version Bump
package.jsonversion:0.4.0→0.5.0server.jsonversion(s): update to matchCHANGELOG.md: add## [0.5.0] - 2026-06-30entry- Git tag:
v0.5.0
Expected Outcome
| Metric | Before | After |
|---|---|---|
full profile tools |
101 | 110 (+9) |
core profile tools |
37 | 38 (+1: query_scene_state) |
| New files | — | lib/tools/scene-management.js, lib/tools/scripts.js |
| Modified files | — | scene.js, lib/prefabs.js, lib/tool-registry.js, lib/tools/assets-advanced.js |
| New scene.js methods | — | renameNode, reparentNode |
| New unit test files | — | test/scene-management.test.js, test/scripts.test.js, test/find-unused-assets.test.js |
| Gaps filled | Scene creation, node clipboard, node rename/reparent, prefab creation, script creation, batch assets, unused asset scan | All high-frequency gaps covered |