'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, };