Skip to content

Commit b4b4bd6

Browse files
Merge pull request #42 from Saqoosha/feature/console-logs-pagination
feat: Add pagination support to console logs to prevent LLM token limits
2 parents b8b9cf8 + 5e35e08 commit b4b4bd6

File tree

7 files changed

+215
-64
lines changed

7 files changed

+215
-64
lines changed

Editor/Resources/GetConsoleLogsResource.cs

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using Newtonsoft.Json.Linq;
23
using McpUnity.Services;
34

@@ -13,38 +14,57 @@ public class GetConsoleLogsResource : McpResourceBase
1314
public GetConsoleLogsResource(IConsoleLogsService consoleLogsService)
1415
{
1516
Name = "get_console_logs";
16-
Description = "Retrieves logs from the Unity console, optionally filtered by type (error, warning, info)";
17+
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.";
1718
Uri = "unity://logs/{logType}";
1819

1920
_consoleLogsService = consoleLogsService;
2021
}
2122

2223
/// <summary>
23-
/// Fetch logs from the Unity console, optionally filtered by type
24+
/// Fetch logs from the Unity console, optionally filtered by type with pagination support
2425
/// </summary>
25-
/// <param name="parameters">Resource parameters as a JObject (may include 'logType')</param>
26-
/// <returns>A JObject containing the list of logs</returns>
26+
/// <param name="parameters">Resource parameters as a JObject (may include 'logType', 'offset', 'limit')</param>
27+
/// <returns>A JObject containing the list of logs with pagination info</returns>
2728
public override JObject Fetch(JObject parameters)
2829
{
29-
string logType = null;
30-
if (parameters != null && parameters.ContainsKey("logType") && parameters["logType"] != null)
31-
{
32-
logType = parameters["logType"].ToString()?.ToLowerInvariant();
33-
if (string.IsNullOrWhiteSpace(logType))
34-
{
35-
logType = null;
36-
}
37-
}
38-
39-
JArray logsArray = _consoleLogsService.GetAllLogsAsJson(logType);
40-
41-
// Create the response
42-
return new JObject
43-
{
44-
["success"] = true,
45-
["message"] = $"Retrieved {logsArray.Count} log entries" + (logType != null ? $" of type '{logType}'" : ""),
46-
["logs"] = logsArray
47-
};
30+
string logType = parameters?["logType"]?.ToString();
31+
if (string.IsNullOrWhiteSpace(logType)) logType = null;
32+
33+
int offset = Math.Max(0, GetIntParameter(parameters, "offset", 0));
34+
int limit = Math.Max(1, Math.Min(1000, GetIntParameter(parameters, "limit", 100)));
35+
36+
// Use the new paginated method
37+
JObject result = _consoleLogsService.GetLogsAsJson(logType, offset, limit);
38+
39+
// Add formatted message with pagination info
40+
string typeFilter = logType != null ? $" of type '{logType}'" : "";
41+
int returnedCount = result["_returnedCount"]?.Value<int>() ?? 0;
42+
int filteredCount = result["_filteredCount"]?.Value<int>() ?? 0;
43+
int totalCount = result["_totalCount"]?.Value<int>() ?? 0;
44+
45+
result["message"] = $"Retrieved {returnedCount} of {filteredCount} log entries{typeFilter} (offset: {offset}, limit: {limit}, total: {totalCount})";
46+
result["success"] = true;
47+
48+
// Remove internal count fields (they're now in the message)
49+
result.Remove("_totalCount");
50+
result.Remove("_filteredCount");
51+
result.Remove("_returnedCount");
52+
53+
return result;
54+
}
55+
56+
/// <summary>
57+
/// Helper method to safely extract integer parameters with default values
58+
/// </summary>
59+
/// <param name="parameters">JObject containing parameters</param>
60+
/// <param name="key">Parameter key to extract</param>
61+
/// <param name="defaultValue">Default value if parameter is missing or invalid</param>
62+
/// <returns>Extracted integer value or default</returns>
63+
private static int GetIntParameter(JObject parameters, string key, int defaultValue)
64+
{
65+
if (parameters?[key] != null && int.TryParse(parameters[key].ToString(), out int value))
66+
return value;
67+
return defaultValue;
4868
}
4969

5070

Editor/Services/ConsoleLogsService.cs

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ private class LogEntry
2929
public DateTime Timestamp { get; set; }
3030
}
3131

32+
// Constants for log management
33+
private const int MaxLogEntries = 1000;
34+
private const int CleanupThreshold = 200; // Remove oldest entries when exceeding max
35+
3236
// Collection to store all log messages
3337
private readonly List<LogEntry> _logEntries = new List<LogEntry>();
3438

