Skip to content

Commit 5b2ebf4

Browse files
Saqooshaclaude
andcommitted
feat: Add search_console_logs tool for Unity log searching
- Add SearchConsoleLogsTool.cs with keyword/regex search support - Support case-sensitive/insensitive search with logType filtering - Include pagination (offset/limit) and optional stack trace inclusion - Implement both C# (Unity) and TypeScript (Node.js) components - Register tool in McpUnityServer and Node.js MCP server - Remove previous resource-based implementation in favor of tool-based approach - Add comprehensive search capabilities: - Keyword search with partial matching - Regular expression pattern matching - Log type filtering (error/warning/info) - Configurable case sensitivity - Stack trace inclusion/exclusion for token optimization - Pagination support for large result sets 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 92b8387 commit 5b2ebf4

File tree

11 files changed

+464
-2
lines changed

11 files changed

+464
-2
lines changed

Editor/Services/ConsoleLogsService.cs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Reflection;
4+
using System.Text.RegularExpressions;
45
using Newtonsoft.Json.Linq;
56
using UnityEditor;
67
using UnityEngine;
@@ -256,6 +257,143 @@ private void OnLogMessageReceived(string logString, string stackTrace, LogType t
256257
}
257258
}
258259

260+
/// <summary>
261+
/// Search logs with keyword or regex pattern
262+
/// </summary>
263+
public JObject SearchLogsAsJson(string keyword = null, string regex = null, string logType = null,
264+
bool includeStackTrace = true, bool caseSensitive = false, int offset = 0, int limit = 50)
265+
{
266+
// Prepare search criteria
267+
bool hasSearchCriteria = !string.IsNullOrEmpty(keyword) || !string.IsNullOrEmpty(regex);
268+
Regex searchRegex = null;
269+
string searchKeyword = keyword;
270+
271+
// If regex is provided, use it instead of keyword
272+
if (!string.IsNullOrEmpty(regex))
273+
{
274+
try
275+
{
276+
searchRegex = new Regex(regex, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
277+
}
278+
catch (ArgumentException ex)
279+
{
280+
return new JObject
281+
{
282+
["logs"] = new JArray(),
283+
["error"] = $"Invalid regex pattern: {ex.Message}",
284+
["success"] = false
285+
};
286+
}
287+
}
288+
else if (!string.IsNullOrEmpty(keyword) && !caseSensitive)
289+
{
290+
searchKeyword = keyword.ToLower();
291+
}
292+
293+
// Map MCP log types to Unity log types
294+
HashSet<string> unityLogTypes = null;
295+
if (!string.IsNullOrEmpty(logType))
296+
{
297+
if (LogTypeMapping.TryGetValue(logType, out var mapped))
298+
{
299+
unityLogTypes = mapped;
300+
}
301+
else
302+
{
303+
unityLogTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { logType };
304+
}
305+
}
306+
307+
JArray logsArray = new JArray();
308+
int totalCount = 0;
309+
int filteredCount = 0;
310+
int matchedCount = 0;
311+
int currentIndex = 0;
312+
313+
lock (_logEntries)
314+
{
315+
totalCount = _logEntries.Count;
316+
317+
// Search through logs (newest first)
318+
for (int i = _logEntries.Count - 1; i >= 0; i--)
319+
{
320+
var entry = _logEntries[i];
321+
322+
// Skip if filtering by log type and entry doesn't match
323+
if (unityLogTypes != null && !unityLogTypes.Contains(entry.Type.ToString()))
324+
continue;
325+
326+
filteredCount++;
327+
328+
// Check if entry matches search criteria
329+
bool matches = true;
330+
if (hasSearchCriteria)
331+
{
332+
matches = false;
333+
334+
// Search in message
335+
if (searchRegex != null)
336+
{
337+
matches = searchRegex.IsMatch(entry.Message);
338+
if (!matches && includeStackTrace && !string.IsNullOrEmpty(entry.StackTrace))
339+
{
340+
matches = searchRegex.IsMatch(entry.StackTrace);
341+
}
342+
}
343+
else if (!string.IsNullOrEmpty(searchKeyword))
344+
{
345+
string messageToSearch = caseSensitive ? entry.Message : entry.Message.ToLower();
346+
matches = messageToSearch.Contains(searchKeyword);
347+
348+
if (!matches && includeStackTrace && !string.IsNullOrEmpty(entry.StackTrace))
349+
{
350+
string stackTraceToSearch = caseSensitive ? entry.StackTrace : entry.StackTrace.ToLower();
351+
matches = stackTraceToSearch.Contains(searchKeyword);
352+
}
353+
}
354+
}
355+
356+
if (!matches) continue;
357+
358+
matchedCount++;
359+
360+
// Check if we're in the offset range and haven't reached the limit yet
361+
if (currentIndex >= offset && logsArray.Count < limit)
362+
{
363+
var logObject = new JObject
364+
{
365+
["message"] = entry.Message,
366+
["type"] = entry.Type.ToString(),
367+
["timestamp"] = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff")
368+
};
369+
370+
// Only include stack trace if requested
371+
if (includeStackTrace)
372+
{
373+
logObject["stackTrace"] = entry.StackTrace;
374+
}
375+
376+
logsArray.Add(logObject);
377+
}
378+
379+
currentIndex++;
380+
381+
// Early exit if we've collected enough logs
382+
if (currentIndex >= offset + limit) break;
383+
}
384+
}
385+
386+
return new JObject
387+
{
388+
["logs"] = logsArray,
389+
["_totalCount"] = totalCount,
390+
["_filteredCount"] = filteredCount,
391+
["_matchedCount"] = matchedCount,
392+
["_returnedCount"] = logsArray.Count,
393+
["success"] = true
394+
};
395+
}
396+
259397
#if UNITY_6000_0_OR_NEWER
260398
/// <summary>
261399
/// Called when the console logs count changes

Editor/Services/ConsoleLogsServiceUnity6.cs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.IO;
44
using System.Linq;
55
using System.Reflection;
6+
using System.Text.RegularExpressions;
67
using Newtonsoft.Json.Linq;
78
using UnityEditor;
89
using UnityEngine;
@@ -362,6 +363,198 @@ public int GetLogCount()
362363
return error + warning + log;
363364
}
364365

