diff --git a/Editor/Resources/GetConsoleLogsResource.cs b/Editor/Resources/GetConsoleLogsResource.cs index 029da3d4..863c50b6 100644 --- a/Editor/Resources/GetConsoleLogsResource.cs +++ b/Editor/Resources/GetConsoleLogsResource.cs @@ -1,3 +1,4 @@ +using System; using Newtonsoft.Json.Linq; using McpUnity.Services; @@ -13,38 +14,57 @@ public class GetConsoleLogsResource : McpResourceBase public GetConsoleLogsResource(IConsoleLogsService consoleLogsService) { Name = "get_console_logs"; - Description = "Retrieves logs from the Unity console, optionally filtered by type (error, warning, info)"; + Description = "Retrieves logs from the Unity console (newest first), optionally filtered by type (error, warning, info). Use pagination parameters (offset, limit) to avoid LLM token limits. Recommended: limit=20-50 for optimal performance."; Uri = "unity://logs/{logType}"; _consoleLogsService = consoleLogsService; } /// - /// Fetch logs from the Unity console, optionally filtered by type + /// Fetch logs from the Unity console, optionally filtered by type with pagination support /// - /// Resource parameters as a JObject (may include 'logType') - /// A JObject containing the list of logs + /// Resource parameters as a JObject (may include 'logType', 'offset', 'limit') + /// A JObject containing the list of logs with pagination info public override JObject Fetch(JObject parameters) { - string logType = null; - if (parameters != null && parameters.ContainsKey("logType") && parameters["logType"] != null) - { - logType = parameters["logType"].ToString()?.ToLowerInvariant(); - if (string.IsNullOrWhiteSpace(logType)) - { - logType = null; - } - } - - JArray logsArray = _consoleLogsService.GetAllLogsAsJson(logType); - - // Create the response - return new JObject - { - ["success"] = true, - ["message"] = $"Retrieved {logsArray.Count} log entries" + (logType != null ? $" of type '{logType}'" : ""), - ["logs"] = logsArray - }; + string logType = parameters?["logType"]?.ToString(); + if (string.IsNullOrWhiteSpace(logType)) logType = null; + + int offset = Math.Max(0, GetIntParameter(parameters, "offset", 0)); + int limit = Math.Max(1, Math.Min(1000, GetIntParameter(parameters, "limit", 100))); + + // Use the new paginated method + JObject result = _consoleLogsService.GetLogsAsJson(logType, offset, limit); + + // Add formatted message with pagination info + string typeFilter = logType != null ? $" of type '{logType}'" : ""; + int returnedCount = result["_returnedCount"]?.Value() ?? 0; + int filteredCount = result["_filteredCount"]?.Value() ?? 0; + int totalCount = result["_totalCount"]?.Value() ?? 0; + + result["message"] = $"Retrieved {returnedCount} of {filteredCount} log entries{typeFilter} (offset: {offset}, limit: {limit}, total: {totalCount})"; + result["success"] = true; + + // Remove internal count fields (they're now in the message) + result.Remove("_totalCount"); + result.Remove("_filteredCount"); + result.Remove("_returnedCount"); + + return result; + } + + /// + /// Helper method to safely extract integer parameters with default values + /// + /// JObject containing parameters + /// Parameter key to extract + /// Default value if parameter is missing or invalid + /// Extracted integer value or default + private static int GetIntParameter(JObject parameters, string key, int defaultValue) + { + if (parameters?[key] != null && int.TryParse(parameters[key].ToString(), out int value)) + return value; + return defaultValue; } diff --git a/Editor/Services/ConsoleLogsService.cs b/Editor/Services/ConsoleLogsService.cs index b0fac8b0..cbca7503 100644 --- a/Editor/Services/ConsoleLogsService.cs +++ b/Editor/Services/ConsoleLogsService.cs @@ -29,6 +29,10 @@ private class LogEntry public DateTime Timestamp { get; set; } } + // Constants for log management + private const int MaxLogEntries = 1000; + private const int CleanupThreshold = 200; // Remove oldest entries when exceeding max + // Collection to store all log messages private readonly List _logEntries = new List(); @@ -73,16 +77,21 @@ public void StopListening() EditorApplication.update -= CheckConsoleClearViaReflection; #endif } - /// - /// Get all logs as a JSON array + /// Get logs as a JSON array with pagination support /// - /// JArray containing all logs - public JArray GetAllLogsAsJson(string logType = "") + /// Filter by log type (empty for all) + /// Starting index (0-based) + /// Maximum number of logs to return (default: 100) + /// JObject containing logs array and pagination info + public JObject GetLogsAsJson(string logType = "", int offset = 0, int limit = 100) { // Convert log entries to a JSON array, filtering by logType if provided JArray logsArray = new JArray(); bool filter = !string.IsNullOrEmpty(logType); + int totalCount = 0; + int filteredCount = 0; + int currentIndex = 0; // Map MCP log types to Unity log types outside the loop for better performance HashSet unityLogTypes = null; @@ -101,21 +110,46 @@ public JArray GetAllLogsAsJson(string logType = "") lock (_logEntries) { - foreach (var entry in _logEntries) + totalCount = _logEntries.Count; + + // Single pass: count filtered entries and collect the requested page (newest first) + for (int i = _logEntries.Count - 1; i >= 0; i--) { + var entry = _logEntries[i]; + + // Skip if filtering and entry doesn't match the filter if (filter && !unityLogTypes.Contains(entry.Type.ToString())) continue; - logsArray.Add(new JObject + + // Count filtered entries + filteredCount++; + + // Check if we're in the offset range and haven't reached the limit yet + if (currentIndex >= offset && logsArray.Count < limit) { - ["message"] = entry.Message, - ["stackTrace"] = entry.StackTrace, - ["type"] = entry.Type.ToString(), - ["timestamp"] = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff") - }); + logsArray.Add(new JObject + { + ["message"] = entry.Message, + ["stackTrace"] = entry.StackTrace, + ["type"] = entry.Type.ToString(), + ["timestamp"] = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff") + }); + } + + currentIndex++; + + // Early exit if we've collected enough logs + if (currentIndex >= offset + limit) break; } } - return logsArray; + return new JObject + { + ["logs"] = logsArray, + ["_totalCount"] = totalCount, + ["_filteredCount"] = filteredCount, + ["_returnedCount"] = logsArray.Count + }; } /// @@ -129,6 +163,34 @@ private void ClearLogs() } } + /// + /// Manually clean up old log entries, keeping only the most recent ones + /// + /// Number of recent entries to keep (default: 500) + public void CleanupOldLogs(int keepCount = 500) + { + lock (_logEntries) + { + if (_logEntries.Count > keepCount) + { + int removeCount = _logEntries.Count - keepCount; + _logEntries.RemoveRange(0, removeCount); + } + } + } + + /// + /// Get current log count + /// + /// Number of stored log entries + public int GetLogCount() + { + lock (_logEntries) + { + return _logEntries.Count; + } + } + /// /// Check if console was cleared using reflection (for Unity 2022.3) /// @@ -177,6 +239,12 @@ private void OnLogMessageReceived(string logString, string stackTrace, LogType t Type = type, Timestamp = DateTime.Now }); + + // Clean up old entries if we exceed the maximum + if (_logEntries.Count > MaxLogEntries) + { + _logEntries.RemoveRange(0, CleanupThreshold); + } } } diff --git a/Editor/Services/IConsoleLogsService.cs b/Editor/Services/IConsoleLogsService.cs index ac96d50c..1c7795fb 100644 --- a/Editor/Services/IConsoleLogsService.cs +++ b/Editor/Services/IConsoleLogsService.cs @@ -11,11 +11,13 @@ namespace McpUnity.Services public interface IConsoleLogsService { /// - /// Get all logs as a JSON array, optionally filtered by log type + /// Get logs as a JSON object with pagination support /// - /// UnityEngine.LogType as string (e.g. "Error", "Warning", "Log"). Empty string for all logs. - /// JArray containing filtered logs - JArray GetAllLogsAsJson(string logType = ""); + /// Filter by log type (empty for all) + /// Starting index (0-based) + /// Maximum number of logs to return (default: 100) + /// JObject containing logs array and pagination info + JObject GetLogsAsJson(string logType = "", int offset = 0, int limit = 100); /// /// Start listening for logs @@ -26,5 +28,17 @@ public interface IConsoleLogsService /// Stop listening for logs /// void StopListening(); + + /// + /// Manually clean up old log entries, keeping only the most recent ones + /// + /// Number of recent entries to keep (default: 500) + void CleanupOldLogs(int keepCount = 500); + + /// + /// Get current log count + /// + /// Number of stored log entries + int GetLogCount(); } } diff --git a/Server~/build/resources/getConsoleLogsResource.js b/Server~/build/resources/getConsoleLogsResource.js index d8a1e29c..877f807f 100644 --- a/Server~/build/resources/getConsoleLogsResource.js +++ b/Server~/build/resources/getConsoleLogsResource.js @@ -3,7 +3,7 @@ import { McpUnityError, ErrorType } from '../utils/errors.js'; // Constants for the resource const resourceName = 'get_console_logs'; const resourceMimeType = 'application/json'; -const resourceUri = 'unity://logs/{logType}'; +const resourceUri = 'unity://logs/{logType}?offset={offset}&limit={limit}'; const resourceTemplate = new ResourceTemplate(resourceUri, { list: () => listLogTypes(resourceMimeType) }); @@ -13,25 +13,25 @@ function listLogTypes(resourceMimeType) { { uri: `unity://logs/`, name: "All logs", - description: "Retrieve all Unity console logs", + description: "Retrieve Unity console logs (newest first). Use pagination to avoid token limits: ?offset=0&limit=50 for recent logs. Default limit=100 may be too large for LLM context.", mimeType: resourceMimeType }, { uri: `unity://logs/error`, name: "Error logs", - description: "Retrieve only error logs from the Unity console", + description: "Retrieve only error logs from Unity console (newest first). Use ?offset=0&limit=20 to avoid token limits. Large log sets may exceed LLM context window.", mimeType: resourceMimeType }, { uri: `unity://logs/warning`, name: "Warning logs", - description: "Retrieve only warning logs from the Unity console", + description: "Retrieve only warning logs from Unity console (newest first). Use pagination ?offset=0&limit=30 to manage token usage effectively.", mimeType: resourceMimeType }, { uri: `unity://logs/info`, name: "Info logs", - description: "Retrieve only info logs from the Unity console", + description: "Retrieve only info logs from Unity console (newest first). Use smaller limits like ?limit=25 to prevent token overflow in LLM responses.", mimeType: resourceMimeType } ] @@ -43,7 +43,7 @@ function listLogTypes(resourceMimeType) { export function registerGetConsoleLogsResource(server, mcpUnity, logger) { logger.info(`Registering resource: ${resourceName}`); server.resource(resourceName, resourceTemplate, { - description: 'Retrieve Unity console logs by type', + description: 'Retrieve Unity console logs by type (newest first). IMPORTANT: Use pagination parameters ?offset=0&limit=50 to avoid LLM token limits. Default limit=100 may exceed context window.', mimeType: resourceMimeType }, async (uri, variables) => { try { @@ -63,11 +63,16 @@ async function resourceHandler(mcpUnity, uri, variables, logger) { let logType = variables["logType"] ? decodeURIComponent(variables["logType"]) : undefined; if (logType === '') logType = undefined; + // Extract pagination parameters + const offset = variables["offset"] ? parseInt(variables["offset"], 10) : 0; + const limit = variables["limit"] ? parseInt(variables["limit"], 10) : 100; // Send request to Unity const response = await mcpUnity.sendRequest({ method: resourceName, params: { - logType: logType + logType: logType, + offset: offset, + limit: limit } }); if (!response.success) { @@ -75,7 +80,7 @@ async function resourceHandler(mcpUnity, uri, variables, logger) { } return { contents: [{ - uri: `unity://logs/${logType ?? ''}`, + uri: `unity://logs/${logType ?? ''}?offset=${offset}&limit=${limit}`, mimeType: resourceMimeType, text: JSON.stringify(response, null, 2) }] diff --git a/Server~/build/tools/getConsoleLogsTool.js b/Server~/build/tools/getConsoleLogsTool.js index 7cff61fc..d4f3a01a 100644 --- a/Server~/build/tools/getConsoleLogsTool.js +++ b/Server~/build/tools/getConsoleLogsTool.js @@ -2,12 +2,25 @@ import * as z from "zod"; import { McpUnityError, ErrorType } from "../utils/errors.js"; // Constants for the tool const toolName = "get_console_logs"; -const toolDescription = "Retrieves logs from the Unity console"; +const toolDescription = "Retrieves logs from the Unity console with pagination support to avoid token limits"; const paramsSchema = z.object({ logType: z .enum(["info", "warning", "error"]) .optional() .describe("The type of logs to retrieve (info, warning, error) - defaults to all logs if not specified"), + offset: z + .number() + .int() + .min(0) + .optional() + .describe("Starting index for pagination (0-based, defaults to 0)"), + limit: z + .number() + .int() + .min(1) + .max(500) + .optional() + .describe("Maximum number of logs to return (defaults to 50, max 500 to avoid token limits)") }); /** * Creates and registers the Get Console Logs tool with the MCP server @@ -42,13 +55,15 @@ export function registerGetConsoleLogsTool(server, mcpUnity, logger) { * @throws McpUnityError if the request to Unity fails */ async function toolHandler(mcpUnity, params) { - const { logType } = params; + const { logType, offset = 0, limit = 50 } = params; // Send request to Unity using the same method name as the resource // This allows reusing the existing Unity-side implementation const response = await mcpUnity.sendRequest({ method: "get_console_logs", params: { logType: logType, + offset: offset, + limit: limit, }, }); if (!response.success) { diff --git a/Server~/src/resources/getConsoleLogsResource.ts b/Server~/src/resources/getConsoleLogsResource.ts index 78ed84e3..f804c5b2 100644 --- a/Server~/src/resources/getConsoleLogsResource.ts +++ b/Server~/src/resources/getConsoleLogsResource.ts @@ -8,7 +8,7 @@ import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js'; // Constants for the resource const resourceName = 'get_console_logs'; const resourceMimeType = 'application/json'; -const resourceUri = 'unity://logs/{logType}'; +const resourceUri = 'unity://logs/{logType}?offset={offset}&limit={limit}'; const resourceTemplate = new ResourceTemplate(resourceUri, { list: () => listLogTypes(resourceMimeType) }); @@ -17,27 +17,27 @@ function listLogTypes(resourceMimeType: string) { return { resources: [ { - uri: `unity://logs/`, + uri: `unity://logs/?offset=0&limit=50`, name: "All logs", - description: "Retrieve all Unity console logs", + description: "Retrieve Unity console logs (newest first). Default pagination offset=0&limit=50 to avoid token limits.", mimeType: resourceMimeType }, { - uri: `unity://logs/error`, + uri: `unity://logs/error?offset=0&limit=20`, name: "Error logs", - description: "Retrieve only error logs from the Unity console", + description: "Retrieve only error logs from Unity console (newest first). Default pagination offset=0&limit=20.", mimeType: resourceMimeType }, { - uri: `unity://logs/warning`, - name: "Warning logs", - description: "Retrieve only warning logs from the Unity console", + uri: `unity://logs/warning?offset=0&limit=30`, + name: "Warning logs", + description: "Retrieve only warning logs from Unity console (newest first). Default pagination offset=0&limit=30.", mimeType: resourceMimeType }, { - uri: `unity://logs/info`, + uri: `unity://logs/info?offset=0&limit=25`, name: "Info logs", - description: "Retrieve only info logs from the Unity console", + description: "Retrieve only info logs from Unity console (newest first). Default pagination offset=0&limit=25.", mimeType: resourceMimeType } ] @@ -54,7 +54,7 @@ export function registerGetConsoleLogsResource(server: McpServer, mcpUnity: McpU resourceName, resourceTemplate, { - description: 'Retrieve Unity console logs by type', + description: 'Retrieve Unity console logs by type (newest first). IMPORTANT: Use pagination parameters ?offset=0&limit=50 to avoid LLM token limits. Default limit=100 may exceed context window.', mimeType: resourceMimeType }, async (uri, variables) => { @@ -75,12 +75,26 @@ async function resourceHandler(mcpUnity: McpUnity, uri: URL, variables: Variable // Extract and convert the parameter from the template variables let logType = variables["logType"] ? decodeURIComponent(variables["logType"] as string) : undefined; if (logType === '') logType = undefined; + + // Extract pagination parameters with validation + const offset = variables["offset"] ? parseInt(variables["offset"] as string, 10) : 0; + const limit = variables["limit"] ? parseInt(variables["limit"] as string, 10) : 100; + + // Validate pagination parameters + if (isNaN(offset) || offset < 0) { + throw new McpUnityError(ErrorType.VALIDATION, 'Invalid offset parameter: must be a non-negative integer'); + } + if (isNaN(limit) || limit <= 0) { + throw new McpUnityError(ErrorType.VALIDATION, 'Invalid limit parameter: must be a positive integer'); + } // Send request to Unity const response = await mcpUnity.sendRequest({ method: resourceName, params: { - logType: logType + logType: logType, + offset: offset, + limit: limit } }); @@ -93,7 +107,7 @@ async function resourceHandler(mcpUnity: McpUnity, uri: URL, variables: Variable return { contents: [{ - uri: `unity://logs/${logType ?? ''}`, + uri: `unity://logs/${logType ?? ''}?offset=${offset}&limit=${limit}`, mimeType: resourceMimeType, text: JSON.stringify(response, null, 2) }] diff --git a/Server~/src/tools/getConsoleLogsTool.ts b/Server~/src/tools/getConsoleLogsTool.ts index a521aed4..d50c5e47 100644 --- a/Server~/src/tools/getConsoleLogsTool.ts +++ b/Server~/src/tools/getConsoleLogsTool.ts @@ -7,7 +7,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; // Constants for the tool const toolName = "get_console_logs"; -const toolDescription = "Retrieves logs from the Unity console"; +const toolDescription = "Retrieves logs from the Unity console with pagination support to avoid token limits"; const paramsSchema = z.object({ logType: z .enum(["info", "warning", "error"]) @@ -15,6 +15,19 @@ const paramsSchema = z.object({ .describe( "The type of logs to retrieve (info, warning, error) - defaults to all logs if not specified" ), + offset: z + .number() + .int() + .min(0) + .optional() + .describe("Starting index for pagination (0-based, defaults to 0)"), + limit: z + .number() + .int() + .min(1) + .max(500) + .optional() + .describe("Maximum number of logs to return (defaults to 50, max 500 to avoid token limits)") }); /** @@ -63,7 +76,7 @@ async function toolHandler( mcpUnity: McpUnity, params: z.infer ): Promise { - const { logType } = params; + const { logType, offset = 0, limit = 50 } = params; // Send request to Unity using the same method name as the resource // This allows reusing the existing Unity-side implementation @@ -71,6 +84,8 @@ async function toolHandler( method: "get_console_logs", params: { logType: logType, + offset: offset, + limit: limit, }, });