Skip to content

feat: Add pagination support to console logs to prevent LLM token limits #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 43 additions & 23 deletions Editor/Resources/GetConsoleLogsResource.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Newtonsoft.Json.Linq;
using McpUnity.Services;

Expand All @@ -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;
}

/// <summary>
/// Fetch logs from the Unity console, optionally filtered by type
/// Fetch logs from the Unity console, optionally filtered by type with pagination support
/// </summary>
/// <param name="parameters">Resource parameters as a JObject (may include 'logType')</param>
/// <returns>A JObject containing the list of logs</returns>
/// <param name="parameters">Resource parameters as a JObject (may include 'logType', 'offset', 'limit')</param>
/// <returns>A JObject containing the list of logs with pagination info</returns>
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<int>() ?? 0;
int filteredCount = result["_filteredCount"]?.Value<int>() ?? 0;
int totalCount = result["_totalCount"]?.Value<int>() ?? 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;
}

/// <summary>
/// Helper method to safely extract integer parameters with default values
/// </summary>
/// <param name="parameters">JObject containing parameters</param>
/// <param name="key">Parameter key to extract</param>
/// <param name="defaultValue">Default value if parameter is missing or invalid</param>
/// <returns>Extracted integer value or default</returns>
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;
}


Expand Down
92 changes: 80 additions & 12 deletions Editor/Services/ConsoleLogsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<LogEntry> _logEntries = new List<LogEntry>();

Expand Down Expand Up @@ -73,16 +77,21 @@ public void StopListening()
EditorApplication.update -= CheckConsoleClearViaReflection;
#endif
}

/// <summary>
/// Get all logs as a JSON array
/// Get logs as a JSON array with pagination support
/// </summary>
/// <returns>JArray containing all logs</returns>
public JArray GetAllLogsAsJson(string logType = "")
/// <param name="logType">Filter by log type (empty for all)</param>
/// <param name="offset">Starting index (0-based)</param>
/// <param name="limit">Maximum number of logs to return (default: 100)</param>
/// <returns>JObject containing logs array and pagination info</returns>
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<string> unityLogTypes = null;
Expand All @@ -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
};
}

/// <summary>
Expand All @@ -129,6 +163,34 @@ private void ClearLogs()
}
}

/// <summary>
/// Manually clean up old log entries, keeping only the most recent ones
/// </summary>
/// <param name="keepCount">Number of recent entries to keep (default: 500)</param>
public void CleanupOldLogs(int keepCount = 500)
{
lock (_logEntries)
{
if (_logEntries.Count > keepCount)
{
int removeCount = _logEntries.Count - keepCount;
_logEntries.RemoveRange(0, removeCount);
}
}
}

/// <summary>
/// Get current log count
/// </summary>
/// <returns>Number of stored log entries</returns>
public int GetLogCount()
{
lock (_logEntries)
{
return _logEntries.Count;
}
}

/// <summary>
/// Check if console was cleared using reflection (for Unity 2022.3)
/// </summary>
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
22 changes: 18 additions & 4 deletions Editor/Services/IConsoleLogsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ namespace McpUnity.Services
public interface IConsoleLogsService
{
/// <summary>
/// Get all logs as a JSON array, optionally filtered by log type
/// Get logs as a JSON object with pagination support
/// </summary>
/// <param name="logType">UnityEngine.LogType as string (e.g. "Error", "Warning", "Log"). Empty string for all logs.</param>
/// <returns>JArray containing filtered logs</returns>
JArray GetAllLogsAsJson(string logType = "");
/// <param name="logType">Filter by log type (empty for all)</param>
/// <param name="offset">Starting index (0-based)</param>
/// <param name="limit">Maximum number of logs to return (default: 100)</param>
/// <returns>JObject containing logs array and pagination info</returns>
JObject GetLogsAsJson(string logType = "", int offset = 0, int limit = 100);

/// <summary>
/// Start listening for logs
Expand All @@ -26,5 +28,17 @@ public interface IConsoleLogsService
/// Stop listening for logs
/// </summary>
void StopListening();

/// <summary>
/// Manually clean up old log entries, keeping only the most recent ones
/// </summary>
/// <param name="keepCount">Number of recent entries to keep (default: 500)</param>
void CleanupOldLogs(int keepCount = 500);

/// <summary>
/// Get current log count
/// </summary>
/// <returns>Number of stored log entries</returns>
int GetLogCount();
}
}
21 changes: 13 additions & 8 deletions Server~/build/resources/getConsoleLogsResource.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
});
Expand All @@ -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
}
]
Expand All @@ -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 {
Expand All @@ -63,19 +63,24 @@ 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;
Comment on lines +67 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add validation for pagination parameters.

The code should validate that parsed values are valid non-negative integers to prevent potential issues with NaN or negative values.

Apply this validation after parsing:

 const offset = variables["offset"] ? parseInt(variables["offset"], 10) : 0;
 const limit = variables["limit"] ? parseInt(variables["limit"], 10) : 100;
+
+// Validate pagination parameters
+if (isNaN(offset) || offset < 0) {
+    throw new McpUnityError(ErrorType.INVALID_PARAMS, 'Invalid offset parameter: must be a non-negative integer');
+}
+if (isNaN(limit) || limit <= 0) {
+    throw new McpUnityError(ErrorType.INVALID_PARAMS, 'Invalid limit parameter: must be a positive integer');
+}
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const offset = variables["offset"] ? parseInt(variables["offset"], 10) : 0;
const limit = variables["limit"] ? parseInt(variables["limit"], 10) : 100;
const offset = variables["offset"] ? parseInt(variables["offset"], 10) : 0;
const limit = variables["limit"] ? parseInt(variables["limit"], 10) : 100;
// Validate pagination parameters
if (isNaN(offset) || offset < 0) {
throw new McpUnityError(ErrorType.INVALID_PARAMS, 'Invalid offset parameter: must be a non-negative integer');
}
if (isNaN(limit) || limit <= 0) {
throw new McpUnityError(ErrorType.INVALID_PARAMS, 'Invalid limit parameter: must be a positive integer');
}
πŸ€– Prompt for AI Agents
In Server~/build/resources/getConsoleLogsResource.js around lines 67 to 68, the
pagination parameters offset and limit are parsed without validation, which can
lead to NaN or negative values. After parsing, add checks to ensure offset and
limit are non-negative integers; if not, set them to default values (0 for
offset, 100 for limit). This validation will prevent invalid pagination inputs
from causing errors.

// Send request to Unity
const response = await mcpUnity.sendRequest({
method: resourceName,
params: {
logType: logType
logType: logType,
offset: offset,
limit: limit
}
});
if (!response.success) {
throw new McpUnityError(ErrorType.RESOURCE_FETCH, response.message || 'Failed to fetch logs from Unity');
}
return {
contents: [{
uri: `unity://logs/${logType ?? ''}`,
uri: `unity://logs/${logType ?? ''}?offset=${offset}&limit=${limit}`,
mimeType: resourceMimeType,
text: JSON.stringify(response, null, 2)
}]
Expand Down
19 changes: 17 additions & 2 deletions Server~/build/tools/getConsoleLogsTool.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Loading