diff --git a/MCPForUnity/Editor/Helpers/McpLog.cs b/MCPForUnity/Editor/Helpers/McpLog.cs index ca9f77131..138a2bf26 100644 --- a/MCPForUnity/Editor/Helpers/McpLog.cs +++ b/MCPForUnity/Editor/Helpers/McpLog.cs @@ -1,3 +1,4 @@ +using System; using MCPForUnity.Editor.Constants; using UnityEditor; using UnityEngine; @@ -11,6 +12,15 @@ internal static class McpLog private const string WarnPrefix = "MCP-FOR-UNITY:"; private const string ErrorPrefix = "MCP-FOR-UNITY:"; + internal static bool IsMcpLogMessage(string message) + { + return message.StartsWith(InfoPrefix, StringComparison.Ordinal) + || message.StartsWith(DebugPrefix, StringComparison.Ordinal) + || message.StartsWith(WarnPrefix, StringComparison.Ordinal) + || message.StartsWith(ErrorPrefix, StringComparison.Ordinal) + || message.StartsWith("MCP-FOR-UNITY:", StringComparison.Ordinal); + } + private static volatile bool _debugEnabled = ReadDebugPreference(); private static bool IsDebugEnabled() => _debugEnabled; diff --git a/MCPForUnity/Editor/Tools/ReadConsole.cs b/MCPForUnity/Editor/Tools/ReadConsole.cs index b4abb5882..a2ab1ae88 100644 --- a/MCPForUnity/Editor/Tools/ReadConsole.cs +++ b/MCPForUnity/Editor/Tools/ReadConsole.cs @@ -192,6 +192,7 @@ public static object HandleCommand(JObject @params) string filterText = p.Get("filterText"); string format = p.Get("format", "plain").ToLower(); bool includeStacktrace = p.GetBool("includeStacktrace", false); + bool includeMcpLogs = p.GetBool("includeMcpLogs", true); if (types.Contains("all")) { @@ -205,7 +206,8 @@ public static object HandleCommand(JObject @params) cursor, filterText, format, - includeStacktrace + includeStacktrace, + includeMcpLogs ); } else @@ -349,7 +351,8 @@ private static object GetConsoleEntries( int? cursor, string filterText, string format, - bool includeStacktrace + bool includeStacktrace, + bool includeMcpLogs ) { List formattedEntries = new List(); @@ -409,6 +412,11 @@ bool includeStacktrace continue; // Skip empty messages } + if (!includeMcpLogs && McpLog.IsMcpLogMessage(message)) + { + continue; + } + // (Calibration removed) // --- Filtering --- diff --git a/Server/src/cli/commands/editor.py b/Server/src/cli/commands/editor.py index 5b0dce795..78d5ed6bb 100644 --- a/Server/src/cli/commands/editor.py +++ b/Server/src/cli/commands/editor.py @@ -76,13 +76,18 @@ def stop(): is_flag=True, help="Include stack traces." ) +@click.option( + "--mcp-logs/--no-mcp-logs", + default=True, + help="Include MCP for Unity's own diagnostics (default: included)." +) @click.option( "--clear", is_flag=True, help="Clear the console instead of reading." ) @handle_unity_errors -def console(log_types: tuple, count: int, filter_text: Optional[str], stacktrace: bool, clear: bool): +def console(log_types: tuple, count: int, filter_text: Optional[str], stacktrace: bool, mcp_logs: bool, clear: bool): """Read or clear the Unity console. \b @@ -90,6 +95,7 @@ def console(log_types: tuple, count: int, filter_text: Optional[str], stacktrace unity-mcp editor console unity-mcp editor console --type error --count 20 unity-mcp editor console --filter "NullReference" --stacktrace + unity-mcp editor console --type error --no-mcp-logs unity-mcp editor console --clear """ config = get_config() @@ -106,6 +112,7 @@ def console(log_types: tuple, count: int, filter_text: Optional[str], stacktrace "types": list(log_types), "count": count, "include_stacktrace": stacktrace, + "include_mcp_logs": mcp_logs, } if filter_text: diff --git a/Server/src/services/tools/read_console.py b/Server/src/services/tools/read_console.py index 6a642dcec..5af71406e 100644 --- a/Server/src/services/tools/read_console.py +++ b/Server/src/services/tools/read_console.py @@ -50,6 +50,8 @@ async def read_console( 'json'], "Output format"] | None = None, include_stacktrace: Annotated[bool | str, "Include stack traces in output (accepts true/false or 'true'/'false')"] | None = None, + include_mcp_logs: Annotated[bool | str, + "Include MCP for Unity's own diagnostics (default true). Set false to exclude them before count and paging limits."] | None = None, ) -> dict[str, Any]: # Get active instance from session state # Removed session_state import @@ -96,6 +98,7 @@ async def read_console( # Coerce booleans defensively (strings like 'true'/'false') include_stacktrace = coerce_bool(include_stacktrace, default=False) + include_mcp_logs = coerce_bool(include_mcp_logs, default=True) coerced_page_size = coerce_int(page_size, default=None) coerced_cursor = coerce_int(cursor, default=None) @@ -125,7 +128,8 @@ async def read_console( "pageSize": coerced_page_size, "cursor": coerced_cursor, "format": format.lower() if isinstance(format, str) else format, - "includeStacktrace": include_stacktrace + "includeStacktrace": include_stacktrace, + "includeMcpLogs": include_mcp_logs } # Remove None values unless it's 'count' (as None might mean 'all') diff --git a/Server/tests/integration/test_read_console_truncate.py b/Server/tests/integration/test_read_console_truncate.py index d7974004b..c84a092a5 100644 --- a/Server/tests/integration/test_read_console_truncate.py +++ b/Server/tests/integration/test_read_console_truncate.py @@ -15,6 +15,26 @@ def setup_console_tools(): return mcp.tools +@pytest.mark.asyncio +@pytest.mark.parametrize("value, expected", [(None, True), (True, True), (False, False), ("true", True), ("false", False)]) +async def test_read_console_include_mcp_logs(monkeypatch, value, expected): + read_console = setup_console_tools()["read_console"] + captured = {} + + async def fake_send(_cmd, params, **_kwargs): + captured.update(params) + return {"success": True, "data": []} + + import services.tools.read_console as read_console_mod + monkeypatch.setattr(read_console_mod, "async_send_command_with_retry", fake_send) + + kwargs = {} if value is None else {"include_mcp_logs": value} + result = await read_console(ctx=DummyContext(), **kwargs) + + assert result["success"] + assert captured["includeMcpLogs"] is expected + + @pytest.mark.asyncio async def test_read_console_full_default(monkeypatch): tools = setup_console_tools() @@ -245,4 +265,4 @@ async def fake_send_with_unity_instance(_send_fn, _unity_instance, _command_type resp = await read_console(ctx=DummyContext(), action="get", types='[1, "error"]') assert resp["success"] is False assert "types entries must be strings" in resp["message"] - assert captured == {} \ No newline at end of file + assert captured == {} diff --git a/Server/tests/test_cli.py b/Server/tests/test_cli.py index 689c4d4a9..4a451207a 100644 --- a/Server/tests/test_cli.py +++ b/Server/tests/test_cli.py @@ -600,6 +600,13 @@ def test_editor_console_clear(self, runner, mock_unity_response): result = runner.invoke(cli, ["editor", "console", "--clear"]) assert result.exit_code == 0 + @pytest.mark.parametrize("options, expected", [([], True), (["--no-mcp-logs"], False), (["--mcp-logs"], True)]) + def test_editor_console_mcp_logs(self, runner, mock_unity_response, options, expected): + with patch("cli.commands.editor.run_command", return_value=mock_unity_response) as command: + result = runner.invoke(cli, ["editor", "console", *options]) + assert result.exit_code == 0, result.output + assert command.call_args.args[1]["include_mcp_logs"] is expected + def test_editor_add_tag(self, runner, mock_unity_response): """Test editor add-tag command.""" with patch("cli.commands.editor.run_command", return_value=mock_unity_response): diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs index 7ba0fbc2c..c471b6fb5 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs @@ -5,12 +5,126 @@ using UnityEditor; using UnityEngine; using MCPForUnity.Editor.Tools; +using MCPForUnity.Editor.Helpers; using static MCPForUnityTests.Editor.TestUtilities; namespace MCPForUnityTests.Editor.Tools { public class ReadConsoleTests { + [TestCase("plain")] + [TestCase("detailed")] + public void HandleCommand_Get_ExcludesMcpLogsBeforeCount(string format) + { + string id = Guid.NewGuid().ToString(); + McpLog.Warn($"Transport diagnostic {id}"); + Debug.LogWarning($"Project warning {id}"); + + var result = ToJObject(ReadConsole.HandleCommand(new JObject + { + ["types"] = new JArray { "warning" }, + ["filterText"] = id, + ["format"] = format, + ["count"] = 1, + ["includeMcpLogs"] = false + })); + + Assert.IsTrue(result.Value("success"), result.ToString()); + var entries = (JArray)result["data"]; + Assert.AreEqual(1, entries.Count); + string message = format == "plain" ? entries[0].ToString() : entries[0].Value("message"); + Assert.AreEqual($"Project warning {id}", message); + } + + [Test] + public void HandleCommand_Get_ExcludesMcpLogsBeforePaging() + { + string id = Guid.NewGuid().ToString(); + McpLog.Info($"Transport diagnostic {id}"); + Debug.Log($"First project log {id}"); + McpLog.Warn($"Transport warning {id}"); + Debug.Log($"Second project log {id}"); + + var parameters = new JObject + { + ["types"] = new JArray { "all" }, + ["filterText"] = id, + ["format"] = "plain", + ["pageSize"] = 1, + ["include_mcp_logs"] = false + }; + var first = ToJObject(ReadConsole.HandleCommand(parameters)); + Assert.IsTrue(first.Value("success"), first.ToString()); + Assert.AreEqual($"First project log {id}", first["data"]["items"][0].ToString()); + Assert.AreEqual("1", first["data"].Value("nextCursor")); + + parameters["cursor"] = 1; + var second = ToJObject(ReadConsole.HandleCommand(parameters)); + Assert.IsTrue(second.Value("success"), second.ToString()); + Assert.AreEqual($"Second project log {id}", second["data"]["items"][0].ToString()); + Assert.IsFalse(second["data"].Value("truncated")); + Assert.AreEqual(JTokenType.Null, second["data"]["nextCursor"].Type); + } + + [TestCase(false)] + [TestCase(true)] + public void HandleCommand_Get_IncludesMcpLogsByDefaultAndWhenRequested(bool explicitOption) + { + string id = Guid.NewGuid().ToString(); + McpLog.Warn($"Transport diagnostic {id}"); + var parameters = new JObject + { + ["types"] = new JArray { "warning" }, + ["filterText"] = id, + ["format"] = "plain" + }; + if (explicitOption) parameters["includeMcpLogs"] = true; + + var result = ToJObject(ReadConsole.HandleCommand(parameters)); + Assert.IsTrue(result.Value("success"), result.ToString()); + Assert.AreEqual(1, ((JArray)result["data"]).Count); + StringAssert.Contains($"Transport diagnostic {id}", result["data"][0].ToString()); + } + + [Test] + public void HandleCommand_Get_KeepsProjectMessagesMentioningMcp() + { + string id = Guid.NewGuid().ToString(); + string message = $"Project warning {id}: MCP-FOR-UNITY: connection unavailable"; + Debug.LogWarning(message); + var result = ToJObject(ReadConsole.HandleCommand(new JObject + { + ["types"] = new JArray { "warning" }, + ["filterText"] = id, + ["format"] = "plain", + ["includeMcpLogs"] = false + })); + + Assert.IsTrue(result.Value("success"), result.ToString()); + Assert.AreEqual(1, ((JArray)result["data"]).Count); + Assert.AreEqual(message, result["data"][0].ToString()); + } + + [TestCase("MCP-FOR-UNITY:")] + [TestCase("MCP-FOR-UNITY:")] + [TestCase("MCP-FOR-UNITY:")] + [TestCase("MCP-FOR-UNITY:")] + [TestCase("MCP-FOR-UNITY:")] + public void HandleCommand_Get_ExcludesKnownMcpPrefixes(string prefix) + { + string id = Guid.NewGuid().ToString(); + Debug.Log($"{prefix} Transport diagnostic {id}"); + var result = ToJObject(ReadConsole.HandleCommand(new JObject + { + ["types"] = new JArray { "all" }, + ["filterText"] = id, + ["includeMcpLogs"] = false + })); + + Assert.IsTrue(result.Value("success"), result.ToString()); + Assert.AreEqual(0, ((JArray)result["data"]).Count); + } + [Test] public void HandleCommand_Clear_Works() { diff --git a/website/docs/reference/tools/core/read_console.md b/website/docs/reference/tools/core/read_console.md index a380721a9..32ce71d91 100644 --- a/website/docs/reference/tools/core/read_console.md +++ b/website/docs/reference/tools/core/read_console.md @@ -26,6 +26,7 @@ Gets messages from or clears the Unity Editor console. Defaults to 10 most recen | `cursor` | `int \| str \| None` | — | Opaque cursor for paging (0-based offset). Defaults to 0. | | `format` | `Literal['plain', 'detailed', 'json'] \| None` | — | Output format | | `include_stacktrace` | `bool \| str \| None` | — | Include stack traces in output (accepts true/false or 'true'/'false') | +| `include_mcp_logs` | `bool \| str \| None` | — | Include MCP for Unity's own diagnostics (default true). Set false to exclude them before count and paging limits. | ## Returns @@ -34,6 +35,21 @@ A `dict` containing the Unity response. The exact shape depends on the action. ## Examples -*No examples yet. Add usage examples here — they will be preserved across regenerations.* +Read project warnings and errors without MCP for Unity's transport diagnostics +consuming the result limit: + +```json +{"action":"get","types":["error","warning"],"count":10,"include_mcp_logs":false} +``` + +MCP diagnostics are included by default. Leave `include_mcp_logs` unset or set it +to `true` when troubleshooting the bridge itself. Exclusion happens before count +and paging limits; project messages that merely mention MCP for Unity are retained. + +The CLI exposes the same filter: + +```bash +unity-mcp editor console --type error --type warning --no-mcp-logs +```