Files
funplay-cocos-mcp/docs/IMPLEMENTATION_PLAN.md
T
mingyuansi b65a4f22c3 fix: Windows spawn, prefab path, preview API, asset deps, error messages; add execute-context resource, tool descriptions, scene/script/asset tools
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
2026-06-30 21:53:06 +08:00

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_dirtyEditor.Message.request('scene', 'query-is-dirty') → returns boolean
  • is_readyEditor.Message.request('scene', 'query-ready') → returns boolean
  • soft_reloadEditor.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:

  • copyEditor.Message.request('scene', 'copy-node', args.uuids)
  • cutEditor.Message.request('scene', 'cut-node', args.uuids)
  • pasteEditor.Message.request('scene', 'paste-node', { target: args.target, keepWorldTransform: args.keepWorldTransform })

Validation:

  • copy/cut requires uuids non-empty
  • paste requires target

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:

  1. Generate template string from scriptName + template + properties
  2. Write to project filesystem via resolveProjectPath + fs.writeFileSync
  3. Refresh asset-db: Editor.Message.request('asset-db', 'refresh-asset', savePath)
  4. Return { created: true, scriptName, savePath, className }

Unit tests (test/scripts.test.js):

  • generateComponentTemplate('PlayerController') → contains @ccclass, export class PlayerController extends Component
  • generateComponentTemplate('Enemy', [{name:'speed',type:'number',default:'5'}]) → contains @property({ type: number }) and speed: number = 5
  • generatePlainTemplate('Utils') → no @ccclass, just export 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:

  • importEditor.Message.request('asset-db', 'import', targetDirectory, sourceDirectory)
  • deleteEditor.Message.request('asset-db', 'delete-assets', urls)

Validation:

  • import requires sourceDirectory + targetDirectory
  • delete requires urls non-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 files
  • npm test — unit tests pass
  • npm 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.json version: 0.4.00.5.0
  • server.json version(s): update to match
  • CHANGELOG.md: add ## [0.5.0] - 2026-06-30 entry
  • 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