366+
/// <summary>
367+
/// Search logs with keyword or regex pattern
368+
/// </summary>
369+
public JObject SearchLogsAsJson(string keyword = null, string regex = null, string logType = null,
370+
bool includeStackTrace = true, bool caseSensitive = false, int offset = 0, int limit = 50)
371+
{
372+
// Prepare search criteria
373+
bool hasSearchCriteria = !string.IsNullOrEmpty(keyword) || !string.IsNullOrEmpty(regex);
374+
Regex searchRegex = null;
375+
string searchKeyword = keyword;
376+
377+
// If regex is provided, use it instead of keyword
378+
if (!string.IsNullOrEmpty(regex))
379+
{
380+
try
381+
{
382+
searchRegex = new Regex(regex, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
383+
}
384+
catch (ArgumentException ex)
385+
{
386+
return new JObject
387+
{
388+
["logs"] = new JArray(),
389+
["error"] = $"Invalid regex pattern: {ex.Message}",
390+
["success"] = false
391+
};
392+
}
393+
}
394+
else if (!string.IsNullOrEmpty(keyword) && !caseSensitive)
395+
{
396+
searchKeyword = keyword.ToLower();
397+
}
398+
399+
// Map MCP log types to Unity log types
400+
HashSet<string> unityLogTypes = null;
401+
if (!string.IsNullOrEmpty(logType))
402+
{
403+
if (LogTypeMapping.TryGetValue(logType, out var mapped))
404+
{
405+
unityLogTypes = mapped;
406+
}
407+
else
408+
{
409+
unityLogTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { logType };
410+
}
411+
}
412+
413+
JArray logsArray = new JArray();
414+
int totalCount = 0;
415+
int filteredCount = 0;
416+
int matchedCount = 0;
417+
int currentIndex = 0;
418+
419+
// Get total count using reflection
420+
try
421+
{
422+
totalCount = (int)_getCountMethod.Invoke(null, null);
423+
}
424+
catch (Exception ex)
425+
{
426+
Debug.LogError($"[MCP Unity] Error getting log count: {ex.Message}");
427+
return new JObject
428+
{
429+
["logs"] = logsArray,
430+
["error"] = "Failed to access Unity console logs",
431+
["success"] = false
432+
};
433+
}
434+
435+
if (totalCount == 0)
436+
{
437+
return new JObject
438+
{
439+
["logs"] = logsArray,
440+
["_totalCount"] = 0,
441+
["_filteredCount"] = 0,
442+
["_matchedCount"] = 0,
443+
["_returnedCount"] = 0,
444+
["success"] = true
445+
};
446+
}
447+
448+
try
449+
{
450+
// Start getting entries
451+
_startGettingEntriesMethod?.Invoke(null, null);
452+
453+
// Search through logs (newest first)
454+
for (int i = totalCount - 1; i >= 0; i--)
455+
{
456+
// Create LogEntry instance
457+
var logEntry = Activator.CreateInstance(_logEntryType);
458+
459+
// GetEntryInternal(int row, LogEntry outputEntry)
460+
bool success = (bool)_getEntryInternalMethod.Invoke(null, new object[] { i, logEntry });
461+
462+
if (!success) continue;
463+
464+
// Extract fields
465+
string fullMessage = _messageField?.GetValue(logEntry) as string ?? "";
466+
string file = _fileField?.GetValue(logEntry) as string ?? "";
467+
int line = _lineField?.GetValue(logEntry) as int? ?? 0;
468+
int mode = _modeField?.GetValue(logEntry) as int? ?? 0;
469+
int callstackStartUTF8 = _callstackTextStartUTF8Field?.GetValue(logEntry) as int? ?? 0;
470+
int callstackStartUTF16 = _callstackTextStartUTF16Field?.GetValue(logEntry) as int? ?? 0;
471+
472+
// Parse message and stack trace
473+
var (actualMessage, stackTrace) = ParseMessageAndStackTrace(fullMessage, callstackStartUTF16, callstackStartUTF8);
474+
475+
// Determine log type
476+
string entryLogType = DetermineLogTypeFromModeAndContent(mode, stackTrace);
477+
478+
// Skip if filtering by log type and entry doesn't match
479+
if (unityLogTypes != null && !unityLogTypes.Contains(entryLogType))
480+
continue;
481+
482+
filteredCount++;
483+
484+
// Check if entry matches search criteria
485+
bool matches = true;
486+
if (hasSearchCriteria)
487+
{
488+
matches = false;
489+
490+
// Search in message
491+
if (searchRegex != null)
492+
{
493+
matches = searchRegex.IsMatch(actualMessage);
494+
if (!matches && includeStackTrace && !string.IsNullOrEmpty(stackTrace))
495+
{
496+
matches = searchRegex.IsMatch(stackTrace);
497+
}
498+
}
499+
else if (!string.IsNullOrEmpty(searchKeyword))
500+
{
501+
string messageToSearch = caseSensitive ? actualMessage : actualMessage.ToLower();
502+
matches = messageToSearch.Contains(searchKeyword);
503+
504+
if (!matches && includeStackTrace && !string.IsNullOrEmpty(stackTrace))
505+
{
506+
string stackTraceToSearch = caseSensitive ? stackTrace : stackTrace.ToLower();
507+
matches = stackTraceToSearch.Contains(searchKeyword);
508+
}
509+
}
510+
}
511+
512+
if (!matches) continue;
513+
514+
matchedCount++;
515+
516+
// Check if we're in the offset range and haven't reached the limit yet
517+
if (currentIndex >= offset && logsArray.Count < limit)
518+
{
519+
var logObject = new JObject
520+
{
521+
["message"] = actualMessage,
522+
["type"] = entryLogType,
523+
["timestamp"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")
524+
};
525+
526+
// Only include stack trace if requested
527+
if (includeStackTrace)
528+
{
529+
logObject["stackTrace"] = stackTrace;
530+
}
531+
532+
logsArray.Add(logObject);
533+
}
534+
535+
currentIndex++;
536+
537+
// Early exit if we've collected enough logs
538+
if (currentIndex >= offset + limit) break;
539+
}
540+
}
541+
finally
542+
{
543+
// End getting entries
544+
_endGettingEntriesMethod?.Invoke(null, null);
545+
}
546+
547+
return new JObject
548+
{
549+
["logs"] = logsArray,
550+
["_totalCount"] = totalCount,
551+
["_filteredCount"] = filteredCount,
552+
["_matchedCount"] = matchedCount,
553+
["_returnedCount"] = logsArray.Count,
554+
["success"] = true
555+
};
556+
}
557+
365558
#if MCP_UNITY_DEBUG_MODE_VALUES
366559
/// <summary>
367560
/// Debug method to write mode values to file for analysis

Editor/Services/IConsoleLogsService.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,19 @@ public interface IConsoleLogsService
4141
/// </summary>
4242
/// <returns>Number of stored log entries</returns>
4343
int GetLogCount();
44+
45+
/// <summary>
46+
/// Search logs with keyword or regex pattern
47+
/// </summary>
48+
/// <param name="keyword">Keyword to search for (partial match)</param>
49+
/// <param name="regex">Regular expression pattern (overrides keyword if provided)</param>
50+
/// <param name="logType">Filter by log type (empty for all)</param>
51+
/// <param name="includeStackTrace">Whether to include stack trace in search (default: true)</param>
52+
/// <param name="caseSensitive">Whether the search is case sensitive (default: false)</param>
53+
/// <param name="offset">Starting index (0-based)</param>
54+
/// <param name="limit">Maximum number of logs to return (default: 50)</param>
55+
/// <returns>JObject containing matching logs array and pagination info</returns>
56+
JObject SearchLogsAsJson(string keyword = null, string regex = null, string logType = null,
57+
bool includeStackTrace = true, bool caseSensitive = false, int offset = 0, int limit = 50);
4458
}
4559
}

0 commit comments

Comments
 (0)