@@ -73,16 +77,21 @@ public void StopListening()
7377
EditorApplication.update -= CheckConsoleClearViaReflection;
7478
#endif
7579
}
76-
7780
/// <summary>
78-
/// Get all logs as a JSON array
81+
/// Get logs as a JSON array with pagination support
7982
/// </summary>
80-
/// <returns>JArray containing all logs</returns>
81-
public JArray GetAllLogsAsJson(string logType = "")
83+
/// <param name="logType">Filter by log type (empty for all)</param>
84+
/// <param name="offset">Starting index (0-based)</param>
85+
/// <param name="limit">Maximum number of logs to return (default: 100)</param>
86+
/// <returns>JObject containing logs array and pagination info</returns>
87+
public JObject GetLogsAsJson(string logType = "", int offset = 0, int limit = 100)
8288
{
8389
// Convert log entries to a JSON array, filtering by logType if provided
8490
JArray logsArray = new JArray();
8591
bool filter = !string.IsNullOrEmpty(logType);
92+
int totalCount = 0;
93+
int filteredCount = 0;
94+
int currentIndex = 0;
8695

8796
// Map MCP log types to Unity log types outside the loop for better performance
8897
HashSet<string> unityLogTypes = null;
@@ -101,21 +110,46 @@ public JArray GetAllLogsAsJson(string logType = "")
101110

102111
lock (_logEntries)
103112
{
104-
foreach (var entry in _logEntries)
113+
totalCount = _logEntries.Count;
114+
115+
// Single pass: count filtered entries and collect the requested page (newest first)
116+
for (int i = _logEntries.Count - 1; i >= 0; i--)
105117
{
118+
var entry = _logEntries[i];
119+
120+
// Skip if filtering and entry doesn't match the filter
106121
if (filter && !unityLogTypes.Contains(entry.Type.ToString()))
107122
continue;
108-
logsArray.Add(new JObject
123+
124+
// Count filtered entries
125+
filteredCount++;
126+
127+
// Check if we're in the offset range and haven't reached the limit yet
128+
if (currentIndex >= offset && logsArray.Count < limit)
109129
{
110-
["message"] = entry.Message,
111-
["stackTrace"] = entry.StackTrace,
112-
["type"] = entry.Type.ToString(),
113-
["timestamp"] = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff")
114-
});
130+
logsArray.Add(new JObject
131+
{
132+
["message"] = entry.Message,
133+
["stackTrace"] = entry.StackTrace,
134+
["type"] = entry.Type.ToString(),
135+
["timestamp"] = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff")
136+
});
137+
}
138+
139+
currentIndex++;
140+
141+
// Early exit if we've collected enough logs
142+
if (currentIndex >= offset + limit) break;
115143
}
116144
}
117145

118-
return logsArray;
146+
return new JObject
147+
{
148+
["logs"] = logsArray,
149+
["_totalCount"] = totalCount,
150+
["_filteredCount"] = filteredCount,
151+
["_returnedCount"] = logsArray.Count
152+
};
119153
}
120154

121155
/// <summary>
@@ -129,6 +163,34 @@ private void ClearLogs()
129163
}
130164
}
131165

166+
/// <summary>
167+
/// Manually clean up old log entries, keeping only the most recent ones
168+
/// </summary>
169+
/// <param name="keepCount">Number of recent entries to keep (default: 500)</param>
170+
public void CleanupOldLogs(int keepCount = 500)
171+
{
172+
lock (_logEntries)
173+
{
174+
if (_logEntries.Count > keepCount)
175+
{
176+
int removeCount = _logEntries.Count - keepCount;
177+
_logEntries.RemoveRange(0, removeCount);
178+
}
179+
}
180+
}
181+
182+
/// <summary>
183+
/// Get current log count
184+
/// </summary>
185+
/// <returns>Number of stored log entries</returns>
186+
public int GetLogCount()
187+
{
188+
lock (_logEntries)
189+
{
190+
return _logEntries.Count;
191+
}
192+
}
193+
132194
/// <summary>
133195
/// Check if console was cleared using reflection (for Unity 2022.3)
134196
/// </summary>
@@ -177,6 +239,12 @@ private void OnLogMessageReceived(string logString, string stackTrace, LogType t
177239
Type = type,
178240
Timestamp = DateTime.Now
179241
});
242+
243+
// Clean up old entries if we exceed the maximum
244+
if (_logEntries.Count > MaxLogEntries)
245+
{
246+
_logEntries.RemoveRange(0, CleanupThreshold);
247+
}
180248
}
181249
}
182250

Editor/Services/IConsoleLogsService.cs

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

2022
/// <summary>
2123
/// Start listening for logs
@@ -26,5 +28,17 @@ public interface IConsoleLogsService
2628
/// Stop listening for logs
2729
/// </summary>
2830
void StopListening();
31+
32+
/// <summary>
33+
/// Manually clean up old log entries, keeping only the most recent ones
34+
/// </summary>
35+
/// <param name="keepCount">Number of recent entries to keep (default: 500)</param>
36+
void CleanupOldLogs(int keepCount = 500);
37+
38+
/// <summary>
39+
/// Get current log count
40+
/// </summary>
41+
/// <returns>Number of stored log entries</returns>
42+
int GetLogCount();
2943
}
3044
}

