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
391 lines
14 KiB
JavaScript
391 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { getCurrentSelection, queryAssetInfo, queryAssetMeta } = require('./assets');
|
|
const { runScriptDiagnostics } = require('./diagnostics');
|
|
const { getRecentProjectLogs } = require('./logs');
|
|
const { resolveProjectPath } = require('./path-safety');
|
|
|
|
function createResource(uri, name, description) {
|
|
return { uri, name, description, mimeType: 'text/plain' };
|
|
}
|
|
|
|
function createTemplate(uriTemplate, name, description) {
|
|
return { uriTemplate, name, description, mimeType: 'text/plain' };
|
|
}
|
|
|
|
function truncate(text, limit = 12000) {
|
|
if (typeof text !== 'string') {
|
|
return text;
|
|
}
|
|
if (text.length <= limit) {
|
|
return text;
|
|
}
|
|
return `${text.slice(0, limit)}\n... (truncated)`;
|
|
}
|
|
|
|
function toText(value) {
|
|
if (typeof value === 'string') {
|
|
return value;
|
|
}
|
|
return JSON.stringify(value, null, 2);
|
|
}
|
|
|
|
function summarizeSelection() {
|
|
if (!global.Editor || !Editor.Selection || typeof Editor.Selection.getSelected !== 'function') {
|
|
return 'Selection API is unavailable in this Cocos environment.';
|
|
}
|
|
|
|
try {
|
|
const selectedNode = Editor.Selection.getSelected('node');
|
|
const selectedAsset = Editor.Selection.getSelected('asset');
|
|
return [
|
|
`Selected node: ${selectedNode || '(none)'}`,
|
|
`Selected asset: ${selectedAsset || '(none)'}`,
|
|
].join('\n');
|
|
} catch (error) {
|
|
return `Failed to inspect current selection: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
class ResourceProvider {
|
|
constructor(getRuntimeContext, sceneBridge, interactionLog, runtimeLog) {
|
|
this.getRuntimeContext = getRuntimeContext;
|
|
this.sceneBridge = sceneBridge;
|
|
this.interactionLog = interactionLog;
|
|
this.runtimeLog = runtimeLog;
|
|
}
|
|
|
|
listResources() {
|
|
const { projectName } = this.getRuntimeContext();
|
|
return [
|
|
createResource('cocos://project/context', `${projectName} Project Context`, 'Live Cocos project context summary.'),
|
|
createResource('cocos://project/summary', `${projectName} Project Summary`, 'Project path, folder summary, and asset overview.'),
|
|
createResource('cocos://scene/active', `${projectName} Active Scene`, 'Summary of the active Cocos scene.'),
|
|
createResource('cocos://scene/current', `${projectName} Current Scene`, 'Alias of the active Cocos scene summary.'),
|
|
createResource('cocos://selection/current', `${projectName} Current Selection`, 'Summary of the current editor selection.'),
|
|
createResource('cocos://selection/asset', `${projectName} Selected Asset`, 'Details for the currently selected asset.'),
|
|
createResource('cocos://errors/scripts', `${projectName} Script Diagnostics`, 'Latest TypeScript diagnostic summary for the project.'),
|
|
createResource('cocos://logs/editor', `${projectName} Editor Logs`, 'Recent MCP runtime logs and tool interaction history.'),
|
|
createResource('cocos://logs/project', `${projectName} Project Logs`, 'Recent tails from common project log files.'),
|
|
createResource('cocos://mcp/interactions', `${projectName} MCP Interactions`, 'Recent MCP tool interaction summaries.'),
|
|
createResource('cocos://mcp/execute-context', `${projectName} Execute Context Guide`, 'Available variables and code patterns for execute_javascript scene and editor contexts.'),
|
|
];
|
|
}
|
|
|
|
listResourceTemplates() {
|
|
return [
|
|
createTemplate('cocos://scene/node/{path}', 'Scene Node', 'Inspect a scene node by hierarchy path.'),
|
|
createTemplate('cocos://asset/path/{relative_path}', 'Asset By Path', 'Read a script or text asset by project-relative path.'),
|
|
createTemplate('cocos://asset/info/{uuid_or_path}', 'Asset Info', 'Inspect an asset by uuid, db url, or path.'),
|
|
];
|
|
}
|
|
|
|
async readResource(uri) {
|
|
const text = await this.resolveResourceText(uri);
|
|
return {
|
|
contents: [
|
|
{
|
|
uri,
|
|
mimeType: 'text/plain',
|
|
text,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
async resolveResourceText(uri) {
|
|
const { projectName, projectPath, cocosVersion, version, config } = this.getRuntimeContext();
|
|
|
|
switch (uri) {
|
|
case 'cocos://project/context':
|
|
return [
|
|
'Funplay Cocos MCP Project Context',
|
|
`Project: ${projectName}`,
|
|
`Project Path: ${projectPath}`,
|
|
`Cocos Creator: ${cocosVersion}`,
|
|
`Extension Version: ${version}`,
|
|
`Tool Profile: ${config.toolProfile}`,
|
|
`Server: http://${config.host}:${config.port}/`,
|
|
'',
|
|
'Selection',
|
|
summarizeSelection(),
|
|
'',
|
|
'Active Scene',
|
|
await this.safeSceneCall('getSceneInfo', { maxDepth: 2 }),
|
|
].join('\n');
|
|
case 'cocos://project/summary':
|
|
return this.buildProjectSummary(projectPath, cocosVersion, version);
|
|
case 'cocos://scene/active':
|
|
case 'cocos://scene/current':
|
|
return await this.safeSceneCall('getSceneInfo', { maxDepth: 3 });
|
|
case 'cocos://selection/current':
|
|
return summarizeSelection();
|
|
case 'cocos://selection/asset':
|
|
return await this.getSelectedAssetText();
|
|
case 'cocos://errors/scripts':
|
|
return await this.getScriptDiagnosticsText(projectPath);
|
|
case 'cocos://logs/editor':
|
|
return this.getEditorLogsText();
|
|
case 'cocos://logs/project':
|
|
return this.getProjectLogsText(projectPath);
|
|
case 'cocos://mcp/interactions':
|
|
return this.interactionLog.summary();
|
|
case 'cocos://mcp/execute-context':
|
|
return this.getExecuteContextGuide();
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (uri.startsWith('cocos://scene/node/')) {
|
|
const nodePath = decodeURIComponent(uri.slice('cocos://scene/node/'.length));
|
|
return await this.safeSceneCall('inspectNode', { path: nodePath });
|
|
}
|
|
|
|
if (uri.startsWith('cocos://asset/path/')) {
|
|
const relativePath = decodeURIComponent(uri.slice('cocos://asset/path/'.length));
|
|
return this.readAssetByPath(relativePath);
|
|
}
|
|
|
|
if (uri.startsWith('cocos://asset/info/')) {
|
|
const uuidOrPath = decodeURIComponent(uri.slice('cocos://asset/info/'.length));
|
|
return await this.getAssetInfoText(uuidOrPath);
|
|
}
|
|
|
|
return `Resource not found: ${uri}`;
|
|
}
|
|
|
|
buildProjectSummary(projectPath, cocosVersion, version) {
|
|
const topLevel = fs.existsSync(projectPath)
|
|
? fs
|
|
.readdirSync(projectPath, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.sort((left, right) => left.localeCompare(right))
|
|
: [];
|
|
|
|
const assetsDir = path.join(projectPath, 'assets');
|
|
const scriptCount = this.countFiles(assetsDir, ['.ts', '.js']);
|
|
const prefabCount = this.countFiles(assetsDir, ['.prefab']);
|
|
const sceneCount = this.countFiles(assetsDir, ['.scene']);
|
|
|
|
return [
|
|
'Project Summary',
|
|
`Project Root: ${projectPath}`,
|
|
`Assets Path: ${assetsDir}`,
|
|
`Cocos Creator: ${cocosVersion}`,
|
|
`Extension Version: ${version}`,
|
|
`Scripts: ${scriptCount}`,
|
|
`Prefabs: ${prefabCount}`,
|
|
`Scenes: ${sceneCount}`,
|
|
'',
|
|
`Top-Level Directories (${topLevel.length})`,
|
|
...topLevel.map((name) => `- ${name}`),
|
|
].join('\n');
|
|
}
|
|
|
|
countFiles(rootDir, extensions) {
|
|
if (!fs.existsSync(rootDir)) {
|
|
return 0;
|
|
}
|
|
|
|
let count = 0;
|
|
const stack = [rootDir];
|
|
while (stack.length) {
|
|
const current = stack.pop();
|
|
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
stack.push(fullPath);
|
|
continue;
|
|
}
|
|
if (extensions.includes(path.extname(entry.name).toLowerCase())) {
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
readAssetByPath(relativePath) {
|
|
const { projectPath } = this.getRuntimeContext();
|
|
const fullPath = resolveProjectPath(projectPath, relativePath);
|
|
if (!fs.existsSync(fullPath)) {
|
|
return `Asset not found: ${relativePath}`;
|
|
}
|
|
return truncate(`[${relativePath}]\n${fs.readFileSync(fullPath, 'utf8')}`);
|
|
}
|
|
|
|
async safeSceneCall(method, payload) {
|
|
try {
|
|
const result = await this.sceneBridge.call(method, payload);
|
|
return toText(result);
|
|
} catch (error) {
|
|
return `Scene bridge error (${method}): ${error.message}`;
|
|
}
|
|
}
|
|
|
|
async getSelectedAssetText() {
|
|
try {
|
|
const selection = getCurrentSelection();
|
|
if (!selection.asset) {
|
|
return 'No asset is currently selected.';
|
|
}
|
|
|
|
return await this.getAssetInfoText(selection.asset);
|
|
} catch (error) {
|
|
return `Selected asset lookup failed: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
async getAssetInfoText(uuidOrPath) {
|
|
try {
|
|
const info = await queryAssetInfo(uuidOrPath);
|
|
const meta = await queryAssetMeta(uuidOrPath).catch(() => null);
|
|
return JSON.stringify({ info, meta }, null, 2);
|
|
} catch (error) {
|
|
return `Asset lookup failed: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
async getScriptDiagnosticsText(projectPath) {
|
|
try {
|
|
const result = await runScriptDiagnostics(projectPath);
|
|
return JSON.stringify(result, null, 2);
|
|
} catch (error) {
|
|
return `Script diagnostics failed: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
getEditorLogsText() {
|
|
return [
|
|
'MCP Runtime Logs',
|
|
this.runtimeLog && typeof this.runtimeLog.summary === 'function'
|
|
? this.runtimeLog.summary(80)
|
|
: 'Runtime log is unavailable.',
|
|
'',
|
|
'MCP Tool Interactions',
|
|
this.interactionLog && typeof this.interactionLog.summary === 'function'
|
|
? this.interactionLog.summary(80)
|
|
: 'Interaction log is unavailable.',
|
|
].join('\n');
|
|
}
|
|
|
|
getProjectLogsText(projectPath) {
|
|
const logs = getRecentProjectLogs(projectPath, { limit: 10, lines: 100 });
|
|
if (!logs.length) {
|
|
return 'No project log files found in common project log directories.';
|
|
}
|
|
|
|
return logs
|
|
.map((log) => [
|
|
`# ${log.path}`,
|
|
`mtime: ${log.mtime}`,
|
|
`size: ${log.size}`,
|
|
'',
|
|
log.text,
|
|
].join('\n'))
|
|
.join('\n\n---\n\n');
|
|
}
|
|
|
|
getExecuteContextGuide() {
|
|
return [
|
|
'Execute JavaScript Context Guide',
|
|
'',
|
|
'This document describes the variables and code patterns available in',
|
|
'execute_javascript for both scene and editor contexts.',
|
|
'',
|
|
'=== Scene Context (context="scene") ===',
|
|
'',
|
|
'Injected Variables:',
|
|
' cc - Cocos engine module. Use cc.Sprite, cc.Node, cc.Vec3, etc.',
|
|
' scene - Active scene root node (cc.Node). Same as director.getScene().',
|
|
' director - cc.director.',
|
|
' require - Node.js require (with Cocos module paths).',
|
|
' Editor - Editor global API (if running in editor).',
|
|
' args - User-passed args object.',
|
|
'',
|
|
'Return Patterns:',
|
|
' 1. Direct return: return { myResult: 42 };',
|
|
' 2. run function: async function run(env) { return result; }',
|
|
' 3. module.exports: module.exports = async (env) => result;',
|
|
'',
|
|
'Key Rules:',
|
|
' - Use cc.Sprite, cc.Node, cc.UITransform — NOT bare Sprite/Node.',
|
|
' - Do NOT redeclare scene, director, cc — they are already in scope.',
|
|
' - Find nodes by traversing scene.children recursively (see pattern below).',
|
|
' - Load assets with callback-style assetManager.loadAny (see pattern below).',
|
|
'',
|
|
'Pattern: Find a node by UUID',
|
|
' let target = null;',
|
|
' function search(n) {',
|
|
' if (n.uuid === "NODE_UUID") { target = n; return; }',
|
|
' for (const c of n.children) { search(c); if (target) return; }',
|
|
' }',
|
|
' search(scene);',
|
|
'',
|
|
'Pattern: Load and set an asset reference (SpriteFrame, Texture, etc.)',
|
|
' const { Sprite, assetManager, SpriteFrame } = cc;',
|
|
' const sf = await new Promise((resolve, reject) => {',
|
|
' assetManager.loadAny(',
|
|
' { uuid: "ASSET_UUID", type: SpriteFrame },',
|
|
' (err, asset) => { if (err) reject(err); else resolve(asset); }',
|
|
' );',
|
|
' });',
|
|
' sprite.spriteFrame = sf;',
|
|
'',
|
|
'Pattern: Create a node with components',
|
|
' const { Node, UITransform, Sprite } = cc;',
|
|
' const node = new Node("MyNode");',
|
|
' node.layer = parent.layer;',
|
|
' node.addComponent(UITransform);',
|
|
' node.addComponent(Sprite);',
|
|
' parent.addChild(node);',
|
|
'',
|
|
'=== Editor Context (context="editor") ===',
|
|
'',
|
|
'Injected Variables:',
|
|
' require - Node.js require.',
|
|
' Editor - Editor global API.',
|
|
' args - User-passed args object.',
|
|
' context - Runtime context (projectPath, projectName, cocosVersion, etc.).',
|
|
' helpers - { getStatus, listTools, readResource, callTool,',
|
|
' listClientTargets, getClientConfig, configureClient }.',
|
|
' fs - Node.js fs module.',
|
|
' path - Node.js path module.',
|
|
' os - Node.js os module.',
|
|
'',
|
|
'Pattern: Call asset-db from editor context',
|
|
' const info = await Editor.Message.request("asset-db", "query-asset-info", "ASSET_UUID");',
|
|
'',
|
|
'Pattern: Call a scene method from editor context',
|
|
' const result = await Editor.Message.request(',
|
|
' "scene", "execute-scene-script", { name: "funplay-cocos-mcp", method: "inspectNode", args: { uuid: "NODE_UUID" } }',
|
|
' );',
|
|
'',
|
|
'=== Common Pitfalls ===',
|
|
'',
|
|
'1. set_component_property cannot set asset references (SpriteFrame, Texture,',
|
|
' Material, Font, AudioClip). Use execute_javascript with loadAny instead.',
|
|
'',
|
|
'2. create_node only accepts parentPath (e.g. "Canvas/Player"), not parentUuid.',
|
|
' Use get_hierarchy first to find the full path.',
|
|
'',
|
|
'3. assetManager.loadAny(uuid) without callback returns null in scene context.',
|
|
' Always use callback style: loadAny({uuid, type}, (err, asset) => {}).',
|
|
'',
|
|
'4. Custom script components (e.g. TestComponent) require the script to be',
|
|
' compiled before add_component or findComponent can find them.',
|
|
'',
|
|
'5. Node hierarchy paths are slash-separated from scene root, e.g.',
|
|
' "should_hide_in_hierarchy/TestPrefab/Sprite". Use get_hierarchy to verify.',
|
|
].join('\n');
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
ResourceProvider,
|
|
};
|