Release v0.2.0
This commit is contained in:
+255
-3
@@ -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}'.`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user