UNPKG

@akiojin/unity-editor-mcp

Version:

MCP server for Unity Editor integration - enables AI assistants to control Unity Editor

286 lines (259 loc) 8.26 kB
import { BaseToolHandler } from '../base/BaseToolHandler.js'; /** * Handler for reading Unity Editor console logs with advanced filtering */ export class ReadConsoleToolHandler extends BaseToolHandler { constructor(unityConnection) { super( 'read_console', 'Read Console logs with filters (type/text/time), formatting, sort, and grouping.', { type: 'object', properties: { count: { type: 'number', description: 'Number of logs to retrieve (1-1000, default: 100)', minimum: 1, maximum: 1000, default: 100 }, logTypes: { type: 'array', description: 'Filter by log types (default: ["All"])', items: { type: 'string', enum: ['Info', 'Warning', 'Error', 'All'] }, default: ['All'] }, filterText: { type: 'string', description: 'Filter logs containing this text (case-insensitive)' }, includeStackTrace: { type: 'boolean', description: 'Include stack traces in results. Unity default: false (set to true for debugging)', default: false }, format: { type: 'string', description: 'Output format for logs. Unity default: compact. RECOMMENDED: compact for general use, detailed for debugging', enum: ['detailed', 'compact', 'json', 'plain'], default: 'compact' }, sinceTimestamp: { type: 'string', description: 'Only return logs after this timestamp (ISO 8601)' }, untilTimestamp: { type: 'string', description: 'Only return logs before this timestamp (ISO 8601)' }, sortOrder: { type: 'string', description: 'Sort order for logs', enum: ['newest', 'oldest'], default: 'newest' }, groupBy: { type: 'string', description: 'Group logs by criteria', enum: ['none', 'type', 'file', 'time'], default: 'none' } }, required: [] } ); this.unityConnection = unityConnection; } /** * Validates the input parameters * @param {Object} params - The input parameters * @throws {Error} If validation fails */ validate(params) { const { count, logTypes, filterText, includeStackTrace, format, sinceTimestamp, untilTimestamp, sortOrder, groupBy } = params; // Validate count if (count !== undefined) { if (typeof count !== 'number' || count < 1 || count > 1000) { throw new Error('count must be between 1 and 1000'); } } // Validate log types if (logTypes !== undefined) { if (!Array.isArray(logTypes)) { throw new Error('logTypes must be an array'); } const validTypes = ['Info', 'Warning', 'Error', 'All']; for (const type of logTypes) { if (!validTypes.includes(type)) { // Invalid types are treated as 'All' later, so just log a warning console.warn(`Invalid log type: ${type}. Will be treated as 'All'.`); } } } // Validate timestamps if (sinceTimestamp !== undefined) { if (!this.isValidISO8601(sinceTimestamp)) { throw new Error('sinceTimestamp must be a valid ISO 8601 timestamp'); } } if (untilTimestamp !== undefined) { if (!this.isValidISO8601(untilTimestamp)) { throw new Error('untilTimestamp must be a valid ISO 8601 timestamp'); } } // Validate timestamp order if (sinceTimestamp && untilTimestamp) { const since = new Date(sinceTimestamp); const until = new Date(untilTimestamp); if (until <= since) { throw new Error('untilTimestamp must be after sinceTimestamp'); } } // Validate format if (format !== undefined) { const validFormats = ['detailed', 'compact', 'json', 'plain']; if (!validFormats.includes(format)) { throw new Error(`format must be one of: ${validFormats.join(', ')}`); } } // Validate sort order if (sortOrder !== undefined) { const validOrders = ['newest', 'oldest']; if (!validOrders.includes(sortOrder)) { throw new Error(`sortOrder must be one of: ${validOrders.join(', ')}`); } } // Validate groupBy if (groupBy !== undefined) { const validGroups = ['none', 'type', 'file', 'time']; if (!validGroups.includes(groupBy)) { throw new Error(`groupBy must be one of: ${validGroups.join(', ')}`); } } } /** * Checks if a string is a valid ISO 8601 timestamp * @param {string} timestamp - The timestamp to validate * @returns {boolean} True if valid */ isValidISO8601(timestamp) { const regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/; if (!regex.test(timestamp)) { return false; } const date = new Date(timestamp); return !isNaN(date.getTime()); } /** * Executes the enhanced log reading operation * @param {Object} params - The input parameters * @returns {Promise<Object>} The result of the log reading */ async execute(params) { const { count, logTypes, filterText, includeStackTrace, format, sinceTimestamp, untilTimestamp, sortOrder, groupBy } = params; // Convert simplified log types to Unity log types let expandedLogTypes = []; if (!logTypes || logTypes.length === 0 || logTypes.includes('All')) { // Default to all types expandedLogTypes = ['Log', 'Warning', 'Error', 'Exception', 'Assert']; } else { // Expand each simplified type to Unity types logTypes.forEach(type => { switch(type) { case 'Info': expandedLogTypes.push('Log'); break; case 'Warning': expandedLogTypes.push('Warning'); break; case 'Error': // Error includes all error-related types expandedLogTypes.push('Error', 'Exception', 'Assert'); break; case 'All': default: // Treat unknown types as 'All' expandedLogTypes.push('Log', 'Warning', 'Error', 'Exception', 'Assert'); break; } }); // Remove duplicates expandedLogTypes = [...new Set(expandedLogTypes)]; } // Ensure connection to Unity if (!this.unityConnection.isConnected()) { await this.unityConnection.connect(); } // Prepare command parameters const commandParams = { count, logTypes: expandedLogTypes, includeStackTrace, format, sortOrder, groupBy }; // Add optional parameters if (filterText !== undefined) { commandParams.filterText = filterText; } if (sinceTimestamp !== undefined) { commandParams.sinceTimestamp = sinceTimestamp; } if (untilTimestamp !== undefined) { commandParams.untilTimestamp = untilTimestamp; } // Send command to Unity const response = await this.unityConnection.sendCommand('read_console', commandParams); // Handle Unity response if (response.success === false) { throw new Error(response.error || 'Failed to read logs'); } // Build result object const result = { logs: response.logs || [], count: response.count || 0, totalCaptured: response.totalCaptured || 0 }; // Include optional fields if available if (response.filteredCount !== undefined) { result.filteredCount = response.filteredCount; } if (response.statistics !== undefined) { result.statistics = response.statistics; } if (response.groupedLogs !== undefined) { result.groupedLogs = response.groupedLogs; } if (response.format !== undefined) { result.format = response.format; } if (response.groupBy !== undefined) { result.groupBy = response.groupBy; } return result; } }