From 925431d447a6567fb03cc3cfb2eed60f3c7df5cd Mon Sep 17 00:00:00 2001 From: Saqoosha Date: Sun, 1 Jun 2025 20:41:43 +0900 Subject: [PATCH 1/3] feat: Add pagination support to console logs to prevent LLM token limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add offset and limit parameters to GetConsoleLogsResource and GetConsoleLogsTool - Implement GetLogsAsJson method with pagination in ConsoleLogsService - Return logs in newest-first order for better debugging workflow - Add automatic memory management with configurable cleanup thresholds - Update resource descriptions with pagination usage recommendations - Enhance Unity-side and Node.js-side implementations consistently 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Editor/Resources/GetConsoleLogsResource.cs | 52 +++++++--- Editor/Services/ConsoleLogsService.cs | 99 +++++++++++++++++-- Editor/Services/IConsoleLogsService.cs | 21 ++++ .../build/resources/getConsoleLogsResource.js | 21 ++-- Server~/build/tools/getConsoleLogsTool.js | 19 +++- .../src/resources/getConsoleLogsResource.ts | 24 +++-- Server~/src/tools/getConsoleLogsTool.ts | 19 +++- 7 files changed, 211 insertions(+), 44 deletions(-) diff --git a/Editor/Resources/GetConsoleLogsResource.cs b/Editor/Resources/GetConsoleLogsResource.cs index 029da3d4..2146614e 100644 --- a/Editor/Resources/GetConsoleLogsResource.cs +++ b/Editor/Resources/GetConsoleLogsResource.cs @@ -13,38 +13,58 @@ 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) + int offset = 0; + int limit = 100; + + if (parameters != null) { - logType = parameters["logType"].ToString()?.ToLowerInvariant(); - if (string.IsNullOrWhiteSpace(logType)) + // Extract logType + if (parameters.ContainsKey("logType") && parameters["logType"] != null) + { + logType = parameters["logType"].ToString()?.ToLowerInvariant(); + if (string.IsNullOrWhiteSpace(logType)) + { + logType = null; + } + } + + // Extract pagination parameters + if (parameters.ContainsKey("offset") && parameters["offset"] != null) + { + int.TryParse(parameters["offset"].ToString(), out offset); + } + + if (parameters.ContainsKey("limit") && parameters["limit"] != null) { - logType = null; + int.TryParse(parameters["limit"].ToString(), out limit); } } - JArray logsArray = _consoleLogsService.GetAllLogsAsJson(logType); + // Use the new paginated method + JObject result = _consoleLogsService.GetLogsAsJson(logType, offset, limit); + + // Add success info to the response + result["success"] = true; + + var pagination = result["pagination"] as JObject; + string typeFilter = logType != null ? $" of type '{logType}'" : ""; + result["message"] = $"Retrieved {pagination["returnedCount"]} of {pagination["filteredCount"]} log entries{typeFilter} (offset: {offset}, limit: {limit})"; - // Create the response - return new JObject - { - ["success"] = true, - ["message"] = $"Retrieved {logsArray.Count} log entries" + (logType != null ? $" of type '{logType}'" : ""), - ["logs"] = logsArray - }; + return result; } diff --git a/Editor/Services/ConsoleLogsService.cs b/Editor/Services/ConsoleLogsService.cs index 74c00c15..97618700 100644 --- a/Editor/Services/ConsoleLogsService.cs +++ b/Editor/Services/ConsoleLogsService.cs @@ -21,6 +21,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(); @@ -71,28 +75,75 @@ public void StopListening() /// /// JArray containing all logs public JArray GetAllLogsAsJson(string logType = "") + { + var result = GetLogsAsJson(logType, 0, int.MaxValue); + return result["logs"] as JArray; + } + + /// + /// Get logs as a JSON array with pagination support + /// + /// 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; lock (_logEntries) { + // First pass: count total and filtered entries foreach (var entry in _logEntries) { + totalCount++; + if (!filter || entry.Type.ToString().Equals(logType, System.StringComparison.OrdinalIgnoreCase)) + { + filteredCount++; + } + } + + // Second pass: collect the requested page (newest first) + for (int i = _logEntries.Count - 1; i >= 0; i--) + { + var entry = _logEntries[i]; if (filter && !entry.Type.ToString().Equals(logType, System.StringComparison.OrdinalIgnoreCase)) continue; - logsArray.Add(new JObject + + 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++; + if (logsArray.Count >= limit) break; } } - return logsArray; + return new JObject + { + ["logs"] = logsArray, + ["pagination"] = new JObject + { + ["offset"] = offset, + ["limit"] = limit, + ["totalCount"] = totalCount, + ["filteredCount"] = filteredCount, + ["returnedCount"] = logsArray.Count, + ["hasMore"] = offset + logsArray.Count < filteredCount + } + }; } /// @@ -106,6 +157,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) /// @@ -154,6 +233,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..183e700a 100644 --- a/Editor/Services/IConsoleLogsService.cs +++ b/Editor/Services/IConsoleLogsService.cs @@ -17,6 +17,15 @@ public interface IConsoleLogsService /// JArray containing filtered logs JArray GetAllLogsAsJson(string logType = ""); + /// + /// Get logs as a JSON object with pagination support + /// + /// 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 +35,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..442a3916 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) }); @@ -19,25 +19,25 @@ function listLogTypes(resourceMimeType: string) { { 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", + name: "Warning logs", + 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 } ] @@ -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,18 @@ 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 + const offset = variables["offset"] ? parseInt(variables["offset"] as string, 10) : 0; + const limit = variables["limit"] ? parseInt(variables["limit"] as string, 10) : 100; // Send request to Unity const response = await mcpUnity.sendRequest({ method: resourceName, params: { - logType: logType + logType: logType, + offset: offset, + limit: limit } }); @@ -93,7 +99,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, }, }); From 6f1ddc0d2628319f35308e13d02163939b72d41a Mon Sep 17 00:00:00 2001 From: Saqoosha Date: Sun, 1 Jun 2025 21:04:35 +0900 Subject: [PATCH 2/3] refactor: Improve parameter validation and safety in GetConsoleLogsResource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add bounds validation for offset (≥0) and limit (1-1000) parameters - Simplify parameter extraction with helper method - Add defensive programming for pagination metadata access - Reduce code duplication and improve maintainability Addresses CodeRabbit review feedback for better error handling and robustness. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Editor/Resources/GetConsoleLogsResource.cs | 48 ++++++++++------------ 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/Editor/Resources/GetConsoleLogsResource.cs b/Editor/Resources/GetConsoleLogsResource.cs index 2146614e..9438d465 100644 --- a/Editor/Resources/GetConsoleLogsResource.cs +++ b/Editor/Resources/GetConsoleLogsResource.cs @@ -26,33 +26,11 @@ public GetConsoleLogsResource(IConsoleLogsService consoleLogsService) /// A JObject containing the list of logs with pagination info public override JObject Fetch(JObject parameters) { - string logType = null; - int offset = 0; - int limit = 100; + string logType = parameters?["logType"]?.ToString(); + if (string.IsNullOrWhiteSpace(logType)) logType = null; - if (parameters != null) - { - // Extract logType - if (parameters.ContainsKey("logType") && parameters["logType"] != null) - { - logType = parameters["logType"].ToString()?.ToLowerInvariant(); - if (string.IsNullOrWhiteSpace(logType)) - { - logType = null; - } - } - - // Extract pagination parameters - if (parameters.ContainsKey("offset") && parameters["offset"] != null) - { - int.TryParse(parameters["offset"].ToString(), out offset); - } - - if (parameters.ContainsKey("limit") && parameters["limit"] != null) - { - int.TryParse(parameters["limit"].ToString(), out limit); - } - } + 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); @@ -62,11 +40,27 @@ public override JObject Fetch(JObject parameters) var pagination = result["pagination"] as JObject; string typeFilter = logType != null ? $" of type '{logType}'" : ""; - result["message"] = $"Retrieved {pagination["returnedCount"]} of {pagination["filteredCount"]} log entries{typeFilter} (offset: {offset}, limit: {limit})"; + int returnedCount = pagination?["returnedCount"]?.Value() ?? 0; + int filteredCount = pagination?["filteredCount"]?.Value() ?? 0; + result["message"] = $"Retrieved {returnedCount} of {filteredCount} log entries{typeFilter} (offset: {offset}, limit: {limit})"; 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; + } + } } From 5e35e08174b5215f9e09e5414af7ed9e4b83e8b5 Mon Sep 17 00:00:00 2001 From: Saqoosha Date: Tue, 3 Jun 2025 06:29:14 +0900 Subject: [PATCH 3/3] refactor: simplify console logs response format based on PR feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove pagination object and consolidate info into message field - Delete deprecated GetAllLogsAsJson method - Optimize log counting with single loop for better performance - Add default offset/limit values to resource templates - Simplify response to only include logs, message, and success fields - Move count information (total, filtered, returned) into message text - Add parameter validation for offset/limit in TypeScript This addresses all review comments from PR #42 to make the response format more concise and easier for AI agents to process. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Editor/Resources/GetConsoleLogsResource.cs | 18 ++++--- Editor/Services/ConsoleLogsService.cs | 47 ++++++------------- Editor/Services/IConsoleLogsService.cs | 7 --- .../src/resources/getConsoleLogsResource.ts | 26 ++++++---- 4 files changed, 44 insertions(+), 54 deletions(-) diff --git a/Editor/Resources/GetConsoleLogsResource.cs b/Editor/Resources/GetConsoleLogsResource.cs index 9438d465..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; @@ -35,14 +36,19 @@ public override JObject Fetch(JObject parameters) // Use the new paginated method JObject result = _consoleLogsService.GetLogsAsJson(logType, offset, limit); - // Add success info to the response + // 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; - var pagination = result["pagination"] as JObject; - string typeFilter = logType != null ? $" of type '{logType}'" : ""; - int returnedCount = pagination?["returnedCount"]?.Value() ?? 0; - int filteredCount = pagination?["filteredCount"]?.Value() ?? 0; - result["message"] = $"Retrieved {returnedCount} of {filteredCount} log entries{typeFilter} (offset: {offset}, limit: {limit})"; + // Remove internal count fields (they're now in the message) + result.Remove("_totalCount"); + result.Remove("_filteredCount"); + result.Remove("_returnedCount"); return result; } diff --git a/Editor/Services/ConsoleLogsService.cs b/Editor/Services/ConsoleLogsService.cs index 4913d03f..cbca7503 100644 --- a/Editor/Services/ConsoleLogsService.cs +++ b/Editor/Services/ConsoleLogsService.cs @@ -77,17 +77,6 @@ public void StopListening() EditorApplication.update -= CheckConsoleClearViaReflection; #endif } - - /// - /// Get all logs as a JSON array - /// - /// JArray containing all logs - public JArray GetAllLogsAsJson(string logType = "") - { - var result = GetLogsAsJson(logType, 0, int.MaxValue); - return result["logs"] as JArray; - } - /// /// Get logs as a JSON array with pagination support /// @@ -121,23 +110,21 @@ public JObject GetLogsAsJson(string logType = "", int offset = 0, int limit = 10 lock (_logEntries) { - // First pass: count total and filtered entries - foreach (var entry in _logEntries) - { - totalCount++; - if (!filter || unityLogTypes.Contains(entry.Type.ToString())) - { - filteredCount++; - } - } + totalCount = _logEntries.Count; - // Second pass: collect the requested page (newest first) + // 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; - + + // 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) { logsArray.Add(new JObject @@ -150,22 +137,18 @@ public JObject GetLogsAsJson(string logType = "", int offset = 0, int limit = 10 } currentIndex++; - if (logsArray.Count >= limit) break; + + // Early exit if we've collected enough logs + if (currentIndex >= offset + limit) break; } } return new JObject { ["logs"] = logsArray, - ["pagination"] = new JObject - { - ["offset"] = offset, - ["limit"] = limit, - ["totalCount"] = totalCount, - ["filteredCount"] = filteredCount, - ["returnedCount"] = logsArray.Count, - ["hasMore"] = offset + logsArray.Count < filteredCount - } + ["_totalCount"] = totalCount, + ["_filteredCount"] = filteredCount, + ["_returnedCount"] = logsArray.Count }; } diff --git a/Editor/Services/IConsoleLogsService.cs b/Editor/Services/IConsoleLogsService.cs index 183e700a..1c7795fb 100644 --- a/Editor/Services/IConsoleLogsService.cs +++ b/Editor/Services/IConsoleLogsService.cs @@ -10,13 +10,6 @@ namespace McpUnity.Services /// public interface IConsoleLogsService { - /// - /// Get all logs as a JSON array, optionally filtered by log type - /// - /// UnityEngine.LogType as string (e.g. "Error", "Warning", "Log"). Empty string for all logs. - /// JArray containing filtered logs - JArray GetAllLogsAsJson(string logType = ""); - /// /// Get logs as a JSON object with pagination support /// diff --git a/Server~/src/resources/getConsoleLogsResource.ts b/Server~/src/resources/getConsoleLogsResource.ts index 442a3916..f804c5b2 100644 --- a/Server~/src/resources/getConsoleLogsResource.ts +++ b/Server~/src/resources/getConsoleLogsResource.ts @@ -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 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.", + 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 Unity console (newest first). Use ?offset=0&limit=20 to avoid token limits. Large log sets may exceed LLM context window.", + description: "Retrieve only error logs from Unity console (newest first). Default pagination offset=0&limit=20.", mimeType: resourceMimeType }, { - uri: `unity://logs/warning`, + uri: `unity://logs/warning?offset=0&limit=30`, name: "Warning logs", - description: "Retrieve only warning logs from Unity console (newest first). Use pagination ?offset=0&limit=30 to manage token usage effectively.", + 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 Unity console (newest first). Use smaller limits like ?limit=25 to prevent token overflow in LLM responses.", + description: "Retrieve only info logs from Unity console (newest first). Default pagination offset=0&limit=25.", mimeType: resourceMimeType } ] @@ -76,9 +76,17 @@ async function resourceHandler(mcpUnity: McpUnity, uri: URL, variables: Variable let logType = variables["logType"] ? decodeURIComponent(variables["logType"] as string) : undefined; if (logType === '') logType = undefined; - // Extract pagination parameters + // 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({