Server~/build/resources/getConsoleLogsResource.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { McpUnityError, ErrorType } from '../utils/errors.js';
33
// Constants for the resource
44
const resourceName = 'get_console_logs';
55
const resourceMimeType = 'application/json';
6-
const resourceUri = 'unity://logs/{logType}';
6+
const resourceUri = 'unity://logs/{logType}?offset={offset}&limit={limit}';
77
const resourceTemplate = new ResourceTemplate(resourceUri, {
88
list: () => listLogTypes(resourceMimeType)
99
});
@@ -13,25 +13,25 @@ function listLogTypes(resourceMimeType) {
1313
{
1414
uri: `unity://logs/`,
1515
name: "All logs",
16-
description: "Retrieve all Unity console logs",
16+
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.",
1717
mimeType: resourceMimeType
1818
},
1919
{
2020
uri: `unity://logs/error`,
2121
name: "Error logs",
22-
description: "Retrieve only error logs from the Unity console",
22+
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.",
2323
mimeType: resourceMimeType
2424
},
2525
{
2626
uri: `unity://logs/warning`,
2727
name: "Warning logs",
28-
description: "Retrieve only warning logs from the Unity console",
28+
description: "Retrieve only warning logs from Unity console (newest first). Use pagination ?offset=0&limit=30 to manage token usage effectively.",
2929
mimeType: resourceMimeType
3030
},
3131
{
3232
uri: `unity://logs/info`,
3333
name: "Info logs",
34-
description: "Retrieve only info logs from the Unity console",
34+
description: "Retrieve only info logs from Unity console (newest first). Use smaller limits like ?limit=25 to prevent token overflow in LLM responses.",
3535
mimeType: resourceMimeType
3636
}
3737
]
@@ -43,7 +43,7 @@ function listLogTypes(resourceMimeType) {
4343
export function registerGetConsoleLogsResource(server, mcpUnity, logger) {
4444
logger.info(`Registering resource: ${resourceName}`);
4545
server.resource(resourceName, resourceTemplate, {
46-
description: 'Retrieve Unity console logs by type',
46+
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.',
4747
mimeType: resourceMimeType
4848
}, async (uri, variables) => {
4949
try {
@@ -63,19 +63,24 @@ async function resourceHandler(mcpUnity, uri, variables, logger) {
6363
let logType = variables["logType"] ? decodeURIComponent(variables["logType"]) : undefined;
6464
if (logType === '')
6565
logType = undefined;
66+
// Extract pagination parameters
67+
const offset = variables["offset"] ? parseInt(variables["offset"], 10) : 0;
68+
const limit = variables["limit"] ? parseInt(variables["limit"], 10) : 100;
6669
// Send request to Unity
6770
const response = await mcpUnity.sendRequest({
6871
method: resourceName,
6972
params: {
70-
logType: logType
73+
logType: logType,
74+
offset: offset,
75+
limit: limit
7176
}
7277
});
7378
if (!response.success) {
7479
throw new McpUnityError(ErrorType.RESOURCE_FETCH, response.message || 'Failed to fetch logs from Unity');
7580
}
7681
return {
7782
contents: [{
78-
uri: `unity://logs/${logType ?? ''}`,
83+
uri: `unity://logs/${logType ?? ''}?offset=${offset}&limit=${limit}`,
7984
mimeType: resourceMimeType,
8085
text: JSON.stringify(response, null, 2)
8186
}]

Server~/build/tools/getConsoleLogsTool.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,25 @@ import * as z from "zod";
22
import { McpUnityError, ErrorType } from "../utils/errors.js";
33
// Constants for the tool
44
const toolName = "get_console_logs";
5-
const toolDescription = "Retrieves logs from the Unity console";
5+
const toolDescription = "Retrieves logs from the Unity console with pagination support to avoid token limits";
66
const paramsSchema = z.object({
77
logType: z
88
.enum(["info", "warning", "error"])
99
.optional()
1010
.describe("The type of logs to retrieve (info, warning, error) - defaults to all logs if not specified"),
11+
offset: z
12+
.number()
13+
.int()
14+
.min(0)
15+
.optional()
16+
.describe("Starting index for pagination (0-based, defaults to 0)"),
17+
limit: z
18+
.number()
19+
.int()
20+
.min(1)
21+
.max(500)
22+
.optional()
23+
.describe("Maximum number of logs to return (defaults to 50, max 500 to avoid token limits)")
1124
});
1225
/**
1326
* Creates and registers the Get Console Logs tool with the MCP server
@@ -42,13 +55,15 @@ export function registerGetConsoleLogsTool(server, mcpUnity, logger) {
4255
* @throws McpUnityError if the request to Unity fails
4356
*/
4457
async function toolHandler(mcpUnity, params) {
45-
const { logType } = params;
58+
const { logType, offset = 0, limit = 50 } = params;
4659
// Send request to Unity using the same method name as the resource
4760
// This allows reusing the existing Unity-side implementation
4861
const response = await mcpUnity.sendRequest({
4962
method: "get_console_logs",
5063
params: {
5164
logType: logType,
65+
offset: offset,
66+
limit: limit,
5267
},
5368
});
5469
if (!response.success) {

0 commit comments

Comments
 (0)