From 664bd568acf536723729fda1b951052013ba53c9 Mon Sep 17 00:00:00 2001 From: dsarno Date: Sat, 3 Jan 2026 20:44:12 -0800 Subject: [PATCH 1/7] Optimize read_console defaults and paging --- MCPForUnity/Editor/Tools/ReadConsole.cs | 70 ++++++++++++++++--- Server/src/services/tools/read_console.py | 18 +++-- .../integration/test_read_console_truncate.py | 2 +- 3 files changed, 76 insertions(+), 14 deletions(-) diff --git a/MCPForUnity/Editor/Tools/ReadConsole.cs b/MCPForUnity/Editor/Tools/ReadConsole.cs index 8bdd3b9da..8f15aaada 100644 --- a/MCPForUnity/Editor/Tools/ReadConsole.cs +++ b/MCPForUnity/Editor/Tools/ReadConsole.cs @@ -165,13 +165,17 @@ public static object HandleCommand(JObject @params) // Extract parameters for 'get' var types = (@params["types"] as JArray)?.Select(t => t.ToString().ToLower()).ToList() - ?? new List { "error", "warning", "log" }; + ?? new List { "error", "warning" }; int? count = @params["count"]?.ToObject(); + int? pageSize = + @params["pageSize"]?.ToObject() + ?? @params["page_size"]?.ToObject(); + int? cursor = @params["cursor"]?.ToObject(); string filterText = @params["filterText"]?.ToString(); string sinceTimestampStr = @params["sinceTimestamp"]?.ToString(); // TODO: Implement timestamp filtering - string format = (@params["format"]?.ToString() ?? "detailed").ToLower(); + string format = (@params["format"]?.ToString() ?? "plain").ToLower(); bool includeStacktrace = - @params["includeStacktrace"]?.ToObject() ?? true; + @params["includeStacktrace"]?.ToObject() ?? false; if (types.Contains("all")) { @@ -186,7 +190,15 @@ public static object HandleCommand(JObject @params) // Need a way to get timestamp per log entry. } - return GetConsoleEntries(types, count, filterText, format, includeStacktrace); + return GetConsoleEntries( + types, + count, + pageSize, + cursor, + filterText, + format, + includeStacktrace + ); } else { @@ -221,6 +233,8 @@ private static object ClearConsole() private static object GetConsoleEntries( List types, int? count, + int? pageSize, + int? cursor, string filterText, string format, bool includeStacktrace @@ -228,6 +242,11 @@ bool includeStacktrace { List formattedEntries = new List(); int retrievedCount = 0; + int totalMatches = 0; + bool usePaging = pageSize.HasValue || cursor.HasValue; + int resolvedPageSize = Mathf.Clamp(pageSize ?? count ?? 50, 1, 500); + int resolvedCursor = Mathf.Max(0, cursor ?? 0); + int pageEndExclusive = resolvedCursor + resolvedPageSize; try { @@ -338,13 +357,26 @@ bool includeStacktrace break; } - formattedEntries.Add(formattedEntry); - retrievedCount++; + totalMatches++; - // Apply count limit (after filtering) - if (count.HasValue && retrievedCount >= count.Value) + if (usePaging) + { + if (totalMatches > resolvedCursor && totalMatches <= pageEndExclusive) + { + formattedEntries.Add(formattedEntry); + retrievedCount++; + } + } + else { - break; + formattedEntries.Add(formattedEntry); + retrievedCount++; + + // Apply count limit (after filtering) + if (count.HasValue && retrievedCount >= count.Value) + { + break; + } } } } @@ -375,6 +407,26 @@ bool includeStacktrace } } + if (usePaging) + { + bool truncated = totalMatches > pageEndExclusive; + string nextCursor = truncated ? pageEndExclusive.ToString() : null; + var payload = new + { + cursor = resolvedCursor, + pageSize = resolvedPageSize, + next_cursor = nextCursor, + truncated = truncated, + total = totalMatches, + items = formattedEntries, + }; + + return new SuccessResponse( + $"Retrieved {formattedEntries.Count} log entries.", + payload + ); + } + // Return the filtered and formatted list (might be empty) return new SuccessResponse( $"Retrieved {formattedEntries.Count} log entries.", diff --git a/Server/src/services/tools/read_console.py b/Server/src/services/tools/read_console.py index dacaeb9e9..c2f6fe78c 100644 --- a/Server/src/services/tools/read_console.py +++ b/Server/src/services/tools/read_console.py @@ -25,6 +25,8 @@ async def read_console( filter_text: Annotated[str, "Text filter for messages"] | None = None, since_timestamp: Annotated[str, "Get messages after this timestamp (ISO 8601)"] | None = None, + page_size: Annotated[int | str, "Page size for paginated console reads."] | None = None, + cursor: Annotated[int | str, "Opaque cursor for paging (offset)."] | None = None, format: Annotated[Literal['plain', 'detailed', 'json'], "Output format"] | None = None, include_stacktrace: Annotated[bool | str, @@ -35,11 +37,13 @@ async def read_console( unity_instance = get_unity_instance_from_context(ctx) # Set defaults if values are None action = action if action is not None else 'get' - types = types if types is not None else ['error', 'warning', 'log'] - format = format if format is not None else 'detailed' + types = types if types is not None else ['error', 'warning'] + format = format if format is not None else 'plain' # Coerce booleans defensively (strings like 'true'/'false') - include_stacktrace = coerce_bool(include_stacktrace, default=True) + include_stacktrace = coerce_bool(include_stacktrace, default=False) + coerced_page_size = coerce_int(page_size, default=None) + coerced_cursor = coerce_int(cursor, default=None) # Normalize action if it's a string if isinstance(action, str): @@ -56,7 +60,7 @@ async def read_console( count = coerce_int(count) if action == "get" and count is None: - count = 200 + count = 50 # Prepare parameters for the C# handler params_dict = { @@ -65,6 +69,8 @@ async def read_console( "count": count, "filterText": filter_text, "sinceTimestamp": since_timestamp, + "pageSize": coerced_page_size, + "cursor": coerced_cursor, "format": format.lower() if isinstance(format, str) else format, "includeStacktrace": include_stacktrace } @@ -88,6 +94,10 @@ async def read_console( for line in data["lines"]: if isinstance(line, dict) and "stacktrace" in line: line.pop("stacktrace", None) + elif isinstance(data, dict) and "items" in data and isinstance(data["items"], list): + for line in data["items"]: + if isinstance(line, dict) and "stacktrace" in line: + line.pop("stacktrace", None) # Handle legacy/direct list format if any elif isinstance(data, list): for line in data: diff --git a/Server/tests/integration/test_read_console_truncate.py b/Server/tests/integration/test_read_console_truncate.py index 17e973869..8fed2f775 100644 --- a/Server/tests/integration/test_read_console_truncate.py +++ b/Server/tests/integration/test_read_console_truncate.py @@ -57,7 +57,7 @@ async def fake_send(cmd, params, **kwargs): "data": {"lines": [{"level": "error", "message": "oops", "stacktrace": "trace", "time": "t"}]}, } assert captured["params"]["count"] == 10 - assert captured["params"]["includeStacktrace"] is True + assert captured["params"]["includeStacktrace"] is False @pytest.mark.asyncio From 7be1e3a984b109d086a3cc9b0964fa20543648d8 Mon Sep 17 00:00:00 2001 From: dsarno Date: Sat, 3 Jan 2026 20:57:17 -0800 Subject: [PATCH 2/7] Fix read_console truncate test expectations --- Server/tests/integration/test_read_console_truncate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/tests/integration/test_read_console_truncate.py b/Server/tests/integration/test_read_console_truncate.py index 8fed2f775..34e18b70a 100644 --- a/Server/tests/integration/test_read_console_truncate.py +++ b/Server/tests/integration/test_read_console_truncate.py @@ -54,7 +54,7 @@ async def fake_send(cmd, params, **kwargs): resp = await read_console(ctx=DummyContext(), action="get", count=10) assert resp == { "success": True, - "data": {"lines": [{"level": "error", "message": "oops", "stacktrace": "trace", "time": "t"}]}, + "data": {"lines": [{"level": "error", "message": "oops", "time": "t"}]}, } assert captured["params"]["count"] == 10 assert captured["params"]["includeStacktrace"] is False From 03ea5d1e4bb5c23eaae50767ab15f9ada7000c4f Mon Sep 17 00:00:00 2001 From: dsarno Date: Sun, 4 Jan 2026 12:43:30 -0800 Subject: [PATCH 3/7] Reduce read_console default count from 50 to 10 Further optimize token usage by reducing the default count from 50 to 10 entries. Even 10-20 messages with stack traces can be token-heavy. Added tests for default behavior and paging functionality. Updated tool description to document defaults and paging support. --- Server/src/services/tools/read_console.py | 4 +- .../integration/test_read_console_truncate.py | 89 +++++++++++++++++++ Server/uv.lock | 2 +- 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/Server/src/services/tools/read_console.py b/Server/src/services/tools/read_console.py index c2f6fe78c..10f9ce715 100644 --- a/Server/src/services/tools/read_console.py +++ b/Server/src/services/tools/read_console.py @@ -12,7 +12,7 @@ @mcp_for_unity_tool( - description="Gets messages from or clears the Unity Editor console. Note: For maximum client compatibility, pass count as a quoted string (e.g., '5')." + description="Gets messages from or clears the Unity Editor console. Defaults to 10 most recent entries. Use page_size/cursor for paging. Note: For maximum client compatibility, pass count as a quoted string (e.g., '5')." ) async def read_console( ctx: Context, @@ -60,7 +60,7 @@ async def read_console( count = coerce_int(count) if action == "get" and count is None: - count = 50 + count = 10 # Prepare parameters for the C# handler params_dict = { diff --git a/Server/tests/integration/test_read_console_truncate.py b/Server/tests/integration/test_read_console_truncate.py index 34e18b70a..fa77b6cdd 100644 --- a/Server/tests/integration/test_read_console_truncate.py +++ b/Server/tests/integration/test_read_console_truncate.py @@ -86,3 +86,92 @@ async def fake_send(cmd, params, **kwargs): assert resp == {"success": True, "data": { "lines": [{"level": "error", "message": "oops"}]}} assert captured["params"]["includeStacktrace"] is False + + +@pytest.mark.asyncio +async def test_read_console_default_count(monkeypatch): + """Test that read_console defaults to count=10 when not specified.""" + tools = setup_tools() + read_console = tools["read_console"] + + captured = {} + + async def fake_send(cmd, params, **kwargs): + captured["params"] = params + return { + "success": True, + "data": {"lines": [{"level": "error", "message": f"error {i}"} for i in range(15)]}, + } + + # Patch the send_command_with_retry function in the tools module + import services.tools.read_console + monkeypatch.setattr( + services.tools.read_console, + "async_send_command_with_retry", + fake_send, + ) + + # Call without specifying count - should default to 10 + resp = await read_console(ctx=DummyContext(), action="get") + assert resp["success"] is True + # Verify that the default count of 10 was used + assert captured["params"]["count"] == 10 + + +@pytest.mark.asyncio +async def test_read_console_paging(monkeypatch): + """Test that read_console paging works with page_size and cursor.""" + tools = setup_tools() + read_console = tools["read_console"] + + captured = {} + + async def fake_send(cmd, params, **kwargs): + captured["params"] = params + # Simulate Unity returning paging info + page_size = params.get("pageSize", 10) + cursor = params.get("cursor", 0) + # Simulate 25 total messages + all_messages = [{"level": "error", "message": f"error {i}"} for i in range(25)] + + # Return a page of results + start = cursor + end = min(start + page_size, len(all_messages)) + messages = all_messages[start:end] + + return { + "success": True, + "data": { + "lines": messages, + "cursor": end if end < len(all_messages) else None, + "hasMore": end < len(all_messages) + }, + } + + # Patch the send_command_with_retry function in the tools module + import services.tools.read_console + monkeypatch.setattr( + services.tools.read_console, + "async_send_command_with_retry", + fake_send, + ) + + # First page - get first 5 entries + resp = await read_console(ctx=DummyContext(), action="get", page_size=5, cursor=0) + assert resp["success"] is True + assert captured["params"]["pageSize"] == 5 + assert captured["params"]["cursor"] == 0 + assert len(resp["data"]["lines"]) == 5 + assert resp["data"]["hasMore"] is True + + # Second page - get next 5 entries + resp = await read_console(ctx=DummyContext(), action="get", page_size=5, cursor=5) + assert resp["success"] is True + assert captured["params"]["cursor"] == 5 + assert len(resp["data"]["lines"]) == 5 + + # Last page - get remaining entries + resp = await read_console(ctx=DummyContext(), action="get", page_size=5, cursor=20) + assert resp["success"] is True + assert len(resp["data"]["lines"]) == 5 + assert resp["data"]["hasMore"] is False diff --git a/Server/uv.lock b/Server/uv.lock index cae446ff9..9cc8cd0ba 100644 --- a/Server/uv.lock +++ b/Server/uv.lock @@ -809,7 +809,7 @@ wheels = [ [[package]] name = "mcpforunityserver" -version = "8.6.0" +version = "8.7.0" source = { editable = "." } dependencies = [ { name = "fastapi" }, From 7bd3976092f4869d66545bae76f6d7a0d601e3ff Mon Sep 17 00:00:00 2001 From: dsarno Date: Sun, 4 Jan 2026 12:49:29 -0800 Subject: [PATCH 4/7] Fix ReadConsoleTests to include log type messages The default types filter changed to ['error', 'warning'] (excluding 'log'), so tests using Debug.Log() need to explicitly request log messages. Also added format='detailed' to HandleCommand_Get_Works test since it accesses structured message fields. --- .../Assets/Tests/EditMode/Tools/ReadConsoleTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs index 1274ed1c1..a791b38d4 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs @@ -19,7 +19,7 @@ public void HandleCommand_Clear_Works() Debug.Log("Log to clear"); // Verify content exists before clear - var getBefore = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "get", ["count"] = 10 })); + var getBefore = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "get", ["types"] = new JArray { "error", "warning", "log" }, ["count"] = 10 })); Assert.IsTrue(getBefore.Value("success"), getBefore.ToString()); var entriesBefore = getBefore["data"] as JArray; @@ -35,7 +35,7 @@ public void HandleCommand_Clear_Works() Assert.IsTrue(result.Value("success"), result.ToString()); // Verify clear effect - var getAfter = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "get", ["count"] = 10 })); + var getAfter = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "get", ["types"] = new JArray { "error", "warning", "log" }, ["count"] = 10 })); Assert.IsTrue(getAfter.Value("success"), getAfter.ToString()); var entriesAfter = getAfter["data"] as JArray; Assert.IsTrue(entriesAfter == null || entriesAfter.Count == 0, "Console should be empty after clear."); @@ -51,6 +51,8 @@ public void HandleCommand_Get_Works() var paramsObj = new JObject { ["action"] = "get", + ["types"] = new JArray { "error", "warning", "log" }, + ["format"] = "detailed", ["count"] = 1000 // Fetch enough to likely catch our message }; @@ -88,4 +90,3 @@ private static JObject ToJObject(object result) } } } - From a4bc5aae79febce95f71fc5e3fa9cd022aae5661 Mon Sep 17 00:00:00 2001 From: dsarno Date: Sun, 4 Jan 2026 13:10:51 -0800 Subject: [PATCH 5/7] Address CodeRabbit review feedback - Fix property naming consistency: next_cursor -> nextCursor (C# camelCase) - Remove redundant EndGettingEntries call from catch block (already in finally) - Extract stacktrace stripping to helper function (reduce duplication) - Fix test mock to match actual C# response structure (items, nextCursor, truncated, total) --- MCPForUnity/Editor/Tools/ReadConsole.cs | 11 ++------ Server/src/services/tools/read_console.py | 26 +++++++++---------- .../integration/test_read_console_truncate.py | 26 ++++++++++++------- 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/MCPForUnity/Editor/Tools/ReadConsole.cs b/MCPForUnity/Editor/Tools/ReadConsole.cs index 8f15aaada..a58e053a1 100644 --- a/MCPForUnity/Editor/Tools/ReadConsole.cs +++ b/MCPForUnity/Editor/Tools/ReadConsole.cs @@ -383,14 +383,7 @@ bool includeStacktrace catch (Exception e) { Debug.LogError($"[ReadConsole] Error while retrieving log entries: {e}"); - // Ensure EndGettingEntries is called even if there's an error during iteration - try - { - _endGettingEntriesMethod.Invoke(null, null); - } - catch - { /* Ignore nested exception */ - } + // EndGettingEntries will be called in the finally block return new ErrorResponse($"Error retrieving log entries: {e.Message}"); } finally @@ -415,7 +408,7 @@ bool includeStacktrace { cursor = resolvedCursor, pageSize = resolvedPageSize, - next_cursor = nextCursor, + nextCursor = nextCursor, truncated = truncated, total = totalMatches, items = formattedEntries, diff --git a/Server/src/services/tools/read_console.py b/Server/src/services/tools/read_console.py index 10f9ce715..713d380fa 100644 --- a/Server/src/services/tools/read_console.py +++ b/Server/src/services/tools/read_console.py @@ -11,6 +11,13 @@ from transport.legacy.unity_connection import async_send_command_with_retry +def _strip_stacktrace_from_list(items: list) -> None: + """Remove stacktrace fields from a list of log entries.""" + for item in items: + if isinstance(item, dict) and "stacktrace" in item: + item.pop("stacktrace", None) + + @mcp_for_unity_tool( description="Gets messages from or clears the Unity Editor console. Defaults to 10 most recent entries. Use page_size/cursor for paging. Note: For maximum client compatibility, pass count as a quoted string (e.g., '5')." ) @@ -89,20 +96,13 @@ async def read_console( # Strip stacktrace fields from returned lines if present try: data = resp.get("data") - # Handle standard format: {"data": {"lines": [...]}} - if isinstance(data, dict) and "lines" in data and isinstance(data["lines"], list): - for line in data["lines"]: - if isinstance(line, dict) and "stacktrace" in line: - line.pop("stacktrace", None) - elif isinstance(data, dict) and "items" in data and isinstance(data["items"], list): - for line in data["items"]: - if isinstance(line, dict) and "stacktrace" in line: - line.pop("stacktrace", None) - # Handle legacy/direct list format if any + if isinstance(data, dict): + for key in ("lines", "items"): + if key in data and isinstance(data[key], list): + _strip_stacktrace_from_list(data[key]) + break elif isinstance(data, list): - for line in data: - if isinstance(line, dict) and "stacktrace" in line: - line.pop("stacktrace", None) + _strip_stacktrace_from_list(data) except Exception: pass return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} diff --git a/Server/tests/integration/test_read_console_truncate.py b/Server/tests/integration/test_read_console_truncate.py index fa77b6cdd..5eb0e1713 100644 --- a/Server/tests/integration/test_read_console_truncate.py +++ b/Server/tests/integration/test_read_console_truncate.py @@ -128,7 +128,7 @@ async def test_read_console_paging(monkeypatch): async def fake_send(cmd, params, **kwargs): captured["params"] = params - # Simulate Unity returning paging info + # Simulate Unity returning paging info matching C# structure page_size = params.get("pageSize", 10) cursor = params.get("cursor", 0) # Simulate 25 total messages @@ -142,9 +142,12 @@ async def fake_send(cmd, params, **kwargs): return { "success": True, "data": { - "lines": messages, - "cursor": end if end < len(all_messages) else None, - "hasMore": end < len(all_messages) + "items": messages, + "cursor": cursor, + "pageSize": page_size, + "nextCursor": str(end) if end < len(all_messages) else None, + "truncated": end < len(all_messages), + "total": len(all_messages), }, } @@ -161,17 +164,22 @@ async def fake_send(cmd, params, **kwargs): assert resp["success"] is True assert captured["params"]["pageSize"] == 5 assert captured["params"]["cursor"] == 0 - assert len(resp["data"]["lines"]) == 5 - assert resp["data"]["hasMore"] is True + assert len(resp["data"]["items"]) == 5 + assert resp["data"]["truncated"] is True + assert resp["data"]["nextCursor"] == "5" + assert resp["data"]["total"] == 25 # Second page - get next 5 entries resp = await read_console(ctx=DummyContext(), action="get", page_size=5, cursor=5) assert resp["success"] is True assert captured["params"]["cursor"] == 5 - assert len(resp["data"]["lines"]) == 5 + assert len(resp["data"]["items"]) == 5 + assert resp["data"]["truncated"] is True + assert resp["data"]["nextCursor"] == "10" # Last page - get remaining entries resp = await read_console(ctx=DummyContext(), action="get", page_size=5, cursor=20) assert resp["success"] is True - assert len(resp["data"]["lines"]) == 5 - assert resp["data"]["hasMore"] is False + assert len(resp["data"]["items"]) == 5 + assert resp["data"]["truncated"] is False + assert resp["data"]["nextCursor"] is None From 66c82f696606cccd6b1ede0249a8bc9bc1d4e075 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Sun, 4 Jan 2026 14:30:29 -0800 Subject: [PATCH 6/7] perf: add early exit optimization for ReadConsole paging - Add early exit in paging loop once page is filled, avoiding iteration through remaining console entries (total becomes 'at least N') - Prefix unused mock arguments with underscores in test_read_console_truncate.py to suppress Ruff linter warnings --- MCPForUnity/Editor/Tools/ReadConsole.cs | 6 ++++++ Server/tests/integration/test_read_console_truncate.py | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/MCPForUnity/Editor/Tools/ReadConsole.cs b/MCPForUnity/Editor/Tools/ReadConsole.cs index a58e053a1..ff320bd79 100644 --- a/MCPForUnity/Editor/Tools/ReadConsole.cs +++ b/MCPForUnity/Editor/Tools/ReadConsole.cs @@ -366,6 +366,12 @@ bool includeStacktrace formattedEntries.Add(formattedEntry); retrievedCount++; } + // Early exit: we've filled the page and only need to check if more exist + else if (totalMatches > pageEndExclusive) + { + // We've passed the page; totalMatches now indicates truncation + break; + } } else { diff --git a/Server/tests/integration/test_read_console_truncate.py b/Server/tests/integration/test_read_console_truncate.py index 5eb0e1713..86fcd6a25 100644 --- a/Server/tests/integration/test_read_console_truncate.py +++ b/Server/tests/integration/test_read_console_truncate.py @@ -36,7 +36,7 @@ async def test_read_console_full_default(monkeypatch): captured = {} - async def fake_send(cmd, params, **kwargs): + async def fake_send(_cmd, params, **_kwargs): captured["params"] = params return { "success": True, @@ -67,7 +67,7 @@ async def test_read_console_truncated(monkeypatch): captured = {} - async def fake_send(cmd, params, **kwargs): + async def fake_send(_cmd, params, **_kwargs): captured["params"] = params return { "success": True, @@ -96,7 +96,7 @@ async def test_read_console_default_count(monkeypatch): captured = {} - async def fake_send(cmd, params, **kwargs): + async def fake_send(_cmd, params, **_kwargs): captured["params"] = params return { "success": True, @@ -126,7 +126,7 @@ async def test_read_console_paging(monkeypatch): captured = {} - async def fake_send(cmd, params, **kwargs): + async def fake_send(_cmd, params, **_kwargs): captured["params"] = params # Simulate Unity returning paging info matching C# structure page_size = params.get("pageSize", 10) From 0ef35364287d97a842b531ea4a123aed17dd51e0 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Sun, 4 Jan 2026 14:43:10 -0800 Subject: [PATCH 7/7] refactor: give pageSize independent default, clarify count semantics - Change pageSize resolution from 'pageSize ?? count ?? 50' to 'pageSize ?? 50' so pageSize has its own default independent of count - count now only serves as the non-paging limit - Add XML docs to GetConsoleEntries with clear parameter descriptions - Update Python tool annotations to document pageSize default (50) and clarify that count is ignored when paging --- MCPForUnity/Editor/Tools/ReadConsole.cs | 14 +++++++++++++- Server/src/services/tools/read_console.py | 6 +++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/MCPForUnity/Editor/Tools/ReadConsole.cs b/MCPForUnity/Editor/Tools/ReadConsole.cs index ff320bd79..9b45f08ba 100644 --- a/MCPForUnity/Editor/Tools/ReadConsole.cs +++ b/MCPForUnity/Editor/Tools/ReadConsole.cs @@ -230,6 +230,17 @@ private static object ClearConsole() } } + /// + /// Retrieves console log entries with optional filtering and paging. + /// + /// Log types to include (e.g., "error", "warning", "log"). + /// Maximum entries to return in non-paging mode. Ignored when paging is active. + /// Number of entries per page. Defaults to 50 when omitted. + /// Starting index for paging (0-based). Defaults to 0. + /// Optional text filter (case-insensitive substring match). + /// Output format: "plain", "detailed", or "json". + /// Whether to include stack traces in the output. + /// A success response with entries, or an error response. private static object GetConsoleEntries( List types, int? count, @@ -244,7 +255,8 @@ bool includeStacktrace int retrievedCount = 0; int totalMatches = 0; bool usePaging = pageSize.HasValue || cursor.HasValue; - int resolvedPageSize = Mathf.Clamp(pageSize ?? count ?? 50, 1, 500); + // pageSize defaults to 50 when omitted; count is the overall non-paging limit only + int resolvedPageSize = Mathf.Clamp(pageSize ?? 50, 1, 500); int resolvedCursor = Mathf.Max(0, cursor ?? 0); int pageEndExclusive = resolvedCursor + resolvedPageSize; diff --git a/Server/src/services/tools/read_console.py b/Server/src/services/tools/read_console.py index 713d380fa..873a7b86e 100644 --- a/Server/src/services/tools/read_console.py +++ b/Server/src/services/tools/read_console.py @@ -28,12 +28,12 @@ async def read_console( types: Annotated[list[Literal['error', 'warning', 'log', 'all']], "Message types to get"] | None = None, count: Annotated[int | str, - "Max messages to return (accepts int or string, e.g., 5 or '5')"] | None = None, + "Max messages to return in non-paging mode (accepts int or string, e.g., 5 or '5'). Ignored when paging with page_size/cursor."] | None = None, filter_text: Annotated[str, "Text filter for messages"] | None = None, since_timestamp: Annotated[str, "Get messages after this timestamp (ISO 8601)"] | None = None, - page_size: Annotated[int | str, "Page size for paginated console reads."] | None = None, - cursor: Annotated[int | str, "Opaque cursor for paging (offset)."] | None = None, + page_size: Annotated[int | str, "Page size for paginated console reads. Defaults to 50 when omitted."] | None = None, + cursor: Annotated[int | str, "Opaque cursor for paging (0-based offset). Defaults to 0."] | None = None, format: Annotated[Literal['plain', 'detailed', 'json'], "Output format"] | None = None, include_stacktrace: Annotated[bool | str,