Release v0.2.0

This commit is contained in:
winlifes
2026-05-20 06:06:36 -07:00
parent e01f058e3e
commit 3ab48e2457
18 changed files with 1417 additions and 99 deletions
+255 -3
View File
@@ -16,10 +16,36 @@ const {
} = 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 { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
const { checkForUpdate } = require('./update-checker');
const { safeStringify } = require('./utils');
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/],
['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',
@@ -31,6 +57,74 @@ function createSchema(properties, required) {
return schema;
}
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 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 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;
@@ -123,7 +217,7 @@ async function refreshAssets(projectPath, targetPath) {
return 'File written outside assets directory; no asset-db refresh was needed.';
}
function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, sceneBridge, editorExecutor }) {
function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runtimeLog, sceneBridge, editorExecutor }) {
const tools = [
{
name: 'execute_javascript',
@@ -225,6 +319,31 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, scen
};
},
},
{
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',
@@ -1016,6 +1135,129 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, scen
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 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 && (!diagnostics || diagnostics.ok) && (!logErrors || logErrors.length === 0),
scene,
runtime,
diagnostics,
logErrors,
};
},
},
{
name: 'get_runtime_state',
profile: 'core',
@@ -1333,20 +1575,30 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, scen
listTools() {
const { config } = getRuntimeContext();
return tools
.filter((tool) => config.toolProfile === 'full' || tool.profile === 'core')
.filter((tool) => isToolExposed(config || {}, tool))
.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
}));
},
listToolCatalog() {
const { config } = getRuntimeContext();
return tools.map((tool) => ({
name: tool.name,
description: tool.description,
profile: tool.profile,
category: toolCategory(tool),
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 (config.toolProfile !== 'full' && tool.profile !== 'core') {
if (!isToolExposed(config || {}, tool)) {
throw new Error(`Tool '${name}' is not exposed by the current MCP tool profile '${config.toolProfile}'.`);
}