mirror of
https://github.com/svnscha/mcp-windbg.git
synced 2026-10-16 20:32:50 +08:00
fix: return debugger errors without a stack trace (#118)
## What this changes Tool calls that fail with a `DebuggerError` (timeouts, a debugger that exited, failed connects) now reach the caller as the error message alone. Unexpected exceptions still include the stack trace. ## Why `_dispatch_tool` appended `traceback.format_exc()` to every error. The last line of that trace repeats the exception message, so since #114 a timeout carrying partial output returned it twice: about 130 KB against a real kernel target, instead of the 64 KiB cap. The trace itself tells the model nothing it can act on. ## How it was verified - New hermetic test through the registered tool handler (cdb and kd): fails before the fix, passes after. - Full suite: 132 passed, 2 skipped. - Against a real KDNET target (TESTBOX-1): both kernel scenarios pass, and `!process 0 7` with an 8 s timeout now returns 61 KB with the partial output once and no traceback.
This commit is contained in:
@@ -24,6 +24,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
instead of every pending command, `wait_for_break`, or session open sitting out
|
||||
its full timeout and then misreporting a timeout (#115). Thanks to
|
||||
@RamanaIntel for the fix.
|
||||
- Debugger errors (timeouts, a debugger that exited, failed connects) reach the
|
||||
caller as their message alone, without a Python stack trace. The trace
|
||||
repeated the message, so a timeout with partial output returned that output
|
||||
twice.
|
||||
|
||||
## [1.3.0] - 2026-09-10
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import Dict, Optional
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from .cdb_session import CDBSession
|
||||
from .debug_session import DEFAULT_WAIT_FOR_BREAK_TIMEOUT
|
||||
from .debug_session import DEFAULT_WAIT_FOR_BREAK_TIMEOUT, DebuggerError
|
||||
from .kd_session import KDSession
|
||||
from .filter_script import FilterScript, load_filter_script
|
||||
from .prompts import load_prompt
|
||||
@@ -517,6 +517,10 @@ def _create_server(
|
||||
|
||||
except MCPError:
|
||||
raise
|
||||
except DebuggerError as e:
|
||||
# An expected debugger failure: its message is the whole story, and a
|
||||
# stack trace would repeat it (partial output included) a second time.
|
||||
raise MCPError(INTERNAL_ERROR, f"Error executing tool {name}: {e}")
|
||||
except Exception as e:
|
||||
traceback_str = traceback.format_exc()
|
||||
raise MCPError(INTERNAL_ERROR, f"Error executing tool {name}: {str(e)}\n{traceback_str}")
|
||||
|
||||
@@ -9,6 +9,7 @@ import pytest
|
||||
from mcp.types import CallToolRequestParams
|
||||
|
||||
from mcp_windbg import server as server_module
|
||||
from mcp_windbg.debug_session import DebuggerError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -278,3 +279,38 @@ async def test_break_in_racing_with_close_reports_debugger_error(tool_handler, k
|
||||
name="send_ctrl_break", arguments={"session_id": "test-session"}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("kind", ["cdb", "kd"])
|
||||
async def test_debugger_error_reaches_caller_without_traceback(tool_handler, kind):
|
||||
class TimingOutSession:
|
||||
is_live_session = True
|
||||
|
||||
def send_command(self, command, timeout):
|
||||
raise DebuggerError(
|
||||
"Command timed out after 8 seconds: !process 0 7\n"
|
||||
"Partial output before timeout:\nPROCESS ffffc48d14520040",
|
||||
partial_output=["PROCESS ffffc48d14520040"],
|
||||
)
|
||||
|
||||
server_module._sessions["test-session"] = {
|
||||
"session": TimingOutSession(),
|
||||
"kind": kind,
|
||||
"label": "test target",
|
||||
}
|
||||
with pytest.raises(server_module.MCPError) as exc:
|
||||
await tool_handler(
|
||||
None,
|
||||
CallToolRequestParams(
|
||||
name=f"run_{kind}_command",
|
||||
arguments={"session_id": "test-session", "command": "!process 0 7"},
|
||||
),
|
||||
)
|
||||
|
||||
# The message is for the model: a stack trace adds nothing, and its last
|
||||
# line would repeat the (possibly 64 KiB of) partial output a second time.
|
||||
message = exc.value.message
|
||||
assert "Command timed out" in message
|
||||
assert "Traceback" not in message
|
||||
assert message.count("PROCESS ffffc48d14520040") == 1
|
||||
|
||||
Reference in New Issue
Block a user